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//! # Default Forwarding
18//!
19//! By default, [`Proxy`] forwards all messages it doesn't handle.
20//! This means a minimal proxy that does nothing is just:
21//!
22//! ```
23//! # use agent_client_protocol::{Proxy, Conductor, ConnectTo};
24//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
25//! Proxy.builder()
26//!     .connect_to(transport)
27//!     .await?;
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! All messages pass through unchanged.
33//!
34//! # Intercepting Messages
35//!
36//! To intercept specific messages, use `on_receive_*_from` with explicit peers:
37//!
38//! ```
39//! # use agent_client_protocol::{Proxy, Client, Agent, Conductor, ConnectTo};
40//! # use agent_client_protocol_test::ProcessRequest;
41//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
42//! Proxy.builder()
43//!     // Intercept requests from the client
44//!     .on_receive_request_from(Client, async |req: ProcessRequest, responder, cx| {
45//!         // Modify the request
46//!         let modified = ProcessRequest {
47//!             data: format!("prefix: {}", req.data),
48//!         };
49//!
50//!         // Forward to agent and relay the response back
51//!         cx.send_request_to(Agent, modified)
52//!             .forward_response_to(responder)
53//!     }, agent_client_protocol::on_receive_request!())
54//!     .connect_to(transport)
55//!     .await?;
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! Messages you don't handle are forwarded automatically.
61//!
62//! # Adding MCP Servers
63//!
64//! A common use case is adding tools via MCP. You can add them globally
65//! (available in all sessions) or per-session.
66//!
67//! ## Global MCP Server
68//!
69//! ```ignore
70//! # use agent_client_protocol::{Proxy, Conductor, ConnectTo};
71//! # use agent_client_protocol::mcp_server::McpServer;
72//! # use agent_client_protocol_rmcp::McpServerExt;
73//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
74//! # let my_mcp_server = McpServer::<Conductor, _>::builder("tools").build();
75//! Proxy.builder()
76//!     .with_mcp_server(my_mcp_server)
77//!     .connect_to(transport)
78//!     .await?;
79//! # Ok(())
80//! # }
81//! ```
82//!
83//! ## Per-Session MCP Server
84//!
85//! ```ignore
86//! # use agent_client_protocol::{Proxy, Client, Conductor, ConnectTo};
87//! # use agent_client_protocol::schema::v1::NewSessionRequest;
88//! # use agent_client_protocol::mcp_server::McpServer;
89//! # use agent_client_protocol_rmcp::McpServerExt;
90//! # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
91//! Proxy.builder()
92//!     .on_receive_request_from(Client, async |req: NewSessionRequest, responder, cx| {
93//!         let my_mcp_server = McpServer::<Conductor, _>::builder("tools").build();
94//!         cx.build_session_from(req)
95//!             .with_mcp_server(my_mcp_server)?
96//!             .on_proxy_session_start(responder, async |session_id| {
97//!                 // Session started with MCP server attached
98//!                 Ok(())
99//!             })
100//!     }, agent_client_protocol::on_receive_request!())
101//!     .connect_to(transport)
102//!     .await?;
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! # The Conductor
108//!
109//! Proxies don't run standalone - they're orchestrated by a **conductor**.
110//! The conductor:
111//!
112//! - Spawns proxy processes
113//! - Chains them together
114//! - Connects the final proxy to the agent
115//!
116//! The [`agent-client-protocol-conductor`] crate provides a conductor binary. You configure
117//! it with a list of proxies to run.
118//!
119//! # Proxy Chains
120//!
121//! Multiple proxies can be chained:
122//!
123//! ```text
124//! Client <-> Proxy A <-> Proxy B <-> Agent
125//! ```
126//!
127//! Each proxy sees messages from its perspective:
128//! - `Client` is "toward the client" (Proxy A, or conductor if first)
129//! - `Agent` is "toward the agent" (Proxy B, or agent if last)
130//!
131//! Messages flow through each proxy in order. Each can inspect, modify,
132//! or handle messages before they continue.
133//!
134//! # Summary
135//!
136//! | Task | Approach |
137//! |------|----------|
138//! | Forward everything | Just `connect_to(transport)` |
139//! | Intercept specific messages | `on_receive_*_from` with explicit peers |
140//! | Add global tools | `with_mcp_server` on builder |
141//! | Add per-session tools | `with_mcp_server` on session builder |
142//!
143//! [`Proxy`]: crate::Proxy
144//! [`Client`]: crate::Client
145//! [`Agent`]: crate::Agent
146//! [`agent-client-protocol-conductor`]: https://crates.io/crates/agent-client-protocol-conductor