Skip to main content

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//! - [`v2_one_shot_prompt`] - Send a draft-v2 prompt and wait for the independent idle update
19//! - [`connecting_as_client`] - More details on connection setup and permission handling
20//!
21//! # Building Proxies
22//!
23//! A proxy sits between client and agent, intercepting and optionally modifying
24//! messages. The most common use case is adding MCP tools. Use
25//! [`Proxy.builder()`](agent_client_protocol::Proxy) for stable protocol v1
26//! proxy connections. With the core SDK's `unstable_protocol_v2` feature, use
27//! `Proxy.v2()` for a draft-v2-only proxy, or `Proxy.protocol_router()` to
28//! expose separate strict v1 and v2 implementations as one component.
29//!
30//! **Important:** Proxies don't run standalone—they need the [`agent-client-protocol-conductor`] to
31//! orchestrate the connection between client, proxies, and agent. See
32//! [`running_proxies_with_conductor`] for how to put the pieces together.
33//!
34//! - [`global_mcp_server`] - Add tools that work across all sessions
35//! - [`per_session_mcp_server`] - Add tools with session-specific state
36//! - [`filtering_tools`] - Enable or disable tools dynamically
37//! - [`reusable_components`] - Package your proxy as a [`ConnectTo`] for composition
38//! - [`running_proxies_with_conductor`] - Run your proxy with an agent
39//!
40//! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
41//!
42//! # Building Agents
43//!
44//! An agent receives prompts and generates responses. Use [`Agent.builder()`](agent_client_protocol::Agent)
45//! to build agent connections.
46//!
47//! - [`building_an_agent`] - Handle initialization, sessions, and prompts
48//! - [`reusable_components`] - Package your agent as a [`ConnectTo`]
49//! - [`custom_message_handlers`] - Fine-grained control over message routing
50//!
51//! [`agent_client_protocol::concepts`]: agent_client_protocol::concepts
52//! [`Client`]: agent_client_protocol::Client
53//! [`Agent`]: agent_client_protocol::Agent
54//! [`Proxy`]: agent_client_protocol::Proxy
55//! [`ConnectTo`]: agent_client_protocol::ConnectTo
56
57pub mod one_shot_prompt {
58    //! Pattern: You Only Prompt Once.
59    //!
60    //! The simplest client pattern: connect to an agent, send one prompt, get the
61    //! response. This is useful for CLI tools, scripts, or any case where you just
62    //! need a single interaction with an agent.
63    //!
64    //! # Example
65    //!
66    //! ```
67    //! use agent_client_protocol::{Client, Agent, ConnectTo};
68    //! use agent_client_protocol::schema::{ProtocolVersion, v1::InitializeRequest};
69    //!
70    //! async fn ask_agent(
71    //!     transport: impl ConnectTo<Client> + 'static,
72    //!     prompt: &str,
73    //! ) -> Result<String, agent_client_protocol::Error> {
74    //!     Client.builder()
75    //!         .name("my-client")
76    //!         .connect_with(transport, async |connection| {
77    //!             // Initialize the connection
78    //!             connection.send_request(InitializeRequest::new(ProtocolVersion::V1))
79    //!                 .block_task().await?;
80    //!
81    //!             // Create a session, send prompt, read response
82    //!             let mut session = connection.build_session_cwd()?
83    //!                 .block_task()
84    //!                 .start_session()
85    //!                 .await?;
86    //!
87    //!             session.send_prompt(prompt)?;
88    //!             session.read_to_string().await
89    //!         })
90    //!         .await
91    //! }
92    //! ```
93    //!
94    //! # How it works
95    //!
96    //! 1. **[`connect_with`]** establishes the transport connection and runs your
97    //!    code while handling messages in the background
98    //! 2. **[`send_request`]** + **[`block_task`]** sends the initialize request
99    //!    and waits for the response
100    //! 3. **[`build_session_cwd`]** creates a session builder using the current working directory
101    //! 4. **[`start_session`]** sends the `NewSessionRequest` and returns an
102    //!    [`ActiveSession`] handle
103    //! 5. **[`send_prompt`]** queues the prompt to send to the agent
104    //! 6. **[`read_to_string`]** reads all text chunks until the agent finishes
105    //!
106    //! # Handling permission requests
107    //!
108    //! Most agents will ask for permission before taking actions like running
109    //! commands or writing files. See [`connecting_as_client`] for how to handle
110    //! [`RequestPermissionRequest`] messages.
111    //!
112    //! [`connect_with`]: agent_client_protocol::Builder::connect_with
113    //! [`send_request`]: agent_client_protocol::ConnectionTo::send_request
114    //! [`block_task`]: agent_client_protocol::SentRequest::block_task
115    //! [`build_session_cwd`]: agent_client_protocol::ConnectionTo::build_session_cwd
116    //! [`start_session`]: agent_client_protocol::SessionBuilder::start_session
117    //! [`ActiveSession`]: agent_client_protocol::ActiveSession
118    //! [`send_prompt`]: agent_client_protocol::ActiveSession::send_prompt
119    //! [`read_to_string`]: agent_client_protocol::ActiveSession::read_to_string
120    //! [`connecting_as_client`]: super::connecting_as_client
121    //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
122}
123
124pub mod v2_one_shot_prompt {
125    //! Pattern: One prompt with the draft protocol v2 lifecycle.
126    //!
127    //! A successful v2 `session/prompt` response acknowledges acceptance; it
128    //! does not contain output and does not mean the work is complete. Install
129    //! update handlers before connecting, then consume matching updates until
130    //! the new session reports `running` and subsequently reports `idle`.
131    //!
132    //! This module is compiled with the cookbook's v2 feature coverage. For a
133    //! runnable CLI pair, see the SDK's `simple_agent_v2` and
134    //! `v2_one_shot_client` examples.
135    //!
136    //! # Example
137    //!
138    //! ```
139    //! use std::collections::HashMap;
140    //! use agent_client_protocol::{Agent, Client, ConnectTo, Error, Responder, V2ConnectionTo};
141    //! use agent_client_protocol::schema::{MaybeUndefined, ProtocolVersion, v2};
142    //! use futures::{StreamExt, channel::mpsc};
143    //!
144    //! #[derive(Default)]
145    //! struct AgentTextProjection {
146    //!     order: Vec<v2::MessageId>,
147    //!     messages: HashMap<v2::MessageId, Vec<v2::ContentBlock>>,
148    //! }
149    //!
150    //! impl AgentTextProjection {
151    //!     fn apply(&mut self, update: v2::SessionUpdate) {
152    //!         match update {
153    //!             v2::SessionUpdate::AgentMessageChunk(chunk) => {
154    //!                 self.message_content(chunk.message_id).push(chunk.content);
155    //!             }
156    //!             v2::SessionUpdate::AgentMessage(message) => {
157    //!                 let content = self.message_content(message.message_id);
158    //!                 match message.content {
159    //!                     // Snapshots patch chunks accumulated for the same message ID.
160    //!                     MaybeUndefined::Undefined => {}
161    //!                     MaybeUndefined::Null => content.clear(),
162    //!                     MaybeUndefined::Value(replacement) => *content = replacement,
163    //!                 }
164    //!             }
165    //!             _ => {}
166    //!         }
167    //!     }
168    //!
169    //!     fn message_content(
170    //!         &mut self,
171    //!         message_id: v2::MessageId,
172    //!     ) -> &mut Vec<v2::ContentBlock> {
173    //!         if !self.messages.contains_key(&message_id) {
174    //!             self.order.push(message_id.clone());
175    //!         }
176    //!         self.messages.entry(message_id).or_default()
177    //!     }
178    //!
179    //!     fn text(&self) -> String {
180    //!         self.order
181    //!             .iter()
182    //!             .filter_map(|message_id| self.messages.get(message_id))
183    //!             .flatten()
184    //!             .filter_map(|content| match content {
185    //!                 v2::ContentBlock::Text(text) => Some(text.text.as_str()),
186    //!                 _ => None,
187    //!             })
188    //!             .collect()
189    //!     }
190    //! }
191    //!
192    //! async fn ask_agent(
193    //!     transport: impl ConnectTo<Client> + 'static,
194    //!     prompt: &str,
195    //! ) -> Result<String, Error> {
196    //!     let (update_tx, mut update_rx) = mpsc::unbounded();
197    //!
198    //!     Client.v2()
199    //!         .on_receive_notification(
200    //!             async move |update: v2::UpdateSessionNotification,
201    //!                         _connection: V2ConnectionTo<Agent>| {
202    //!                 update_tx
203    //!                     .unbounded_send(update)
204    //!                     .map_err(Error::into_internal_error)
205    //!             },
206    //!             agent_client_protocol::on_receive_notification!(),
207    //!         )
208    //!         .on_receive_request(
209    //!             async move |_request: v2::RequestPermissionRequest,
210    //!                         responder: Responder<v2::RequestPermissionResponse>,
211    //!                         _connection: V2ConnectionTo<Agent>| {
212    //!                 // This non-interactive recipe rejects permission requests.
213    //!                 responder.respond(v2::RequestPermissionResponse::new(
214    //!                     v2::RequestPermissionOutcome::Cancelled,
215    //!                 ))
216    //!             },
217    //!             agent_client_protocol::on_receive_request!(),
218    //!         )
219    //!         .connect_with(transport, async move |connection| {
220    //!             let initialized = connection
221    //!                 .send_request(v2::InitializeRequest::new(
222    //!                     ProtocolVersion::V2,
223    //!                     v2::Implementation::new("example-client", "0.1.0"),
224    //!                 ))
225    //!                 .block_task()
226    //!                 .await?;
227    //!             if initialized.capabilities.session.is_none() {
228    //!                 return Err(Error::invalid_params()
229    //!                     .data("agent did not advertise session support"));
230    //!             }
231    //!
232    //!             let session = connection
233    //!                 .build_session_cwd()?
234    //!                 .start_session()
235    //!                 .block_task()
236    //!                 .await?
237    //!                 .into_session();
238    //!
239    //!             // This response only means that the prompt was accepted.
240    //!             session.send_prompt(prompt).block_task().await?;
241    //!
242    //!             let mut projection = AgentTextProjection::default();
243    //!             let mut observed_running = false;
244    //!             while let Some(notification) = update_rx.next().await {
245    //!                 if &notification.session_id != session.session_id() {
246    //!                     continue;
247    //!                 }
248    //!                 match notification.update {
249    //!                     v2::SessionUpdate::StateUpdate(v2::StateUpdate::Running(_)) => {
250    //!                         observed_running = true;
251    //!                     }
252    //!                     v2::SessionUpdate::StateUpdate(v2::StateUpdate::Idle(_))
253    //!                         if observed_running =>
254    //!                     {
255    //!                         session.close().block_task().await?;
256    //!                         return Ok(projection.text());
257    //!                     }
258    //!                     update if observed_running => projection.apply(update),
259    //!                     _ => {}
260    //!                 }
261    //!             }
262    //!             Err(Error::internal_error()
263    //!                 .data("agent disconnected before the prompt ran to completion"))
264    //!         })
265    //!         .await
266    //! }
267    //! ```
268}
269
270pub mod connecting_as_client {
271    //! Pattern: Connecting as a client.
272    //!
273    //! To connect to an ACP agent and send requests, use [`connect_with`].
274    //! This runs your code while the connection handles incoming messages
275    //! in the background.
276    //!
277    //! # Basic Example
278    //!
279    //! ```
280    //! use agent_client_protocol::{Client, Agent, ConnectTo};
281    //! use agent_client_protocol::schema::{ProtocolVersion, v1::InitializeRequest};
282    //!
283    //! async fn connect_to_agent(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
284    //!     Client.builder()
285    //!         .name("my-client")
286    //!         .connect_with(transport, async |connection| {
287    //!             // Initialize the connection
288    //!             connection.send_request(InitializeRequest::new(ProtocolVersion::V1))
289    //!                 .block_task().await?;
290    //!
291    //!             // Create a session and send a prompt
292    //!             connection.build_session_cwd()?
293    //!                 .block_task()
294    //!                 .run_until(async |mut session| {
295    //!                     session.send_prompt("Hello, agent!")?;
296    //!                     let response = session.read_to_string().await?;
297    //!                     println!("Agent said: {}", response);
298    //!                     Ok(())
299    //!                 })
300    //!                 .await
301    //!         })
302    //!         .await
303    //! }
304    //! ```
305    //!
306    //! # Using the Session Builder
307    //!
308    //! The [`build_session`] method creates a [`SessionBuilder`] that handles
309    //! session creation and provides convenient methods for interacting with
310    //! the session:
311    //!
312    //! - [`send_prompt`] - Send a text prompt to the agent
313    //! - [`read_update`] - Read the next update (text chunk, tool call, etc.)
314    //! - [`read_to_string`] - Read all text until the turn ends
315    //!
316    //! With the core SDK's `unstable_mcp_over_acp` feature, the session builder
317    //! also supports adding MCP servers with [`with_mcp_server`].
318    //!
319    //! Existing stable-v1 sessions can be reopened with [`load_session`] to
320    //! replay history or [`resume_session`] to continue without replay. Their
321    //! blocking `start_session` returns a [`RestoredSession`] containing the
322    //! active session and complete operation response; `on_session_start`
323    //! delivers the same value to its callback. Use either restore operation
324    //! only after initialization advertises its matching capability.
325    //!
326    //! # Handling Permission Requests
327    //!
328    //! Agents may send [`RequestPermissionRequest`] to ask for user approval
329    //! before taking actions. Handle these with [`on_receive_request`]:
330    //!
331    //! ```ignore
332    //! Client.builder()
333    //!     .on_receive_request(async |req: RequestPermissionRequest, responder, _connection| {
334    //!         // Auto-approve by selecting the first option (YOLO mode)
335    //!         let option_id = req.options.first().map(|opt| opt.option_id.clone());
336    //!         responder.respond(RequestPermissionResponse::new(
337    //!             match option_id {
338    //!                 Some(id) => RequestPermissionOutcome::Selected(
339    //!                     SelectedPermissionOutcome::new(id),
340    //!                 ),
341    //!                 None => RequestPermissionOutcome::Cancelled,
342    //!             }
343    //!         ))
344    //!     }, agent_client_protocol::on_receive_request!())
345    //!     .connect_with(transport, async |connection| { /* ... */ })
346    //!     .await
347    //! ```
348    //!
349    //! # Note on `block_task`
350    //!
351    //! Using [`block_task`] is safe inside `connect_with` because its foreground
352    //! future runs alongside, rather than inside, the dispatch loop. The loop
353    //! continues processing messages (including the response you're waiting for)
354    //! while the foreground future waits.
355    //!
356    //! [`connect_with`]: agent_client_protocol::Builder::connect_with
357    //! [`block_task`]: agent_client_protocol::SentRequest::block_task
358    //! [`build_session`]: agent_client_protocol::ConnectionTo::build_session
359    //! [`load_session`]: agent_client_protocol::ConnectionTo::load_session
360    //! [`resume_session`]: agent_client_protocol::ConnectionTo::resume_session
361    //! [`SessionBuilder`]: agent_client_protocol::SessionBuilder
362    //! [`RestoredSession`]: agent_client_protocol::RestoredSession
363    //! [`send_prompt`]: agent_client_protocol::ActiveSession::send_prompt
364    //! [`read_update`]: agent_client_protocol::ActiveSession::read_update
365    //! [`read_to_string`]: agent_client_protocol::ActiveSession::read_to_string
366    //! [`with_mcp_server`]: agent_client_protocol::SessionBuilder::with_mcp_server
367    //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
368    //! [`on_receive_request`]: agent_client_protocol::Builder::on_receive_request
369}
370
371pub mod building_an_agent {
372    //! Pattern: Building an agent.
373    //!
374    //! An agent handles prompts and generates responses. At minimum, an agent must:
375    //!
376    //! 1. Handle [`InitializeRequest`] to establish the connection
377    //! 2. Handle [`NewSessionRequest`] to create sessions
378    //! 3. Handle [`PromptRequest`] to process prompts
379    //!
380    //! Use [`Agent.builder()`](agent_client_protocol::Agent) to build agent connections.
381    //!
382    //! # Minimal Example
383    //!
384    //! ```
385    //! use agent_client_protocol::{Agent, ConnectTo};
386    //! use agent_client_protocol::schema::v1::{
387    //!     InitializeRequest, InitializeResponse, AgentCapabilities,
388    //!     NewSessionRequest, NewSessionResponse, SessionId,
389    //!     PromptRequest, PromptResponse, StopReason,
390    //! };
391    //!
392    //! async fn run_agent(transport: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
393    //!     Agent.builder()
394    //!         .name("my-agent")
395    //!         // Handle initialization
396    //!         .on_receive_request(async |req: InitializeRequest, responder, _connection| {
397    //!             responder.respond(
398    //!                 InitializeResponse::new(req.protocol_version)
399    //!                     .agent_capabilities(AgentCapabilities::new())
400    //!             )
401    //!         }, agent_client_protocol::on_receive_request!())
402    //!         // Handle session creation
403    //!         .on_receive_request(async |req: NewSessionRequest, responder, _connection| {
404    //!             responder.respond(NewSessionResponse::new(SessionId::new("session-1")))
405    //!         }, agent_client_protocol::on_receive_request!())
406    //!         // Handle prompts
407    //!         .on_receive_request(async |req: PromptRequest, responder, connection| {
408    //!             // Send streaming updates via notifications
409    //!             // connection.send_notification(SessionNotification { ... })?;
410    //!
411    //!             // Return final response
412    //!             responder.respond(PromptResponse::new(StopReason::EndTurn))
413    //!         }, agent_client_protocol::on_receive_request!())
414    //!         // Unknown requests receive Method not found automatically;
415    //!         // unhandled notifications are ignored.
416    //!         .connect_to(transport)
417    //!         .await
418    //! }
419    //! ```
420    //!
421    //! # Streaming Responses
422    //!
423    //! To stream text or other updates to the client, send [`SessionNotification`]s
424    //! while processing a prompt:
425    //!
426    //! ```ignore
427    //! .on_receive_request(async |req: PromptRequest, responder, connection| {
428    //!     // Stream some text
429    //!     connection.send_notification(SessionNotification::new(
430    //!         req.session_id.clone(),
431    //!         SessionUpdate::AgentMessageChunk(ContentChunk::new("Hello, ".into())),
432    //!     ))?;
433    //!
434    //!     connection.send_notification(SessionNotification::new(
435    //!         req.session_id.clone(),
436    //!         SessionUpdate::AgentMessageChunk(ContentChunk::new("world!".into())),
437    //!     ))?;
438    //!
439    //!     responder.respond(PromptResponse::new(StopReason::EndTurn))
440    //! }, agent_client_protocol::on_receive_request!())
441    //! ```
442    //!
443    //! # Requesting Permissions
444    //!
445    //! Before taking actions that require user approval (like running commands
446    //! or writing files), send a [`RequestPermissionRequest`]:
447    //!
448    //! ```ignore
449    //! let response = connection.send_request(RequestPermissionRequest::new(
450    //!     session_id.clone(),
451    //!     ToolCallUpdate::new(
452    //!         "dangerous-command",
453    //!         ToolCallUpdateFields::new()
454    //!             .title("Run rm -rf /")
455    //!             .kind(ToolKind::Execute)
456    //!             .status(ToolCallStatus::Pending),
457    //!     ),
458    //!     vec![
459    //!         PermissionOption::new("allow", "Allow", PermissionOptionKind::AllowOnce),
460    //!         PermissionOption::new("deny", "Deny", PermissionOptionKind::RejectOnce),
461    //!     ],
462    //! )).block_task().await?;
463    //!
464    //! match response.outcome {
465    //!     RequestPermissionOutcome::Selected(selected) if selected.option_id == "allow" => {
466    //!         // User approved, proceed with action
467    //!     }
468    //!     _ => {
469    //!         // User denied or cancelled
470    //!     }
471    //! }
472    //! ```
473    //!
474    //! # As a Reusable Component
475    //!
476    //! For agents that will be composed with proxies, implement [`ConnectTo`].
477    //! See [`reusable_components`] for the pattern.
478    //!
479    //! [`InitializeRequest`]: agent_client_protocol::schema::v1::InitializeRequest
480    //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
481    //! [`PromptRequest`]: agent_client_protocol::schema::v1::PromptRequest
482    //! [`SessionNotification`]: agent_client_protocol::schema::v1::SessionNotification
483    //! [`RequestPermissionRequest`]: agent_client_protocol::schema::v1::RequestPermissionRequest
484    //! [`Agent`]: agent_client_protocol::Agent
485    //! [`ConnectTo`]: agent_client_protocol::ConnectTo
486    //! [`reusable_components`]: super::reusable_components
487}
488
489pub mod reusable_components {
490    //! Pattern: Defining reusable components.
491    //!
492    //! When building agents or proxies that will be composed together (for example,
493    //! with [`agent-client-protocol-conductor`]), define a struct that implements [`ConnectTo`].
494    //! This allows your component to be connected to other components in a type-safe way.
495    //!
496    //! # Example
497    //!
498    //! ```
499    //! use agent_client_protocol::{ConnectTo, Agent, Client};
500    //! use agent_client_protocol::schema::v1::{
501    //!     InitializeRequest, InitializeResponse, AgentCapabilities,
502    //! };
503    //!
504    //! struct MyAgent {
505    //!     name: String,
506    //! }
507    //!
508    //! impl ConnectTo<Client> for MyAgent {
509    //!     async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<(), agent_client_protocol::Error> {
510    //!         Agent.builder()
511    //!             .name(&self.name)
512    //!             .on_receive_request(async move |req: InitializeRequest, responder, _connection| {
513    //!                 responder.respond(
514    //!                     InitializeResponse::new(req.protocol_version)
515    //!                         .agent_capabilities(AgentCapabilities::new())
516    //!                 )
517    //!             }, agent_client_protocol::on_receive_request!())
518    //!             .connect_to(client)
519    //!             .await
520    //!     }
521    //! }
522    //!
523    //! let agent = MyAgent { name: "my-agent".into() };
524    //! ```
525    //!
526    //! # Important: Don't block the event loop
527    //!
528    //! Message handlers run on the event loop. Blocking in a handler prevents the
529    //! connection from processing new messages:
530    //!
531    //! - Use [`ConnectionTo::spawn`] to offload work to a background task
532    //! - Use [`on_receiving_result`] for bounded, ordered response handling; if it
533    //!   must await later traffic, spawn that work from the callback and return
534    //!
535    //! [`ConnectTo`]: agent_client_protocol::ConnectTo
536    //! [`ConnectionTo::spawn`]: agent_client_protocol::ConnectionTo::spawn
537    //! [`on_receiving_result`]: agent_client_protocol::SentRequest::on_receiving_result
538    //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
539}
540
541pub mod custom_message_handlers {
542    //! Pattern: Custom message handlers.
543    //!
544    //! For reusable message handling logic, implement [`HandleDispatchFrom`] and use
545    //! [`MatchDispatch`] or [`MatchDispatchFrom`] for type-safe dispatching.
546    //!
547    //! This is useful when you need to:
548    //! - Share message handling logic across multiple components
549    //! - Build complex routing logic that doesn't fit the builder pattern
550    //! - Integrate with existing handler infrastructure
551    //!
552    //! # Example
553    //!
554    //! ```
555    //! use agent_client_protocol::{HandleDispatchFrom, Dispatch, Handled, ConnectionTo, UntypedRole};
556    //! use agent_client_protocol::schema::v1::{AgentCapabilities, InitializeRequest, InitializeResponse};
557    //! use agent_client_protocol::util::MatchDispatch;
558    //!
559    //! struct MyHandler;
560    //!
561    //! impl HandleDispatchFrom<UntypedRole> for MyHandler {
562    //!     async fn handle_dispatch_from(
563    //!         &mut self,
564    //!         message: Dispatch,
565    //!         _connection: ConnectionTo<UntypedRole>,
566    //!     ) -> Result<Handled<Dispatch>, agent_client_protocol::Error> {
567    //!         MatchDispatch::new(message)
568    //!             .if_request(async |req: InitializeRequest, responder| {
569    //!                 responder.respond(
570    //!                     InitializeResponse::new(req.protocol_version)
571    //!                         .agent_capabilities(AgentCapabilities::new())
572    //!                 )
573    //!             })
574    //!             .await
575    //!             .done()
576    //!     }
577    //!
578    //!     fn describe_chain(&self) -> impl std::fmt::Debug {
579    //!         "MyHandler"
580    //!     }
581    //! }
582    //! ```
583    //!
584    //! # When to use `MatchDispatch` vs `MatchDispatchFrom`
585    //!
586    //! - [`MatchDispatch`] - Use when you don't need peer-aware handling
587    //! - [`MatchDispatchFrom`] - Use in proxies where messages come from different
588    //!   peers (`Client` vs `Agent`) and may need different handling
589    //!
590    //! [`HandleDispatchFrom`]: agent_client_protocol::HandleDispatchFrom
591    //! [`MatchDispatch`]: agent_client_protocol::util::MatchDispatch
592    //! [`MatchDispatchFrom`]: agent_client_protocol::util::MatchDispatchFrom
593}
594
595pub mod global_mcp_server {
596    //! Pattern: Global MCP server in handler chain.
597    //!
598    //! Use this pattern when you want a single MCP server that handles tool calls
599    //! for all sessions. The server is added to the connection's handler chain and
600    //! automatically injects itself into every supported session setup request.
601    //! This pattern requires the core SDK's `unstable_mcp_over_acp` feature (or
602    //! the rmcp crate's matching passthrough feature). Draft v2 additionally
603    //! requires `unstable_protocol_v2`.
604    //!
605    //! # When to use
606    //!
607    //! - The MCP server provides stateless tools (no per-session state needed)
608    //! - You want the simplest setup with minimal boilerplate
609    //! - Tools don't need access to session-specific context
610    //!
611    //! # Using the builder API
612    //!
613    //! The simplest way to create an MCP server is with [`McpServer::builder`]:
614    //!
615    //! ```
616    //! use agent_client_protocol::mcp_server::McpServer;
617    //! use agent_client_protocol_rmcp::McpServerExt;
618    //! use agent_client_protocol::{ConnectTo, RunWithConnectionTo, Proxy, Conductor};
619    //! use schemars::JsonSchema;
620    //! use serde::{Deserialize, Serialize};
621    //!
622    //! #[derive(Debug, Deserialize, JsonSchema)]
623    //! struct EchoParams { message: String }
624    //!
625    //! #[derive(Debug, Serialize, JsonSchema)]
626    //! struct EchoOutput { echoed: String }
627    //!
628    //! // Build the MCP server with tools
629    //! let mcp_server = McpServer::builder("my-tools")
630    //!     .tool_fn("echo", "Echoes the input",
631    //!         async |params: EchoParams, _cx| {
632    //!             Ok(EchoOutput { echoed: params.message })
633    //!         },
634    //!         agent_client_protocol::tool_fn!())
635    //!     .build();
636    //!
637    //! // The proxy component is generic over the MCP server's runner type
638    //! struct MyProxy<R> {
639    //!     mcp_server: McpServer<Conductor, R>,
640    //! }
641    //!
642    //! impl<R: RunWithConnectionTo<Conductor> + Send + 'static> ConnectTo<Conductor> for MyProxy<R> {
643    //!     async fn connect_to(self, conductor: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
644    //!         Proxy.builder()
645    //!             .with_mcp_server(self.mcp_server)
646    //!             .connect_to(conductor)
647    //!             .await
648    //!     }
649    //! }
650    //!
651    //! let proxy = MyProxy { mcp_server };
652    //! ```
653    //!
654    //! The example uses stable protocol v1. A draft v2 proxy selects its API
655    //! before attaching the server:
656    //!
657    //! ```rust,ignore
658    //! Proxy.v2()
659    //!     .with_mcp_server(mcp_server)
660    //!     .connect_to(conductor)
661    //!     .await?;
662    //! ```
663    //!
664    //! # Using rmcp
665    //!
666    //! If you have an existing [rmcp](https://docs.rs/rmcp) server implementation,
667    //! use [`McpServer::from_rmcp`] from the `agent-client-protocol-rmcp` crate:
668    //!
669    //! ```
670    //! use rmcp::{ServerHandler, tool, tool_router, tool_handler};
671    //! use rmcp::handler::server::router::tool::ToolRouter;
672    //! use rmcp::handler::server::wrapper::Parameters;
673    //! use rmcp::model::*;
674    //! use agent_client_protocol::mcp_server::McpServer;
675    //! use agent_client_protocol::Conductor;
676    //! use agent_client_protocol_rmcp::McpServerExt;
677    //! use serde::{Deserialize, Serialize};
678    //!
679    //! #[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
680    //! struct EchoParams {
681    //!     message: String,
682    //! }
683    //!
684    //! #[derive(Clone)]
685    //! struct MyMcpServer {
686    //!     tool_router: ToolRouter<Self>,
687    //! }
688    //!
689    //! impl MyMcpServer {
690    //!     fn new() -> Self {
691    //!         Self { tool_router: Self::tool_router() }
692    //!     }
693    //! }
694    //!
695    //! #[tool_router]
696    //! impl MyMcpServer {
697    //!     #[tool(description = "Echoes back the input message")]
698    //!     async fn echo(&self, Parameters(params): Parameters<EchoParams>) -> Result<CallToolResult, rmcp::ErrorData> {
699    //!         Ok(CallToolResult::success(vec![ContentBlock::text(format!("Echo: {}", params.message))]))
700    //!     }
701    //! }
702    //!
703    //! #[tool_handler]
704    //! impl ServerHandler for MyMcpServer {
705    //!     fn get_info(&self) -> ServerInfo {
706    //!         ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
707    //!             .with_protocol_version(ProtocolVersion::V_2024_11_05)
708    //!             .with_server_info(Implementation::from_build_env())
709    //!     }
710    //! }
711    //!
712    //! // Create an MCP server from the rmcp service
713    //! let mcp_server = McpServer::<Conductor, _>::from_rmcp("my-server", MyMcpServer::new);
714    //! ```
715    //!
716    //! The `from_rmcp` function takes a factory closure that creates a new server
717    //! instance. This allows each MCP connection to get a fresh server instance.
718    //!
719    //! # How it works
720    //!
721    //! When you call [`with_mcp_server`], the MCP server is added as a message
722    //! handler. It:
723    //!
724    //! 1. Intercepts session setup requests and adds a schema-native
725    //!    `McpServer::Acp` declaration with one connection-scoped server ID.
726    //!    V1 injects it into `session/new`, `session/load`, `session/resume`,
727    //!    and feature-gated `session/fork`; v2 injects it into
728    //!    `session/new`, `session/resume`, and feature-gated `session/fork`
729    //!    while preserving unrelated request fields
730    //! 2. Passes the modified request through to the next handler
731    //! 3. Handles `mcp/connect`, `mcp/message`, and `mcp/disconnect` for that server ID
732    //!
733    //! [`McpServer::builder`]: agent_client_protocol_rmcp::McpServerExt::builder
734    //! [`McpServer::from_rmcp`]: agent_client_protocol_rmcp::McpServerExt::from_rmcp
735    //! [`with_mcp_server`]: agent_client_protocol::Builder::with_mcp_server
736}
737
738pub mod per_session_mcp_server {
739    //! Pattern: Per-session MCP server with workspace context.
740    //!
741    //! Use this pattern when each session needs its own MCP server instance
742    //! with access to session-specific context like the working directory.
743    //! It requires the core SDK's `unstable_mcp_over_acp` feature (or the rmcp
744    //! crate's matching passthrough feature). Draft v2 additionally requires
745    //! `unstable_protocol_v2`.
746    //!
747    //! # When to use
748    //!
749    //! - Tools need access to the session's working directory
750    //! - You want eventual active-session tracking that does not need to precede later traffic
751    //! - Tools need to customize behavior based on session parameters
752    //!
753    //! # Stable v1 pattern with `on_proxy_session_start`
754    //!
755    //! The most common pattern intercepts [`NewSessionRequest`], extracts context,
756    //! creates a per-session MCP server, and uses [`on_proxy_session_start`] to
757    //! run code after the session is established:
758    //!
759    //! ```
760    //! use agent_client_protocol::mcp_server::McpServer;
761    //! use agent_client_protocol_rmcp::McpServerExt;
762    //! use agent_client_protocol::schema::v1::NewSessionRequest;
763    //! use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
764    //!
765    //! async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
766    //!     Proxy.builder()
767    //!         .on_receive_request_from(Client, async move |request: NewSessionRequest, responder, connection| {
768    //!             // Extract session context from the request
769    //!             let workspace_path = request.cwd.clone();
770    //!
771    //!             // Create tools that capture the workspace path
772    //!             let mcp_server = McpServer::builder("workspace-tools")
773    //!                 .tool_fn("get_workspace", "Returns the session's workspace directory", {
774    //!                     async move |_params: (), _cx| {
775    //!                         Ok(workspace_path.display().to_string())
776    //!                     }
777    //!                 }, agent_client_protocol::tool_fn!())
778    //!                 .build();
779    //!
780    //!             // Build the session and run code after it starts
781    //!             connection.build_session_from(request)
782    //!                 .with_mcp_server(mcp_server)?
783    //!                 .on_proxy_session_start(responder, async move |session_id| {
784    //!                     // Session proxying is installed before this callback is spawned.
785    //!                     //
786    //!                     // Use this for follow-up work that may wait for later connection
787    //!                     // traffic. Register ID-independent state in the request handler.
788    //!                     // For ID-keyed state, preinstall a gate that later handlers await,
789    //!                     // then populate it here.
790    //!                     tracing::info!(%session_id, "Session started");
791    //!                     Ok(())
792    //!                 })
793    //!         }, agent_client_protocol::on_receive_request!())
794    //!         .connect_to(transport)
795    //!         .await
796    //! }
797    //! ```
798    //!
799    //! # How `on_proxy_session_start` works
800    //!
801    //! [`on_proxy_session_start`] is the non-blocking way to set up a proxy session:
802    //!
803    //! 1. Sends `NewSessionRequest` to the agent
804    //! 2. When the response arrives, responds to the client automatically
805    //! 3. Sets up message proxying for the session
806    //! 4. Runs your callback with the `SessionId`
807    //!
808    //! The callback runs after the session is established but doesn't block
809    //! the message handler. It is suitable for follow-up work and eventual
810    //! session tracking. Because it runs concurrently with later traffic, it
811    //! does not guarantee that bookkeeping keyed by `SessionId` completes
812    //! first. Register ID-independent state before calling the helper. For
813    //! ID-keyed state, preinstall a gate or placeholder that later handlers
814    //! await, then populate it from the callback.
815    //!
816    //! # Draft v2 pattern
817    //!
818    //! `Proxy.v2()` exposes the same non-blocking setup shape with v2 schema
819    //! types. `V2SessionBuilder` handles `session/new`, while
820    //! `V2ResumeSessionBuilder` handles `session/resume`. With
821    //! `unstable_session_fork`, `V2ForkSessionBuilder` handles `session/fork`.
822    //! Their `on_proxy_session_start` callbacks receive an `OpenedV2Session`,
823    //! not just a session ID, so they retain both the command-only handle and
824    //! the complete operation-specific response:
825    //!
826    //! ```rust,ignore
827    //! use agent_client_protocol::schema::v2;
828    //!
829    //! Proxy.v2()
830    //!     .on_receive_request_from(
831    //!         Client,
832    //!         async |request: v2::NewSessionRequest, responder, connection| {
833    //!             let workspace_path = request.cwd.clone();
834    //!             let mcp_server = build_workspace_server(workspace_path);
835    //!
836    //!             connection
837    //!                 .build_session_from(request)
838    //!                 .with_mcp_server(mcp_server)?
839    //!                 .on_proxy_session_start(responder, async move |opened| {
840    //!                     let (session, setup_response) = opened.into_parts();
841    //!                     tracing::info!(
842    //!                         session_id = %session.session_id(),
843    //!                         ?setup_response,
844    //!                         "Session started"
845    //!                     );
846    //!                     Ok(())
847    //!                 })
848    //!         },
849    //!         agent_client_protocol::on_receive_request!(),
850    //!     );
851    //! ```
852    //!
853    //! Resume uses the same terminal helper after constructing the per-session
854    //! server from the resume request:
855    //!
856    //! ```rust,ignore
857    //! Proxy.v2()
858    //!     .on_receive_request_from(
859    //!         Client,
860    //!         async |request: v2::ResumeSessionRequest, responder, connection| {
861    //!             let mcp_server = build_workspace_server(request.cwd.clone());
862    //!             connection
863    //!                 .resume_session_from(request)
864    //!                 .with_mcp_server(mcp_server)?
865    //!                 .on_proxy_session_start(responder, async move |opened| {
866    //!                     tracing::info!(session_id = %opened.session().session_id(), "Session resumed");
867    //!                     Ok(())
868    //!                 })
869    //!         },
870    //!         agent_client_protocol::on_receive_request!(),
871    //!     );
872    //! ```
873    //!
874    //! Fork uses the same terminal helper after
875    //! `connection.fork_session_from(request)`. Its returned handle and route
876    //! use the newly allocated ID from the complete `ForkSessionResponse`, not
877    //! the source session ID.
878    //!
879    //! All helpers forward upstream cancellation and the complete setup
880    //! response. New-session and fork routing are installed before later
881    //! inbound traffic; resume routing and MCP readiness are established
882    //! before the request is published so replay can precede its response. The
883    //! callback runs outside the ordering barrier. V2 updates and interactive
884    //! requests remain independent connection traffic.
885    //!
886    //! # Stable v1 alternative: spawning `start_session_proxy`
887    //!
888    //! If you need the linear [`start_session_proxy`] API, move it into a
889    //! spawned task. Awaiting it directly in the request handler would block
890    //! the dispatch loop that must receive the agent's response:
891    //!
892    //! ```
893    //! # use agent_client_protocol::mcp_server::McpServer;
894    //! # use agent_client_protocol_rmcp::McpServerExt;
895    //! # use agent_client_protocol::schema::v1::NewSessionRequest;
896    //! # use agent_client_protocol::{Client, Proxy, Conductor, ConnectTo};
897    //! # async fn run_proxy(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
898    //!     Proxy.builder()
899    //!         .on_receive_request_from(Client, async |request: NewSessionRequest, responder, connection| {
900    //!             let cwd = request.cwd.clone();
901    //!             let mcp_server = McpServer::builder("tools")
902    //!                 .tool_fn("get_cwd", "Returns working directory", {
903    //!                     async move |_params: (), _cx| Ok(cwd.display().to_string())
904    //!                 }, agent_client_protocol::tool_fn!())
905    //!                 .build();
906    //!
907    //!             let task_connection = connection.clone();
908    //!             connection.spawn(async move {
909    //!                 let session_id = task_connection.build_session_from(request)
910    //!                     .with_mcp_server(mcp_server)?
911    //!                     .block_task()
912    //!                     .start_session_proxy(responder)
913    //!                     .await?;
914    //!
915    //!                 tracing::info!(%session_id, "Session started");
916    //!                 Ok(())
917    //!             })?;
918    //!             Ok(())
919    //!         }, agent_client_protocol::on_receive_request!())
920    //!         .connect_to(transport)
921    //!         .await
922    //! # }
923    //! ```
924    //!
925    //! For patterns where you need to interact with the session before proxying,
926    //! use [`start_session`] + [`proxy_remaining_messages`] instead.
927    //!
928    //! [`start_session`]: agent_client_protocol::SessionBuilder::start_session
929    //! [`proxy_remaining_messages`]: agent_client_protocol::ActiveSession::proxy_remaining_messages
930    //!
931    //! [`NewSessionRequest`]: agent_client_protocol::schema::v1::NewSessionRequest
932    //! [`on_proxy_session_start`]: agent_client_protocol::SessionBuilder::on_proxy_session_start
933    //! [`block_task`]: agent_client_protocol::SessionBuilder::block_task
934    //! [`start_session_proxy`]: agent_client_protocol::SessionBuilder::start_session_proxy
935}
936
937pub mod filtering_tools {
938    //! Pattern: Filtering which tools are available.
939    //!
940    //! Use [`disable_tool`] and [`enable_tool`] to control which tools are
941    //! visible to clients. This is useful when:
942    //!
943    //! - Some tools should only be available in certain configurations
944    //! - You want to conditionally expose tools based on runtime settings
945    //! - You need to restrict access to sensitive tools
946    //!
947    //! # Disabling specific tools (deny-list)
948    //!
949    //! By default, all registered tools are enabled. Use [`disable_tool`] to
950    //! hide specific tools:
951    //!
952    //! ```
953    //! use agent_client_protocol::mcp_server::McpServer;
954    //! use agent_client_protocol_rmcp::McpServerExt;
955    //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
956    //! use schemars::JsonSchema;
957    //! use serde::Deserialize;
958    //!
959    //! #[derive(Debug, Deserialize, JsonSchema)]
960    //! struct Params {}
961    //!
962    //! fn build_server(enable_admin: bool) -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
963    //!     let mut builder = McpServer::builder("my-server")
964    //!         .tool_fn("echo", "Echo a message",
965    //!             async |_p: Params, _cx| Ok("echoed"),
966    //!             agent_client_protocol::tool_fn!())
967    //!         .tool_fn("admin", "Admin-only tool",
968    //!             async |_p: Params, _cx| Ok("admin action"),
969    //!             agent_client_protocol::tool_fn!());
970    //!
971    //!     // Conditionally disable the admin tool
972    //!     if !enable_admin {
973    //!         builder = builder.disable_tool("admin")?;
974    //!     }
975    //!
976    //!     Ok(builder.build())
977    //! }
978    //! ```
979    //!
980    //! Disabled tools:
981    //! - Don't appear in `list_tools` responses
982    //! - Return "tool not found" errors if called directly
983    //!
984    //! # Enabling only specific tools (allow-list)
985    //!
986    //! Use [`disable_all_tools`] followed by [`enable_tool`] to create an
987    //! allow-list where only explicitly enabled tools are available:
988    //!
989    //! ```
990    //! use agent_client_protocol::mcp_server::McpServer;
991    //! use agent_client_protocol_rmcp::McpServerExt;
992    //! use agent_client_protocol::{Conductor, RunWithConnectionTo};
993    //! use schemars::JsonSchema;
994    //! use serde::Deserialize;
995    //!
996    //! #[derive(Debug, Deserialize, JsonSchema)]
997    //! struct Params {}
998    //!
999    //! fn build_restricted_server() -> Result<McpServer<Conductor, impl RunWithConnectionTo<Conductor>>, agent_client_protocol::Error> {
1000    //!     McpServer::builder("restricted-server")
1001    //!         .tool_fn("safe", "Safe operation",
1002    //!             async |_p: Params, _cx| Ok("safe"),
1003    //!             agent_client_protocol::tool_fn!())
1004    //!         .tool_fn("dangerous", "Dangerous operation",
1005    //!             async |_p: Params, _cx| Ok("danger!"),
1006    //!             agent_client_protocol::tool_fn!())
1007    //!         .tool_fn("experimental", "Experimental feature",
1008    //!             async |_p: Params, _cx| Ok("experimental"),
1009    //!             agent_client_protocol::tool_fn!())
1010    //!         // Start with all tools disabled
1011    //!         .disable_all_tools()
1012    //!         // Only enable the safe tool
1013    //!         .enable_tool("safe")
1014    //!         .map(|b| b.build())
1015    //! }
1016    //! ```
1017    //!
1018    //! # Error handling
1019    //!
1020    //! Both [`enable_tool`] and [`disable_tool`] return `Result` and will error
1021    //! if the tool name doesn't match any registered tool. This helps catch typos:
1022    //!
1023    //! ```
1024    //! use agent_client_protocol::mcp_server::McpServer;
1025    //! use agent_client_protocol_rmcp::McpServerExt;
1026    //! use agent_client_protocol::Conductor;
1027    //!
1028    //! // This will error because "ech" is not a registered tool
1029    //! let result = McpServer::<Conductor, _>::builder("server")
1030    //!     .disable_tool("ech");  // Typo! Should be "echo"
1031    //!
1032    //! assert!(result.is_err());
1033    //! ```
1034    //!
1035    //! Calling enable/disable on an already enabled/disabled tool is not an error -
1036    //! the operations are idempotent.
1037    //!
1038    //! [`disable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::disable_tool
1039    //! [`enable_tool`]: agent_client_protocol_rmcp::McpServerBuilder::enable_tool
1040    //! [`disable_all_tools`]: agent_client_protocol_rmcp::McpServerBuilder::disable_all_tools
1041}
1042
1043pub mod running_proxies_with_conductor {
1044    //! Pattern: Running proxies with the conductor.
1045    //!
1046    //! Proxies don't run standalone. To add an MCP server (or other proxy behavior)
1047    //! to an existing agent, you need the **conductor** to orchestrate the connection.
1048    //!
1049    //! The conductor:
1050    //! 1. Accepts connections from clients
1051    //! 2. Chains your proxies together
1052    //! 3. Connects to the final agent
1053    //! 4. Routes messages through the entire chain
1054    //!
1055    //! # Using the `agent-client-protocol-conductor` binary
1056    //!
1057    //! The simplest way to run a proxy is with the [`agent-client-protocol-conductor`] binary.
1058    //! Pass the proxy commands followed by the final agent command:
1059    //!
1060    //! ```bash
1061    //! agent-client-protocol-conductor agent \
1062    //!   "cargo run --bin my-proxy" \
1063    //!   "claude-code --agent"
1064    //! ```
1065    //!
1066    //! # Using the conductor as a library
1067    //!
1068    //! For more control, use [`agent-client-protocol-conductor`] as a library with the `ConductorImpl` type:
1069    //!
1070    //! ```ignore
1071    //! use agent_client_protocol::{AcpAgent, ConnectTo};
1072    //! use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent};
1073    //!
1074    //! // Define your proxy as a ConnectTo<Conductor>
1075    //! let my_proxy = MyProxy::new();
1076    //!
1077    //! // Configure the agent process
1078    //! let agent = AcpAgent::from_args(["claude-code", "--agent"])?;
1079    //!
1080    //! // Create the conductor with your proxy chain
1081    //! let conductor = ConductorImpl::new_agent(
1082    //!     "my-conductor",
1083    //!     ProxiesAndAgent::new(agent).proxy(my_proxy),
1084    //! );
1085    //!
1086    //! // Run the conductor (it will accept client connections on stdin/stdout)
1087    //! conductor.connect_to(client_transport).await?;
1088    //! ```
1089    //!
1090    //! # Why can't I just connect my proxy directly to an agent?
1091    //!
1092    //! ACP uses a message envelope format for proxy chains. When a proxy sends a
1093    //! message toward the agent, it gets wrapped in a [`SuccessorMessage`] envelope.
1094    //! The conductor handles this wrapping/unwrapping automatically.
1095    //!
1096    //! If you connected directly to an agent, your proxy would send `SuccessorMessage`
1097    //! envelopes that the agent doesn't understand.
1098    //!
1099    //! # Example: Complete proxy with conductor
1100    //!
1101    //! See the [`agent-client-protocol-conductor` tests] for complete working examples of proxies
1102    //! running with the conductor.
1103    //!
1104    //! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor
1105    //! [`SuccessorMessage`]: agent_client_protocol::schema::SuccessorMessage
1106    //! [`agent-client-protocol-conductor` tests]: https://github.com/agentclientprotocol/rust-sdk/tree/main/src/agent-client-protocol-conductor/tests
1107}