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