agent_client_protocol_cookbook/lib.rs
1//! Cookbook of common patterns for building ACP components.
2//!
3//! This crate contains guides and examples for the three main things you can build with ACP:
4//!
5//! - **Clients** - Connect to an existing agent and send prompts
6//! - **Proxies** - Sit between client and agent to add capabilities (like MCP tools)
7//! - **Agents** - Respond to prompts with AI-powered responses
8//!
9//! See the [`agent_client_protocol::concepts`] module for detailed explanations of
10//! the concepts behind the API.
11//!
12//! # Building Clients
13//!
14//! A client connects to an agent, sends requests, and handles responses. Use
15//! [`Client.builder()`](agent_client_protocol::Client) to build connections.
16//!
17//! - [`one_shot_prompt`] - Send a single prompt and get a response (simplest pattern)
18//! - [`connecting_as_client`] - More details on connection setup and permission handling
19//!
20//! # Building Proxies
21//!
22//! A proxy sits between client and agent, intercepting and optionally modifying
23//! messages. The most common use case is adding MCP tools. Use [`Proxy.builder()`](agent_client_protocol::Proxy)
24//! to build proxy connections.
25//!
26//! **Important:** Proxies don't run standalone—they need the [`agent-client-protocol-conductor`] to
27//! orchestrate the connection between client, proxies, and agent. See
28//! [`running_proxies_with_conductor`] for how to put the pieces together.
29//!
30//! - [`global_mcp_server`] - Add tools that work across all sessions
31//! - [`per_session_mcp_server`] - Add tools with session-specific state
32//! - [`filtering_tools`] - Enable or disable tools dynamically
33//! - [`reusable_components`] - Package your proxy as a [`ConnectTo`] for composition
34//! - [`running_proxies_with_conductor`] - Run your proxy with an agent
35//!
36//! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
37//!
38//! # Building Agents
39//!
40//! An agent receives prompts and generates responses. Use [`Agent.builder()`](agent_client_protocol::Agent)
41//! to build agent connections.
42//!
43//! - [`building_an_agent`] - Handle initialization, sessions, and prompts
44//! - [`reusable_components`] - Package your agent as a [`ConnectTo`]
45//! - [`custom_message_handlers`] - Fine-grained control over message routing
46//!
47//! [`agent_client_protocol::concepts`]: agent_client_protocol::concepts
48//! [`Client`]: agent_client_protocol::Client
49//! [`Agent`]: agent_client_protocol::Agent
50//! [`Proxy`]: agent_client_protocol::Proxy
51//! [`ConnectTo`]: agent_client_protocol::ConnectTo
52
53pub mod one_shot_prompt {
54 //! Pattern: You Only Prompt Once.
55 //!
56 //! The simplest client pattern: connect to an agent, send one prompt, get the
57 //! response. This is useful for CLI tools, scripts, or any case where you just
58 //! need a single interaction with an agent.
59 //!
60 //! # Example
61 //!
62 //! ```
63 //! use agent_client_protocol::{Client, Agent, ConnectTo};
64 //! use agent_client_protocol::schema::{ProtocolVersion, v1::InitializeRequest};
65 //!
66 //! async fn ask_agent(
67 //! transport: impl ConnectTo<Client> + 'static,
68 //! prompt: &str,
69 //! ) -> Result<String, agent_client_protocol::Error> {
70 //! Client.builder()
71 //! .name("my-client")
72 //! .connect_with(transport, async |connection| {
73 //! // Initialize the connection
74 //! connection.send_request(InitializeRequest::new(ProtocolVersion::V1))
75 //! .block_task().await?;
76 //!
77 //! // Create a session, send prompt, read response
78 //! let mut session = connection.build_session_cwd()?
79 //! .block_task()
80 //! .start_session()
81 //! .await?;
82 //!
83 //! session.send_prompt(prompt)?;
84 //! session.read_to_string().await
85 //! })
86 //! .await
87 //! }
88 //! ```
89 //!
90 //! # How it works
91 //!
92 //! 1. **[`connect_with`]** establishes the transport connection and runs your
93 //! code while handling messages in the background
94 //! 2. **[`send_request`]** + **[`block_task`]** sends the initialize request
95 //! and waits for the response
96 //! 3. **[`build_session_cwd`]** creates a session builder using the current working directory
97 //! 4. **[`start_session`]** sends the `NewSessionRequest` and returns an
98 //! [`ActiveSession`] handle
99 //! 5. **[`send_prompt`]** queues the prompt to send to the agent
100 //! 6. **[`read_to_string`]** reads all text chunks until the agent finishes
101 //!
102 //! # Handling permission requests
103 //!
104 //! Most agents will ask for permission before taking actions like running
105 //! commands or writing files. See [`connecting_as_client`] for how to handle
106 //! [`RequestPermissionRequest`] messages.
107 //!
108 //! [`connect_with`]: agent_client_protocol::Builder::connect_with
109 //! [`send_request`]: agent_client_protocol::ConnectionTo::send_request
110 //! [`block_task`]: agent_client_protocol::SentRequest::block_task
111 //! [`build_session_cwd`]: agent_client_protocol::ConnectionTo::build_session_cwd
112 //! [`start_session`]: agent_client_protocol::SessionBuilder::start_session
113 //! [`ActiveSession`]: agent_client_protocol::ActiveSession
114 //! [`send_prompt`]: agent_client_protocol::ActiveSession::send_prompt
115 //! [`read_to_string`]: agent_client_protocol::ActiveSession::read_to_string
116 //! [`connecting_as_client`]: super::connecting_as_client
117 //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
118}
119
120pub mod connecting_as_client {
121 //! Pattern: Connecting as a client.
122 //!
123 //! To connect to an ACP agent and send requests, use [`connect_with`].
124 //! This runs your code while the connection handles incoming messages
125 //! in the background.
126 //!
127 //! # Basic Example
128 //!
129 //! ```
130 //! use agent_client_protocol::{Client, Agent, ConnectTo};
131 //! use agent_client_protocol::schema::{ProtocolVersion, v1::InitializeRequest};
132 //!
133 //! async fn connect_to_agent(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
134 //! Client.builder()
135 //! .name("my-client")
136 //! .connect_with(transport, async |connection| {
137 //! // Initialize the connection
138 //! connection.send_request(InitializeRequest::new(ProtocolVersion::V1))
139 //! .block_task().await?;
140 //!
141 //! // Create a session and send a prompt
142 //! connection.build_session_cwd()?
143 //! .block_task()
144 //! .run_until(async |mut session| {
145 //! session.send_prompt("Hello, agent!")?;
146 //! let response = session.read_to_string().await?;
147 //! println!("Agent said: {}", response);
148 //! Ok(())
149 //! })
150 //! .await
151 //! })
152 //! .await
153 //! }
154 //! ```
155 //!
156 //! # Using the Session Builder
157 //!
158 //! The [`build_session`] method creates a [`SessionBuilder`] that handles
159 //! session creation and provides convenient methods for interacting with
160 //! the session:
161 //!
162 //! - [`send_prompt`] - Send a text prompt to the agent
163 //! - [`read_update`] - Read the next update (text chunk, tool call, etc.)
164 //! - [`read_to_string`] - Read all text until the turn ends
165 //!
166 //! With the core SDK's `unstable_mcp_over_acp` feature, the session builder
167 //! also supports adding MCP servers with [`with_mcp_server`].
168 //!
169 //! # Handling Permission Requests
170 //!
171 //! Agents may send [`RequestPermissionRequest`] to ask for user approval
172 //! before taking actions. Handle these with [`on_receive_request`]:
173 //!
174 //! ```ignore
175 //! Client.builder()
176 //! .on_receive_request(async |req: RequestPermissionRequest, responder, _connection| {
177 //! // Auto-approve by selecting the first option (YOLO mode)
178 //! let option_id = req.options.first().map(|opt| opt.option_id.clone());
179 //! responder.respond(RequestPermissionResponse::new(
180 //! match option_id {
181 //! Some(id) => RequestPermissionOutcome::Selected(
182 //! SelectedPermissionOutcome::new(id),
183 //! ),
184 //! None => RequestPermissionOutcome::Cancelled,
185 //! }
186 //! ))
187 //! }, agent_client_protocol::on_receive_request!())
188 //! .connect_with(transport, async |connection| { /* ... */ })
189 //! .await
190 //! ```
191 //!
192 //! # Note on `block_task`
193 //!
194 //! Using [`block_task`] is safe inside `connect_with` because its foreground
195 //! future runs alongside, rather than inside, the dispatch loop. The loop
196 //! continues processing messages (including the response you're waiting for)
197 //! while the foreground future waits.
198 //!
199 //! [`connect_with`]: agent_client_protocol::Builder::connect_with
200 //! [`block_task`]: agent_client_protocol::SentRequest::block_task
201 //! [`build_session`]: agent_client_protocol::ConnectionTo::build_session
202 //! [`SessionBuilder`]: agent_client_protocol::SessionBuilder
203 //! [`send_prompt`]: agent_client_protocol::ActiveSession::send_prompt
204 //! [`read_update`]: agent_client_protocol::ActiveSession::read_update
205 //! [`read_to_string`]: agent_client_protocol::ActiveSession::read_to_string
206 //! [`with_mcp_server`]: agent_client_protocol::SessionBuilder::with_mcp_server
207 //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
208 //! [`on_receive_request`]: agent_client_protocol::Builder::on_receive_request
209}
210
211pub mod building_an_agent {
212 //! Pattern: Building an agent.
213 //!
214 //! An agent handles prompts and generates responses. At minimum, an agent must:
215 //!
216 //! 1. Handle [`InitializeRequest`] to establish the connection
217 //! 2. Handle [`NewSessionRequest`] to create sessions
218 //! 3. Handle [`PromptRequest`] to process prompts
219 //!
220 //! Use [`Agent.builder()`](agent_client_protocol::Agent) to build agent connections.
221 //!
222 //! # Minimal Example
223 //!
224 //! ```
225 //! use agent_client_protocol::{Agent, ConnectTo};
226 //! use agent_client_protocol::schema::v1::{
227 //! InitializeRequest, InitializeResponse, AgentCapabilities,
228 //! NewSessionRequest, NewSessionResponse, SessionId,
229 //! PromptRequest, PromptResponse, StopReason,
230 //! };
231 //!
232 //! async fn run_agent(transport: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
233 //! Agent.builder()
234 //! .name("my-agent")
235 //! // Handle initialization
236 //! .on_receive_request(async |req: InitializeRequest, responder, _connection| {
237 //! responder.respond(
238 //! InitializeResponse::new(req.protocol_version)
239 //! .agent_capabilities(AgentCapabilities::new())
240 //! )
241 //! }, agent_client_protocol::on_receive_request!())
242 //! // Handle session creation
243 //! .on_receive_request(async |req: NewSessionRequest, responder, _connection| {
244 //! responder.respond(NewSessionResponse::new(SessionId::new("session-1")))
245 //! }, agent_client_protocol::on_receive_request!())
246 //! // Handle prompts
247 //! .on_receive_request(async |req: PromptRequest, responder, connection| {
248 //! // Send streaming updates via notifications
249 //! // connection.send_notification(SessionNotification { ... })?;
250 //!
251 //! // Return final response
252 //! responder.respond(PromptResponse::new(StopReason::EndTurn))
253 //! }, agent_client_protocol::on_receive_request!())
254 //! // Unknown requests receive Method not found automatically;
255 //! // unhandled notifications are ignored.
256 //! .connect_to(transport)
257 //! .await
258 //! }
259 //! ```
260 //!
261 //! # Streaming Responses
262 //!
263 //! To stream text or other updates to the client, send [`SessionNotification`]s
264 //! while processing a prompt:
265 //!
266 //! ```ignore
267 //! .on_receive_request(async |req: PromptRequest, responder, connection| {
268 //! // Stream some text
269 //! connection.send_notification(SessionNotification::new(
270 //! req.session_id.clone(),
271 //! SessionUpdate::AgentMessageChunk(ContentChunk::new("Hello, ".into())),
272 //! ))?;
273 //!
274 //! connection.send_notification(SessionNotification::new(
275 //! req.session_id.clone(),
276 //! SessionUpdate::AgentMessageChunk(ContentChunk::new("world!".into())),
277 //! ))?;
278 //!
279 //! responder.respond(PromptResponse::new(StopReason::EndTurn))
280 //! }, agent_client_protocol::on_receive_request!())
281 //! ```
282 //!
283 //! # Requesting Permissions
284 //!
285 //! Before taking actions that require user approval (like running commands
286 //! or writing files), send a [`RequestPermissionRequest`]:
287 //!
288 //! ```ignore
289 //! let response = connection.send_request(RequestPermissionRequest::new(
290 //! session_id.clone(),
291 //! ToolCallUpdate::new(
292 //! "dangerous-command",
293 //! ToolCallUpdateFields::new()
294 //! .title("Run rm -rf /")
295 //! .kind(ToolKind::Execute)
296 //! .status(ToolCallStatus::Pending),
297 //! ),
298 //! vec![
299 //! PermissionOption::new("allow", "Allow", PermissionOptionKind::AllowOnce),
300 //! PermissionOption::new("deny", "Deny", PermissionOptionKind::RejectOnce),
301 //! ],
302 //! )).block_task().await?;
303 //!
304 //! match response.outcome {
305 //! RequestPermissionOutcome::Selected(selected) if selected.option_id == "allow" => {
306 //! // User approved, proceed with action
307 //! }
308 //! _ => {
309 //! // User denied or cancelled
310 //! }
311 //! }
312 //! ```
313 //!
314 //! # As a Reusable Component
315 //!
316 //! For agents that will be composed with proxies, implement [`ConnectTo`].
317 //! See [`reusable_components`] for the pattern.
318 //!
319 //! [`InitializeRequest`]: agent_client_protocol::schema::v1::InitializeRequest
320 //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
321 //! [`PromptRequest`]: agent_client_protocol::schema::v1::PromptRequest
322 //! [`SessionNotification`]: agent_client_protocol::schema::v1::SessionNotification
323 //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
324 //! [`Agent`]: agent_client_protocol::Agent
325 //! [`ConnectTo`]: agent_client_protocol::ConnectTo
326 //! [`reusable_components`]: super::reusable_components
327}
328
329pub mod reusable_components {
330 //! Pattern: Defining reusable components.
331 //!
332 //! When building agents or proxies that will be composed together (for example,
333 //! with [`agent-client-protocol-conductor`]), define a struct that implements [`ConnectTo`].
334 //! This allows your component to be connected to other components in a type-safe way.
335 //!
336 //! # Example
337 //!
338 //! ```
339 //! use agent_client_protocol::{ConnectTo, Agent, Client};
340 //! use agent_client_protocol::schema::v1::{
341 //! InitializeRequest, InitializeResponse, AgentCapabilities,
342 //! };
343 //!
344 //! struct MyAgent {
345 //! name: String,
346 //! }
347 //!
348 //! impl ConnectTo<Client> for MyAgent {
349 //! async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
350 //! Agent.builder()
351 //! .name(&self.name)
352 //! .on_receive_request(async move |req: InitializeRequest, responder, _connection| {
353 //! responder.respond(
354 //! InitializeResponse::new(req.protocol_version)
355 //! .agent_capabilities(AgentCapabilities::new())
356 //! )
357 //! }, agent_client_protocol::on_receive_request!())
358 //! .connect_to(client)
359 //! .await
360 //! }
361 //! }
362 //!
363 //! let agent = MyAgent { name: "my-agent".into() };
364 //! ```
365 //!
366 //! # Important: Don't block the event loop
367 //!
368 //! Message handlers run on the event loop. Blocking in a handler prevents the
369 //! connection from processing new messages:
370 //!
371 //! - Use [`ConnectionTo::spawn`] to offload work to a background task
372 //! - Use [`on_receiving_result`] for bounded, ordered response handling; if it
373 //! must await later traffic, spawn that work from the callback and return
374 //!
375 //! [`ConnectTo`]: agent_client_protocol::ConnectTo
376 //! [`ConnectionTo::spawn`]: agent_client_protocol::ConnectionTo::spawn
377 //! [`on_receiving_result`]: agent_client_protocol::SentRequest::on_receiving_result
378 //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
379}
380
381pub mod custom_message_handlers {
382 //! Pattern: Custom message handlers.
383 //!
384 //! For reusable message handling logic, implement [`HandleDispatchFrom`] and use
385 //! [`MatchDispatch`] or [`MatchDispatchFrom`] for type-safe dispatching.
386 //!
387 //! This is useful when you need to:
388 //! - Share message handling logic across multiple components
389 //! - Build complex routing logic that doesn't fit the builder pattern
390 //! - Integrate with existing handler infrastructure
391 //!
392 //! # Example
393 //!
394 //! ```
395 //! use agent_client_protocol::{HandleDispatchFrom, Dispatch, Handled, ConnectionTo, UntypedRole};
396 //! use agent_client_protocol::schema::v1::{AgentCapabilities, InitializeRequest, InitializeResponse};
397 //! use agent_client_protocol::util::MatchDispatch;
398 //!
399 //! struct MyHandler;
400 //!
401 //! impl HandleDispatchFrom<UntypedRole> for MyHandler {
402 //! async fn handle_dispatch_from(
403 //! &mut self,
404 //! message: Dispatch,
405 //! _connection: ConnectionTo<UntypedRole>,
406 //! ) -> Result<Handled<Dispatch>, agent_client_protocol::Error> {
407 //! MatchDispatch::new(message)
408 //! .if_request(async |req: InitializeRequest, responder| {
409 //! responder.respond(
410 //! InitializeResponse::new(req.protocol_version)
411 //! .agent_capabilities(AgentCapabilities::new())
412 //! )
413 //! })
414 //! .await
415 //! .done()
416 //! }
417 //!
418 //! fn describe_chain(&self) -> impl std::fmt::Debug {
419 //! "MyHandler"
420 //! }
421 //! }
422 //! ```
423 //!
424 //! # When to use `MatchDispatch` vs `MatchDispatchFrom`
425 //!
426 //! - [`MatchDispatch`] - Use when you don't need peer-aware handling
427 //! - [`MatchDispatchFrom`] - Use in proxies where messages come from different
428 //! peers (`Client` vs `Agent`) and may need different handling
429 //!
430 //! [`HandleDispatchFrom`]: agent_client_protocol::HandleDispatchFrom
431 //! [`MatchDispatch`]: agent_client_protocol::util::MatchDispatch
432 //! [`MatchDispatchFrom`]: agent_client_protocol::util::MatchDispatchFrom
433}
434
435pub mod global_mcp_server {
436 //! Pattern: Global MCP server in handler chain.
437 //!
438 //! Use this pattern when you want a single MCP server that handles tool calls
439 //! for all sessions. The server is added to the connection's handler chain and
440 //! automatically injects itself into every supported session setup request.
441 //! This pattern requires the core SDK's `unstable_mcp_over_acp` feature (or
442 //! the rmcp crate's matching passthrough feature).
443 //!
444 //! # When to use
445 //!
446 //! - The MCP server provides stateless tools (no per-session state needed)
447 //! - You want the simplest setup with minimal boilerplate
448 //! - Tools don't need access to session-specific context
449 //!
450 //! # Using the builder API
451 //!
452 //! The simplest way to create an MCP server is with [`McpServer::builder`]:
453 //!
454 //! ```
455 //! use agent_client_protocol::mcp_server::McpServer;
456 //! use agent_client_protocol_rmcp::McpServerExt;
457 //! use agent_client_protocol::{ConnectTo, RunWithConnectionTo, Proxy, Conductor};
458 //! use schemars::JsonSchema;
459 //! use serde::{Deserialize, Serialize};
460 //!
461 //! #[derive(Debug, Deserialize, JsonSchema)]
462 //! struct EchoParams { message: String }
463 //!
464 //! #[derive(Debug, Serialize, JsonSchema)]
465 //! struct EchoOutput { echoed: String }
466 //!
467 //! // Build the MCP server with tools
468 //! let mcp_server = McpServer::builder("my-tools")
469 //! .tool_fn("echo", "Echoes the input",
470 //! async |params: EchoParams, _cx| {
471 //! Ok(EchoOutput { echoed: params.message })
472 //! },
473 //! agent_client_protocol::tool_fn!())
474 //! .build();
475 //!
476 //! // The proxy component is generic over the MCP server's runner type
477 //! struct MyProxy<R> {
478 //! mcp_server: McpServer<Conductor, R>,
479 //! }
480 //!
481 //! impl<R: RunWithConnectionTo<Conductor> + Send + 'static> ConnectTo<Conductor> for MyProxy<R> {
482 //! async fn connect_to(self, conductor: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
483 //! Proxy.builder()
484 //! .with_mcp_server(self.mcp_server)
485 //! .connect_to(conductor)
486 //! .await
487 //! }
488 //! }
489 //!
490 //! let proxy = MyProxy { mcp_server };
491 //! ```
492 //!
493 //! # Using rmcp
494 //!
495 //! If you have an existing [rmcp](https://docs.rs/rmcp) server implementation,
496 //! use [`McpServer::from_rmcp`] from the `agent-client-protocol-rmcp` crate:
497 //!
498 //! ```
499 //! use rmcp::{ServerHandler, tool, tool_router, tool_handler};
500 //! use rmcp::handler::server::router::tool::ToolRouter;
501 //! use rmcp::handler::server::wrapper::Parameters;
502 //! use rmcp::model::*;
503 //! use agent_client_protocol::mcp_server::McpServer;
504 //! use agent_client_protocol::Conductor;
505 //! use agent_client_protocol_rmcp::McpServerExt;
506 //! use serde::{Deserialize, Serialize};
507 //!
508 //! #[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
509 //! struct EchoParams {
510 //! message: String,
511 //! }
512 //!
513 //! #[derive(Clone)]
514 //! struct MyMcpServer {
515 //! tool_router: ToolRouter<Self>,
516 //! }
517 //!
518 //! impl MyMcpServer {
519 //! fn new() -> Self {
520 //! Self { tool_router: Self::tool_router() }
521 //! }
522 //! }
523 //!
524 //! #[tool_router]
525 //! impl MyMcpServer {
526 //! #[tool(description = "Echoes back the input message")]
527 //! async fn echo(&self, Parameters(params): Parameters<EchoParams>) -> Result<CallToolResult, rmcp::ErrorData> {
528 //! Ok(CallToolResult::success(vec![ContentBlock::text(format!("Echo: {}", params.message))]))
529 //! }
530 //! }
531 //!
532 //! #[tool_handler]
533 //! impl ServerHandler for MyMcpServer {
534 //! fn get_info(&self) -> ServerInfo {
535 //! ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
536 //! .with_protocol_version(ProtocolVersion::V_2024_11_05)
537 //! .with_server_info(Implementation::from_build_env())
538 //! }
539 //! }
540 //!
541 //! // Create an MCP server from the rmcp service
542 //! let mcp_server = McpServer::<Conductor, _>::from_rmcp("my-server", MyMcpServer::new);
543 //! ```
544 //!
545 //! The `from_rmcp` function takes a factory closure that creates a new server
546 //! instance. This allows each MCP connection to get a fresh server instance.
547 //!
548 //! # How it works
549 //!
550 //! When you call [`with_mcp_server`], the MCP server is added as a message
551 //! handler. It:
552 //!
553 //! 1. Intercepts session setup requests and adds a schema-native
554 //! `McpServer::Acp` declaration with one connection-scoped server ID,
555 //! reused in each request's `mcp_servers` list (`session/new`,
556 //! `session/load`, `session/resume`, and feature-gated `session/fork`)
557 //! 2. Passes the modified request through to the next handler
558 //! 3. Handles `mcp/connect`, `mcp/message`, and `mcp/disconnect` for that server ID
559 //!
560 //! [`McpServer::builder`]: agent_client_protocol_rmcp::McpServerExt::builder
561 //! [`McpServer::from_rmcp`]: agent_client_protocol_rmcp::McpServerExt::from_rmcp
562 //! [`with_mcp_server`]: agent_client_protocol::Builder::with_mcp_server
563}
564
565pub mod per_session_mcp_server {
566 //! Pattern: Per-session MCP server with workspace context.
567 //!
568 //! Use this pattern when each session needs its own MCP server instance
569 //! with access to session-specific context like the working directory.
570 //! It requires the core SDK's `unstable_mcp_over_acp` feature (or the rmcp
571 //! crate's matching passthrough feature).
572 //!
573 //! # When to use
574 //!
575 //! - Tools need access to the session's working directory
576 //! - You want eventual active-session tracking that does not need to precede later traffic
577 //! - Tools need to customize behavior based on session parameters
578 //!
579 //! # Basic pattern with `on_proxy_session_start`
580 //!
581 //! The most common pattern intercepts [`NewSessionRequest`], extracts context,
582 //! creates a per-session MCP server, and uses [`on_proxy_session_start`] to
583 //! run code after the session is established:
584 //!
585 //! ```
586 //! use agent_client_protocol::mcp_server::McpServer;
587 //! use agent_client_protocol_rmcp::McpServerExt;
588 //! use agent_client_protocol::schema::v1::NewSessionRequest;
589 //! use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
590 //!
591 //! async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
592 //! Proxy.builder()
593 //! .on_receive_request_from(Client, async move |request: NewSessionRequest, responder, connection| {
594 //! // Extract session context from the request
595 //! let workspace_path = request.cwd.clone();
596 //!
597 //! // Create tools that capture the workspace path
598 //! let mcp_server = McpServer::builder("workspace-tools")
599 //! .tool_fn("get_workspace", "Returns the session's workspace directory", {
600 //! async move |_params: (), _cx| {
601 //! Ok(workspace_path.display().to_string())
602 //! }
603 //! }, agent_client_protocol::tool_fn!())
604 //! .build();
605 //!
606 //! // Build the session and run code after it starts
607 //! connection.build_session_from(request)
608 //! .with_mcp_server(mcp_server)?
609 //! .on_proxy_session_start(responder, async move |session_id| {
610 //! // Session proxying is installed before this callback is spawned.
611 //! //
612 //! // Use this for follow-up work that may wait for later connection
613 //! // traffic. Register ID-independent state in the request handler.
614 //! // For ID-keyed state, preinstall a gate that later handlers await,
615 //! // then populate it here.
616 //! tracing::info!(%session_id, "Session started");
617 //! Ok(())
618 //! })
619 //! }, agent_client_protocol::on_receive_request!())
620 //! .connect_to(transport)
621 //! .await
622 //! }
623 //! ```
624 //!
625 //! # How `on_proxy_session_start` works
626 //!
627 //! [`on_proxy_session_start`] is the non-blocking way to set up a proxy session:
628 //!
629 //! 1. Sends `NewSessionRequest` to the agent
630 //! 2. When the response arrives, responds to the client automatically
631 //! 3. Sets up message proxying for the session
632 //! 4. Runs your callback with the `SessionId`
633 //!
634 //! The callback runs after the session is established but doesn't block
635 //! the message handler. It is suitable for follow-up work and eventual
636 //! session tracking. Because it runs concurrently with later traffic, it
637 //! does not guarantee that bookkeeping keyed by `SessionId` completes
638 //! first. Register ID-independent state before calling the helper. For
639 //! ID-keyed state, preinstall a gate or placeholder that later handlers
640 //! await, then populate it from the callback.
641 //!
642 //! # Alternative: spawning `start_session_proxy`
643 //!
644 //! If you need the linear [`start_session_proxy`] API, move it into a
645 //! spawned task. Awaiting it directly in the request handler would block
646 //! the dispatch loop that must receive the agent's response:
647 //!
648 //! ```
649 //! # use agent_client_protocol::mcp_server::McpServer;
650 //! # use agent_client_protocol_rmcp::McpServerExt;
651 //! # use agent_client_protocol::schema::v1::NewSessionRequest;
652 //! # use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
653 //! # async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
654 //! Proxy.builder()
655 //! .on_receive_request_from(Client, async |request: NewSessionRequest, responder, connection| {
656 //! let cwd = request.cwd.clone();
657 //! let mcp_server = McpServer::builder("tools")
658 //! .tool_fn("get_cwd", "Returns working directory", {
659 //! async move |_params: (), _cx| Ok(cwd.display().to_string())
660 //! }, agent_client_protocol::tool_fn!())
661 //! .build();
662 //!
663 //! let task_connection = connection.clone();
664 //! connection.spawn(async move {
665 //! let session_id = task_connection.build_session_from(request)
666 //! .with_mcp_server(mcp_server)?
667 //! .block_task()
668 //! .start_session_proxy(responder)
669 //! .await?;
670 //!
671 //! tracing::info!(%session_id, "Session started");
672 //! Ok(())
673 //! })?;
674 //! Ok(())
675 //! }, agent_client_protocol::on_receive_request!())
676 //! .connect_to(transport)
677 //! .await
678 //! # }
679 //! ```
680 //!
681 //! For patterns where you need to interact with the session before proxying,
682 //! use [`start_session`] + [`proxy_remaining_messages`] instead.
683 //!
684 //! [`start_session`]: agent_client_protocol::SessionBuilder::start_session
685 //! [`proxy_remaining_messages`]: agent_client_protocol::ActiveSession::proxy_remaining_messages
686 //!
687 //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
688 //! [`on_proxy_session_start`]: agent_client_protocol::SessionBuilder::on_proxy_session_start
689 //! [`block_task`]: agent_client_protocol::SessionBuilder::block_task
690 //! [`start_session_proxy`]: agent_client_protocol::SessionBuilder::start_session_proxy
691}
692
693pub mod filtering_tools {
694 //! Pattern: Filtering which tools are available.
695 //!
696 //! Use [`disable_tool`] and [`enable_tool`] to control which tools are
697 //! visible to clients. This is useful when:
698 //!
699 //! - Some tools should only be available in certain configurations
700 //! - You want to conditionally expose tools based on runtime settings
701 //! - You need to restrict access to sensitive tools
702 //!
703 //! # Disabling specific tools (deny-list)
704 //!
705 //! By default, all registered tools are enabled. Use [`disable_tool`] to
706 //! hide specific tools:
707 //!
708 //! ```
709 //! use agent_client_protocol::mcp_server::McpServer;
710 //! use agent_client_protocol_rmcp::McpServerExt;
711 //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
712 //! use schemars::JsonSchema;
713 //! use serde::Deserialize;
714 //!
715 //! #[derive(Debug, Deserialize, JsonSchema)]
716 //! struct Params {}
717 //!
718 //! fn build_server(enable_admin: bool) -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
719 //! let mut builder = McpServer::builder("my-server")
720 //! .tool_fn("echo", "Echo a message",
721 //! async |_p: Params, _cx| Ok("echoed"),
722 //! agent_client_protocol::tool_fn!())
723 //! .tool_fn("admin", "Admin-only tool",
724 //! async |_p: Params, _cx| Ok("admin action"),
725 //! agent_client_protocol::tool_fn!());
726 //!
727 //! // Conditionally disable the admin tool
728 //! if !enable_admin {
729 //! builder = builder.disable_tool("admin")?;
730 //! }
731 //!
732 //! Ok(builder.build())
733 //! }
734 //! ```
735 //!
736 //! Disabled tools:
737 //! - Don't appear in `list_tools` responses
738 //! - Return "tool not found" errors if called directly
739 //!
740 //! # Enabling only specific tools (allow-list)
741 //!
742 //! Use [`disable_all_tools`] followed by [`enable_tool`] to create an
743 //! allow-list where only explicitly enabled tools are available:
744 //!
745 //! ```
746 //! use agent_client_protocol::mcp_server::McpServer;
747 //! use agent_client_protocol_rmcp::McpServerExt;
748 //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
749 //! use schemars::JsonSchema;
750 //! use serde::Deserialize;
751 //!
752 //! #[derive(Debug, Deserialize, JsonSchema)]
753 //! struct Params {}
754 //!
755 //! fn build_restricted_server() -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
756 //! McpServer::builder("restricted-server")
757 //! .tool_fn("safe", "Safe operation",
758 //! async |_p: Params, _cx| Ok("safe"),
759 //! agent_client_protocol::tool_fn!())
760 //! .tool_fn("dangerous", "Dangerous operation",
761 //! async |_p: Params, _cx| Ok("danger!"),
762 //! agent_client_protocol::tool_fn!())
763 //! .tool_fn("experimental", "Experimental feature",
764 //! async |_p: Params, _cx| Ok("experimental"),
765 //! agent_client_protocol::tool_fn!())
766 //! // Start with all tools disabled
767 //! .disable_all_tools()
768 //! // Only enable the safe tool
769 //! .enable_tool("safe")
770 //! .map(|b| b.build())
771 //! }
772 //! ```
773 //!
774 //! # Error handling
775 //!
776 //! Both [`enable_tool`] and [`disable_tool`] return `Result` and will error
777 //! if the tool name doesn't match any registered tool. This helps catch typos:
778 //!
779 //! ```
780 //! use agent_client_protocol::mcp_server::McpServer;
781 //! use agent_client_protocol_rmcp::McpServerExt;
782 //! use agent_client_protocol::Conductor;
783 //!
784 //! // This will error because "ech" is not a registered tool
785 //! let result = McpServer::<Conductor, _>::builder("server")
786 //! .disable_tool("ech"); // Typo! Should be "echo"
787 //!
788 //! assert!(result.is_err());
789 //! ```
790 //!
791 //! Calling enable/disable on an already enabled/disabled tool is not an error -
792 //! the operations are idempotent.
793 //!
794 //! [`disable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::disable_tool
795 //! [`enable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::enable_tool
796 //! [`disable_all_tools`]: agent_client_protocol_rmcp::McpServerBuilder::disable_all_tools
797}
798
799pub mod running_proxies_with_conductor {
800 //! Pattern: Running proxies with the conductor.
801 //!
802 //! Proxies don't run standalone. To add an MCP server (or other proxy behavior)
803 //! to an existing agent, you need the **conductor** to orchestrate the connection.
804 //!
805 //! The conductor:
806 //! 1. Accepts connections from clients
807 //! 2. Chains your proxies together
808 //! 3. Connects to the final agent
809 //! 4. Routes messages through the entire chain
810 //!
811 //! # Using the `agent-client-protocol-conductor` binary
812 //!
813 //! The simplest way to run a proxy is with the [`agent-client-protocol-conductor`] binary.
814 //! Pass the proxy commands followed by the final agent command:
815 //!
816 //! ```bash
817 //! agent-client-protocol-conductor agent \
818 //! "cargo run --bin my-proxy" \
819 //! "claude-code --agent"
820 //! ```
821 //!
822 //! # Using the conductor as a library
823 //!
824 //! For more control, use [`agent-client-protocol-conductor`] as a library with the `ConductorImpl` type:
825 //!
826 //! ```ignore
827 //! use agent_client_protocol::{AcpAgent, ConnectTo};
828 //! use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent};
829 //!
830 //! // Define your proxy as a ConnectTo<Conductor>
831 //! let my_proxy = MyProxy::new();
832 //!
833 //! // Configure the agent process
834 //! let agent = AcpAgent::from_args(["claude-code", "--agent"])?;
835 //!
836 //! // Create the conductor with your proxy chain
837 //! let conductor = ConductorImpl::new_agent(
838 //! "my-conductor",
839 //! ProxiesAndAgent::new(agent).proxy(my_proxy),
840 //! );
841 //!
842 //! // Run the conductor (it will accept client connections on stdin/stdout)
843 //! conductor.connect_to(client_transport).await?;
844 //! ```
845 //!
846 //! # Why can't I just connect my proxy directly to an agent?
847 //!
848 //! ACP uses a message envelope format for proxy chains. When a proxy sends a
849 //! message toward the agent, it gets wrapped in a [`SuccessorMessage`] envelope.
850 //! The conductor handles this wrapping/unwrapping automatically.
851 //!
852 //! If you connected directly to an agent, your proxy would send `SuccessorMessage`
853 //! envelopes that the agent doesn't understand.
854 //!
855 //! # Example: Complete proxy with conductor
856 //!
857 //! See the [`agent-client-protocol-conductor` tests] for complete working examples of proxies
858 //! running with the conductor.
859 //!
860 //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
861 //! [`SuccessorMessage`]: agent_client_protocol::schema::SuccessorMessage
862 //! [`agent-client-protocol-conductor` tests]: https://github.com/agentclientprotocol/rust-sdk/tree/main/src/agent-client-protocol-conductor/tests
863}