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 ¬ification.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}