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 //! The session builder also supports adding MCP servers with [`with_mcp_server`].
167 //!
168 //! # Handling Permission Requests
169 //!
170 //! Agents may send [`RequestPermissionRequest`] to ask for user approval
171 //! before taking actions. Handle these with [`on_receive_request`]:
172 //!
173 //! ```ignore
174 //! Client.builder()
175 //! .on_receive_request(async |req: RequestPermissionRequest, responder, _connection| {
176 //! // Auto-approve by selecting the first option (YOLO mode)
177 //! let option_id = req.options.first().map(|opt| opt.id.clone());
178 //! responder.respond(RequestPermissionResponse {
179 //! outcome: match option_id {
180 //! Some(id) => RequestPermissionOutcome::Selected { option_id: id },
181 //! None => RequestPermissionOutcome::Cancelled,
182 //! },
183 //! meta: None,
184 //! })
185 //! }, agent_client_protocol::on_receive_request!())
186 //! .connect_with(transport, async |connection| { /* ... */ })
187 //! .await
188 //! ```
189 //!
190 //! # Note on `block_task`
191 //!
192 //! Using [`block_task`] is safe inside `connect_with` because the closure runs
193 //! as a spawned task, not on the event loop. The event loop continues processing
194 //! messages (including the response you're waiting for) while your task blocks.
195 //!
196 //! [`connect_with`]: agent_client_protocol::Builder::connect_with
197 //! [`block_task`]: agent_client_protocol::SentRequest::block_task
198 //! [`build_session`]: agent_client_protocol::ConnectionTo::build_session
199 //! [`SessionBuilder`]: agent_client_protocol::SessionBuilder
200 //! [`send_prompt`]: agent_client_protocol::ActiveSession::send_prompt
201 //! [`read_update`]: agent_client_protocol::ActiveSession::read_update
202 //! [`read_to_string`]: agent_client_protocol::ActiveSession::read_to_string
203 //! [`with_mcp_server`]: agent_client_protocol::SessionBuilder::with_mcp_server
204 //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
205 //! [`on_receive_request`]: agent_client_protocol::Builder::on_receive_request
206}
207
208pub mod building_an_agent {
209 //! Pattern: Building an agent.
210 //!
211 //! An agent handles prompts and generates responses. At minimum, an agent must:
212 //!
213 //! 1. Handle [`InitializeRequest`] to establish the connection
214 //! 2. Handle [`NewSessionRequest`] to create sessions
215 //! 3. Handle [`PromptRequest`] to process prompts
216 //!
217 //! Use [`Agent.builder()`](agent_client_protocol::Agent) to build agent connections.
218 //!
219 //! # Minimal Example
220 //!
221 //! ```
222 //! use agent_client_protocol::{Agent, Client, ConnectTo, Dispatch, ConnectionTo};
223 //! use agent_client_protocol::schema::v1::{
224 //! InitializeRequest, InitializeResponse, AgentCapabilities,
225 //! NewSessionRequest, NewSessionResponse, SessionId,
226 //! PromptRequest, PromptResponse, StopReason,
227 //! };
228 //!
229 //! async fn run_agent(transport: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
230 //! Agent.builder()
231 //! .name("my-agent")
232 //! // Handle initialization
233 //! .on_receive_request(async |req: InitializeRequest, responder, _connection| {
234 //! responder.respond(
235 //! InitializeResponse::new(req.protocol_version)
236 //! .agent_capabilities(AgentCapabilities::new())
237 //! )
238 //! }, agent_client_protocol::on_receive_request!())
239 //! // Handle session creation
240 //! .on_receive_request(async |req: NewSessionRequest, responder, _connection| {
241 //! responder.respond(NewSessionResponse::new(SessionId::new("session-1")))
242 //! }, agent_client_protocol::on_receive_request!())
243 //! // Handle prompts
244 //! .on_receive_request(async |req: PromptRequest, responder, connection| {
245 //! // Send streaming updates via notifications
246 //! // connection.send_notification(SessionNotification { ... })?;
247 //!
248 //! // Return final response
249 //! responder.respond(PromptResponse::new(StopReason::EndTurn))
250 //! }, agent_client_protocol::on_receive_request!())
251 //! // Reject unknown messages
252 //! .on_receive_dispatch(async |message: Dispatch, connection: ConnectionTo<Client>| {
253 //! message.respond_with_error(agent_client_protocol::Error::method_not_found(), connection)
254 //! }, agent_client_protocol::on_receive_dispatch!())
255 //! .connect_to(transport)
256 //! .await
257 //! }
258 //! ```
259 //!
260 //! # Streaming Responses
261 //!
262 //! To stream text or other updates to the client, send [`SessionNotification`]s
263 //! while processing a prompt:
264 //!
265 //! ```ignore
266 //! .on_receive_request(async |req: PromptRequest, responder, connection| {
267 //! // Stream some text
268 //! connection.send_notification(SessionNotification {
269 //! session_id: req.session_id.clone(),
270 //! update: SessionUpdate::Text(TextUpdate {
271 //! text: "Hello, ".into(),
272 //! // ...
273 //! }),
274 //! meta: None,
275 //! })?;
276 //!
277 //! connection.send_notification(SessionNotification {
278 //! session_id: req.session_id.clone(),
279 //! update: SessionUpdate::Text(TextUpdate {
280 //! text: "world!".into(),
281 //! // ...
282 //! }),
283 //! meta: None,
284 //! })?;
285 //!
286 //! responder.respond(PromptResponse {
287 //! stop_reason: StopReason::EndTurn,
288 //! meta: None,
289 //! })
290 //! }, agent_client_protocol::on_receive_request!())
291 //! ```
292 //!
293 //! # Requesting Permissions
294 //!
295 //! Before taking actions that require user approval (like running commands
296 //! or writing files), send a [`RequestPermissionRequest`]:
297 //!
298 //! ```ignore
299 //! let response = connection.send_request(RequestPermissionRequest {
300 //! session_id: session_id.clone(),
301 //! action: PermissionAction::Bash { command: "rm -rf /".into() },
302 //! options: vec![
303 //! PermissionOption { id: "allow".into(), label: "Allow".into() },
304 //! PermissionOption { id: "deny".into(), label: "Deny".into() },
305 //! ],
306 //! meta: None,
307 //! }).block_task().await?;
308 //!
309 //! match response.outcome {
310 //! RequestPermissionOutcome::Selected { option_id } if option_id == "allow" => {
311 //! // User approved, proceed with action
312 //! }
313 //! _ => {
314 //! // User denied or cancelled
315 //! }
316 //! }
317 //! ```
318 //!
319 //! # As a Reusable Component
320 //!
321 //! For agents that will be composed with proxies, implement [`ConnectTo`].
322 //! See [`reusable_components`] for the pattern.
323 //!
324 //! [`InitializeRequest`]: agent_client_protocol::schema::v1::InitializeRequest
325 //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
326 //! [`PromptRequest`]: agent_client_protocol::schema::v1::PromptRequest
327 //! [`SessionNotification`]: agent_client_protocol::schema::v1::SessionNotification
328 //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
329 //! [`Agent`]: agent_client_protocol::Agent
330 //! [`ConnectTo`]: agent_client_protocol::ConnectTo
331 //! [`reusable_components`]: super::reusable_components
332}
333
334pub mod reusable_components {
335 //! Pattern: Defining reusable components.
336 //!
337 //! When building agents or proxies that will be composed together (for example,
338 //! with [`agent-client-protocol-conductor`]), define a struct that implements [`ConnectTo`].
339 //! This allows your component to be connected to other components in a type-safe way.
340 //!
341 //! # Example
342 //!
343 //! ```
344 //! use agent_client_protocol::{ConnectTo, Agent, Client};
345 //! use agent_client_protocol::schema::v1::{
346 //! InitializeRequest, InitializeResponse, AgentCapabilities,
347 //! };
348 //!
349 //! struct MyAgent {
350 //! name: String,
351 //! }
352 //!
353 //! impl ConnectTo<Client> for MyAgent {
354 //! async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
355 //! Agent.builder()
356 //! .name(&self.name)
357 //! .on_receive_request(async move |req: InitializeRequest, responder, _connection| {
358 //! responder.respond(
359 //! InitializeResponse::new(req.protocol_version)
360 //! .agent_capabilities(AgentCapabilities::new())
361 //! )
362 //! }, agent_client_protocol::on_receive_request!())
363 //! .connect_to(client)
364 //! .await
365 //! }
366 //! }
367 //!
368 //! let agent = MyAgent { name: "my-agent".into() };
369 //! ```
370 //!
371 //! # Important: Don't block the event loop
372 //!
373 //! Message handlers run on the event loop. Blocking in a handler prevents the
374 //! connection from processing new messages. For expensive work:
375 //!
376 //! - Use [`ConnectionTo::spawn`] to offload work to a background task
377 //! - Use [`on_receiving_result`] to schedule work when a response arrives
378 //!
379 //! [`ConnectTo`]: agent_client_protocol::ConnectTo
380 //! [`ConnectionTo::spawn`]: agent_client_protocol::ConnectionTo::spawn
381 //! [`on_receiving_result`]: agent_client_protocol::SentRequest::on_receiving_result
382 //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
383}
384
385pub mod custom_message_handlers {
386 //! Pattern: Custom message handlers.
387 //!
388 //! For reusable message handling logic, implement [`HandleDispatchFrom`] and use
389 //! [`MatchDispatch`] or [`MatchDispatchFrom`] for type-safe dispatching.
390 //!
391 //! This is useful when you need to:
392 //! - Share message handling logic across multiple components
393 //! - Build complex routing logic that doesn't fit the builder pattern
394 //! - Integrate with existing handler infrastructure
395 //!
396 //! # Example
397 //!
398 //! ```
399 //! use agent_client_protocol::{HandleDispatchFrom, Dispatch, Handled, ConnectionTo, UntypedRole};
400 //! use agent_client_protocol::schema::v1::{AgentCapabilities, InitializeRequest, InitializeResponse};
401 //! use agent_client_protocol::util::MatchDispatch;
402 //!
403 //! struct MyHandler;
404 //!
405 //! impl HandleDispatchFrom<UntypedRole> for MyHandler {
406 //! async fn handle_dispatch_from(
407 //! &mut self,
408 //! message: Dispatch,
409 //! _connection: ConnectionTo<UntypedRole>,
410 //! ) -> Result<Handled<Dispatch>, agent_client_protocol::Error> {
411 //! MatchDispatch::new(message)
412 //! .if_request(async |req: InitializeRequest, responder| {
413 //! responder.respond(
414 //! InitializeResponse::new(req.protocol_version)
415 //! .agent_capabilities(AgentCapabilities::new())
416 //! )
417 //! })
418 //! .await
419 //! .done()
420 //! }
421 //!
422 //! fn describe_chain(&self) -> impl std::fmt::Debug {
423 //! "MyHandler"
424 //! }
425 //! }
426 //! ```
427 //!
428 //! # When to use `MatchDispatch` vs `MatchDispatchFrom`
429 //!
430 //! - [`MatchDispatch`] - Use when you don't need peer-aware handling
431 //! - [`MatchDispatchFrom`] - Use in proxies where messages come from different
432 //! peers (`Client` vs `Agent`) and may need different handling
433 //!
434 //! [`HandleDispatchFrom`]: agent_client_protocol::HandleDispatchFrom
435 //! [`MatchDispatch`]: agent_client_protocol::util::MatchDispatch
436 //! [`MatchDispatchFrom`]: agent_client_protocol::util::MatchDispatchFrom
437}
438
439pub mod global_mcp_server {
440 //! Pattern: Global MCP server in handler chain.
441 //!
442 //! Use this pattern when you want a single MCP server that handles tool calls
443 //! for all sessions. The server is added to the connection's handler chain and
444 //! automatically injects itself into every `NewSessionRequest` that passes through.
445 //!
446 //! # When to use
447 //!
448 //! - The MCP server provides stateless tools (no per-session state needed)
449 //! - You want the simplest setup with minimal boilerplate
450 //! - Tools don't need access to session-specific context
451 //!
452 //! # Using the builder API
453 //!
454 //! The simplest way to create an MCP server is with [`McpServer::builder`]:
455 //!
456 //! ```
457 //! use agent_client_protocol::mcp_server::McpServer;
458 //! use agent_client_protocol_rmcp::McpServerExt;
459 //! use agent_client_protocol::{ConnectTo, RunWithConnectionTo, Proxy, Conductor};
460 //! use schemars::JsonSchema;
461 //! use serde::{Deserialize, Serialize};
462 //!
463 //! #[derive(Debug, Deserialize, JsonSchema)]
464 //! struct EchoParams { message: String }
465 //!
466 //! #[derive(Debug, Serialize, JsonSchema)]
467 //! struct EchoOutput { echoed: String }
468 //!
469 //! // Build the MCP server with tools
470 //! let mcp_server = McpServer::builder("my-tools")
471 //! .tool_fn("echo", "Echoes the input",
472 //! async |params: EchoParams, _cx| {
473 //! Ok(EchoOutput { echoed: params.message })
474 //! },
475 //! agent_client_protocol::tool_fn!())
476 //! .build();
477 //!
478 //! // The proxy component is generic over the MCP server's responder type
479 //! struct MyProxy<R> {
480 //! mcp_server: McpServer<Conductor, R>,
481 //! }
482 //!
483 //! impl<R: RunWithConnectionTo<Conductor> + Send + 'static> ConnectTo<Conductor> for MyProxy<R> {
484 //! async fn connect_to(self, conductor: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
485 //! Proxy.builder()
486 //! .with_mcp_server(self.mcp_server)
487 //! .connect_to(conductor)
488 //! .await
489 //! }
490 //! }
491 //!
492 //! let proxy = MyProxy { mcp_server };
493 //! ```
494 //!
495 //! # Using rmcp
496 //!
497 //! If you have an existing [rmcp](https://docs.rs/rmcp) server implementation,
498 //! use [`McpServer::from_rmcp`] from the `agent-client-protocol-rmcp` crate:
499 //!
500 //! ```
501 //! use rmcp::{ServerHandler, tool, tool_router, tool_handler};
502 //! use rmcp::handler::server::router::tool::ToolRouter;
503 //! use rmcp::handler::server::wrapper::Parameters;
504 //! use rmcp::model::*;
505 //! use agent_client_protocol::mcp_server::McpServer;
506 //! use agent_client_protocol::Conductor;
507 //! use agent_client_protocol_rmcp::McpServerExt;
508 //! use serde::{Deserialize, Serialize};
509 //!
510 //! #[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
511 //! struct EchoParams {
512 //! message: String,
513 //! }
514 //!
515 //! #[derive(Clone)]
516 //! struct MyMcpServer {
517 //! tool_router: ToolRouter<Self>,
518 //! }
519 //!
520 //! impl MyMcpServer {
521 //! fn new() -> Self {
522 //! Self { tool_router: Self::tool_router() }
523 //! }
524 //! }
525 //!
526 //! #[tool_router]
527 //! impl MyMcpServer {
528 //! #[tool(description = "Echoes back the input message")]
529 //! async fn echo(&self, Parameters(params): Parameters<EchoParams>) -> Result<CallToolResult, rmcp::ErrorData> {
530 //! Ok(CallToolResult::success(vec![ContentBlock::text(format!("Echo: {}", params.message))]))
531 //! }
532 //! }
533 //!
534 //! #[tool_handler]
535 //! impl ServerHandler for MyMcpServer {
536 //! fn get_info(&self) -> ServerInfo {
537 //! ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
538 //! .with_protocol_version(ProtocolVersion::V_2024_11_05)
539 //! .with_server_info(Implementation::from_build_env())
540 //! }
541 //! }
542 //!
543 //! // Create an MCP server from the rmcp service
544 //! let mcp_server = McpServer::<Conductor, _>::from_rmcp("my-server", MyMcpServer::new);
545 //! ```
546 //!
547 //! The `from_rmcp` function takes a factory closure that creates a new server
548 //! instance. This allows each MCP connection to get a fresh server instance.
549 //!
550 //! # How it works
551 //!
552 //! When you call [`with_mcp_server`], the MCP server is added as a message
553 //! handler. It:
554 //!
555 //! 1. Intercepts `NewSessionRequest` messages and adds its `acp:UUID` URL to the
556 //! request's `mcp_servers` list
557 //! 2. Passes the modified request through to the next handler
558 //! 3. Handles incoming MCP protocol messages (tool calls, etc.) for its URL
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 //!
571 //! # When to use
572 //!
573 //! - Tools need access to the session's working directory
574 //! - You want to track active sessions or maintain per-session state
575 //! - Tools need to customize behavior based on session parameters
576 //!
577 //! # Basic pattern with `on_proxy_session_start`
578 //!
579 //! The most common pattern intercepts [`NewSessionRequest`], extracts context,
580 //! creates a per-session MCP server, and uses [`on_proxy_session_start`] to
581 //! run code after the session is established:
582 //!
583 //! ```
584 //! use agent_client_protocol::mcp_server::McpServer;
585 //! use agent_client_protocol_rmcp::McpServerExt;
586 //! use agent_client_protocol::schema::v1::NewSessionRequest;
587 //! use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
588 //!
589 //! async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
590 //! Proxy.builder()
591 //! .on_receive_request_from(Client, async move |request: NewSessionRequest, responder, connection| {
592 //! // Extract session context from the request
593 //! let workspace_path = request.cwd.clone();
594 //!
595 //! // Create tools that capture the workspace path
596 //! let mcp_server = McpServer::builder("workspace-tools")
597 //! .tool_fn("get_workspace", "Returns the session's workspace directory", {
598 //! async move |_params: (), _cx| {
599 //! Ok(workspace_path.display().to_string())
600 //! }
601 //! }, agent_client_protocol::tool_fn!())
602 //! .build();
603 //!
604 //! // Build the session and run code after it starts
605 //! connection.build_session_from(request)
606 //! .with_mcp_server(mcp_server)?
607 //! .on_proxy_session_start(responder, async move |session_id| {
608 //! // This callback runs after the session-id has been sent to the
609 //! // client but before any further messages from the client or agent
610 //! // related to this session have been processed.
611 //! //
612 //! // You can use this to store the `session_id` before processing
613 //! // future messages, or to send a first prompt to the agent before
614 //! // the client has a chance to do so.
615 //! tracing::info!(%session_id, "Session started");
616 //! Ok(())
617 //! })
618 //! }, agent_client_protocol::on_receive_request!())
619 //! .connect_to(transport)
620 //! .await
621 //! }
622 //! ```
623 //!
624 //! # How `on_proxy_session_start` works
625 //!
626 //! [`on_proxy_session_start`] is the non-blocking way to set up a proxy session:
627 //!
628 //! 1. Sends `NewSessionRequest` to the agent
629 //! 2. When the response arrives, responds to the client automatically
630 //! 3. Sets up message proxying for the session
631 //! 4. Runs your callback with the `SessionId`
632 //!
633 //! The callback runs after the session is established but doesn't block
634 //! the message handler. This is ideal for proxies that just need to inject
635 //! tools and track sessions.
636 //!
637 //! # Alternative: blocking with `start_session_proxy`
638 //!
639 //! If you need the simpler blocking API (e.g., in a client context where
640 //! blocking is safe), use [`block_task`] + [`start_session_proxy`]:
641 //!
642 //! ```
643 //! # use agent_client_protocol::mcp_server::McpServer;
644 //! # use agent_client_protocol_rmcp::McpServerExt;
645 //! # use agent_client_protocol::schema::v1::NewSessionRequest;
646 //! # use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
647 //! # async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
648 //! Proxy.builder()
649 //! .on_receive_request_from(Client, async |request: NewSessionRequest, responder, connection| {
650 //! let cwd = request.cwd.clone();
651 //! let mcp_server = McpServer::builder("tools")
652 //! .tool_fn("get_cwd", "Returns working directory", {
653 //! async move |_params: (), _cx| Ok(cwd.display().to_string())
654 //! }, agent_client_protocol::tool_fn!())
655 //! .build();
656 //!
657 //! let session_id = connection.build_session_from(request)
658 //! .with_mcp_server(mcp_server)?
659 //! .block_task()
660 //! .start_session_proxy(responder)
661 //! .await?;
662 //!
663 //! tracing::info!(%session_id, "Session started");
664 //! Ok(())
665 //! }, agent_client_protocol::on_receive_request!())
666 //! .connect_to(transport)
667 //! .await
668 //! # }
669 //! ```
670 //!
671 //! For patterns where you need to interact with the session before proxying,
672 //! use [`start_session`] + [`proxy_remaining_messages`] instead.
673 //!
674 //! [`start_session`]: agent_client_protocol::SessionBuilder::start_session
675 //! [`proxy_remaining_messages`]: agent_client_protocol::ActiveSession::proxy_remaining_messages
676 //!
677 //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
678 //! [`on_proxy_session_start`]: agent_client_protocol::SessionBuilder::on_proxy_session_start
679 //! [`block_task`]: agent_client_protocol::SessionBuilder::block_task
680 //! [`start_session_proxy`]: agent_client_protocol::SessionBuilder::start_session_proxy
681}
682
683pub mod filtering_tools {
684 //! Pattern: Filtering which tools are available.
685 //!
686 //! Use [`disable_tool`] and [`enable_tool`] to control which tools are
687 //! visible to clients. This is useful when:
688 //!
689 //! - Some tools should only be available in certain configurations
690 //! - You want to conditionally expose tools based on runtime settings
691 //! - You need to restrict access to sensitive tools
692 //!
693 //! # Disabling specific tools (deny-list)
694 //!
695 //! By default, all registered tools are enabled. Use [`disable_tool`] to
696 //! hide specific tools:
697 //!
698 //! ```
699 //! use agent_client_protocol::mcp_server::McpServer;
700 //! use agent_client_protocol_rmcp::McpServerExt;
701 //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
702 //! use schemars::JsonSchema;
703 //! use serde::Deserialize;
704 //!
705 //! #[derive(Debug, Deserialize, JsonSchema)]
706 //! struct Params {}
707 //!
708 //! fn build_server(enable_admin: bool) -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
709 //! let mut builder = McpServer::builder("my-server")
710 //! .tool_fn("echo", "Echo a message",
711 //! async |_p: Params, _cx| Ok("echoed"),
712 //! agent_client_protocol::tool_fn!())
713 //! .tool_fn("admin", "Admin-only tool",
714 //! async |_p: Params, _cx| Ok("admin action"),
715 //! agent_client_protocol::tool_fn!());
716 //!
717 //! // Conditionally disable the admin tool
718 //! if !enable_admin {
719 //! builder = builder.disable_tool("admin")?;
720 //! }
721 //!
722 //! Ok(builder.build())
723 //! }
724 //! ```
725 //!
726 //! Disabled tools:
727 //! - Don't appear in `list_tools` responses
728 //! - Return "tool not found" errors if called directly
729 //!
730 //! # Enabling only specific tools (allow-list)
731 //!
732 //! Use [`disable_all_tools`] followed by [`enable_tool`] to create an
733 //! allow-list where only explicitly enabled tools are available:
734 //!
735 //! ```
736 //! use agent_client_protocol::mcp_server::McpServer;
737 //! use agent_client_protocol_rmcp::McpServerExt;
738 //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
739 //! use schemars::JsonSchema;
740 //! use serde::Deserialize;
741 //!
742 //! #[derive(Debug, Deserialize, JsonSchema)]
743 //! struct Params {}
744 //!
745 //! fn build_restricted_server() -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
746 //! McpServer::builder("restricted-server")
747 //! .tool_fn("safe", "Safe operation",
748 //! async |_p: Params, _cx| Ok("safe"),
749 //! agent_client_protocol::tool_fn!())
750 //! .tool_fn("dangerous", "Dangerous operation",
751 //! async |_p: Params, _cx| Ok("danger!"),
752 //! agent_client_protocol::tool_fn!())
753 //! .tool_fn("experimental", "Experimental feature",
754 //! async |_p: Params, _cx| Ok("experimental"),
755 //! agent_client_protocol::tool_fn!())
756 //! // Start with all tools disabled
757 //! .disable_all_tools()
758 //! // Only enable the safe tool
759 //! .enable_tool("safe")
760 //! .map(|b| b.build())
761 //! }
762 //! ```
763 //!
764 //! # Error handling
765 //!
766 //! Both [`enable_tool`] and [`disable_tool`] return `Result` and will error
767 //! if the tool name doesn't match any registered tool. This helps catch typos:
768 //!
769 //! ```
770 //! use agent_client_protocol::mcp_server::McpServer;
771 //! use agent_client_protocol_rmcp::McpServerExt;
772 //! use agent_client_protocol::Conductor;
773 //!
774 //! // This will error because "ech" is not a registered tool
775 //! let result = McpServer::<Conductor, _>::builder("server")
776 //! .disable_tool("ech"); // Typo! Should be "echo"
777 //!
778 //! assert!(result.is_err());
779 //! ```
780 //!
781 //! Calling enable/disable on an already enabled/disabled tool is not an error -
782 //! the operations are idempotent.
783 //!
784 //! [`disable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::disable_tool
785 //! [`enable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::enable_tool
786 //! [`disable_all_tools`]: agent_client_protocol_rmcp::McpServerBuilder::disable_all_tools
787}
788
789pub mod running_proxies_with_conductor {
790 //! Pattern: Running proxies with the conductor.
791 //!
792 //! Proxies don't run standalone. To add an MCP server (or other proxy behavior)
793 //! to an existing agent, you need the **conductor** to orchestrate the connection.
794 //!
795 //! The conductor:
796 //! 1. Accepts connections from clients
797 //! 2. Chains your proxies together
798 //! 3. Connects to the final agent
799 //! 4. Routes messages through the entire chain
800 //!
801 //! # Using the `agent-client-protocol-conductor` binary
802 //!
803 //! The simplest way to run a proxy is with the [`agent-client-protocol-conductor`] binary.
804 //! Configure it with a JSON file:
805 //!
806 //! ```json
807 //! {
808 //! "proxies": [
809 //! { "command": ["cargo", "run", "--bin", "my-proxy"] }
810 //! ],
811 //! "agent": { "command": ["claude-code", "--agent"] }
812 //! }
813 //! ```
814 //!
815 //! Then run:
816 //!
817 //! ```bash
818 //! agent-client-protocol-conductor --config conductor.json
819 //! ```
820 //!
821 //! # Using the conductor as a library
822 //!
823 //! For more control, use [`agent-client-protocol-conductor`] as a library with the `ConductorImpl` type:
824 //!
825 //! ```ignore
826 //! use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent};
827 //!
828 //! // Define your proxy as a ConnectTo<Conductor>
829 //! let my_proxy = MyProxy::new();
830 //!
831 //! // Spawn the agent process
832 //! let agent_process = agent_client_protocol::spawn_process("claude-code", &["--agent"]).await?;
833 //!
834 //! // Create the conductor with your proxy chain
835 //! let conductor = ConductorImpl::new(ProxiesAndAgent {
836 //! proxies: vec![Box::new(my_proxy)],
837 //! agent: agent_process,
838 //! });
839 //!
840 //! // Run the conductor (it will accept client connections on stdin/stdout)
841 //! conductor.connect_to(client_transport).await?;
842 //! ```
843 //!
844 //! # Why can't I just connect my proxy directly to an agent?
845 //!
846 //! ACP uses a message envelope format for proxy chains. When a proxy sends a
847 //! message toward the agent, it gets wrapped in a [`SuccessorMessage`] envelope.
848 //! The conductor handles this wrapping/unwrapping automatically.
849 //!
850 //! If you connected directly to an agent, your proxy would send `SuccessorMessage`
851 //! envelopes that the agent doesn't understand.
852 //!
853 //! # Example: Complete proxy with conductor
854 //!
855 //! See the [`agent-client-protocol-conductor` tests] for complete working examples of proxies
856 //! running with the conductor.
857 //!
858 //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
859 //! [`SuccessorMessage`]: agent_client_protocol::schema::SuccessorMessage
860 //! [`agent-client-protocol-conductor` tests]: https://github.com/anthropics/acp-rust-sdk/tree/main/src/agent-client-protocol-conductor/tests
861}