Skip to main content

agent_client_protocol/concepts/
proxies.rs

1//! Building proxies that intercept and modify messages.
2//!
3//! A **proxy** sits between a client and an agent, intercepting messages
4//! in both directions. This is how you add capabilities like MCP tools,
5//! logging, or message transformation.
6//!
7//! # The Proxy Role Type
8//!
9//! Proxies use the [`Proxy`] role type, which has two peers:
10//!
11//! - [`Client`] - messages from/to the client direction
12//! - [`Agent`] - messages from/to the agent direction
13//!
14//! Unlike simpler links, there's no default peer - you must always specify
15//! which direction you're communicating with.
16//!
17//! # Choosing a Protocol Version
18//!
19//! `Proxy::builder` creates a stable protocol v1 proxy. With the
20//! `unstable_protocol_v2` feature, `Proxy.v2()` creates a v2-only proxy whose
21//! fluent callbacks receive `V2ConnectionTo<Conductor>`. The builder
22//! validates `_proxy/initialize` and later traffic against the selected
23//! version.
24//!
25//! Use `Proxy.protocol_router()` to package separate v1 and v2 implementations
26//! behind one `ConnectTo<Conductor>` component. It dispatches the
27//! conductor-selected `_proxy/initialize` version exactly and performs no
28//! cross-version conversion.
29//!
30//! Low-level infrastructure implementing custom raw version routing can use
31//! `Proxy.builder().without_acp_version_guard()`. Disabling the guard is an
32//! explicit version-neutral escape hatch, not the ordinary way to author a v2
33//! proxy.
34//!
35//! # Default Forwarding
36//!
37//! By default, [`Proxy`] forwards all messages it doesn't handle.
38//! This means a minimal stable v1 proxy that does nothing is just:
39//!
40//! ```
41//! # use agent_client_protocol::{Proxy, Conductor, ConnectTo};
42//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
43//! Proxy.builder()
44//!     .connect_to(transport)
45//!     .await?;
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! All messages pass through unchanged.
51//!
52//! # Intercepting Messages
53//!
54//! To intercept specific messages, use `on_receive_*_from` with explicit peers:
55//!
56//! ```
57//! # use agent_client_protocol::{Proxy, Client, Agent, Conductor, ConnectTo};
58//! # use agent_client_protocol_test::ProcessRequest;
59//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
60//! Proxy.builder()
61//!     // Intercept requests from the client
62//!     .on_receive_request_from(Client, async |req: ProcessRequest, responder, cx| {
63//!         // Modify the request
64//!         let modified = ProcessRequest {
65//!             data: format!("prefix: {}", req.data),
66//!         };
67//!
68//!         // Forward to agent and relay the response back
69//!         cx.send_request_to(Agent, modified)
70//!             .forward_response_to(responder)
71//!     }, agent_client_protocol::on_receive_request!())
72//!     .connect_to(transport)
73//!     .await?;
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! Messages you don't handle are forwarded automatically.
79//!
80//! # Adding MCP Servers
81//!
82//! A common use case is adding tools via MCP. You can add them globally
83//! (available in all sessions) or per-session.
84//!
85//! These ACP attachment APIs require the `unstable_mcp_over_acp` feature.
86//! Draft v2 attachment additionally requires `unstable_protocol_v2`.
87//!
88//! ## Global MCP Server
89//!
90//! ```ignore
91//! # use agent_client_protocol::{Proxy, Conductor, ConnectTo};
92//! # use agent_client_protocol::mcp_server::McpServer;
93//! # use agent_client_protocol_rmcp::McpServerExt;
94//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
95//! # let my_mcp_server = McpServer::<Conductor, _>::builder("tools").build();
96//! Proxy.builder()
97//!     .with_mcp_server(my_mcp_server)
98//!     .connect_to(transport)
99//!     .await?;
100//! # Ok(())
101//! # }
102//! ```
103//!
104//! For draft v2, select the v2 proxy builder before attaching the global
105//! server:
106//!
107//! ```rust,ignore
108//! Proxy.v2()
109//!     .with_mcp_server(my_mcp_server)
110//!     .connect_to(transport)
111//!     .await?;
112//! ```
113//!
114//! The v1 builder injects the declaration into new, load, resume, and
115//! feature-gated fork requests. The v2 builder injects it into new, resume,
116//! and feature-gated fork requests while preserving unrelated setup fields.
117//! Both reuse one connection-scoped server ID.
118//!
119//! ## Per-Session MCP Server
120//!
121//! ```ignore
122//! # use agent_client_protocol::{Proxy, Client, Conductor, ConnectTo};
123//! # use agent_client_protocol::schema::v1::NewSessionRequest;
124//! # use agent_client_protocol::mcp_server::McpServer;
125//! # use agent_client_protocol_rmcp::McpServerExt;
126//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
127//! Proxy.builder()
128//!     .on_receive_request_from(Client, async |req: NewSessionRequest, responder, cx| {
129//!         let my_mcp_server = McpServer::<Conductor, _>::builder("tools").build();
130//!         cx.build_session_from(req)
131//!             .with_mcp_server(my_mcp_server)?
132//!             .on_proxy_session_start(responder, async |session_id| {
133//!                 // Session started with MCP server attached
134//!                 Ok(())
135//!             })
136//!     }, agent_client_protocol::on_receive_request!())
137//!     .connect_to(transport)
138//!     .await?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! The corresponding v2 proxy uses `Proxy.v2()`, a
144//! `schema::v2::NewSessionRequest`, and the same fluent session-builder shape.
145//! Its `V2SessionBuilder::on_proxy_session_start` callback receives an
146//! `OpenedV2Session` containing both the command-only v2 session handle and the
147//! complete `NewSessionResponse`:
148//!
149//! ```rust,ignore
150//! Proxy.v2()
151//!     .on_receive_request_from(
152//!         Client,
153//!         async |request: schema::v2::NewSessionRequest, responder, cx| {
154//!             cx.build_session_from(request)
155//!                 .with_mcp_server(my_mcp_server)?
156//!                 .on_proxy_session_start(responder, async |opened| {
157//!                     let (session, response) = opened.into_parts();
158//!                     track_session(session.session_id(), response);
159//!                     Ok(())
160//!                 })
161//!         },
162//!         agent_client_protocol::on_receive_request!(),
163//!     );
164//! ```
165//!
166//! For `schema::v2::ResumeSessionRequest`, use
167//! `cx.resume_session_from(request)` and the resulting
168//! `V2ResumeSessionBuilder` in the same shape. With
169//! `unstable_session_fork`, use `cx.fork_session_from(request)` and
170//! `V2ForkSessionBuilder` for `ForkSessionRequest`; the returned session and
171//! installed route use the new ID from `ForkSessionResponse`, not the source
172//! session ID. Resume routing and any per-session MCP attachment are ready
173//! before the downstream request is published, allowing replay to precede the
174//! complete response. All setup helpers forward that operation's response
175//! before spawning the callback. Later updates and interactive requests remain
176//! independent traffic handled by typed connection callbacks.
177//!
178//! # The Conductor
179//!
180//! Proxies don't run standalone - they're orchestrated by a **conductor**.
181//! The conductor:
182//!
183//! - Spawns proxy processes
184//! - Chains them together
185//! - Connects the final proxy to the agent
186//!
187//! The [`agent-client-protocol-conductor`] crate provides a conductor binary. You configure
188//! it with a list of proxies to run.
189//!
190//! # Proxy Chains
191//!
192//! Multiple proxies can be chained:
193//!
194//! ```text
195//! Client <-> Proxy A <-> Proxy B <-> Agent
196//! ```
197//!
198//! Each proxy sees messages from its perspective:
199//! - `Client` is "toward the client" (Proxy A, or conductor if first)
200//! - `Agent` is "toward the agent" (Proxy B, or agent if last)
201//!
202//! Messages flow through each proxy in order. Each can inspect, modify,
203//! or handle messages before they continue.
204//!
205//! # Summary
206//!
207//! | Task | Approach |
208//! |------|----------|
209//! | Forward everything | Just `connect_to(transport)` |
210//! | Author a v1 or v2 proxy | `Proxy.builder()` or `Proxy.v2()` |
211//! | Package v1 and v2 implementations | `Proxy.protocol_router().with_v1(...).with_v2(...)` |
212//! | Implement custom raw routing | `without_acp_version_guard` on the raw proxy builder |
213//! | Intercept specific messages | `on_receive_*_from` with explicit peers |
214//! | Add global tools | `with_mcp_server` on builder |
215//! | Add per-session tools | `with_mcp_server` on session builder |
216//!
217//! [`Proxy`]: crate::Proxy
218//! [`Client`]: crate::Client
219//! [`Agent`]: crate::Agent
220//! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor