Skip to main content

Module proxies

Module proxies 

Source
Expand description

Building proxies that intercept and modify messages.

A proxy sits between a client and an agent, intercepting messages in both directions. This is how you add capabilities like MCP tools, logging, or message transformation.

§The Proxy Role Type

Proxies use the Proxy role type, which has two peers:

  • Client - messages from/to the client direction
  • Agent - messages from/to the agent direction

Unlike simpler links, there’s no default peer - you must always specify which direction you’re communicating with.

§Choosing a Protocol Version

Proxy::builder creates a stable protocol v1 proxy. With the unstable_protocol_v2 feature, Proxy.v2() creates a v2-only proxy whose fluent callbacks receive V2ConnectionTo<Conductor>. The builder validates _proxy/initialize and later traffic against the selected version.

Use Proxy.protocol_router() to package separate v1 and v2 implementations behind one ConnectTo<Conductor> component. It dispatches the conductor-selected _proxy/initialize version exactly and performs no cross-version conversion.

Low-level infrastructure implementing custom raw version routing can use Proxy.builder().without_acp_version_guard(). Disabling the guard is an explicit version-neutral escape hatch, not the ordinary way to author a v2 proxy.

§Default Forwarding

By default, Proxy forwards all messages it doesn’t handle. This means a minimal stable v1 proxy that does nothing is just:

Proxy.builder()
    .connect_to(transport)
    .await?;

All messages pass through unchanged.

§Intercepting Messages

To intercept specific messages, use on_receive_*_from with explicit peers:

Proxy.builder()
    // Intercept requests from the client
    .on_receive_request_from(Client, async |req: ProcessRequest, responder, cx| {
        // Modify the request
        let modified = ProcessRequest {
            data: format!("prefix: {}", req.data),
        };

        // Forward to agent and relay the response back
        cx.send_request_to(Agent, modified)
            .forward_response_to(responder)
    }, agent_client_protocol::on_receive_request!())
    .connect_to(transport)
    .await?;

Messages you don’t handle are forwarded automatically.

§Adding MCP Servers

A common use case is adding tools via MCP. You can add them globally (available in all sessions) or per-session.

These ACP attachment APIs require the unstable_mcp_over_acp feature. Draft v2 attachment additionally requires unstable_protocol_v2.

§Global MCP Server

Proxy.builder()
    .with_mcp_server(my_mcp_server)
    .connect_to(transport)
    .await?;

For draft v2, select the v2 proxy builder before attaching the global server:

Proxy.v2()
    .with_mcp_server(my_mcp_server)
    .connect_to(transport)
    .await?;

The v1 builder injects the declaration into new, load, resume, and feature-gated fork requests. The v2 builder injects it into new, resume, and feature-gated fork requests while preserving unrelated setup fields. Both reuse one connection-scoped server ID.

§Per-Session MCP Server

Proxy.builder()
    .on_receive_request_from(Client, async |req: NewSessionRequest, responder, cx| {
        let my_mcp_server = McpServer::<Conductor, _>::builder("tools").build();
        cx.build_session_from(req)
            .with_mcp_server(my_mcp_server)?
            .on_proxy_session_start(responder, async |session_id| {
                // Session started with MCP server attached
                Ok(())
            })
    }, agent_client_protocol::on_receive_request!())
    .connect_to(transport)
    .await?;

The corresponding v2 proxy uses Proxy.v2(), a schema::v2::NewSessionRequest, and the same fluent session-builder shape. Its V2SessionBuilder::on_proxy_session_start callback receives an OpenedV2Session containing both the command-only v2 session handle and the complete NewSessionResponse:

Proxy.v2()
    .on_receive_request_from(
        Client,
        async |request: schema::v2::NewSessionRequest, responder, cx| {
            cx.build_session_from(request)
                .with_mcp_server(my_mcp_server)?
                .on_proxy_session_start(responder, async |opened| {
                    let (session, response) = opened.into_parts();
                    track_session(session.session_id(), response);
                    Ok(())
                })
        },
        agent_client_protocol::on_receive_request!(),
    );

For schema::v2::ResumeSessionRequest, use cx.resume_session_from(request) and the resulting V2ResumeSessionBuilder in the same shape. With unstable_session_fork, use cx.fork_session_from(request) and V2ForkSessionBuilder for ForkSessionRequest; the returned session and installed route use the new ID from ForkSessionResponse, not the source session ID. Resume routing and any per-session MCP attachment are ready before the downstream request is published, allowing replay to precede the complete response. All setup helpers forward that operation’s response before spawning the callback. Later updates and interactive requests remain independent traffic handled by typed connection callbacks.

§The Conductor

Proxies don’t run standalone - they’re orchestrated by a conductor. The conductor:

  • Spawns proxy processes
  • Chains them together
  • Connects the final proxy to the agent

The agent-client-protocol-conductor crate provides a conductor binary. You configure it with a list of proxies to run.

§Proxy Chains

Multiple proxies can be chained:

Client <-> Proxy A <-> Proxy B <-> Agent

Each proxy sees messages from its perspective:

  • Client is “toward the client” (Proxy A, or conductor if first)
  • Agent is “toward the agent” (Proxy B, or agent if last)

Messages flow through each proxy in order. Each can inspect, modify, or handle messages before they continue.

§Summary

TaskApproach
Forward everythingJust connect_to(transport)
Author a v1 or v2 proxyProxy.builder() or Proxy.v2()
Package v1 and v2 implementationsProxy.protocol_router().with_v1(...).with_v2(...)
Implement custom raw routingwithout_acp_version_guard on the raw proxy builder
Intercept specific messageson_receive_*_from with explicit peers
Add global toolswith_mcp_server on builder
Add per-session toolswith_mcp_server on session builder