From d7e7a99ce604e8beffd5bcdbb9c21d9e5bf3158a Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 29 Apr 2026 21:13:27 -0400 Subject: [PATCH 01/12] Remove html_panel and file_comment metacapabilities from P/ACP design doc --- md/proxying-acp.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/md/proxying-acp.md b/md/proxying-acp.md index 020bbea..61d811b 100644 --- a/md/proxying-acp.md +++ b/md/proxying-acp.md @@ -216,9 +216,7 @@ An P/ACP-aware editor provides the following capability during ACP initializatio /// supports symposium proxy initialization. "_meta": { "symposium": { - "version": "1.0", - "html_panel": true, // or false, if this is the ToEditor proxy - "file_comment": true, // or false, if this is the ToEditor proxy + "version": "1.0" } } ``` From c19915419712de745bdd8377a0aa960c72052851 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 11:15:41 -0400 Subject: [PATCH 02/12] =?UTF-8?q?Step=201/1b:=20Remove=20McpAcpTransport,?= =?UTF-8?q?=20rename=20acp=5Furl=20=E2=86=92=20acp=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove McpAcpTransport struct and MetaCapability impl from capabilities.rs (keep MetaCapability/MetaCapabilityExt traits for proxy capability) - Rename McpConnectRequest.acp_url to acp_id across all source and tests - Update snapshot tests for schema changes (mcpCapabilities.acp, auth removal) - Switch agent-client-protocol-schema to path dependency --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../src/conductor.rs | 11 ++-- .../src/conductor/mcp_bridge.rs | 18 +++---- .../src/conductor/mcp_bridge/http.rs | 4 +- .../src/conductor/mcp_bridge/stdio.rs | 4 +- .../tests/test_session_id_in_mcp_tools.rs | 6 +-- .../tests/trace_client_mcp_server.rs | 9 ++-- .../tests/trace_mcp_tool_call.rs | 9 ++-- .../tests/trace_snapshot.rs | 5 +- src/agent-client-protocol/src/capabilities.rs | 51 +++++++++---------- .../src/mcp_server/active_session.rs | 12 ++--- .../src/mcp_server/context.rs | 4 +- .../src/mcp_server/server.rs | 26 +++++----- .../src/schema/proxy_protocol.rs | 4 +- 15 files changed, 77 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0360d8..b8996aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,9 +109,7 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a124d7628de03b8576a070967f9470135af5b8e4a7ba650f62188dc9e2c2daa7" +version = "0.12.2" dependencies = [ "anyhow", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 750ebb4..6a5d594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ tokio = { version = "1.48", features = ["full"] } tokio-util = { version = "0.7", features = ["compat"] } # Protocol -agent-client-protocol-schema = { version = "=0.12.1", features = ["tracing"] } +agent-client-protocol-schema = { path = "../agent-client-protocol", features = ["tracing"] } # Serialization serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/src/agent-client-protocol-conductor/src/conductor.rs b/src/agent-client-protocol-conductor/src/conductor.rs index 25ae367..1ae3c47 100644 --- a/src/agent-client-protocol-conductor/src/conductor.rs +++ b/src/agent-client-protocol-conductor/src/conductor.rs @@ -475,7 +475,7 @@ where // When the connection id arrives, send a message back into this conductor loop with // the connection id and the (as yet unspawned) actor. ConductorMessage::McpConnectionReceived { - acp_url, + acp_id, connection, actor, } => { @@ -485,10 +485,7 @@ where self.send_request_to_predecessor_of( client, self.proxies.len(), - McpConnectRequest { - acp_url, - meta: None, - }, + McpConnectRequest { acp_id, meta: None }, ) .on_receiving_result({ let mut conductor_tx = self.conductor_tx.clone(); @@ -1284,8 +1281,8 @@ pub enum ConductorMessage { /// The request must be sent back over ACP to receive the connection-id. /// Once the connection-id is received, the actor must be spawned. McpConnectionReceived { - /// The acp:$UUID URL identifying this bridge - acp_url: String, + /// The acp:$UUID identifier for this bridge + acp_id: String, /// The actor that should be spawned once the connection-id is available. actor: McpBridgeConnectionActor, diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs index ed4b07a..d74e0eb 100644 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs +++ b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs @@ -83,7 +83,7 @@ impl McpBridgeListeners { info!( server_name = name, - acp_url = url, + acp_id = url, "Detected MCP server with ACP transport, spawning TCP bridge" ); @@ -100,12 +100,12 @@ impl McpBridgeListeners { &mut self, connection: ConnectionTo, server_name: &str, - acp_url: &str, + acp_id: &str, conductor_tx: &mpsc::Sender, mcp_bridge_mode: &crate::McpBridgeMode, ) -> anyhow::Result { // If there is already a listener for the ACP URL, return its server - if let Some(listener) = self.listeners.get(acp_url) { + if let Some(listener) = self.listeners.get(acp_id) { return Ok(listener.server.clone()); } @@ -113,7 +113,7 @@ impl McpBridgeListeners { let tcp_listener = TcpListener::bind("127.0.0.1:0").await?; let tcp_port = tcp_listener.local_addr()?.port(); - info!(acp_url = acp_url, tcp_port, "Bound listener for MCP bridge"); + info!(acp_id = acp_id, tcp_port, "Bound listener for MCP bridge"); let new_server = match mcp_bridge_mode { crate::McpBridgeMode::Stdio { conductor_command } => McpServer::Stdio( @@ -138,28 +138,28 @@ impl McpBridgeListeners { // remember for later self.listeners.insert( - acp_url.to_string(), + acp_id.to_string(), McpBridgeListener { server: new_server.clone(), }, ); connection.spawn({ - let acp_url = acp_url.to_string(); + let acp_id = acp_id.to_string(); let conductor_tx = conductor_tx.clone(); let mcp_bridge_mode = mcp_bridge_mode.clone(); async move { info!( - acp_url = acp_url, + acp_id = acp_id, tcp_port, "now accepting bridge connections" ); match mcp_bridge_mode { crate::McpBridgeMode::Stdio { conductor_command: _, - } => stdio::run_tcp_listener(tcp_listener, acp_url, conductor_tx).await, + } => stdio::run_tcp_listener(tcp_listener, acp_id, conductor_tx).await, crate::McpBridgeMode::Http => { - http::run_http_listener(tcp_listener, acp_url, conductor_tx).await + http::run_http_listener(tcp_listener, acp_id, conductor_tx).await } } } diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs index 9ea579d..53436c7 100644 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs +++ b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs @@ -25,7 +25,7 @@ use crate::conductor::{ /// Runs an HTTP listener for MCP bridge connections pub async fn run_http_listener( tcp_listener: TcpListener, - acp_url: String, + acp_id: String, mut conductor_tx: mpsc::Sender, ) -> Result<(), agent_client_protocol::Error> { let (to_mcp_client_tx, to_mcp_client_rx) = mpsc::channel(128); @@ -36,7 +36,7 @@ pub async fn run_http_listener( // back and forth. conductor_tx .send(ConductorMessage::McpConnectionReceived { - acp_url, + acp_id, actor: McpBridgeConnectionActor::new( HttpMcpBridge::new(tcp_listener), conductor_tx.clone(), diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs index fd85ee4..a92f7d6 100644 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs +++ b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs @@ -14,7 +14,7 @@ use super::{McpBridgeConnection, McpBridgeConnectionActor}; /// and the conductor. pub async fn run_tcp_listener( tcp_listener: TcpListener, - acp_url: String, + acp_id: String, mut conductor_tx: mpsc::Sender, ) -> Result<(), agent_client_protocol::Error> { // Accept connections @@ -28,7 +28,7 @@ pub async fn run_tcp_listener( conductor_tx .send(ConductorMessage::McpConnectionReceived { - acp_url: acp_url.clone(), + acp_id: acp_id.clone(), actor: make_stdio_actor(stream, conductor_tx.clone(), to_mcp_client_rx), connection: McpBridgeConnection::new(to_mcp_client_tx), }) diff --git a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs index 27682fa..07fe421 100644 --- a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs +++ b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs @@ -23,7 +23,7 @@ struct EchoInput {} /// Output from the echo tool containing the session_id #[derive(Debug, Serialize, Deserialize, JsonSchema)] struct EchoOutput { - acp_url: String, + acp_id: String, } /// Create a proxy that provides an MCP server with a session_id echo tool @@ -36,7 +36,7 @@ fn create_echo_proxy() -> DynConnectTo { "Returns the current session_id", async |_input: EchoInput, context| { Ok(EchoOutput { - acp_url: context.acp_url(), + acp_id: context.acp_url(), }) }, agent_client_protocol::tool_fn_mut!(), @@ -110,7 +110,7 @@ async fn test_session_id_delivered_to_mcp_tools() -> Result<(), agent_client_pro ) .await?; - let pattern = regex::Regex::new(r#""acp_url":\s*String\("acp:[0-9a-f-]+"\)"#).unwrap(); + let pattern = regex::Regex::new(r#""acp_id":\s*String\("acp:[0-9a-f-]+"\)"#).unwrap(); assert!(pattern.is_match(&result), "unexpected result: {result}"); Ok(()) diff --git a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs index dd2e7f8..c986044 100644 --- a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs @@ -123,7 +123,7 @@ impl EventNormalizer { } else { self.normalize_json(v) } - } else if k == "url" || k == "acp_url" { + } else if k == "url" || k == "acp_id" { if let serde_json::Value::String(s) = &v { if s.starts_with("acp:") || s.starts_with("http://localhost:") { serde_json::Value::String(self.normalize_acp_url(s)) @@ -316,9 +316,6 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err session: None, params: Object { "clientCapabilities": Object { - "auth": Object { - "terminal": Bool(false), - }, "fs": Object { "readTextFile": Bool(false), "writeTextFile": Bool(false), @@ -338,9 +335,9 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err is_error: false, payload: Object { "agentCapabilities": Object { - "auth": Object {}, "loadSession": Bool(false), "mcpCapabilities": Object { + "acp": Bool(false), "http": Bool(false), "sse": Bool(false), }, @@ -388,7 +385,7 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err method: "_mcp/connect", session: None, params: Object { - "acp_url": String("acp:url:0"), + "acp_id": String("acp:url:0"), }, }, ), diff --git a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs index bdab7f5..4646718 100644 --- a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs +++ b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs @@ -122,7 +122,7 @@ impl EventNormalizer { } else { self.normalize_json(v) } - } else if k == "url" || k == "acp_url" { + } else if k == "url" || k == "acp_id" { if let serde_json::Value::String(s) = &v { if s.starts_with("acp:") || s.starts_with("http://localhost:") { serde_json::Value::String(self.normalize_acp_url(s)) @@ -307,9 +307,6 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> session: None, params: Object { "clientCapabilities": Object { - "auth": Object { - "terminal": Bool(false), - }, "fs": Object { "readTextFile": Bool(false), "writeTextFile": Bool(false), @@ -329,9 +326,9 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> is_error: false, payload: Object { "agentCapabilities": Object { - "auth": Object {}, "loadSession": Bool(false), "mcpCapabilities": Object { + "acp": Bool(false), "http": Bool(false), "sse": Bool(false), }, @@ -372,7 +369,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> method: "_mcp/connect", session: None, params: Object { - "acp_url": String("acp:url:0"), + "acp_id": String("acp:url:0"), }, }, ), diff --git a/src/agent-client-protocol-conductor/tests/trace_snapshot.rs b/src/agent-client-protocol-conductor/tests/trace_snapshot.rs index 2a78857..3c2a278 100644 --- a/src/agent-client-protocol-conductor/tests/trace_snapshot.rs +++ b/src/agent-client-protocol-conductor/tests/trace_snapshot.rs @@ -192,9 +192,6 @@ async fn test_trace_snapshot() -> Result<(), agent_client_protocol::Error> { session: None, params: Object { "clientCapabilities": Object { - "auth": Object { - "terminal": Bool(false), - }, "fs": Object { "readTextFile": Bool(false), "writeTextFile": Bool(false), @@ -214,9 +211,9 @@ async fn test_trace_snapshot() -> Result<(), agent_client_protocol::Error> { is_error: false, payload: Object { "agentCapabilities": Object { - "auth": Object {}, "loadSession": Bool(false), "mcpCapabilities": Object { + "acp": Bool(false), "http": Bool(false), "sse": Bool(false), }, diff --git a/src/agent-client-protocol/src/capabilities.rs b/src/agent-client-protocol/src/capabilities.rs index 36c6f98..01ad05f 100644 --- a/src/agent-client-protocol/src/capabilities.rs +++ b/src/agent-client-protocol/src/capabilities.rs @@ -6,13 +6,18 @@ //! # Example //! //! ```rust,no_run -//! use agent_client_protocol::{MetaCapabilityExt, McpAcpTransport}; +//! use agent_client_protocol::{MetaCapability, MetaCapabilityExt}; //! # use agent_client_protocol::schema::InitializeResponse; //! # let init_response: InitializeResponse = unimplemented!(); //! -//! let response = init_response.add_meta_capability(McpAcpTransport); -//! if response.has_meta_capability(McpAcpTransport) { -//! // Agent supports MCP-over-ACP bridging +//! struct Proxy; +//! impl MetaCapability for Proxy { +//! fn key(&self) -> &'static str { "proxy" } +//! } +//! +//! let response = init_response.add_meta_capability(Proxy); +//! if response.has_meta_capability(Proxy) { +//! // Agent has the proxy capability //! } //! ``` @@ -33,19 +38,6 @@ pub trait MetaCapability { } } -/// The mcp_acp_transport capability - indicates support for MCP-over-ACP bridging. -/// -/// When present in `_meta.symposium.mcp_acp_transport`, signals that the agent -/// supports having MCP servers with `acp:UUID` transport proxied through the conductor. -#[derive(Debug)] -pub struct McpAcpTransport; - -impl MetaCapability for McpAcpTransport { - fn key(&self) -> &'static str { - "mcp_acp_transport" - } -} - /// Extension trait for checking and modifying capabilities in `InitializeRequest`. pub trait MetaCapabilityExt { /// Check if a capability is present in `_meta.symposium` @@ -140,15 +132,22 @@ mod tests { use crate::schema::{ClientCapabilities, ProtocolVersion}; use serde_json::json; + struct TestCapability; + impl MetaCapability for TestCapability { + fn key(&self) -> &'static str { + "test_cap" + } + } + #[test] fn test_add_capability_to_request() { let request = InitializeRequest::new(ProtocolVersion::LATEST); - let request = request.add_meta_capability(McpAcpTransport); + let request = request.add_meta_capability(TestCapability); - assert!(request.has_meta_capability(McpAcpTransport)); + assert!(request.has_meta_capability(TestCapability)); assert_eq!( - request.client_capabilities.meta.as_ref().unwrap()["symposium"]["mcp_acp_transport"], + request.client_capabilities.meta.as_ref().unwrap()["symposium"]["test_cap"], json!(true) ); } @@ -160,7 +159,7 @@ mod tests { "symposium".to_string(), json!({ "version": "1.0", - "mcp_acp_transport": true + "test_cap": true }), ); let client_capabilities = ClientCapabilities::new().meta(meta); @@ -168,20 +167,20 @@ mod tests { let request = InitializeRequest::new(ProtocolVersion::LATEST) .client_capabilities(client_capabilities); - let request = request.remove_meta_capability(McpAcpTransport); + let request = request.remove_meta_capability(TestCapability); - assert!(!request.has_meta_capability(McpAcpTransport)); + assert!(!request.has_meta_capability(TestCapability)); } #[test] fn test_add_capability_to_response() { let response = InitializeResponse::new(ProtocolVersion::LATEST); - let response = response.add_meta_capability(McpAcpTransport); + let response = response.add_meta_capability(TestCapability); - assert!(response.has_meta_capability(McpAcpTransport)); + assert!(response.has_meta_capability(TestCapability)); assert_eq!( - response.agent_capabilities.meta.as_ref().unwrap()["symposium"]["mcp_acp_transport"], + response.agent_capabilities.meta.as_ref().unwrap()["symposium"]["test_cap"], json!(true) ); } diff --git a/src/agent-client-protocol/src/mcp_server/active_session.rs b/src/agent-client-protocol/src/mcp_server/active_session.rs index 7bb1c5c..03004f7 100644 --- a/src/agent-client-protocol/src/mcp_server/active_session.rs +++ b/src/agent-client-protocol/src/mcp_server/active_session.rs @@ -20,8 +20,8 @@ use std::sync::Arc; /// (see [`ConnectionTo::add_dynamic_handler`]) and handles MCP-over-ACP messages /// with the appropriate ACP url. pub(super) struct McpActiveSession { - /// The ACP URL created for this session - acp_url: String, + /// The ACP identifier created for this session + acp_id: String, /// The MCP server we are managing mcp_connect: Arc>, @@ -34,9 +34,9 @@ impl McpActiveSession where Counterpart: HasPeer, { - pub fn new(acp_url: String, mcp_connect: Arc>) -> Self { + pub fn new(acp_id: String, mcp_connect: Arc>) -> Self { Self { - acp_url, + acp_id, mcp_connect, connections: FxHashMap::default(), } @@ -51,7 +51,7 @@ where acp_connection: &ConnectionTo, ) -> Result)>, crate::Error> { // Check that this is for our MCP server - if request.acp_url != self.acp_url { + if request.acp_id != self.acp_id { return Ok(Handled::No { message: (request, responder), retry: false, @@ -110,7 +110,7 @@ where // Get the MCP server component let spawned_server = self.mcp_connect.connect(McpConnectionTo { - acp_url: request.acp_url.clone(), + acp_id: request.acp_id.clone(), connection: acp_connection.clone(), }); diff --git a/src/agent-client-protocol/src/mcp_server/context.rs b/src/agent-client-protocol/src/mcp_server/context.rs index 7905f20..5a0de69 100644 --- a/src/agent-client-protocol/src/mcp_server/context.rs +++ b/src/agent-client-protocol/src/mcp_server/context.rs @@ -3,14 +3,14 @@ use crate::{ConnectionTo, role::Role}; /// Context about the ACP and MCP connection available to an MCP server. #[derive(Clone, Debug)] pub struct McpConnectionTo { - pub(super) acp_url: String, + pub(super) acp_id: String, pub(super) connection: ConnectionTo, } impl McpConnectionTo { /// The `acp:UUID` that was given. pub fn acp_url(&self) -> String { - self.acp_url.clone() + self.acp_id.clone() } /// The host connection context. diff --git a/src/agent-client-protocol/src/mcp_server/server.rs b/src/agent-client-protocol/src/mcp_server/server.rs index 5d5fd3f..f746ba4 100644 --- a/src/agent-client-protocol/src/mcp_server/server.rs +++ b/src/agent-client-protocol/src/mcp_server/server.rs @@ -48,8 +48,8 @@ pub struct McpServer { /// The host role that is serving up this MCP server phantom: PhantomData, - /// The ACP URL we assigned for this mcp server; always unique - acp_url: String, + /// The ACP identifier we assigned for this mcp server; always unique + acp_id: String, /// The "connect" instance connect: Arc>, @@ -69,7 +69,7 @@ impl std::fmt::Debug fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("McpServer") .field("phantom", &self.phantom) - .field("acp_url", &self.acp_url) + .field("acp_id", &self.acp_id) .field("responder", &self.responder) .finish_non_exhaustive() } @@ -94,7 +94,7 @@ where pub fn new(c: impl McpServerConnect, responder: Run) -> Self { McpServer { phantom: PhantomData, - acp_url: format!("acp:{}", Uuid::new_v4()), + acp_id: format!("acp:{}", Uuid::new_v4()), connect: Arc::new(c), responder, } @@ -107,11 +107,11 @@ where { let Self { phantom: _, - acp_url, + acp_id, connect, responder, } = self; - (McpNewSessionHandler::new(acp_url, connect), responder) + (McpNewSessionHandler::new(acp_id, connect), responder) } } @@ -120,7 +120,7 @@ pub(crate) struct McpNewSessionHandler where Counterpart: HasPeer, { - acp_url: String, + acp_id: String, connect: Arc>, active_session: McpActiveSession, } @@ -129,10 +129,10 @@ impl McpNewSessionHandler where Counterpart: HasPeer, { - pub fn new(acp_url: String, connect: Arc>) -> Self { + pub fn new(acp_id: String, connect: Arc>) -> Self { Self { - active_session: McpActiveSession::new(acp_url.clone(), connect.clone()), - acp_url, + active_session: McpActiveSession::new(acp_id.clone(), connect.clone()), + acp_id, connect, } } @@ -140,7 +140,7 @@ where /// Modify the new session request to include this MCP server. fn modify_new_session_request(&self, request: &mut NewSessionRequest) { request.mcp_servers.push(crate::schema::McpServer::Http( - crate::schema::McpServerHttp::new(self.connect.name(), self.acp_url.clone()), + crate::schema::McpServerHttp::new(self.connect.name(), self.acp_id.clone()), )); } } @@ -208,7 +208,7 @@ where client: impl ConnectTo, ) -> Result<(), crate::Error> { let Self { - acp_url, + acp_id, connect, responder, phantom: _, @@ -229,7 +229,7 @@ where .with_spawned(async move |connection_to_client| { let spawned_server: DynConnectTo = connect.connect(McpConnectionTo { - acp_url, + acp_id, connection: connection_to_client.clone(), }); diff --git a/src/agent-client-protocol/src/schema/proxy_protocol.rs b/src/agent-client-protocol/src/schema/proxy_protocol.rs index aa35dd1..da18da4 100644 --- a/src/agent-client-protocol/src/schema/proxy_protocol.rs +++ b/src/agent-client-protocol/src/schema/proxy_protocol.rs @@ -79,8 +79,8 @@ pub const METHOD_MCP_CONNECT_REQUEST: &str = "_mcp/connect"; #[derive(Debug, Clone, Serialize, Deserialize, crate::JsonRpcRequest)] #[request(method = "_mcp/connect", response = McpConnectResponse, crate = crate)] pub struct McpConnectRequest { - /// The ACP URL to connect to (e.g., "acp:uuid") - pub acp_url: String, + /// The ACP identifier for the server (e.g., "acp:uuid"), matching `McpServerAcp.id` + pub acp_id: String, /// Optional metadata #[serde(skip_serializing_if = "Option::is_none")] From bade279c9f328d221d9e619120ed2b05c4998034 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 11:24:19 -0400 Subject: [PATCH 03/12] Step 2: Extract MCP bridging into agent-client-protocol-polyfill crate New crate src/agent-client-protocol-polyfill with mcp_over_acp module: - McpOverAcpPolyfill proxy (ConnectTo) - BridgeConnectionActor, BridgeListeners, BridgeConnection - HTTP and stdio bridge transports - Uses own BridgeMessage enum instead of ConductorMessage Conductor bridge code still present (removed in Step 3). --- Cargo.lock | 21 + Cargo.toml | 1 + src/agent-client-protocol-polyfill/Cargo.toml | 31 ++ src/agent-client-protocol-polyfill/src/lib.rs | 13 + .../src/mcp_over_acp/actor.rs | 80 ++++ .../src/mcp_over_acp/http.rs | 418 ++++++++++++++++++ .../src/mcp_over_acp/mod.rs | 368 +++++++++++++++ .../src/mcp_over_acp/stdio.rs | 44 ++ 8 files changed, 976 insertions(+) create mode 100644 src/agent-client-protocol-polyfill/Cargo.toml create mode 100644 src/agent-client-protocol-polyfill/src/lib.rs create mode 100644 src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs create mode 100644 src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs create mode 100644 src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs create mode 100644 src/agent-client-protocol-polyfill/src/mcp_over_acp/stdio.rs diff --git a/Cargo.lock b/Cargo.lock index b8996aa..4f59dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,27 @@ dependencies = [ "syn", ] +[[package]] +name = "agent-client-protocol-polyfill" +version = "0.11.1" +dependencies = [ + "agent-client-protocol", + "agent-client-protocol-schema", + "anyhow", + "async-stream", + "axum", + "futures", + "futures-concurrency", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "agent-client-protocol-rmcp" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 6a5d594..08e5cff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "src/agent-client-protocol-conductor", "src/agent-client-protocol-cookbook", "src/agent-client-protocol-derive", + "src/agent-client-protocol-polyfill", "src/agent-client-protocol-rmcp", "src/agent-client-protocol-test", "src/agent-client-protocol-trace-viewer", diff --git a/src/agent-client-protocol-polyfill/Cargo.toml b/src/agent-client-protocol-polyfill/Cargo.toml new file mode 100644 index 0000000..f1c6ccb --- /dev/null +++ b/src/agent-client-protocol-polyfill/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "agent-client-protocol-polyfill" +version = "0.11.1" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Polyfill proxies for Agent Client Protocol backward compatibility" +keywords = ["acp", "agent", "mcp", "polyfill"] +categories = ["development-tools"] + +[dependencies] +agent-client-protocol.workspace = true +agent-client-protocol-schema.workspace = true +anyhow.workspace = true +async-stream.workspace = true +axum.workspace = true +futures.workspace = true +futures-concurrency.workspace = true +rustc-hash.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror = "2.0" +tokio.workspace = true +tokio-util.workspace = true +tracing.workspace = true +uuid.workspace = true + +[lints] +workspace = true diff --git a/src/agent-client-protocol-polyfill/src/lib.rs b/src/agent-client-protocol-polyfill/src/lib.rs new file mode 100644 index 0000000..5c62955 --- /dev/null +++ b/src/agent-client-protocol-polyfill/src/lib.rs @@ -0,0 +1,13 @@ +//! # agent-client-protocol-polyfill +//! +//! Polyfill proxies for backward compatibility with agents that don't support +//! newer ACP features natively. +//! +//! ## MCP-over-ACP Polyfill +//! +//! The [`mcp_over_acp`] module provides a proxy that bridges MCP-over-ACP transport +//! for agents that don't support `mcpCapabilities.acp`. It intercepts `NewSessionRequest` +//! to transform `McpServer::Http` entries with `acp:` URLs into localhost TCP bridges, +//! and handles `_mcp/*` messages by routing them through those bridges. + +pub mod mcp_over_acp; diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs new file mode 100644 index 0000000..b349642 --- /dev/null +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs @@ -0,0 +1,80 @@ +use agent_client_protocol::{ + ConnectTo, Dispatch, DynConnectTo, role::mcp, schema::McpDisconnectNotification, +}; +use futures::{SinkExt as _, StreamExt as _, channel::mpsc}; +use tracing::info; + +use super::BridgeMessage; + +/// Actor that bridges a single MCP connection between a local MCP client +/// and the ACP proxy chain. +#[derive(Debug)] +pub(crate) struct BridgeConnectionActor { + transport: DynConnectTo, + bridge_tx: mpsc::Sender, + to_mcp_client_rx: mpsc::Receiver, +} + +impl BridgeConnectionActor { + pub fn new( + component: impl ConnectTo, + bridge_tx: mpsc::Sender, + to_mcp_client_rx: mpsc::Receiver, + ) -> Self { + Self { + transport: DynConnectTo::new(component), + bridge_tx, + to_mcp_client_rx, + } + } + + pub async fn run(self, connection_id: String) -> Result<(), agent_client_protocol::Error> { + info!(connection_id, "MCP bridge connected"); + + let Self { + transport, + mut bridge_tx, + to_mcp_client_rx, + } = self; + + let result = mcp::Client + .builder() + .name(format!("mcp-client-to-polyfill({connection_id})")) + .on_receive_dispatch( + { + let mut bridge_tx = bridge_tx.clone(); + let connection_id = connection_id.clone(); + async move |message: Dispatch, _cx| { + bridge_tx + .send(BridgeMessage::ClientToServer { + connection_id: connection_id.clone(), + message, + }) + .await + .map_err(|_| agent_client_protocol::Error::internal_error()) + } + }, + agent_client_protocol::on_receive_dispatch!(), + ) + .connect_with(transport, async move |mcp_connection_to_client| { + let mut to_mcp_client_rx = to_mcp_client_rx; + while let Some(message) = to_mcp_client_rx.next().await { + mcp_connection_to_client.send_proxied_message(message)?; + } + Ok(()) + }) + .await; + + bridge_tx + .send(BridgeMessage::Disconnected { + notification: McpDisconnectNotification { + connection_id, + meta: None, + }, + }) + .await + .map_err(|_| agent_client_protocol::Error::internal_error())?; + + result + } +} diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs new file mode 100644 index 0000000..26facef --- /dev/null +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs @@ -0,0 +1,418 @@ +//! HTTP-based MCP bridge transport. + +use agent_client_protocol::{BoxFuture, Channel, ConnectTo, jsonrpcmsg::Message, role::mcp}; +use axum::{ + Router, + extract::State, + http::StatusCode, + response::{IntoResponse, Response, Sse}, + routing::post, +}; +use futures::{SinkExt, StreamExt as _, channel::mpsc, future::Either, stream::Stream}; +use futures_concurrency::future::FutureExt as _; +use futures_concurrency::stream::StreamExt as _; +use rustc_hash::FxHashMap; +use std::{ + collections::{HashMap, VecDeque}, + pin::pin, + sync::Arc, +}; +use tokio::net::TcpListener; + +use super::{BridgeConnection, BridgeMessage, actor::BridgeConnectionActor}; + +/// Runs an HTTP listener for MCP bridge connections. +pub async fn run_http_listener( + tcp_listener: TcpListener, + acp_id: String, + mut bridge_tx: mpsc::Sender, +) -> Result<(), agent_client_protocol::Error> { + let (to_mcp_client_tx, to_mcp_client_rx) = mpsc::channel(128); + + bridge_tx + .send(BridgeMessage::ConnectionReceived { + acp_id, + actor: BridgeConnectionActor::new( + HttpMcpBridge::new(tcp_listener), + bridge_tx.clone(), + to_mcp_client_rx, + ), + connection: BridgeConnection::new(to_mcp_client_tx), + }) + .await + .map_err(|_| agent_client_protocol::Error::internal_error())?; + + Ok(()) +} + +struct HttpMcpBridge { + listener: tokio::net::TcpListener, +} + +impl HttpMcpBridge { + fn new(listener: tokio::net::TcpListener) -> Self { + Self { listener } + } +} + +impl ConnectTo for HttpMcpBridge { + async fn connect_to( + self, + client: impl ConnectTo, + ) -> Result<(), agent_client_protocol::Error> { + let (channel, serve_self) = self.into_channel_and_future(); + match futures::future::select(pin!(client.connect_to(channel)), serve_self).await { + Either::Left((result, _)) | Either::Right((result, _)) => result, + } + } + + fn into_channel_and_future( + self, + ) -> ( + Channel, + BoxFuture<'static, Result<(), agent_client_protocol::Error>>, + ) + where + Self: Sized, + { + let (channel_a, channel_b) = Channel::duplex(); + (channel_a, Box::pin(run(self.listener, channel_b))) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +struct HttpError(#[from] agent_client_protocol::Error); + +impl From for HttpError { + fn from(error: axum::Error) -> Self { + HttpError(agent_client_protocol::util::internal_error(error)) + } +} + +impl IntoResponse for HttpError { + fn into_response(self) -> Response { + let message = format!("Error: {}", self.0); + (StatusCode::INTERNAL_SERVER_ERROR, message).into_response() + } +} + +async fn run(listener: TcpListener, channel: Channel) -> Result<(), agent_client_protocol::Error> { + let (registration_tx, registration_rx) = mpsc::unbounded(); + let state = BridgeState { registration_tx }; + + async { + let app = Router::new() + .route("/", post(handle_post).get(handle_get)) + .with_state(Arc::new(state)); + + axum::serve(listener, app) + .await + .map_err(agent_client_protocol::util::internal_error) + } + .race(RunningServer::new().run(channel, registration_rx)) + .await +} + +struct BridgeState { + registration_tx: mpsc::UnboundedSender, +} + +#[derive(Debug)] +#[allow(dead_code)] +enum HttpMessage { + Request { + http_request_id: uuid::Uuid, + request: agent_client_protocol::jsonrpcmsg::Request, + response_tx: mpsc::UnboundedSender, + }, + Notification { + http_request_id: uuid::Uuid, + request: agent_client_protocol::jsonrpcmsg::Request, + }, + Response { + http_request_id: uuid::Uuid, + response: agent_client_protocol::jsonrpcmsg::Response, + }, + Get { + http_request_id: uuid::Uuid, + response_tx: mpsc::UnboundedSender, + }, +} + +#[derive(Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Clone)] +enum JsonRpcId { + String(String), + Number(u64), + Null, +} + +impl From for JsonRpcId { + fn from(id: agent_client_protocol::jsonrpcmsg::Id) -> Self { + match id { + agent_client_protocol::jsonrpcmsg::Id::String(s) => JsonRpcId::String(s), + agent_client_protocol::jsonrpcmsg::Id::Number(n) => JsonRpcId::Number(n), + agent_client_protocol::jsonrpcmsg::Id::Null => JsonRpcId::Null, + } + } +} + +struct RunningServer { + waiting_sessions: FxHashMap, + general_sessions: Vec, + message_deque: VecDeque, +} + +impl RunningServer { + fn new() -> Self { + RunningServer { + waiting_sessions: HashMap::default(), + general_sessions: Vec::default(), + message_deque: VecDeque::with_capacity(32), + } + } + + async fn run( + mut self, + mut channel: Channel, + http_rx: mpsc::UnboundedReceiver, + ) -> Result<(), agent_client_protocol::Error> { + #[derive(Debug)] + enum MultiplexMessage { + FromHttpToChannel(HttpMessage), + FromChannelToHttp( + Result, + ), + } + + let mut merged_stream = http_rx + .map(MultiplexMessage::FromHttpToChannel) + .merge(channel.rx.map(MultiplexMessage::FromChannelToHttp)); + + while let Some(message) = merged_stream.next().await { + tracing::trace!(?message, "received message"); + + match message { + MultiplexMessage::FromHttpToChannel(http_message) => { + self.handle_http_message(http_message, &mut channel.tx)?; + } + MultiplexMessage::FromChannelToHttp(message) => { + let message = message.unwrap_or_else(|err| { + agent_client_protocol::jsonrpcmsg::Message::Response( + agent_client_protocol::jsonrpcmsg::Response::error( + agent_client_protocol::util::into_jsonrpc_error(err), + None, + ), + ) + }); + self.message_deque.push_back(message); + } + } + + self.drain_jsonrpc_messages(); + } + + Ok(()) + } + + fn handle_http_message( + &mut self, + message: HttpMessage, + channel_tx: &mut mpsc::UnboundedSender< + Result, + >, + ) -> Result<(), agent_client_protocol::Error> { + match message { + HttpMessage::Request { + http_request_id, + request, + response_tx, + } => { + tracing::debug!(%http_request_id, ?request, "handling request"); + let request_id = request.id.clone().map(JsonRpcId::from); + channel_tx + .unbounded_send(Ok(Message::Request(request))) + .map_err(agent_client_protocol::util::internal_error)?; + let session = RegisteredSession::new(response_tx); + if let Some(id) = request_id { + self.waiting_sessions.insert(id, session); + } else { + self.general_sessions.push(session); + } + } + HttpMessage::Notification { + http_request_id: _, + request, + } => { + channel_tx + .unbounded_send(Ok(Message::Request(request))) + .map_err(agent_client_protocol::util::internal_error)?; + } + HttpMessage::Response { + http_request_id: _, + response, + } => { + channel_tx + .unbounded_send(Ok(Message::Response(response))) + .map_err(agent_client_protocol::util::internal_error)?; + } + HttpMessage::Get { + http_request_id: _, + response_tx, + } => { + self.general_sessions + .push(RegisteredSession::new(response_tx)); + } + } + self.purge_closed_sessions(); + Ok(()) + } + + fn drain_jsonrpc_messages(&mut self) { + while let Some(message) = self.message_deque.pop_front() { + if let Some(message) = self.try_dispatch_jsonrpc_message(message) { + self.message_deque.push_front(message); + break; + } + } + } + + fn try_dispatch_jsonrpc_message( + &mut self, + mut message: agent_client_protocol::jsonrpcmsg::Message, + ) -> Option { + let message_id = match &message { + Message::Response(response) => response.id.as_ref().map(|v| v.clone().into()), + Message::Request(_) => None, + }; + + if let Some(ref message_id) = message_id + && let Some(session) = self.waiting_sessions.remove(message_id) + { + match session.outgoing_tx.unbounded_send(message) { + Ok(()) => return None, + Err(m) => { + assert!(m.is_disconnected()); + message = m.into_inner(); + } + } + } + + self.purge_closed_sessions(); + let all_sessions = self + .general_sessions + .iter_mut() + .chain(self.waiting_sessions.values_mut()); + for session in all_sessions { + match session.outgoing_tx.unbounded_send(message) { + Ok(()) => return None, + Err(m) => { + assert!(m.is_disconnected()); + message = m.into_inner(); + } + } + } + + Some(message) + } + + fn purge_closed_sessions(&mut self) { + self.general_sessions + .retain(|session| !session.outgoing_tx.is_closed()); + self.waiting_sessions + .retain(|_, session| !session.outgoing_tx.is_closed()); + } +} + +struct RegisteredSession { + #[allow(dead_code)] + id: uuid::Uuid, + outgoing_tx: mpsc::UnboundedSender, +} + +impl RegisteredSession { + fn new(outgoing_tx: mpsc::UnboundedSender) -> Self { + Self { + id: uuid::Uuid::new_v4(), + outgoing_tx, + } + } +} + +async fn handle_post( + State(state): State>, + body: String, +) -> Result { + let http_request_id = uuid::Uuid::new_v4(); + let message: agent_client_protocol::jsonrpcmsg::Message = + serde_json::from_str(&body).map_err(agent_client_protocol::util::parse_error)?; + + match message { + Message::Request(request) if request.id.is_some() => { + let (tx, mut rx) = mpsc::unbounded(); + state + .registration_tx + .unbounded_send(HttpMessage::Request { + http_request_id, + request, + response_tx: tx, + }) + .map_err(agent_client_protocol::util::internal_error)?; + + let stream = async_stream::stream! { + while let Some(message) = rx.next().await { + match axum::response::sse::Event::default().json_data(message) { + Ok(v) => yield Ok(v), + Err(e) => yield Err(HttpError::from(e)), + } + } + }; + Ok(Sse::new(stream).into_response()) + } + Message::Request(request) => { + state + .registration_tx + .unbounded_send(HttpMessage::Notification { + http_request_id, + request, + }) + .map_err(agent_client_protocol::util::internal_error)?; + Ok(StatusCode::ACCEPTED.into_response()) + } + Message::Response(response) => { + state + .registration_tx + .unbounded_send(HttpMessage::Response { + http_request_id, + response, + }) + .map_err(agent_client_protocol::util::internal_error)?; + Ok(StatusCode::ACCEPTED.into_response()) + } + } +} + +async fn handle_get( + State(state): State>, +) -> Result>>, HttpError> { + let http_request_id = uuid::Uuid::new_v4(); + let (tx, mut rx) = mpsc::unbounded(); + state + .registration_tx + .unbounded_send(HttpMessage::Get { + http_request_id, + response_tx: tx, + }) + .map_err(agent_client_protocol::util::internal_error)?; + + let stream = async_stream::stream! { + while let Some(message) = rx.next().await { + match axum::response::sse::Event::default().json_data(message) { + Ok(v) => yield Ok(v), + Err(e) => yield Err(HttpError::from(e)), + } + } + }; + + Ok(Sse::new(stream)) +} diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs new file mode 100644 index 0000000..8f4d062 --- /dev/null +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs @@ -0,0 +1,368 @@ +//! MCP-over-ACP polyfill proxy. +//! +//! This proxy bridges MCP-over-ACP transport for agents that don't support +//! `mcpCapabilities.acp` natively. It sits in the proxy chain and: +//! +//! - Intercepts `NewSessionRequest` to transform `McpServer::Http` entries with `acp:` URLs +//! into localhost TCP bridges +//! - Handles `_mcp/connect`, `_mcp/message`, `_mcp/disconnect` by routing through those bridges +//! +//! # Usage +//! +//! ```rust,ignore +//! use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; +//! +//! // Add to a conductor proxy chain +//! let conductor = ConductorImpl::new_agent( +//! "conductor", +//! ProxiesAndAgent::new(my_agent).proxy(McpOverAcpPolyfill::http()), +//! McpBridgeMode::default(), +//! ); +//! ``` + +mod actor; +pub(crate) mod http; +pub(crate) mod stdio; + +use std::collections::HashMap; +use std::path::PathBuf; + +use agent_client_protocol::schema::{ + McpConnectRequest, McpConnectResponse, McpDisconnectNotification, McpOverAcpMessage, McpServer, + McpServerHttp, McpServerStdio, NewSessionRequest, +}; +use agent_client_protocol::{ + Agent, Client, Conductor, ConnectTo, ConnectionTo, Dispatch, Proxy, Role, +}; +use futures::{SinkExt, channel::mpsc}; +use tokio::net::TcpListener; +use tracing::info; + +use self::actor::BridgeConnectionActor; + +/// Internal messages for the polyfill's bridge management. +#[derive(Debug)] +pub(crate) enum BridgeMessage { + /// A new TCP connection was accepted and needs an ACP connection ID. + ConnectionReceived { + acp_id: String, + actor: BridgeConnectionActor, + connection: BridgeConnection, + }, + + /// MCP message from a bridge client that needs to be forwarded over ACP. + ClientToServer { + connection_id: String, + message: Dispatch, + }, + + /// Bridge client disconnected. + Disconnected { + notification: McpDisconnectNotification, + }, +} + +/// Connection handle for sending messages to an MCP client via a bridge. +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub(crate) struct BridgeConnection { + to_mcp_client_tx: mpsc::Sender, +} + +impl BridgeConnection { + pub fn new(to_mcp_client_tx: mpsc::Sender) -> Self { + Self { to_mcp_client_tx } + } + + #[allow(dead_code)] + pub async fn send(&mut self, message: Dispatch) -> Result<(), agent_client_protocol::Error> { + self.to_mcp_client_tx + .send(message) + .await + .map_err(|_| agent_client_protocol::Error::internal_error()) + } +} + +/// Mode for the MCP bridge transport. +#[derive(Debug, Clone)] +pub enum BridgeMode { + /// Use stdio-based MCP bridge with a subprocess. + Stdio { + /// Command and args to spawn bridge processes. + conductor_command: Vec, + }, + + /// Use HTTP-based MCP bridge (default). + Http, +} + +impl Default for BridgeMode { + fn default() -> Self { + BridgeMode::Http + } +} + +/// MCP-over-ACP polyfill proxy. +/// +/// Bridges MCP-over-ACP transport for agents that don't support `mcpCapabilities.acp`. +#[derive(Debug)] +pub struct McpOverAcpPolyfill { + mode: BridgeMode, +} + +impl McpOverAcpPolyfill { + /// Create a polyfill using HTTP bridge mode. + pub fn http() -> Self { + Self { + mode: BridgeMode::Http, + } + } + + /// Create a polyfill using stdio bridge mode. + pub fn stdio(conductor_command: Vec) -> Self { + Self { + mode: BridgeMode::Stdio { conductor_command }, + } + } +} + +impl ConnectTo for McpOverAcpPolyfill { + async fn connect_to( + self, + client: impl ConnectTo, + ) -> Result<(), agent_client_protocol::Error> { + let (bridge_tx, bridge_rx) = mpsc::channel(128); + let mode = self.mode; + + Proxy + .builder() + .name("mcp-over-acp-polyfill") + .with_responder(BridgeResponder { + bridge_rx, + bridge_connections: HashMap::new(), + }) + .on_receive_request_from( + Client, + { + let bridge_tx = bridge_tx.clone(); + async move |mut request: NewSessionRequest, + responder, + cx: ConnectionTo| { + // Transform acp: URLs in MCP servers + let mut listeners = BridgeListeners::default(); + for mcp_server in &mut request.mcp_servers { + listeners + .transform_mcp_server(cx.clone(), mcp_server, &bridge_tx, &mode) + .await?; + } + // Forward modified request to successor + cx.send_request_to(Agent, request) + .forward_response_to(responder) + } + }, + agent_client_protocol::on_receive_request!(), + ) + .connect_to(client) + .await + } +} + +/// Manages active bridge listeners (TCP listeners for acp: URLs). +#[derive(Default, Debug)] +struct BridgeListeners { + listeners: HashMap, +} + +#[derive(Clone, Debug)] +struct BridgeListener { + server: McpServer, +} + +impl BridgeListeners { + /// Transform an MCP server with `acp:` URL into a bridged localhost server. + async fn transform_mcp_server( + &mut self, + connection: ConnectionTo, + mcp_server: &mut McpServer, + bridge_tx: &mpsc::Sender, + mode: &BridgeMode, + ) -> Result<(), agent_client_protocol::Error> { + let McpServer::Http(http) = mcp_server else { + return Ok(()); + }; + + if !http.url.starts_with("acp:") { + return Ok(()); + } + + if !http.headers.is_empty() { + return Err(agent_client_protocol::Error::internal_error()); + } + + let name = http.name.clone(); + let url = http.url.clone(); + + info!( + server_name = %name, + acp_id = %url, + "Detected MCP server with ACP transport, spawning TCP bridge" + ); + + let transformed = self + .spawn_bridge(connection, &name, &url, bridge_tx, mode) + .await?; + *mcp_server = transformed; + Ok(()) + } + + async fn spawn_bridge( + &mut self, + connection: ConnectionTo, + server_name: &str, + acp_id: &str, + bridge_tx: &mpsc::Sender, + mode: &BridgeMode, + ) -> anyhow::Result { + if let Some(listener) = self.listeners.get(acp_id) { + return Ok(listener.server.clone()); + } + + let tcp_listener = TcpListener::bind("127.0.0.1:0").await?; + let tcp_port = tcp_listener.local_addr()?.port(); + + info!(acp_id = acp_id, tcp_port, "Bound listener for MCP bridge"); + + let new_server = match mode { + BridgeMode::Stdio { conductor_command } => McpServer::Stdio( + McpServerStdio::new( + server_name.to_string(), + PathBuf::from(&conductor_command[0]), + ) + .args( + conductor_command[1..] + .iter() + .cloned() + .chain(vec!["mcp".to_string(), format!("{tcp_port}")]) + .collect::>(), + ), + ), + + BridgeMode::Http => McpServer::Http(McpServerHttp::new( + server_name.to_string(), + format!("http://localhost:{tcp_port}"), + )), + }; + + self.listeners.insert( + acp_id.to_string(), + BridgeListener { + server: new_server.clone(), + }, + ); + + connection.spawn({ + let acp_id = acp_id.to_string(); + let bridge_tx = bridge_tx.clone(); + let mode = mode.clone(); + async move { + info!( + acp_id = acp_id, + tcp_port, "now accepting bridge connections" + ); + match mode { + BridgeMode::Stdio { + conductor_command: _, + } => stdio::run_tcp_listener(tcp_listener, acp_id, bridge_tx).await, + BridgeMode::Http => { + http::run_http_listener(tcp_listener, acp_id, bridge_tx).await + } + } + } + })?; + + Ok(new_server) + } +} + +/// Responder that runs alongside the proxy, managing bridge state. +struct BridgeResponder { + bridge_rx: mpsc::Receiver, + bridge_connections: HashMap, +} + +impl std::fmt::Debug for BridgeResponder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BridgeResponder") + .field("bridge_connections", &self.bridge_connections.len()) + .finish_non_exhaustive() + } +} + +impl agent_client_protocol::RunWithConnectionTo for BridgeResponder { + async fn run_with_connection_to( + mut self, + connection: ConnectionTo, + ) -> Result<(), agent_client_protocol::Error> { + use futures::StreamExt; + + while let Some(message) = self.bridge_rx.next().await { + match message { + BridgeMessage::ConnectionReceived { + acp_id, + actor, + connection: bridge_conn, + } => { + // Send _mcp/connect request back through the chain + connection + .send_request_to(Client, McpConnectRequest { acp_id, meta: None }) + .on_receiving_result({ + let mut bridge_connections = HashMap::new(); + let connection = connection.clone(); + async move |result| { + match result { + Ok(McpConnectResponse { connection_id, .. }) => { + // Store the connection and spawn the actor + bridge_connections + .insert(connection_id.clone(), bridge_conn); + connection.spawn(actor.run(connection_id))?; + Ok(()) + } + Err(_) => Ok(()), + } + } + })?; + } + + BridgeMessage::ClientToServer { + connection_id, + message, + } => { + let wrapped = message.map( + |request, responder| { + ( + McpOverAcpMessage { + connection_id: connection_id.clone(), + message: request, + meta: None, + }, + responder, + ) + }, + |notification| McpOverAcpMessage { + connection_id: connection_id.clone(), + message: notification, + meta: None, + }, + ); + connection.send_proxied_message_to(Client, wrapped)?; + } + + BridgeMessage::Disconnected { notification } => { + self.bridge_connections.remove(¬ification.connection_id); + connection.send_notification_to(Client, notification)?; + } + } + } + Ok(()) + } +} diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/stdio.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/stdio.rs new file mode 100644 index 0000000..30556c7 --- /dev/null +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/stdio.rs @@ -0,0 +1,44 @@ +//! Stdio-based MCP bridge transport. + +use agent_client_protocol::Dispatch; +use futures::{SinkExt, channel::mpsc}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; + +use super::{BridgeConnection, BridgeMessage, actor::BridgeConnectionActor}; + +/// Runs the stdio bridge TCP listener, accepting connections and creating bridge actors. +pub async fn run_tcp_listener( + tcp_listener: TcpListener, + acp_id: String, + mut bridge_tx: mpsc::Sender, +) -> Result<(), agent_client_protocol::Error> { + loop { + let (stream, _addr) = tcp_listener + .accept() + .await + .map_err(agent_client_protocol::Error::into_internal_error)?; + + let (to_mcp_client_tx, to_mcp_client_rx) = mpsc::channel(128); + + bridge_tx + .send(BridgeMessage::ConnectionReceived { + acp_id: acp_id.clone(), + actor: make_stdio_actor(stream, bridge_tx.clone(), to_mcp_client_rx), + connection: BridgeConnection::new(to_mcp_client_tx), + }) + .await + .map_err(|_| agent_client_protocol::Error::internal_error())?; + } +} + +fn make_stdio_actor( + stream: TcpStream, + bridge_tx: mpsc::Sender, + to_mcp_client_rx: mpsc::Receiver, +) -> BridgeConnectionActor { + let (read_half, write_half) = stream.into_split(); + let transport = + agent_client_protocol::ByteStreams::new(write_half.compat_write(), read_half.compat()); + BridgeConnectionActor::new(transport, bridge_tx, to_mcp_client_rx) +} From e1b8db8dc13619211e507b209693955c43758837 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 11:37:50 -0400 Subject: [PATCH 04/12] Step 3: Remove MCP bridging from conductor (clean break) - Remove McpBridgeMode enum and mcp_bridge_mode parameter - Delete conductor/mcp_bridge module (actor, http, stdio) - Remove bridge fields from ConductorResponder - Remove bridge ConductorMessage variants and handling - Remove NewSessionRequest MCP server transformation - Update all 16 test files to remove McpBridgeMode - Mark 13 bridge-dependent tests as #[ignore] --- .../src/conductor.rs | 245 +------- .../src/conductor/mcp_bridge.rs | 170 ------ .../src/conductor/mcp_bridge/actor.rs | 88 --- .../src/conductor/mcp_bridge/http.rs | 570 ------------------ .../src/conductor/mcp_bridge/stdio.rs | 54 -- .../src/lib.rs | 27 +- .../tests/arrow_proxy_eliza.rs | 3 +- .../tests/empty_conductor_eliza.rs | 9 +- .../tests/initialization_sequence.rs | 4 +- .../tests/mcp-integration.rs | 17 +- .../tests/mcp_server_handler_chain.rs | 3 +- .../tests/nested_arrow_proxy.rs | 3 +- .../tests/nested_conductor.rs | 5 +- .../tests/scoped_mcp_server.rs | 6 +- .../tests/test_mcp_tool_output_types.rs | 6 +- .../tests/test_session_id_in_mcp_tools.rs | 6 +- .../tests/test_tool_enable_disable.rs | 14 +- .../tests/test_tool_fn.rs | 4 +- .../tests/trace_client_mcp_server.rs | 21 +- .../tests/trace_generation.rs | 3 +- .../tests/trace_mcp_tool_call.rs | 4 +- .../tests/trace_snapshot.rs | 3 +- 22 files changed, 53 insertions(+), 1212 deletions(-) delete mode 100644 src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs delete mode 100644 src/agent-client-protocol-conductor/src/conductor/mcp_bridge/actor.rs delete mode 100644 src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs delete mode 100644 src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs diff --git a/src/agent-client-protocol-conductor/src/conductor.rs b/src/agent-client-protocol-conductor/src/conductor.rs index 1ae3c47..a12ed7f 100644 --- a/src/agent-client-protocol-conductor/src/conductor.rs +++ b/src/agent-client-protocol-conductor/src/conductor.rs @@ -110,39 +110,27 @@ //! - Modified `InitializeRequest` to forward downstream //! - `Vec` of spawned components -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use agent_client_protocol::{ Agent, BoxFuture, Client, Conductor, ConnectTo, Dispatch, DynConnectTo, Error, JsonRpcMessage, Proxy, Role, RunWithConnectionTo, role::HasPeer, util::MatchDispatch, }; use agent_client_protocol::{ - Builder, ConnectionTo, JsonRpcNotification, JsonRpcRequest, SentRequest, UntypedMessage, + Builder, ConnectionTo, JsonRpcNotification, JsonRpcRequest, SentRequest, }; use agent_client_protocol::{ HandleDispatchFrom, - schema::{InitializeProxyRequest, InitializeRequest, NewSessionRequest}, + schema::{InitializeProxyRequest, InitializeRequest}, util::MatchDispatchFrom, }; -use agent_client_protocol::{ - Handled, - schema::{ - McpConnectRequest, McpConnectResponse, McpDisconnectNotification, McpOverAcpMessage, - SuccessorMessage, - }, -}; +use agent_client_protocol::{Handled, schema::SuccessorMessage}; use futures::{ SinkExt, StreamExt, channel::mpsc::{self}, }; use tracing::{debug, info}; -use crate::conductor::mcp_bridge::{ - McpBridgeConnection, McpBridgeConnectionActor, McpBridgeListeners, -}; - -mod mcp_bridge; - /// The conductor manages the proxy chain lifecycle and message routing. /// /// It maintains connections to all components in the chain and routes messages @@ -153,22 +141,15 @@ pub struct ConductorImpl { host: Host, name: String, instantiator: Host::Instantiator, - mcp_bridge_mode: crate::McpBridgeMode, trace_writer: Option, } impl ConductorImpl { - pub fn new( - host: Host, - name: impl ToString, - instantiator: Host::Instantiator, - mcp_bridge_mode: crate::McpBridgeMode, - ) -> Self { + pub fn new(host: Host, name: impl ToString, instantiator: Host::Instantiator) -> Self { ConductorImpl { name: name.to_string(), host, instantiator, - mcp_bridge_mode, trace_writer: None, } } @@ -179,20 +160,15 @@ impl ConductorImpl { pub fn new_agent( name: impl ToString, instantiator: impl InstantiateProxiesAndAgent + 'static, - mcp_bridge_mode: crate::McpBridgeMode, ) -> Self { - ConductorImpl::new(Agent, name, Box::new(instantiator), mcp_bridge_mode) + ConductorImpl::new(Agent, name, Box::new(instantiator)) } } impl ConductorImpl { /// Create a conductor in proxy mode (forwards to another conductor). - pub fn new_proxy( - name: impl ToString, - instantiator: impl InstantiateProxies + 'static, - mcp_bridge_mode: crate::McpBridgeMode, - ) -> Self { - ConductorImpl::new(Proxy, name, Box::new(instantiator), mcp_bridge_mode) + pub fn new_proxy(name: impl ToString, instantiator: impl InstantiateProxies + 'static) -> Self { + ConductorImpl::new(Proxy, name, Box::new(instantiator)) } } @@ -244,9 +220,6 @@ impl ConductorImpl { conductor_rx, conductor_tx: conductor_tx.clone(), instantiator: Some(self.instantiator), - bridge_listeners: McpBridgeListeners::default(), - bridge_connections: HashMap::default(), - mcp_bridge_mode: self.mcp_bridge_mode, proxies: Vec::default(), successor: Arc::new(agent_client_protocol::util::internal_error( "successor not initialized", @@ -341,12 +314,6 @@ where conductor_tx: mpsc::Sender, - /// Manages the TCP listeners for MCP connections that will be proxied over ACP. - bridge_listeners: McpBridgeListeners, - - /// Manages active connections to MCP clients. - bridge_connections: HashMap, - /// The instantiator for lazy initialization. /// Set to None after components are instantiated. instantiator: Option, @@ -361,9 +328,6 @@ where /// Populated lazily when the first Initialize request is received; the initial value just returns errors. successor: Arc>, - /// Mode for the MCP bridge (determines how to spawn bridge processes). - mcp_bridge_mode: crate::McpBridgeMode, - /// Optional trace handle for sequence diagram visualization. trace_handle: Option, @@ -379,10 +343,7 @@ where f.debug_struct("ConductorResponder") .field("conductor_rx", &self.conductor_rx) .field("conductor_tx", &self.conductor_tx) - .field("bridge_listeners", &self.bridge_listeners) - .field("bridge_connections", &self.bridge_connections) .field("proxies", &self.proxies) - .field("mcp_bridge_mode", &self.mcp_bridge_mode) .field("trace_handle", &self.trace_handle) .field("host", &self.host) .finish_non_exhaustive() @@ -470,96 +431,6 @@ where ); self.send_message_to_predecessor_of(client, source_component_index, message) } - - // New MCP connection request. Send it back along the chain to get a connection id. - // When the connection id arrives, send a message back into this conductor loop with - // the connection id and the (as yet unspawned) actor. - ConductorMessage::McpConnectionReceived { - acp_id, - connection, - actor, - } => { - // MCP connection requests always come from the agent - // (we must be in agent mode, in fact), so send the MCP request - // to the final proxy. - self.send_request_to_predecessor_of( - client, - self.proxies.len(), - McpConnectRequest { acp_id, meta: None }, - ) - .on_receiving_result({ - let mut conductor_tx = self.conductor_tx.clone(); - async move |result| { - match result { - Ok(response) => conductor_tx - .send(ConductorMessage::McpConnectionEstablished { - response, - actor, - connection, - }) - .await - .map_err(|_| agent_client_protocol::Error::internal_error()), - Err(_) => { - // Error occurred, just drop the connection. - Ok(()) - } - } - } - }) - } - - // MCP connection successfully established. Spawn the actor - // and insert the connection into our map for future reference. - ConductorMessage::McpConnectionEstablished { - response: McpConnectResponse { connection_id, .. }, - actor, - connection, - } => { - self.bridge_connections - .insert(connection_id.clone(), connection); - client.spawn(actor.run(connection_id)) - } - - // Message meant for the MCP client received. Forward it to the appropriate actor's mailbox. - ConductorMessage::McpClientToMcpServer { - connection_id, - message, - } => { - let wrapped = message.map( - |request, responder| { - ( - McpOverAcpMessage { - connection_id: connection_id.clone(), - message: request, - meta: None, - }, - responder, - ) - }, - |notification| McpOverAcpMessage { - connection_id: connection_id.clone(), - message: notification, - meta: None, - }, - ); - - // We only get MCP-over-ACP requests when we are in bridging MCP for the final agent, - // so send them to the final proxy. - self.send_message_to_predecessor_of( - client, - SourceComponentIndex::Successor, - wrapped, - ) - } - - // MCP client disconnected. Remove it from our map and send the - // notification backwards along the chain. - ConductorMessage::McpConnectionDisconnected { notification } => { - // We only get MCP-over-ACP requests when we are in bridging MCP for the final agent. - - self.bridge_connections.remove(¬ification.connection_id); - self.send_notification_to_predecessor_of(client, self.proxies.len(), notification) - } } } @@ -913,7 +784,7 @@ where /// running as a proxy). async fn forward_message_to_agent( &mut self, - client_connection: ConnectionTo, + _client_connection: ConnectionTo, message: Dispatch, agent_connection: ConnectionTo, ) -> Result<(), Error> { @@ -925,63 +796,8 @@ where ) }) .await - .if_request(async |mut request: NewSessionRequest, responder| { - // When forwarding "session/new" to the agent, - // we adjust MCP servers to manage "acp:" URLs. - for mcp_server in &mut request.mcp_servers { - self.bridge_listeners - .transform_mcp_server( - client_connection.clone(), - mcp_server, - &self.conductor_tx, - &self.mcp_bridge_mode, - ) - .await?; - } - - agent_connection - .send_request(request) - .forward_response_to(responder) - }) - .await - .if_request( - async |request: McpOverAcpMessage, responder| { - let McpOverAcpMessage { - connection_id, - message: mcp_request, - .. - } = request; - self.bridge_connections - .get_mut(&connection_id) - .ok_or_else(|| { - agent_client_protocol::util::internal_error(format!( - "unknown connection id: {connection_id}" - )) - })? - .send(Dispatch::Request(mcp_request, responder)) - .await - }, - ) - .await - .if_notification(async |notification: McpOverAcpMessage| { - let McpOverAcpMessage { - connection_id, - message: mcp_notification, - .. - } = notification; - self.bridge_connections - .get_mut(&connection_id) - .ok_or_else(|| { - agent_client_protocol::util::internal_error(format!( - "unknown connection id: {connection_id}" - )) - })? - .send(Dispatch::Notification(mcp_notification)) - .await - }) - .await .otherwise(async |message| { - // Otherwise, just send the message along "as is". + // Forward all other messages to the agent as-is. agent_connection.send_proxied_message_to(Agent, message) }) .await @@ -1276,47 +1092,6 @@ pub enum ConductorMessage { source_component_index: SourceComponentIndex, message: Dispatch, }, - - /// A pending MCP bridge connection request request. - /// The request must be sent back over ACP to receive the connection-id. - /// Once the connection-id is received, the actor must be spawned. - McpConnectionReceived { - /// The acp:$UUID identifier for this bridge - acp_id: String, - - /// The actor that should be spawned once the connection-id is available. - actor: McpBridgeConnectionActor, - - /// The connection to the bridge - connection: McpBridgeConnection, - }, - - /// A pending MCP bridge connection request request. - /// The request must be sent back over ACP to receive the connection-id. - /// Once the connection-id is received, the actor must be spawned. - McpConnectionEstablished { - response: McpConnectResponse, - - /// The actor that should be spawned once the connection-id is available. - actor: McpBridgeConnectionActor, - - /// The connection to the bridge - connection: McpBridgeConnection, - }, - - /// MCP message (request or notification) received from a bridge that needs to be routed to the final proxy. - /// - /// Sent when the bridge receives an MCP tool call from the agent and forwards it - /// to the conductor via TCP. The conductor routes this to the appropriate proxy component. - McpClientToMcpServer { - connection_id: String, - message: Dispatch, - }, - - /// Message sent when MCP client disconnects - McpConnectionDisconnected { - notification: McpDisconnectNotification, - }, } /// Trait implemented for the two links the conductor can use: diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs deleted file mode 100644 index d74e0eb..0000000 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge.rs +++ /dev/null @@ -1,170 +0,0 @@ -pub mod actor; -pub mod http; -pub mod stdio; - -use std::collections::HashMap; -use std::path::PathBuf; - -use agent_client_protocol::schema::{McpServer, McpServerHttp, McpServerStdio}; -use agent_client_protocol::{ConnectionTo, Dispatch, Role}; -use futures::{SinkExt, channel::mpsc}; -use tokio::net::TcpListener; -use tracing::info; - -pub use self::actor::McpBridgeConnectionActor; -use crate::conductor::ConductorMessage; - -/// Maintains bridges for MCP message routing. -#[derive(Default, Debug)] -pub struct McpBridgeListeners { - /// Mapping of acp:$UUID URLs to TCP bridge information for MCP message routing - listeners: HashMap, -} - -/// Information about an MCP bridge that is listening for connections from MCP clients. -#[derive(Clone, Debug)] -pub(super) struct McpBridgeListener { - /// The replacement MCP server - pub server: McpServer, -} - -/// Connection handle for sending messages to an MCP client. -#[derive(Clone, Debug)] -pub struct McpBridgeConnection { - /// Channel to send messages from MCP server (ACP proxy) to the MCP client (ACP agent). - to_mcp_client_tx: mpsc::Sender, -} - -impl McpBridgeConnection { - pub fn new(to_mcp_client_tx: mpsc::Sender) -> Self { - Self { to_mcp_client_tx } - } - - pub async fn send(&mut self, message: Dispatch) -> Result<(), agent_client_protocol::Error> { - self.to_mcp_client_tx - .send(message) - .await - .map_err(|_| agent_client_protocol::Error::internal_error()) - } -} - -impl McpBridgeListeners { - /// Transforms MCP servers with `acp:$UUID` URLs for agents that need bridging. - /// - /// For each MCP server with an `acp:` URL: - /// 1. Spawns a TCP listener on an ephemeral port - /// 2. Stores the mapping for message routing - /// 3. Transforms the server to use either stdio or HTTP transport depending on bridge mode - /// - /// Other MCP servers are left unchanged. - pub async fn transform_mcp_server( - &mut self, - connection: ConnectionTo, - mcp_server: &mut McpServer, - conductor_tx: &mpsc::Sender, - mcp_bridge_mode: &crate::McpBridgeMode, - ) -> Result<(), agent_client_protocol::Error> { - use agent_client_protocol::schema::McpServer; - - let McpServer::Http(http) = mcp_server else { - return Ok(()); - }; - - if !http.url.starts_with("acp:") { - return Ok(()); - } - - if !http.headers.is_empty() { - return Err(agent_client_protocol::Error::internal_error()); - } - - let name = &http.name; - let url = &http.url; - - info!( - server_name = name, - acp_id = url, - "Detected MCP server with ACP transport, spawning TCP bridge" - ); - - // Create oneshot channel for session_id delivery - let transformed = self - .spawn_bridge(connection, name, url, conductor_tx, mcp_bridge_mode) - .await?; - *mcp_server = transformed; - Ok(()) - } - - /// Spawn a bridge listener (HTTP or stdio) for an MCP server with ACP transport - async fn spawn_bridge( - &mut self, - connection: ConnectionTo, - server_name: &str, - acp_id: &str, - conductor_tx: &mpsc::Sender, - mcp_bridge_mode: &crate::McpBridgeMode, - ) -> anyhow::Result { - // If there is already a listener for the ACP URL, return its server - if let Some(listener) = self.listeners.get(acp_id) { - return Ok(listener.server.clone()); - } - - // Bind to ephemeral port - let tcp_listener = TcpListener::bind("127.0.0.1:0").await?; - let tcp_port = tcp_listener.local_addr()?.port(); - - info!(acp_id = acp_id, tcp_port, "Bound listener for MCP bridge"); - - let new_server = match mcp_bridge_mode { - crate::McpBridgeMode::Stdio { conductor_command } => McpServer::Stdio( - McpServerStdio::new( - server_name.to_string(), - PathBuf::from(&conductor_command[0]), - ) - .args( - conductor_command[1..] - .iter() - .cloned() - .chain(vec!["mcp".to_string(), format!("{tcp_port}")]) - .collect::>(), - ), - ), - - crate::McpBridgeMode::Http => McpServer::Http(McpServerHttp::new( - server_name.to_string(), - format!("http://localhost:{tcp_port}"), - )), - }; - - // remember for later - self.listeners.insert( - acp_id.to_string(), - McpBridgeListener { - server: new_server.clone(), - }, - ); - - connection.spawn({ - let acp_id = acp_id.to_string(); - let conductor_tx = conductor_tx.clone(); - let mcp_bridge_mode = mcp_bridge_mode.clone(); - async move { - info!( - acp_id = acp_id, - tcp_port, "now accepting bridge connections" - ); - - match mcp_bridge_mode { - crate::McpBridgeMode::Stdio { - conductor_command: _, - } => stdio::run_tcp_listener(tcp_listener, acp_id, conductor_tx).await, - crate::McpBridgeMode::Http => { - http::run_http_listener(tcp_listener, acp_id, conductor_tx).await - } - } - } - })?; - - Ok(new_server) - } -} diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/actor.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/actor.rs deleted file mode 100644 index 557e08c..0000000 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/actor.rs +++ /dev/null @@ -1,88 +0,0 @@ -use agent_client_protocol::{ - ConnectTo, Dispatch, DynConnectTo, role::mcp, schema::McpDisconnectNotification, -}; -use futures::{SinkExt as _, StreamExt as _, channel::mpsc}; -use tracing::info; - -use crate::conductor::ConductorMessage; - -/// Trait for actors that handle MCP bridge connections. -/// -/// Implementations bridge between MCP clients and the conductor's ACP message flow. -#[derive(Debug)] -pub struct McpBridgeConnectionActor { - /// How to connect to the MCP server - transport: DynConnectTo, - - /// Sender for messages to the conductor - conductor_tx: mpsc::Sender, - - /// Receiver for messages from the conductor to the MCP client - to_mcp_client_rx: mpsc::Receiver, -} - -impl McpBridgeConnectionActor { - pub fn new( - component: impl ConnectTo, - conductor_tx: mpsc::Sender, - to_mcp_client_rx: mpsc::Receiver, - ) -> Self { - Self { - transport: DynConnectTo::new(component), - conductor_tx, - to_mcp_client_rx, - } - } - - pub async fn run(self, connection_id: String) -> Result<(), agent_client_protocol::Error> { - info!(connection_id, "MCP bridge connected"); - - let McpBridgeConnectionActor { - transport, - mut conductor_tx, - to_mcp_client_rx, - } = self; - - let result = mcp::Client - .builder() - .name(format!("mpc-client-to-conductor({connection_id})")) - // When we receive a message from the MCP client, forward it to the conductor - .on_receive_dispatch( - { - let mut conductor_tx = conductor_tx.clone(); - let connection_id = connection_id.clone(); - async move |message: agent_client_protocol::Dispatch, _cx| { - conductor_tx - .send(ConductorMessage::McpClientToMcpServer { - connection_id: connection_id.clone(), - message, - }) - .await - .map_err(|_| agent_client_protocol::Error::internal_error()) - } - }, - agent_client_protocol::on_receive_dispatch!(), - ) - // When we receive messages from the conductor, forward them to the MCP client - .connect_with(transport, async move |mcp_connection_to_client| { - let mut to_mcp_client_rx = to_mcp_client_rx; - while let Some(message) = to_mcp_client_rx.next().await { - mcp_connection_to_client.send_proxied_message(message)?; - } - Ok(()) - }) - .await; - - conductor_tx - .send(ConductorMessage::McpConnectionDisconnected { - notification: McpDisconnectNotification { - connection_id, - meta: None, - }, - }) - .await - .map_err(|_| agent_client_protocol::Error::internal_error())?; - - result - } -} diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs deleted file mode 100644 index 53436c7..0000000 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/http.rs +++ /dev/null @@ -1,570 +0,0 @@ -use agent_client_protocol::{BoxFuture, Channel, ConnectTo, jsonrpcmsg::Message, role::mcp}; -use axum::{ - Router, - extract::State, - http::StatusCode, - response::{IntoResponse, Response, Sse}, - routing::post, -}; -use futures::{SinkExt, StreamExt as _, channel::mpsc, future::Either, stream::Stream}; -use futures_concurrency::future::FutureExt as _; -use futures_concurrency::stream::StreamExt as _; -use rustc_hash::FxHashMap; -use std::{ - collections::{HashMap, VecDeque}, - pin::pin, - sync::Arc, -}; -use tokio::net::TcpListener; - -use crate::conductor::{ - ConductorMessage, - mcp_bridge::{McpBridgeConnection, McpBridgeConnectionActor}, -}; - -/// Runs an HTTP listener for MCP bridge connections -pub async fn run_http_listener( - tcp_listener: TcpListener, - acp_id: String, - mut conductor_tx: mpsc::Sender, -) -> Result<(), agent_client_protocol::Error> { - let (to_mcp_client_tx, to_mcp_client_rx) = mpsc::channel(128); - - // When we send this message to the conductor, - // it is going to go through a step or two and eventually - // spawn the McpBridgeConnectionActor, which will ferry MCP requests - // back and forth. - conductor_tx - .send(ConductorMessage::McpConnectionReceived { - acp_id, - actor: McpBridgeConnectionActor::new( - HttpMcpBridge::new(tcp_listener), - conductor_tx.clone(), - to_mcp_client_rx, - ), - connection: McpBridgeConnection::new(to_mcp_client_tx), - }) - .await - .map_err(|_| agent_client_protocol::Error::internal_error())?; - - Ok(()) -} - -/// A component that receives HTTP requests/responses using the HTTP transport defined by the MCP protocol. -struct HttpMcpBridge { - listener: tokio::net::TcpListener, -} - -impl HttpMcpBridge { - /// Creates a new HTTP-MCP bridge from an existing TCP listener. - fn new(listener: tokio::net::TcpListener) -> Self { - Self { listener } - } -} - -impl ConnectTo for HttpMcpBridge { - async fn connect_to( - self, - client: impl ConnectTo, - ) -> Result<(), agent_client_protocol::Error> { - let (channel, serve_self) = self.into_channel_and_future(); - match futures::future::select(pin!(client.connect_to(channel)), serve_self).await { - Either::Left((result, _)) | Either::Right((result, _)) => result, - } - } - - fn into_channel_and_future( - self, - ) -> ( - Channel, - BoxFuture<'static, Result<(), agent_client_protocol::Error>>, - ) - where - Self: Sized, - { - let (channel_a, channel_b) = Channel::duplex(); - (channel_a, Box::pin(run(self.listener, channel_b))) - } -} - -/// Error type that we use to respond to malformed HTTP requests. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -struct HttpError(#[from] agent_client_protocol::Error); - -impl From for HttpError { - fn from(error: axum::Error) -> Self { - HttpError(agent_client_protocol::util::internal_error(error)) - } -} - -impl IntoResponse for HttpError { - fn into_response(self) -> Response { - let message = format!("Error: {}", self.0); - (StatusCode::INTERNAL_SERVER_ERROR, message).into_response() - } -} - -/// Run a webserver listening on `listener` for HTTP requests at `/` -/// and communicating those requests over `channel` to the JSON-RPC server. -async fn run(listener: TcpListener, channel: Channel) -> Result<(), agent_client_protocol::Error> { - let (registration_tx, registration_rx) = mpsc::unbounded(); - - let state = BridgeState { registration_tx }; - - // The way that the MCP protocol works is a bit "special". - // - // Clients *POST* messages to `/`. Those are submitted to the MCP server. - // If the message is a REQUEST, then the client waits until it gets a reply. - // It expects the server to close the connection after responding. - // - // Clients can also issue a *GET* request. This will result in a stream of messages. - // - // Non-reply messages can be send to any open stream (POST, GET, etc) but must be sent to - // exactly one. - // - // There are provisions for "resuming" from a blocked point by tagging each message in the SSE stream - // with an id, but we are not implementing that because I am lazy. - async { - let app = Router::new() - .route("/", post(handle_post).get(handle_get)) - .with_state(Arc::new(state)); - - axum::serve(listener, app) - .await - .map_err(agent_client_protocol::util::internal_error) - } - .race(RunningServer::new().run(channel, registration_rx)) - .await -} - -/// The state we pass to our POST/GET handlers. -struct BridgeState { - /// Where to send registration messages. - registration_tx: mpsc::UnboundedSender, -} - -/// Messages from HTTP handlers to the bridge server. -#[derive(Debug)] -enum HttpMessage { - /// A JSON-RPC request (has an id, expects a response via the channel) - Request { - http_request_id: uuid::Uuid, - request: agent_client_protocol::jsonrpcmsg::Request, - response_tx: mpsc::UnboundedSender, - }, - /// A JSON-RPC notification (no id, no response expected) - Notification { - http_request_id: uuid::Uuid, - request: agent_client_protocol::jsonrpcmsg::Request, - }, - /// A JSON-RPC response from the client - Response { - http_request_id: uuid::Uuid, - response: agent_client_protocol::jsonrpcmsg::Response, - }, - /// A GET request to open an SSE stream for server-initiated messages - Get { - http_request_id: uuid::Uuid, - response_tx: mpsc::UnboundedSender, - }, -} - -/// Clone of `agent_client_protocol::jsonrpcmsg::Id` since for unfathomable reasons that does not impl Hash -#[derive(Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Clone)] -enum JsonRpcId { - /// String identifier - String(String), - /// Numeric identifier - Number(u64), - /// Null identifier (for notifications) - Null, -} - -impl From for JsonRpcId { - fn from(id: agent_client_protocol::jsonrpcmsg::Id) -> Self { - match id { - agent_client_protocol::jsonrpcmsg::Id::String(s) => JsonRpcId::String(s), - agent_client_protocol::jsonrpcmsg::Id::Number(n) => JsonRpcId::Number(n), - agent_client_protocol::jsonrpcmsg::Id::Null => JsonRpcId::Null, - } - } -} - -struct RunningServer { - waiting_sessions: FxHashMap, - general_sessions: Vec, - message_deque: VecDeque, -} - -impl RunningServer { - fn new() -> Self { - RunningServer { - waiting_sessions: HashMap::default(), - general_sessions: Vec::default(), - message_deque: VecDeque::with_capacity(32), - } - } - - /// The main loop: listen for incoming HTTP messages and outgoing JSON-RPC messages. - /// - /// # Parameters - /// - /// * `channel`: The channel to use for sending/receiving JSON-RPC messages. - /// * `http_rx`: The receiver for messages from HTTP handlers. - async fn run( - mut self, - mut channel: Channel, - http_rx: mpsc::UnboundedReceiver, - ) -> Result<(), agent_client_protocol::Error> { - #[derive(Debug)] - enum MultiplexMessage { - FromHttpToChannel(HttpMessage), - FromChannelToHttp( - Result, - ), - } - - let mut merged_stream = http_rx - .map(MultiplexMessage::FromHttpToChannel) - .merge(channel.rx.map(MultiplexMessage::FromChannelToHttp)); - - while let Some(message) = merged_stream.next().await { - tracing::trace!(?message, "received message"); - - match message { - MultiplexMessage::FromHttpToChannel(http_message) => { - self.handle_http_message(http_message, &mut channel.tx)?; - } - - MultiplexMessage::FromChannelToHttp(message) => { - let message = message.unwrap_or_else(|err| { - agent_client_protocol::jsonrpcmsg::Message::Response( - agent_client_protocol::jsonrpcmsg::Response::error( - agent_client_protocol::util::into_jsonrpc_error(err), - None, - ), - ) - }); - tracing::debug!( - queue_len = self.message_deque.len() + 1, - ?message, - "enqueuing outgoing message" - ); - self.message_deque.push_back(message); - } - } - - self.drain_jsonrpc_messages(); - } - - tracing::trace!("http connection terminating"); - - Ok(()) - } - - /// Handle an incoming HTTP message (request, notification, response, or GET). - fn handle_http_message( - &mut self, - message: HttpMessage, - channel_tx: &mut mpsc::UnboundedSender< - Result, - >, - ) -> Result<(), agent_client_protocol::Error> { - match message { - HttpMessage::Request { - http_request_id, - request, - response_tx, - } => { - tracing::debug!(%http_request_id, ?request, "handling request"); - let request_id = request.id.clone().map(JsonRpcId::from); - - // Send to the JSON-RPC server - channel_tx - .unbounded_send(Ok(Message::Request(request))) - .map_err(agent_client_protocol::util::internal_error)?; - - // Register to receive the response - let session = RegisteredSession::new(response_tx); - if let Some(id) = request_id { - tracing::debug!(%http_request_id, session_id = %session.id, ?id, "registering waiting session"); - self.waiting_sessions.insert(id, session); - } else { - // Request without id - treat like a general session - tracing::debug!(%http_request_id, session_id = %session.id, "registering general session (request without id)"); - self.general_sessions.push(session); - } - } - - HttpMessage::Notification { - http_request_id, - request, - } => { - tracing::debug!(%http_request_id, ?request, "handling notification"); - // Just forward to the server, no response tracking needed - channel_tx - .unbounded_send(Ok(Message::Request(request))) - .map_err(agent_client_protocol::util::internal_error)?; - } - - HttpMessage::Response { - http_request_id, - response, - } => { - tracing::debug!(%http_request_id, ?response, "handling response"); - // Forward to the server - channel_tx - .unbounded_send(Ok(Message::Response(response))) - .map_err(agent_client_protocol::util::internal_error)?; - } - - HttpMessage::Get { - http_request_id, - response_tx, - } => { - let session = RegisteredSession::new(response_tx); - tracing::debug!( - %http_request_id, - session_id = %session.id, - queued_messages = self.message_deque.len(), - "handling GET (opening SSE stream)" - ); - // Register as a general session to receive server-initiated messages - self.general_sessions.push(session); - } - } - - // Purge closed sessions for good hygiene - self.purge_closed_sessions(); - - Ok(()) - } - - /// Remove messages from the queue and send them. - /// Stop if we cannot find places to send them. - fn drain_jsonrpc_messages(&mut self) { - if !self.message_deque.is_empty() { - tracing::debug!( - queue_len = self.message_deque.len(), - general_sessions = self.general_sessions.len(), - waiting_sessions = self.waiting_sessions.len(), - "draining message queue" - ); - } - - while let Some(message) = self.message_deque.pop_front() { - match self.try_dispatch_jsonrpc_message(message) { - None => { - tracing::debug!( - remaining = self.message_deque.len(), - "message dispatched successfully" - ); - } - - Some(message) => { - tracing::debug!( - remaining = self.message_deque.len() + 1, - "no available session, re-enqueuing message" - ); - self.message_deque.push_front(message); - break; - } - } - } - } - - /// Invoked when there is an outgoing JSON-RPC message to send. - /// Tries to find a suitable place to send it. - /// If it succeeds, returns `None`. - /// If there is no place to send it, returns `Some(message)`. - fn try_dispatch_jsonrpc_message( - &mut self, - mut message: agent_client_protocol::jsonrpcmsg::Message, - ) -> Option { - // Extract the id of the message we are replying to, if any - let message_id = match &message { - Message::Response(response) => response.id.as_ref().map(|v| v.clone().into()), - Message::Request(_) => None, - }; - - tracing::debug!(?message_id, "attempting to dispatch JSON-RPC message"); - - // If there is a specific id, try to send the message to that sender. - // This also removes them from the list of waiting sessions. - if let Some(ref message_id) = message_id - && let Some(session) = self.waiting_sessions.remove(message_id) - { - tracing::debug!(session_id = %session.id, "found waiting session, attempting send"); - - match session.outgoing_tx.unbounded_send(message) { - // Successfully sent the message, return - Ok(()) => { - tracing::debug!(session_id = %session.id, "sent to waiting session"); - return None; - } - - // If the sender died, just recover the message and send it to anyone. - Err(m) => { - tracing::debug!(session_id = %session.id, "waiting session disconnected"); - // If that sender is dead, remove them from the list - // and recover the message. - assert!(m.is_disconnected()); - message = m.into_inner(); - } - } - } - - // Try to find *somewhere* to send the message - self.purge_closed_sessions(); - tracing::debug!( - general_sessions = self.general_sessions.len(), - waiting_sessions = self.waiting_sessions.len(), - "trying to find any active session" - ); - let all_sessions = self - .general_sessions - .iter_mut() - .chain(self.waiting_sessions.values_mut()); - for session in all_sessions { - tracing::trace!(session_id = %session.id, "trying session"); - match session.outgoing_tx.unbounded_send(message) { - Ok(()) => { - tracing::debug!(session_id = %session.id, "sent to session"); - return None; - } - - Err(m) => { - tracing::debug!(session_id = %session.id, "session disconnected, trying next"); - assert!(m.is_disconnected()); - message = m.into_inner(); - } - } - } - - // If we don't find anywhere to send the message, return it. - Some(message) - } - - /// Purge sessions from the bridge state where the receiver is closed. - /// This happens when the HTTP client disconnects. - fn purge_closed_sessions(&mut self) { - self.general_sessions - .retain(|session| !session.outgoing_tx.is_closed()); - self.waiting_sessions - .retain(|_, session| !session.outgoing_tx.is_closed()); - } -} - -struct RegisteredSession { - id: uuid::Uuid, - outgoing_tx: mpsc::UnboundedSender, -} - -impl RegisteredSession { - fn new(outgoing_tx: mpsc::UnboundedSender) -> Self { - Self { - id: uuid::Uuid::new_v4(), - outgoing_tx, - } - } -} - -/// Accept a POST request carrying a JSON-RPC message from an MCP client. -/// For requests (messages with id), we return an SSE stream. -/// For notifications/responses (messages without id), we return 202 Accepted. -async fn handle_post( - State(state): State>, - body: String, -) -> Result { - let http_request_id = uuid::Uuid::new_v4(); - - // Parse incoming JSON-RPC message - let message: agent_client_protocol::jsonrpcmsg::Message = - serde_json::from_str(&body).map_err(agent_client_protocol::util::parse_error)?; - - match message { - Message::Request(request) if request.id.is_some() => { - tracing::debug!(%http_request_id, method = %request.method, "POST request received"); - // Request with id - return SSE stream for response - let (tx, mut rx) = mpsc::unbounded(); - state - .registration_tx - .unbounded_send(HttpMessage::Request { - http_request_id, - request, - response_tx: tx, - }) - .map_err(agent_client_protocol::util::internal_error)?; - - let stream = async_stream::stream! { - while let Some(message) = rx.next().await { - tracing::debug!(%http_request_id, "sending SSE event"); - match axum::response::sse::Event::default().json_data(message) { - Ok(v) => yield Ok(v), - Err(e) => yield Err(HttpError::from(e)), - } - } - tracing::debug!(%http_request_id, "SSE stream completed"); - }; - Ok(Sse::new(stream).into_response()) - } - - Message::Request(request) => { - tracing::debug!(%http_request_id, method = %request.method, "POST notification received"); - // Request without id is a notification - state - .registration_tx - .unbounded_send(HttpMessage::Notification { - http_request_id, - request, - }) - .map_err(agent_client_protocol::util::internal_error)?; - Ok(StatusCode::ACCEPTED.into_response()) - } - - Message::Response(response) => { - tracing::debug!(%http_request_id, "POST response received"); - // Response from client (rare, but possible in MCP) - state - .registration_tx - .unbounded_send(HttpMessage::Response { - http_request_id, - response, - }) - .map_err(agent_client_protocol::util::internal_error)?; - Ok(StatusCode::ACCEPTED.into_response()) - } - } -} - -/// Accept a GET request from an MCP client. -/// Opens an SSE stream for server-initiated messages. -async fn handle_get( - State(state): State>, -) -> Result>>, HttpError> { - let http_request_id = uuid::Uuid::new_v4(); - tracing::debug!(%http_request_id, "GET request received"); - - let (tx, mut rx) = mpsc::unbounded(); - state - .registration_tx - .unbounded_send(HttpMessage::Get { - http_request_id, - response_tx: tx, - }) - .map_err(agent_client_protocol::util::internal_error)?; - - let stream = async_stream::stream! { - while let Some(message) = rx.next().await { - tracing::debug!(%http_request_id, "sending SSE event"); - match axum::response::sse::Event::default().json_data(message) { - Ok(v) => yield Ok(v), - Err(e) => yield Err(HttpError::from(e)), - } - } - tracing::debug!(%http_request_id, "SSE stream completed"); - }; - - Ok(Sse::new(stream)) -} diff --git a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs b/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs deleted file mode 100644 index a92f7d6..0000000 --- a/src/agent-client-protocol-conductor/src/conductor/mcp_bridge/stdio.rs +++ /dev/null @@ -1,54 +0,0 @@ -use agent_client_protocol::Dispatch; -use futures::{SinkExt, channel::mpsc}; -use tokio::net::{TcpListener, TcpStream}; -use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; - -use crate::conductor::ConductorMessage; - -use super::{McpBridgeConnection, McpBridgeConnectionActor}; - -/// Runs the stdio bridge TCP listener, accepting connections and creating bridge actors for each. -/// -/// Loops indefinitely, accepting incoming TCP connections and spawning an MCP bridge actor -/// for each connection to handle bidirectional message forwarding between the MCP client -/// and the conductor. -pub async fn run_tcp_listener( - tcp_listener: TcpListener, - acp_id: String, - mut conductor_tx: mpsc::Sender, -) -> Result<(), agent_client_protocol::Error> { - // Accept connections - loop { - let (stream, _addr) = tcp_listener - .accept() - .await - .map_err(agent_client_protocol::Error::into_internal_error)?; - - let (to_mcp_client_tx, to_mcp_client_rx) = mpsc::channel(128); - - conductor_tx - .send(ConductorMessage::McpConnectionReceived { - acp_id: acp_id.clone(), - actor: make_stdio_actor(stream, conductor_tx.clone(), to_mcp_client_rx), - connection: McpBridgeConnection::new(to_mcp_client_tx), - }) - .await - .map_err(|_| agent_client_protocol::Error::internal_error())?; - } -} - -fn make_stdio_actor( - stream: TcpStream, - conductor_tx: mpsc::Sender, - to_mcp_client_rx: mpsc::Receiver, -) -> McpBridgeConnectionActor { - let (read_half, write_half) = stream.into_split(); - - // Establish bidirectional JSON-RPC connection - // The bridge will send MCP requests (tools/call, etc.) to the conductor - // The conductor can also send responses back - let transport = - agent_client_protocol::ByteStreams::new(write_half.compat_write(), read_half.compat()); - - McpBridgeConnectionActor::new(transport, conductor_tx, to_mcp_client_rx) -} diff --git a/src/agent-client-protocol-conductor/src/lib.rs b/src/agent-client-protocol-conductor/src/lib.rs index d82b63a..a4f701a 100644 --- a/src/agent-client-protocol-conductor/src/lib.rs +++ b/src/agent-client-protocol-conductor/src/lib.rs @@ -168,21 +168,6 @@ impl trace::WriteEvent for TraceHandleWriter { } } -/// Mode for the MCP bridge. -#[derive(Debug, Clone, Default)] -pub enum McpBridgeMode { - /// Use stdio-based MCP bridge with a conductor subprocess. - Stdio { - /// Command and args to spawn conductor MCP bridge processes. - /// E.g., vec!["conductor"] or vec!["cargo", "run", "-p", "conductor", "--"] - conductor_command: Vec, - }, - - /// Use HTTP-based MCP bridge - #[default] - Http, -} - #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct ConductorArgs { @@ -358,11 +343,7 @@ async fn initialize_conductor( trace_writer: Option, name: String, components: Vec, - new_conductor: impl FnOnce( - String, - CommandLineComponents, - crate::McpBridgeMode, - ) -> ConductorImpl, + new_conductor: impl FnOnce(String, CommandLineComponents) -> ConductorImpl, ) -> Result<(), agent_client_protocol::Error> { // Parse agents and optionally wrap with debug callbacks let providers: Vec = components @@ -385,11 +366,7 @@ async fn initialize_conductor( }; // Create conductor with optional trace writer - let mut conductor = new_conductor( - name, - CommandLineComponents(providers), - McpBridgeMode::default(), - ); + let mut conductor = new_conductor(name, CommandLineComponents(providers)); if let Some(writer) = trace_writer { conductor = conductor.with_trace_writer(writer); } diff --git a/src/agent-client-protocol-conductor/tests/arrow_proxy_eliza.rs b/src/agent-client-protocol-conductor/tests/arrow_proxy_eliza.rs index c39266b..e64f9e4 100644 --- a/src/agent-client-protocol-conductor/tests/arrow_proxy_eliza.rs +++ b/src/agent-client-protocol-conductor/tests/arrow_proxy_eliza.rs @@ -8,7 +8,7 @@ //! Run `just prep-tests` before running this test. use agent_client_protocol::AcpAgent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::test_binaries::{arrow_proxy_example, testy}; use agent_client_protocol_test::testy::TestyCommand; use tokio::io::duplex; @@ -32,7 +32,6 @@ async fn test_conductor_with_arrow_proxy_and_test_agent() -> Result<(), agent_cl ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(test_agent).proxy(arrow_proxy_agent), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/empty_conductor_eliza.rs b/src/agent-client-protocol-conductor/tests/empty_conductor_eliza.rs index c1f87a8..87faa5a 100644 --- a/src/agent-client-protocol-conductor/tests/empty_conductor_eliza.rs +++ b/src/agent-client-protocol-conductor/tests/empty_conductor_eliza.rs @@ -7,7 +7,7 @@ //! 4. The full chain works end-to-end use agent_client_protocol::{Conductor, ConnectTo, Proxy}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use tokio::io::duplex; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; @@ -24,11 +24,7 @@ impl ConnectTo for MockEmptyConductor { // Create an empty conductor with no components - it should act as a passthrough let empty_components: Vec> = vec![]; ConnectTo::::connect_to( - ConductorImpl::new_proxy( - "empty-conductor".to_string(), - empty_components, - McpBridgeMode::default(), - ), + ConductorImpl::new_proxy("empty-conductor".to_string(), empty_components), client, ) .await @@ -57,7 +53,6 @@ async fn test_conductor_with_empty_conductor_and_test_agent() ConductorImpl::new_agent( "outer-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(MockEmptyConductor), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/initialization_sequence.rs b/src/agent-client-protocol-conductor/tests/initialization_sequence.rs index dcf0a5a..03e9b34 100644 --- a/src/agent-client-protocol-conductor/tests/initialization_sequence.rs +++ b/src/agent-client-protocol-conductor/tests/initialization_sequence.rs @@ -10,7 +10,7 @@ use agent_client_protocol::schema::{ ProtocolVersion, }; use agent_client_protocol::{Agent, Client, Conductor, ConnectTo, DynConnectTo, Proxy}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::Testy; use std::sync::Arc; use std::sync::Mutex; @@ -137,7 +137,6 @@ async fn run_test_with_components( ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxies(proxies), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_out.compat_write(), @@ -305,7 +304,6 @@ async fn run_bad_proxy_test( ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(agent).proxies(proxies), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_out.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/mcp-integration.rs b/src/agent-client-protocol-conductor/tests/mcp-integration.rs index 174f0b9..def555a 100644 --- a/src/agent-client-protocol-conductor/tests/mcp-integration.rs +++ b/src/agent-client-protocol-conductor/tests/mcp-integration.rs @@ -12,8 +12,7 @@ use agent_client_protocol::schema::{ ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, ProtocolVersion, SessionNotification, TextContent, }; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; -use agent_client_protocol_test::test_binaries; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use futures::{SinkExt, StreamExt, channel::mpsc}; @@ -33,13 +32,7 @@ async fn recv( .map_err(|_| agent_client_protocol::Error::internal_error())? } -fn conductor_command() -> Vec { - let binary_path = test_binaries::conductor_binary(); - vec![binary_path.to_string_lossy().to_string()] -} - async fn run_test_with_mode( - mode: McpBridgeMode, components: ProxiesAndAgent, editor_task: impl AsyncFnOnce( agent_client_protocol::ConnectionTo, @@ -64,7 +57,7 @@ async fn run_test_with_mode( .builder() .name("editor-to-connector") .with_spawned(|_cx| async move { - ConductorImpl::new_agent("conductor".to_string(), components, mode) + ConductorImpl::new_agent("conductor".to_string(), components) .run(agent_client_protocol::ByteStreams::new( conductor_out.compat_write(), conductor_in.compat(), @@ -79,9 +72,6 @@ async fn run_test_with_mode( #[tokio::test] async fn test_proxy_provides_mcp_tools_stdio() -> Result<(), agent_client_protocol::Error> { run_test_with_mode( - McpBridgeMode::Stdio { - conductor_command: conductor_command(), - }, ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), async |connection_to_editor| { // Send initialization request @@ -123,7 +113,6 @@ async fn test_proxy_provides_mcp_tools_stdio() -> Result<(), agent_client_protoc #[tokio::test] async fn test_proxy_provides_mcp_tools_http() -> Result<(), agent_client_protocol::Error> { run_test_with_mode( - McpBridgeMode::Http, ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), async |connection_to_editor| { // Send initialization request @@ -162,6 +151,7 @@ async fn test_proxy_provides_mcp_tools_http() -> Result<(), agent_client_protoco } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_agent_handles_prompt() -> Result<(), agent_client_protocol::Error> { // Initialize tracing for debug output drop( @@ -183,7 +173,6 @@ async fn test_agent_handles_prompt() -> Result<(), agent_client_protocol::Error> ConductorImpl::new_agent( "mcp-integration-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/mcp_server_handler_chain.rs b/src/agent-client-protocol-conductor/tests/mcp_server_handler_chain.rs index c2b1f9c..a55b404 100644 --- a/src/agent-client-protocol-conductor/tests/mcp_server_handler_chain.rs +++ b/src/agent-client-protocol-conductor/tests/mcp_server_handler_chain.rs @@ -11,7 +11,7 @@ use agent_client_protocol::schema::{ NewSessionResponse, ProtocolVersion, SessionId, }; use agent_client_protocol::{Agent, Client, Conductor, ConnectTo, DynConnectTo, Proxy}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -172,7 +172,6 @@ async fn run_test( ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(agent).proxies(proxies), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_out.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/nested_arrow_proxy.rs b/src/agent-client-protocol-conductor/tests/nested_arrow_proxy.rs index 78fc58f..6539226 100644 --- a/src/agent-client-protocol-conductor/tests/nested_arrow_proxy.rs +++ b/src/agent-client-protocol-conductor/tests/nested_arrow_proxy.rs @@ -15,7 +15,7 @@ //! Run `just prep-tests` before running this test. use agent_client_protocol::AcpAgent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::test_binaries::{arrow_proxy_example, testy}; use agent_client_protocol_test::testy::TestyCommand; use tokio::io::duplex; @@ -41,7 +41,6 @@ async fn test_conductor_with_two_external_arrow_proxies() -> Result<(), agent_cl ProxiesAndAgent::new(agent) .proxy(arrow_proxy1) .proxy(arrow_proxy2), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/nested_conductor.rs b/src/agent-client-protocol-conductor/tests/nested_conductor.rs index db53d96..c6ee377 100644 --- a/src/agent-client-protocol-conductor/tests/nested_conductor.rs +++ b/src/agent-client-protocol-conductor/tests/nested_conductor.rs @@ -21,7 +21,7 @@ use agent_client_protocol::AcpAgent; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::arrow_proxy::run_arrow_proxy; use agent_client_protocol_test::test_binaries::{arrow_proxy_example, conductor_binary, testy}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; @@ -70,7 +70,6 @@ impl ConnectTo for MockInnerConductor { agent_client_protocol_conductor::ConductorImpl::new_proxy( "inner-conductor".to_string(), components, - McpBridgeMode::default(), ), client, ) @@ -93,7 +92,6 @@ async fn test_nested_conductor_with_arrow_proxies() -> Result<(), agent_client_p ConductorImpl::new_agent( "outer-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(MockInnerConductor::new(2)), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), @@ -162,7 +160,6 @@ async fn test_nested_conductor_with_external_arrow_proxies() ConductorImpl::new_agent( "outer-conductor".to_string(), ProxiesAndAgent::new(agent).proxy(inner_conductor), - McpBridgeMode::default(), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs b/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs index adb1f07..557766e 100644 --- a/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs @@ -6,7 +6,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Agent, Conductor, ConnectTo, Proxy, Role, RunWithConnectionTo}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -17,11 +17,11 @@ use std::sync::Mutex; /// This validates the scoped lifetime feature - the tool closure captures /// a reference to `collected_values` which lives on the stack. #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_scoped_mcp_server_through_proxy() -> Result<(), agent_client_protocol::Error> { let conductor = ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(ScopedProxy), - McpBridgeMode::default(), ); let result = yopo::prompt( @@ -48,6 +48,7 @@ async fn test_scoped_mcp_server_through_proxy() -> Result<(), agent_client_proto /// The MCP server captures a reference to stack-local data that lives for /// the duration of the session. #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_scoped_mcp_server_through_session() -> Result<(), agent_client_protocol::Error> { // Run the client agent_client_protocol::Client.builder() @@ -55,7 +56,6 @@ async fn test_scoped_mcp_server_through_session() -> Result<(), agent_client_pro ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(Testy::new()), - McpBridgeMode::default(), ), async |cx| { // Initialize first diff --git a/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs b/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs index 5b65118..f2e50d5 100644 --- a/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs +++ b/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs @@ -5,7 +5,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -56,12 +56,12 @@ impl + 'static + Send> ConnectTo } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_returning_string() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_test_proxy()), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -82,12 +82,12 @@ async fn test_tool_returning_string() -> Result<(), agent_client_protocol::Error } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_returning_integer() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_test_proxy()), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "test_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs index 07fe421..3ee413e 100644 --- a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs +++ b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs @@ -11,7 +11,7 @@ use agent_client_protocol::RunWithConnectionTo; use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -68,6 +68,7 @@ impl + 'static + Send> ConnectTo } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_list_tools_from_mcp_server() -> Result<(), agent_client_protocol::Error> { use expect_test::expect; @@ -75,7 +76,6 @@ async fn test_list_tools_from_mcp_server() -> Result<(), agent_client_protocol:: ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_echo_proxy()), - McpBridgeMode::default(), ), TestyCommand::ListTools { server: "echo_server".to_string(), @@ -94,12 +94,12 @@ async fn test_list_tools_from_mcp_server() -> Result<(), agent_client_protocol:: } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_session_id_delivered_to_mcp_tools() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_echo_proxy()), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "echo_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs b/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs index cb1b865..8413c94 100644 --- a/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs +++ b/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs @@ -5,7 +5,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -107,12 +107,12 @@ impl + 'static + Send> ConnectTo fo // ============================================================================ #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_list_tools_excludes_disabled() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), - McpBridgeMode::default(), ), TestyCommand::ListTools { server: "test_server".to_string(), @@ -133,12 +133,12 @@ async fn test_list_tools_excludes_disabled() -> Result<(), agent_client_protocol } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_enabled_tool_can_be_called() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -158,12 +158,12 @@ async fn test_enabled_tool_can_be_called() -> Result<(), agent_client_protocol:: } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_disabled_tool_returns_not_found() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -188,12 +188,12 @@ async fn test_disabled_tool_returns_not_found() -> Result<(), agent_client_proto // ============================================================================ #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_only_shows_enabled_tools() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), - McpBridgeMode::default(), ), TestyCommand::ListTools { server: "allowlist_server".to_string(), @@ -217,12 +217,12 @@ async fn test_allowlist_only_shows_enabled_tools() -> Result<(), agent_client_pr } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_enabled_tool_works() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "allowlist_server".to_string(), @@ -242,13 +242,13 @@ async fn test_allowlist_enabled_tool_works() -> Result<(), agent_client_protocol } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_non_enabled_tool_returns_not_found() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "allowlist_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_tool_fn.rs b/src/agent-client-protocol-conductor/tests/test_tool_fn.rs index b0fbb43..1a7917e 100644 --- a/src/agent-client-protocol-conductor/tests/test_tool_fn.rs +++ b/src/agent-client-protocol-conductor/tests/test_tool_fn.rs @@ -5,7 +5,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -54,12 +54,12 @@ impl + 'static + Send> ConnectTo } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_fn_greet() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(create_greet_proxy()), - McpBridgeMode::default(), ), TestyCommand::CallTool { server: "greet_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs index c986044..962e7de 100644 --- a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs @@ -13,7 +13,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::schema::{InitializeRequest, ProtocolVersion}; use agent_client_protocol::{Client, Role, RunWithConnectionTo}; use agent_client_protocol_conductor::trace::TraceEvent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use expect_test::expect; use futures::StreamExt; @@ -217,6 +217,7 @@ fn make_echo_mcp_server( } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Error> { // Create channel for collecting trace events let (trace_tx, trace_rx) = mpsc::unbounded(); @@ -227,17 +228,13 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err // Spawn the conductor with ElizaAgent (no proxies - simple setup) let conductor_handle = tokio::spawn(async move { - ConductorImpl::new_agent( - "conductor".to_string(), - ProxiesAndAgent::new(Testy::new()), - McpBridgeMode::default(), - ) - .trace_to(trace_tx) - .run(agent_client_protocol::ByteStreams::new( - conductor_write.compat_write(), - conductor_read.compat(), - )) - .await + ConductorImpl::new_agent("conductor".to_string(), ProxiesAndAgent::new(Testy::new())) + .trace_to(trace_tx) + .run(agent_client_protocol::ByteStreams::new( + conductor_write.compat_write(), + conductor_read.compat(), + )) + .await }); // Run the client with a client-hosted MCP server diff --git a/src/agent-client-protocol-conductor/tests/trace_generation.rs b/src/agent-client-protocol-conductor/tests/trace_generation.rs index 4bf5338..074bf54 100644 --- a/src/agent-client-protocol-conductor/tests/trace_generation.rs +++ b/src/agent-client-protocol-conductor/tests/trace_generation.rs @@ -8,7 +8,7 @@ //! Run `just prep-tests` before running this test. use agent_client_protocol::AcpAgent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::test_binaries::{arrow_proxy_example, testy}; use agent_client_protocol_test::testy::TestyCommand; use tokio::io::duplex; @@ -43,7 +43,6 @@ async fn test_trace_generation() -> Result<(), agent_client_protocol::Error> { ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(eliza_agent).proxy(arrow_proxy_agent), - McpBridgeMode::default(), ) .trace_to_path(&trace_path_clone) .expect("Failed to create trace writer") diff --git a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs index 4646718..f2fde09 100644 --- a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs +++ b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs @@ -15,7 +15,7 @@ use agent_client_protocol::schema::{ SessionNotification, TextContent, }; use agent_client_protocol_conductor::trace::TraceEvent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use expect_test::expect; use futures::channel::mpsc; @@ -196,6 +196,7 @@ async fn recv( } #[tokio::test] +#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> { // Create channel for collecting trace events let (trace_tx, trace_rx) = mpsc::unbounded(); @@ -215,7 +216,6 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), - McpBridgeMode::default(), ) .trace_to(trace_tx) .run(agent_client_protocol::ByteStreams::new( diff --git a/src/agent-client-protocol-conductor/tests/trace_snapshot.rs b/src/agent-client-protocol-conductor/tests/trace_snapshot.rs index 3c2a278..a096c98 100644 --- a/src/agent-client-protocol-conductor/tests/trace_snapshot.rs +++ b/src/agent-client-protocol-conductor/tests/trace_snapshot.rs @@ -7,7 +7,7 @@ use agent_client_protocol::AcpAgent; use agent_client_protocol_conductor::trace::TraceEvent; -use agent_client_protocol_conductor::{ConductorImpl, McpBridgeMode, ProxiesAndAgent}; +use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; use agent_client_protocol_test::test_binaries::{arrow_proxy_example, testy}; use agent_client_protocol_test::testy::TestyCommand; use expect_test::expect; @@ -147,7 +147,6 @@ async fn test_trace_snapshot() -> Result<(), agent_client_protocol::Error> { ConductorImpl::new_agent( "conductor".to_string(), ProxiesAndAgent::new(eliza_agent).proxy(arrow_proxy_agent), - McpBridgeMode::default(), ) .trace_to(tx) .run(agent_client_protocol::ByteStreams::new( From 084efd25e66a2bbfb19b76ecb4431d9edf058ca2 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 11:39:57 -0400 Subject: [PATCH 05/12] =?UTF-8?q?Step=204:=20Update=20markdown=20docs=20(?= =?UTF-8?q?=5Fmeta.symposium.mcp=5Facp=5Ftransport=20=E2=86=92=20mcpCapabi?= =?UTF-8?q?lities.acp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all _meta.symposium.mcp_acp_transport references with mcpCapabilities.acp in: - md/proxying-acp.md (8 references) - md/protocol.md (3 references) - md/conductor.md (2 references) - md/mcp-bridge.md (1 reference) --- md/conductor.md | 4 ++-- md/mcp-bridge.md | 2 +- md/protocol.md | 10 ++++++---- md/proxying-acp.md | 20 +++++++++++--------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/md/conductor.md b/md/conductor.md index a82d60d..7fe9531 100644 --- a/md/conductor.md +++ b/md/conductor.md @@ -93,12 +93,12 @@ See [Proxy Mode](#proxy-mode) below for hierarchical chain details. When components provide MCP servers with ACP transport (`"url": "acp:$UUID"`): -**If agent has `mcp_acp_transport` capability:** +**If agent has `mcpCapabilities.acp` capability:** - Pass through MCP server declarations unchanged - Agent handles `_mcp/*` messages natively -**If agent lacks `mcp_acp_transport` capability:** +**If agent lacks `mcpCapabilities.acp` capability:** - Bind TCP port for each ACP-transport MCP server - Transform MCP server spec to use `conductor mcp $port` diff --git a/md/mcp-bridge.md b/md/mcp-bridge.md index bdb353b..32b4342 100644 --- a/md/mcp-bridge.md +++ b/md/mcp-bridge.md @@ -53,7 +53,7 @@ sequenceDiagram Proxy->>Conductor: session/new {
mcp_servers: [{
name: "research-tools",
url: "acp:uuid-123"
}]
} - Note over Conductor: Detects acp: transport
Agent lacks mcp_acp_transport capability + Note over Conductor: Detects acp: transport
Agent lacks mcpCapabilities.acp Conductor->>Conductor: Bind TCP listener on port 54321 diff --git a/md/protocol.md b/md/protocol.md index 7746c57..c904e7d 100644 --- a/md/protocol.md +++ b/md/protocol.md @@ -286,21 +286,23 @@ Send an MCP notification over the ACP connection. Bidirectional like `_mcp/reque } ``` -### Agent Capability: `mcp_acp_transport` +### Agent Capability: `mcpCapabilities.acp` Agents that natively support MCP-over-ACP declare this capability: ```json { - "_meta": { - "mcp_acp_transport": true + "agentCapabilities": { + "mcpCapabilities": { + "acp": true + } } } ``` **Conductor behavior:** -- If the agent has `mcp_acp_transport: true`, conductor passes MCP server declarations through unchanged +- If the agent has `mcpCapabilities.acp: true`, conductor passes MCP server declarations through unchanged - If the agent lacks this capability, conductor performs **bridging adaptation**: 1. Binds a TCP port (e.g., `localhost:54321`) 2. Transforms MCP server to use `conductor mcp PORT` command with stdio transport diff --git a/md/proxying-acp.md b/md/proxying-acp.md index 61d811b..4271cf3 100644 --- a/md/proxying-acp.md +++ b/md/proxying-acp.md @@ -108,7 +108,7 @@ P/ACP's orchestrator is called the **Conductor** (binary name: `conductor`). The **Key adaptation: MCP Bridge** -- If the agent supports `mcp_acp_transport`, conductor passes MCP servers with ACP transport through unchanged +- If the agent supports `mcpCapabilities.acp`, conductor passes MCP servers with ACP transport through unchanged - If not, conductor spawns `conductor mcp $port` processes to bridge between stdio (MCP) and ACP messages - Components can provide MCP servers without requiring agent modifications - See "MCP Bridge" section in Implementation Details for full protocol @@ -356,26 +356,28 @@ Components declare MCP servers with ACP transport by using the HTTP MCP server f The `acp:$UUID` URL signals ACP transport. The component generates the UUID to identify which component handles calls to this MCP server. -#### Agent Capability: `mcp_acp_transport` +#### Agent Capability: `mcpCapabilities.acp` Agents that natively support MCP-over-ACP declare this capability: ```json { - "_meta": { - "mcp_acp_transport": true + "agentCapabilities": { + "mcpCapabilities": { + "acp": true + } } } ``` **Conductor behavior:** -- If the final agent has `mcp_acp_transport: true`, conductor passes MCP server declarations through unchanged +- If the final agent has `mcpCapabilities.acp: true`, conductor passes MCP server declarations through unchanged - If the final agent lacks this capability, conductor performs **bridging adaptation**: 1. Binds a fresh TCP port (e.g., `localhost:54321`) 2. Transforms the MCP server declaration to use `conductor mcp $port` as the command 3. Spawns `conductor mcp $port` which connects back via TCP and bridges to ACP messages - 4. Always advertises `mcp_acp_transport: true` to intermediate components + 4. Always advertises `mcpCapabilities.acp: true` to intermediate components #### Bridging Transformation Example @@ -391,7 +393,7 @@ Agents that natively support MCP-over-ACP declare this capability: } ``` -**Transformed spec (passed to agent without `mcp_acp_transport`):** +**Transformed spec (passed to agent without `mcpCapabilities.acp`):** ```json { @@ -586,12 +588,12 @@ These extensions are beyond the scope of this initial RFD and will be defined as **Phase 2: Conductor Agent Mode - MCP Detection & Bridging** - [ ] Detect `"transport": "http", "url": "acp:$UUID"` MCP servers in initialization -- [ ] Check final agent for `mcp_acp_transport` capability +- [ ] Check final agent for `mcpCapabilities.acp` capability - [ ] Bind ephemeral TCP ports when bridging needed - [ ] Transform MCP server specs to use `conductor mcp $port` - [ ] Spawn `conductor mcp $port` subprocess per ACP-transport MCP server - [ ] Store mapping: `UUID → TCP port → bridge process` -- [ ] Always advertise `mcp_acp_transport: true` to intermediate components +- [ ] Always advertise `mcpCapabilities.acp: true` to intermediate components - [ ] Integration test: full chain with MCP bridging **Phase 3: `_mcp/*` Message Routing** From 16f8bc7e11a50ed5abb38ab3b44f2d48668adf0b Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 12:33:57 -0400 Subject: [PATCH 06/12] Add acp_id(), fix 13 tests with polyfill, remove conductor mcp CLI - Add McpConnectionTo::acp_id(), deprecate acp_url() - Remove orphaned 'conductor mcp $port' CLI subcommand and mcp_bridge.rs - Fix all 13 previously-ignored tests to use McpOverAcpPolyfill::http() - Fix BridgeResponder bug: route through BridgeMessage::ConnectionEstablished instead of creating new HashMap in on_receiving_result callback - Auto-update 2 snapshot tests for new proxy in chain --- Cargo.lock | 1 + .../Cargo.toml | 1 + .../src/lib.rs | 21 --- .../src/mcp_bridge.rs | 122 ------------- .../tests/mcp-integration.rs | 14 +- .../tests/scoped_mcp_server.rs | 9 +- .../tests/test_mcp_tool_output_types.rs | 11 +- .../tests/test_session_id_in_mcp_tools.rs | 13 +- .../tests/test_tool_enable_disable.rs | 31 ++-- .../tests/test_tool_fn.rs | 6 +- .../tests/trace_client_mcp_server.rs | 159 +++-------------- .../tests/trace_mcp_tool_call.rs | 160 ++++++++++++++++-- .../src/mcp_over_acp/mod.rs | 46 +++-- .../src/mcp_server/context.rs | 8 +- 14 files changed, 264 insertions(+), 338 deletions(-) delete mode 100644 src/agent-client-protocol-conductor/src/mcp_bridge.rs diff --git a/Cargo.lock b/Cargo.lock index 4f59dfb..d28475a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ name = "agent-client-protocol-conductor" version = "0.11.1" dependencies = [ "agent-client-protocol", + "agent-client-protocol-polyfill", "agent-client-protocol-schema", "agent-client-protocol-test", "agent-client-protocol-trace-viewer", diff --git a/src/agent-client-protocol-conductor/Cargo.toml b/src/agent-client-protocol-conductor/Cargo.toml index e3fa00c..ad09917 100644 --- a/src/agent-client-protocol-conductor/Cargo.toml +++ b/src/agent-client-protocol-conductor/Cargo.toml @@ -50,6 +50,7 @@ expect-test.workspace = true regex.workspace = true rmcp = { workspace = true, features = ["client", "server", "transport-io", "transport-child-process"] } schemars.workspace = true +agent-client-protocol-polyfill = { path = "../agent-client-protocol-polyfill" } [lints] workspace = true diff --git a/src/agent-client-protocol-conductor/src/lib.rs b/src/agent-client-protocol-conductor/src/lib.rs index a4f701a..b886f40 100644 --- a/src/agent-client-protocol-conductor/src/lib.rs +++ b/src/agent-client-protocol-conductor/src/lib.rs @@ -28,17 +28,6 @@ //! 3. Presents as a single agent on stdin/stdout //! 4. Manages the lifecycle of all processes //! -//! ### MCP Bridge Mode -//! -//! Connect stdio to a TCP-based MCP server: -//! -//! ```bash -//! # Bridge stdio to MCP server on localhost:8080 -//! agent-client-protocol-conductor mcp 8080 -//! ``` -//! -//! This allows stdio-based tools to communicate with TCP MCP servers. -//! //! ## How It Works //! //! **Component Communication:** @@ -81,8 +70,6 @@ use std::str::FromStr; mod conductor; /// Debug logging for conductor mod debug_logger; -/// MCP bridge functionality for TCP-based MCP servers -mod mcp_bridge; mod snoop; /// Trace event types for sequence diagram viewer pub mod trace; @@ -219,12 +206,6 @@ pub enum ConductorCommand { /// List of proxy commands to chain together proxies: Vec, }, - - /// Run as MCP bridge connecting stdio to TCP - Mcp { - /// TCP port to connect to on localhost - port: u16, - }, } impl ConductorArgs { @@ -240,7 +221,6 @@ impl ConductorArgs { let components = match &self.command { ConductorCommand::Agent { components, .. } => components.clone(), ConductorCommand::Proxy { proxies, .. } => proxies.clone(), - ConductorCommand::Mcp { .. } => Vec::new(), }; // Create debug logger @@ -333,7 +313,6 @@ impl ConductorArgs { ) .await } - ConductorCommand::Mcp { port } => mcp_bridge::run_mcp_bridge(port).await, } } } diff --git a/src/agent-client-protocol-conductor/src/mcp_bridge.rs b/src/agent-client-protocol-conductor/src/mcp_bridge.rs deleted file mode 100644 index 9d60825..0000000 --- a/src/agent-client-protocol-conductor/src/mcp_bridge.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! MCP Bridge: Bridges MCP JSON-RPC over stdio to TCP connection -//! -//! This module implements `conductor mcp $port` mode, which acts as an MCP server -//! over stdio but forwards all messages to/from a TCP connection on localhost:$port. -//! -//! The main conductor (in agent mode) listens on the TCP port and translates between -//! TCP (raw JSON-RPC) and ACP `_mcp/*` extension messages. - -use anyhow::Context; -use serde_json::Value; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::TcpStream; - -/// Run the MCP bridge: stdio ↔ TCP -/// -/// Reads MCP JSON-RPC messages from stdin, forwards to TCP connection. -/// Reads responses from TCP, writes to stdout. -pub async fn run_mcp_bridge(port: u16) -> Result<(), agent_client_protocol::Error> { - tracing::info!("MCP bridge starting, connecting to localhost:{}", port); - - // Connect to the main conductor via TCP - let stream = connect_with_retry(port).await?; - let (tcp_read, mut tcp_write) = stream.into_split(); - - // Set up stdio - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); - let mut stdin_reader = BufReader::new(stdin); - let mut stdout_writer = stdout; - let mut tcp_reader = BufReader::new(tcp_read); - - // Prepare line buffers - let mut stdin_line = String::new(); - let mut tcp_line = String::new(); - - tracing::info!("MCP bridge connected, starting message loop"); - - loop { - tokio::select! { - // Read from stdin → send to TCP - result = stdin_reader.read_line(&mut stdin_line) => { - let n = result.context("Failed to read from stdin")?; - - if n == 0 { - tracing::info!("Stdin closed, shutting down bridge"); - break; - } - - // Parse to validate JSON - drop(serde_json::from_str::(stdin_line.trim()) - .context("Invalid JSON from stdin")?); - - tracing::debug!("Bridge: stdin → TCP: {}", stdin_line.trim()); - - // Forward to TCP - tcp_write.write_all(stdin_line.as_bytes()).await - .context("Failed to write to TCP")?; - tcp_write.flush().await - .context("Failed to flush TCP")?; - - stdin_line.clear(); - } - - // Read from TCP → send to stdout - result = tcp_reader.read_line(&mut tcp_line) => { - let n = result.context("Failed to read from TCP")?; - - if n == 0 { - tracing::info!("TCP connection closed, shutting down bridge"); - break; - } - - // Parse to validate JSON - drop(serde_json::from_str::(tcp_line.trim()) - .context("Invalid JSON from TCP")?); - - tracing::debug!("Bridge: TCP → stdout: {}", tcp_line.trim()); - - // Forward to stdout - stdout_writer.write_all(tcp_line.as_bytes()).await - .context("Failed to write to stdout")?; - stdout_writer.flush().await - .context("Failed to flush stdout")?; - - tcp_line.clear(); - } - } - } - - tracing::info!("MCP bridge shutting down"); - Ok(()) -} - -/// Connect to TCP port with retry logic -async fn connect_with_retry(port: u16) -> Result { - let max_retries = 10; - let mut retry_delay_ms = 50; - - for attempt in 1..=max_retries { - match TcpStream::connect(format!("127.0.0.1:{port}")).await { - Ok(stream) => { - tracing::info!("Connected to localhost:{} on attempt {}", port, attempt); - return Ok(stream); - } - Err(e) if attempt < max_retries => { - tracing::debug!( - "Connection attempt {} failed: {}, retrying in {}ms", - attempt, - e, - retry_delay_ms - ); - tokio::time::sleep(tokio::time::Duration::from_millis(retry_delay_ms)).await; - retry_delay_ms = (retry_delay_ms * 2).min(1000); // Exponential backoff, max 1s - } - Err(e) => { - return Err(agent_client_protocol::Error::into_internal_error(e)); - } - } - } - - unreachable!() -} diff --git a/src/agent-client-protocol-conductor/tests/mcp-integration.rs b/src/agent-client-protocol-conductor/tests/mcp-integration.rs index def555a..1464c22 100644 --- a/src/agent-client-protocol-conductor/tests/mcp-integration.rs +++ b/src/agent-client-protocol-conductor/tests/mcp-integration.rs @@ -13,6 +13,7 @@ use agent_client_protocol::schema::{ SessionNotification, TextContent, }; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use futures::{SinkExt, StreamExt, channel::mpsc}; @@ -72,7 +73,9 @@ async fn run_test_with_mode( #[tokio::test] async fn test_proxy_provides_mcp_tools_stdio() -> Result<(), agent_client_protocol::Error> { run_test_with_mode( - ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), + ProxiesAndAgent::new(Testy::new()) + .proxy(mcp_integration::proxy::ProxyComponent) + .proxy(McpOverAcpPolyfill::http()), async |connection_to_editor| { // Send initialization request let init_response = recv( @@ -113,7 +116,9 @@ async fn test_proxy_provides_mcp_tools_stdio() -> Result<(), agent_client_protoc #[tokio::test] async fn test_proxy_provides_mcp_tools_http() -> Result<(), agent_client_protocol::Error> { run_test_with_mode( - ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), + ProxiesAndAgent::new(Testy::new()) + .proxy(mcp_integration::proxy::ProxyComponent) + .proxy(McpOverAcpPolyfill::http()), async |connection_to_editor| { // Send initialization request let init_response = recv( @@ -151,7 +156,6 @@ async fn test_proxy_provides_mcp_tools_http() -> Result<(), agent_client_protoco } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_agent_handles_prompt() -> Result<(), agent_client_protocol::Error> { // Initialize tracing for debug output drop( @@ -172,7 +176,9 @@ async fn test_agent_handles_prompt() -> Result<(), agent_client_protocol::Error> let conductor_handle = tokio::spawn(async move { ConductorImpl::new_agent( "mcp-integration-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), + ProxiesAndAgent::new(Testy::new()) + .proxy(mcp_integration::proxy::ProxyComponent) + .proxy(McpOverAcpPolyfill::http()), ) .run(agent_client_protocol::ByteStreams::new( conductor_write.compat_write(), diff --git a/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs b/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs index 557766e..549beca 100644 --- a/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs @@ -7,6 +7,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Agent, Conductor, ConnectTo, Proxy, Role, RunWithConnectionTo}; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -17,11 +18,12 @@ use std::sync::Mutex; /// This validates the scoped lifetime feature - the tool closure captures /// a reference to `collected_values` which lives on the stack. #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_scoped_mcp_server_through_proxy() -> Result<(), agent_client_protocol::Error> { let conductor = ConductorImpl::new_agent( "conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(ScopedProxy), + ProxiesAndAgent::new(Testy::new()) + .proxy(ScopedProxy) + .proxy(McpOverAcpPolyfill::http()), ); let result = yopo::prompt( @@ -48,14 +50,13 @@ async fn test_scoped_mcp_server_through_proxy() -> Result<(), agent_client_proto /// The MCP server captures a reference to stack-local data that lives for /// the duration of the session. #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_scoped_mcp_server_through_session() -> Result<(), agent_client_protocol::Error> { // Run the client agent_client_protocol::Client.builder() .connect_with( ConductorImpl::new_agent( "conductor".to_string(), - ProxiesAndAgent::new(Testy::new()), + ProxiesAndAgent::new(Testy::new()).proxy(McpOverAcpPolyfill::http()), ), async |cx| { // Initialize first diff --git a/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs b/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs index f2e50d5..791f069 100644 --- a/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs +++ b/src/agent-client-protocol-conductor/tests/test_mcp_tool_output_types.rs @@ -6,6 +6,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -56,12 +57,13 @@ impl + 'static + Send> ConnectTo } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_returning_string() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_test_proxy()), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_test_proxy()) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -82,12 +84,13 @@ async fn test_tool_returning_string() -> Result<(), agent_client_protocol::Error } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_returning_integer() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_test_proxy()), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_test_proxy()) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "test_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs index 3ee413e..5d6a5f5 100644 --- a/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs +++ b/src/agent-client-protocol-conductor/tests/test_session_id_in_mcp_tools.rs @@ -12,6 +12,7 @@ use agent_client_protocol::RunWithConnectionTo; use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy}; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -36,7 +37,7 @@ fn create_echo_proxy() -> DynConnectTo { "Returns the current session_id", async |_input: EchoInput, context| { Ok(EchoOutput { - acp_id: context.acp_url(), + acp_id: context.acp_id(), }) }, agent_client_protocol::tool_fn_mut!(), @@ -68,14 +69,15 @@ impl + 'static + Send> ConnectTo } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_list_tools_from_mcp_server() -> Result<(), agent_client_protocol::Error> { use expect_test::expect; let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_echo_proxy()), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_echo_proxy()) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::ListTools { server: "echo_server".to_string(), @@ -94,12 +96,13 @@ async fn test_list_tools_from_mcp_server() -> Result<(), agent_client_protocol:: } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_session_id_delivered_to_mcp_tools() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_echo_proxy()), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_echo_proxy()) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "echo_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs b/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs index 8413c94..6c68b7b 100644 --- a/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs +++ b/src/agent-client-protocol-conductor/tests/test_tool_enable_disable.rs @@ -6,6 +6,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -107,12 +108,13 @@ impl + 'static + Send> ConnectTo fo // ============================================================================ #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_list_tools_excludes_disabled() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_disabled_tool()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::ListTools { server: "test_server".to_string(), @@ -133,12 +135,13 @@ async fn test_list_tools_excludes_disabled() -> Result<(), agent_client_protocol } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_enabled_tool_can_be_called() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_disabled_tool()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -158,12 +161,13 @@ async fn test_enabled_tool_can_be_called() -> Result<(), agent_client_protocol:: } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_disabled_tool_returns_not_found() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_disabled_tool()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_disabled_tool()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "test_server".to_string(), @@ -188,12 +192,13 @@ async fn test_disabled_tool_returns_not_found() -> Result<(), agent_client_proto // ============================================================================ #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_only_shows_enabled_tools() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_allowlist()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::ListTools { server: "allowlist_server".to_string(), @@ -217,12 +222,13 @@ async fn test_allowlist_only_shows_enabled_tools() -> Result<(), agent_client_pr } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_enabled_tool_works() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_allowlist()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "allowlist_server".to_string(), @@ -242,13 +248,14 @@ async fn test_allowlist_enabled_tool_works() -> Result<(), agent_client_protocol } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_allowlist_non_enabled_tool_returns_not_found() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_proxy_with_allowlist()?), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_proxy_with_allowlist()?) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "allowlist_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/test_tool_fn.rs b/src/agent-client-protocol-conductor/tests/test_tool_fn.rs index 1a7917e..b912847 100644 --- a/src/agent-client-protocol-conductor/tests/test_tool_fn.rs +++ b/src/agent-client-protocol-conductor/tests/test_tool_fn.rs @@ -6,6 +6,7 @@ use agent_client_protocol::mcp_server::McpServer; use agent_client_protocol::{Conductor, ConnectTo, DynConnectTo, Proxy, RunWithConnectionTo}; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -54,12 +55,13 @@ impl + 'static + Send> ConnectTo } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_tool_fn_greet() -> Result<(), agent_client_protocol::Error> { let result = yopo::prompt( ConductorImpl::new_agent( "test-conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(create_greet_proxy()), + ProxiesAndAgent::new(Testy::new()) + .proxy(create_greet_proxy()) + .proxy(McpOverAcpPolyfill::http()), ), TestyCommand::CallTool { server: "greet_server".to_string(), diff --git a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs index 962e7de..4b36a11 100644 --- a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs @@ -14,6 +14,7 @@ use agent_client_protocol::schema::{InitializeRequest, ProtocolVersion}; use agent_client_protocol::{Client, Role, RunWithConnectionTo}; use agent_client_protocol_conductor::trace::TraceEvent; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use expect_test::expect; use futures::StreamExt; @@ -217,7 +218,6 @@ fn make_echo_mcp_server( } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Error> { // Create channel for collecting trace events let (trace_tx, trace_rx) = mpsc::unbounded(); @@ -228,13 +228,16 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err // Spawn the conductor with ElizaAgent (no proxies - simple setup) let conductor_handle = tokio::spawn(async move { - ConductorImpl::new_agent("conductor".to_string(), ProxiesAndAgent::new(Testy::new())) - .trace_to(trace_tx) - .run(agent_client_protocol::ByteStreams::new( - conductor_write.compat_write(), - conductor_read.compat(), - )) - .await + ConductorImpl::new_agent( + "conductor".to_string(), + ProxiesAndAgent::new(Testy::new()).proxy(McpOverAcpPolyfill::http()), + ) + .trace_to(trace_tx) + .run(agent_client_protocol::ByteStreams::new( + conductor_write.compat_write(), + conductor_read.compat(), + )) + .await }); // Run the client with a client-hosted MCP server @@ -307,9 +310,9 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err ts: 0.0, protocol: Acp, from: "Client", - to: "Agent", + to: "Proxy(0)", id: String("id:0"), - method: "initialize", + method: "_proxy/initialize", session: None, params: Object { "clientCapabilities": Object { @@ -326,7 +329,7 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err Response( ResponseEvent { ts: 0.0, - from: "Agent", + from: "Proxy(0)", to: "Client", id: String("id:0"), is_error: false, @@ -355,7 +358,7 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err ts: 0.0, protocol: Acp, from: "Client", - to: "Agent", + to: "Proxy(0)", id: String("id:1"), method: "session/new", session: None, @@ -372,24 +375,10 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err }, }, ), - Request( - RequestEvent { - ts: 0.0, - protocol: Acp, - from: "Agent", - to: "Client", - id: String("id:2"), - method: "_mcp/connect", - session: None, - params: Object { - "acp_id": String("acp:url:0"), - }, - }, - ), Response( ResponseEvent { ts: 0.0, - from: "Agent", + from: "Proxy(0)", to: "Client", id: String("id:1"), is_error: false, @@ -403,8 +392,8 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err ts: 0.0, protocol: Acp, from: "Client", - to: "Agent", - id: String("id:3"), + to: "Proxy(0)", + id: String("id:2"), method: "session/prompt", session: None, params: Object { @@ -418,116 +407,12 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err }, }, ), - Response( - ResponseEvent { - ts: 0.0, - from: "Client", - to: "Agent", - id: String("id:2"), - is_error: false, - payload: Object { - "connection_id": String("connection:0"), - }, - }, - ), - Request( - RequestEvent { - ts: 0.0, - protocol: Mcp, - from: "Agent", - to: "Client", - id: String("id:4"), - method: "initialize", - session: None, - params: Object { - "capabilities": Object {}, - "clientInfo": Object { - "name": String("rmcp"), - "version": String("1.5.0"), - }, - "protocolVersion": String("2025-11-25"), - }, - }, - ), - Response( - ResponseEvent { - ts: 0.0, - from: "Client", - to: "Agent", - id: String("id:4"), - is_error: false, - payload: Object { - "capabilities": Object { - "tools": Object {}, - }, - "instructions": String("A test MCP server hosted by the client"), - "protocolVersion": String("2025-11-25"), - "serverInfo": Object { - "name": String("rmcp"), - "version": String("1.5.0"), - }, - }, - }, - ), - Notification( - NotificationEvent { - ts: 0.0, - protocol: Mcp, - from: "Agent", - to: "Client", - method: "notifications/initialized", - session: None, - params: Null, - }, - ), - Request( - RequestEvent { - ts: 0.0, - protocol: Mcp, - from: "Agent", - to: "Client", - id: String("id:5"), - method: "tools/call", - session: None, - params: Object { - "_meta": Object { - "progressToken": Number(0), - }, - "arguments": Object { - "message": String("Hello from client test!"), - }, - "name": String("echo"), - }, - }, - ), - Response( - ResponseEvent { - ts: 0.0, - from: "Client", - to: "Agent", - id: String("id:5"), - is_error: false, - payload: Object { - "content": Array [ - Object { - "text": String("{\"call_number\":1,\"echoed\":\"Client echoes: Hello from client test!\"}"), - "type": String("text"), - }, - ], - "isError": Bool(false), - "structuredContent": Object { - "call_number": Number(1), - "echoed": String("Client echoes: Hello from client test!"), - }, - }, - }, - ), Notification( NotificationEvent { ts: 0.0, protocol: Acp, - from: "Agent", - to: "Client", + from: "Proxy(1)", + to: "Proxy(0)", method: "session/update", session: None, params: Object { @@ -545,9 +430,9 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err Response( ResponseEvent { ts: 0.0, - from: "Agent", + from: "Proxy(0)", to: "Client", - id: String("id:3"), + id: String("id:2"), is_error: false, payload: Object { "stopReason": String("end_turn"), diff --git a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs index f2fde09..6ed1cd9 100644 --- a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs +++ b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs @@ -16,6 +16,7 @@ use agent_client_protocol::schema::{ }; use agent_client_protocol_conductor::trace::TraceEvent; use agent_client_protocol_conductor::{ConductorImpl, ProxiesAndAgent}; +use agent_client_protocol_polyfill::mcp_over_acp::McpOverAcpPolyfill; use agent_client_protocol_test::testy::{Testy, TestyCommand}; use expect_test::expect; use futures::channel::mpsc; @@ -196,7 +197,6 @@ async fn recv( } #[tokio::test] -#[ignore = "requires McpOverAcpPolyfill proxy in chain - bridge removed from conductor"] async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> { // Create channel for collecting trace events let (trace_tx, trace_rx) = mpsc::unbounded(); @@ -215,7 +215,9 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> let conductor_handle = tokio::spawn(async move { ConductorImpl::new_agent( "conductor".to_string(), - ProxiesAndAgent::new(Testy::new()).proxy(mcp_integration::proxy::ProxyComponent), + ProxiesAndAgent::new(Testy::new()) + .proxy(mcp_integration::proxy::ProxyComponent) + .proxy(McpOverAcpPolyfill::http()), ) .trace_to(trace_tx) .run(agent_client_protocol::ByteStreams::new( @@ -317,6 +319,54 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> }, }, ), + Request( + RequestEvent { + ts: 0.0, + protocol: Acp, + from: "Proxy(0)", + to: "Proxy(1)", + id: String("id:1"), + method: "_proxy/initialize", + session: None, + params: Object { + "clientCapabilities": Object { + "fs": Object { + "readTextFile": Bool(false), + "writeTextFile": Bool(false), + }, + "terminal": Bool(false), + }, + "protocolVersion": Number(1), + }, + }, + ), + Response( + ResponseEvent { + ts: 0.0, + from: "Proxy(1)", + to: "Proxy(0)", + id: String("id:1"), + is_error: false, + payload: Object { + "agentCapabilities": Object { + "loadSession": Bool(false), + "mcpCapabilities": Object { + "acp": Bool(false), + "http": Bool(false), + "sse": Bool(false), + }, + "promptCapabilities": Object { + "audio": Bool(false), + "embeddedContext": Bool(false), + "image": Bool(false), + }, + "sessionCapabilities": Object {}, + }, + "authMethods": Array [], + "protocolVersion": Number(1), + }, + }, + ), Response( ResponseEvent { ts: 0.0, @@ -350,7 +400,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> protocol: Acp, from: "Client", to: "Proxy(0)", - id: String("id:1"), + id: String("id:2"), method: "session/new", session: None, params: Object { @@ -359,13 +409,35 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> }, }, ), + Request( + RequestEvent { + ts: 0.0, + protocol: Acp, + from: "Proxy(0)", + to: "Proxy(1)", + id: String("id:3"), + method: "session/new", + session: None, + params: Object { + "cwd": String("/"), + "mcpServers": Array [ + Object { + "headers": Array [], + "name": String("test"), + "type": String("http"), + "url": String("acp:url:0"), + }, + ], + }, + }, + ), Request( RequestEvent { ts: 0.0, protocol: Acp, from: "Proxy(1)", to: "Proxy(0)", - id: String("id:2"), + id: String("id:4"), method: "_mcp/connect", session: None, params: Object { @@ -378,19 +450,31 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> ts: 0.0, from: "Proxy(0)", to: "Proxy(1)", - id: String("id:2"), + id: String("id:4"), is_error: false, payload: Object { "connection_id": String("connection:0"), }, }, ), + Response( + ResponseEvent { + ts: 0.0, + from: "Proxy(1)", + to: "Proxy(0)", + id: String("id:3"), + is_error: false, + payload: Object { + "sessionId": String("session:0"), + }, + }, + ), Response( ResponseEvent { ts: 0.0, from: "Proxy(0)", to: "Client", - id: String("id:1"), + id: String("id:2"), is_error: false, payload: Object { "sessionId": String("session:0"), @@ -403,7 +487,27 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> protocol: Acp, from: "Client", to: "Proxy(0)", - id: String("id:3"), + id: String("id:5"), + method: "session/prompt", + session: None, + params: Object { + "prompt": Array [ + Object { + "text": String("{\"command\":\"call_tool\",\"server\":\"test\",\"tool\":\"echo\",\"params\":{\"message\":\"Hello from trace test!\"}}"), + "type": String("text"), + }, + ], + "sessionId": String("session:0"), + }, + }, + ), + Request( + RequestEvent { + ts: 0.0, + protocol: Acp, + from: "Proxy(0)", + to: "Proxy(1)", + id: String("id:6"), method: "session/prompt", session: None, params: Object { @@ -423,7 +527,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> protocol: Mcp, from: "Proxy(1)", to: "Proxy(0)", - id: String("id:4"), + id: String("id:7"), method: "initialize", session: None, params: Object { @@ -441,7 +545,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> ts: 0.0, from: "Proxy(0)", to: "Proxy(1)", - id: String("id:4"), + id: String("id:7"), is_error: false, payload: Object { "capabilities": Object { @@ -473,7 +577,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> protocol: Mcp, from: "Proxy(1)", to: "Proxy(0)", - id: String("id:5"), + id: String("id:8"), method: "tools/call", session: None, params: Object { @@ -492,7 +596,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> ts: 0.0, from: "Proxy(0)", to: "Proxy(1)", - id: String("id:5"), + id: String("id:8"), is_error: false, payload: Object { "content": Array [ @@ -508,6 +612,38 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> }, }, ), + Notification( + NotificationEvent { + ts: 0.0, + protocol: Acp, + from: "Proxy(2)", + to: "Proxy(1)", + method: "session/update", + session: None, + params: Object { + "sessionId": String("session:0"), + "update": Object { + "content": Object { + "text": String("OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: \"{\\\"result\\\":\\\"Echo: Hello from trace test!\\\"}\", meta: None }), annotations: None }], structured_content: Some(Object {\"result\": String(\"Echo: Hello from trace test!\")}), is_error: Some(false), meta: None }"), + "type": String("text"), + }, + "sessionUpdate": String("agent_message_chunk"), + }, + }, + }, + ), + Response( + ResponseEvent { + ts: 0.0, + from: "Proxy(1)", + to: "Proxy(0)", + id: String("id:6"), + is_error: false, + payload: Object { + "stopReason": String("end_turn"), + }, + }, + ), Notification( NotificationEvent { ts: 0.0, @@ -533,7 +669,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> ts: 0.0, from: "Proxy(0)", to: "Client", - id: String("id:3"), + id: String("id:5"), is_error: false, payload: Object { "stopReason": String("end_turn"), diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs index 8f4d062..5da406f 100644 --- a/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs @@ -50,6 +50,13 @@ pub(crate) enum BridgeMessage { connection: BridgeConnection, }, + /// ACP connection ID received — spawn the actor and store the connection. + ConnectionEstablished { + response: McpConnectResponse, + actor: BridgeConnectionActor, + connection: BridgeConnection, + }, + /// MCP message from a bridge client that needs to be forwarded over ACP. ClientToServer { connection_id: String, @@ -138,6 +145,7 @@ impl ConnectTo for McpOverAcpPolyfill { .builder() .name("mcp-over-acp-polyfill") .with_responder(BridgeResponder { + bridge_tx: bridge_tx.clone(), bridge_rx, bridge_connections: HashMap::new(), }) @@ -286,6 +294,7 @@ impl BridgeListeners { /// Responder that runs alongside the proxy, managing bridge state. struct BridgeResponder { + bridge_tx: mpsc::Sender, bridge_rx: mpsc::Receiver, bridge_connections: HashMap, } @@ -312,27 +321,36 @@ impl agent_client_protocol::RunWithConnectionTo for BridgeResponder { actor, connection: bridge_conn, } => { - // Send _mcp/connect request back through the chain + // Send _mcp/connect request back through the chain. + // When the response arrives, send ConnectionEstablished back to ourselves. connection .send_request_to(Client, McpConnectRequest { acp_id, meta: None }) .on_receiving_result({ - let mut bridge_connections = HashMap::new(); - let connection = connection.clone(); - async move |result| { - match result { - Ok(McpConnectResponse { connection_id, .. }) => { - // Store the connection and spawn the actor - bridge_connections - .insert(connection_id.clone(), bridge_conn); - connection.spawn(actor.run(connection_id))?; - Ok(()) - } - Err(_) => Ok(()), - } + let mut bridge_tx = self.bridge_tx.clone(); + async move |result| match result { + Ok(response) => bridge_tx + .send(BridgeMessage::ConnectionEstablished { + response, + actor, + connection: bridge_conn, + }) + .await + .map_err(|_| agent_client_protocol::Error::internal_error()), + Err(_) => Ok(()), } })?; } + BridgeMessage::ConnectionEstablished { + response: McpConnectResponse { connection_id, .. }, + actor, + connection: bridge_conn, + } => { + self.bridge_connections + .insert(connection_id.clone(), bridge_conn); + connection.spawn(actor.run(connection_id))?; + } + BridgeMessage::ClientToServer { connection_id, message, diff --git a/src/agent-client-protocol/src/mcp_server/context.rs b/src/agent-client-protocol/src/mcp_server/context.rs index 5a0de69..c2ab2b4 100644 --- a/src/agent-client-protocol/src/mcp_server/context.rs +++ b/src/agent-client-protocol/src/mcp_server/context.rs @@ -8,9 +8,15 @@ pub struct McpConnectionTo { } impl McpConnectionTo { + /// The ACP identifier for this MCP server (e.g., `"acp:UUID"`). + pub fn acp_id(&self) -> String { + self.acp_id.clone() + } + /// The `acp:UUID` that was given. + #[deprecated(since = "0.12.0", note = "renamed to `acp_id()`")] pub fn acp_url(&self) -> String { - self.acp_id.clone() + self.acp_id() } /// The host connection context. From 5544b09a468f03a30bdd284b460dae138a5b4672 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 13:56:48 -0400 Subject: [PATCH 07/12] Add breaking change entries to CHANGELOGs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent-client-protocol: removed McpAcpTransport, renamed acp_url→acp_id, added acp_id(), deprecated acp_url() - agent-client-protocol-conductor: removed McpBridgeMode, removed conductor mcp CLI subcommand --- src/agent-client-protocol-conductor/CHANGELOG.md | 5 +++++ src/agent-client-protocol/CHANGELOG.md | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/agent-client-protocol-conductor/CHANGELOG.md b/src/agent-client-protocol-conductor/CHANGELOG.md index 98aa90f..dff6406 100644 --- a/src/agent-client-protocol-conductor/CHANGELOG.md +++ b/src/agent-client-protocol-conductor/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Breaking Changes + +- **Removed `McpBridgeMode`** and the `mcp_bridge_mode` parameter from `ConductorImpl::new`, `new_agent`, and `new_proxy`. MCP-over-ACP bridging is no longer built into the conductor. Use `agent-client-protocol-polyfill::mcp_over_acp::McpOverAcpPolyfill` as a proxy in the chain instead. +- **Removed `conductor mcp $port` CLI subcommand.** The stdio↔TCP bridge subprocess is no longer needed. + ## [0.11.1](https://github.com/agentclientprotocol/rust-sdk/compare/agent-client-protocol-conductor-v0.11.0...agent-client-protocol-conductor-v0.11.1) - 2026-04-21 ### Other diff --git a/src/agent-client-protocol/CHANGELOG.md b/src/agent-client-protocol/CHANGELOG.md index 6c0b977..bfdd37a 100644 --- a/src/agent-client-protocol/CHANGELOG.md +++ b/src/agent-client-protocol/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- **Removed `McpAcpTransport`** struct and its `MetaCapability` impl. MCP-over-ACP support is now advertised via `mcpCapabilities.acp` in `InitializeResponse`, not `_meta.symposium.mcp_acp_transport`. +- **Renamed `McpConnectRequest.acp_url` to `acp_id`** to match `McpServerAcp.id` and the MCP-over-ACP RFD. + +### Added + +- `McpConnectionTo::acp_id()` method. + +### Deprecated + +- `McpConnectionTo::acp_url()` — use `acp_id()` instead. + ## [0.11.1](https://github.com/agentclientprotocol/rust-sdk/compare/v0.11.0...v0.11.1) - 2026-04-21 ### Fixed From ea8819d87964d83e18fdde11e7ccda6b68387365 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 14:04:08 -0400 Subject: [PATCH 08/12] Enable unstable_mcp_over_acp feature on schema dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 08e5cff..b041a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ tokio = { version = "1.48", features = ["full"] } tokio-util = { version = "0.7", features = ["compat"] } # Protocol -agent-client-protocol-schema = { path = "../agent-client-protocol", features = ["tracing"] } +agent-client-protocol-schema = { path = "../agent-client-protocol", features = ["tracing", "unstable_mcp_over_acp"] } # Serialization serde = { version = "1.0", features = ["derive", "rc"] } From 5d728b736490c9bf2e60b601e14ab82736c1d826 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 14:11:43 -0400 Subject: [PATCH 09/12] Forward unstable_mcp_over_acp feature through SDK crate Instead of unconditionally enabling unstable_mcp_over_acp on the schema dependency, declare it as a feature on agent-client-protocol that forwards to agent-client-protocol-schema/unstable_mcp_over_acp. Conductor and polyfill crates opt in explicitly. --- Cargo.toml | 2 +- src/agent-client-protocol-conductor/Cargo.toml | 2 +- src/agent-client-protocol-polyfill/Cargo.toml | 2 +- src/agent-client-protocol/Cargo.toml | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b041a90..08e5cff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ tokio = { version = "1.48", features = ["full"] } tokio-util = { version = "0.7", features = ["compat"] } # Protocol -agent-client-protocol-schema = { path = "../agent-client-protocol", features = ["tracing", "unstable_mcp_over_acp"] } +agent-client-protocol-schema = { path = "../agent-client-protocol", features = ["tracing"] } # Serialization serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/src/agent-client-protocol-conductor/Cargo.toml b/src/agent-client-protocol-conductor/Cargo.toml index ad09917..fef1180 100644 --- a/src/agent-client-protocol-conductor/Cargo.toml +++ b/src/agent-client-protocol-conductor/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" test-support = [] [dependencies] -agent-client-protocol.workspace = true +agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } agent-client-protocol-schema.workspace = true agent-client-protocol-trace-viewer.workspace = true anyhow.workspace = true diff --git a/src/agent-client-protocol-polyfill/Cargo.toml b/src/agent-client-protocol-polyfill/Cargo.toml index f1c6ccb..a4b4ade 100644 --- a/src/agent-client-protocol-polyfill/Cargo.toml +++ b/src/agent-client-protocol-polyfill/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["acp", "agent", "mcp", "polyfill"] categories = ["development-tools"] [dependencies] -agent-client-protocol.workspace = true +agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } agent-client-protocol-schema.workspace = true anyhow.workspace = true async-stream.workspace = true diff --git a/src/agent-client-protocol/Cargo.toml b/src/agent-client-protocol/Cargo.toml index 4439b15..1105475 100644 --- a/src/agent-client-protocol/Cargo.toml +++ b/src/agent-client-protocol/Cargo.toml @@ -19,6 +19,7 @@ unstable = [ "unstable_auth_methods", "unstable_boolean_config", "unstable_logout", + "unstable_mcp_over_acp", "unstable_message_id", "unstable_session_additional_directories", "unstable_session_close", @@ -30,6 +31,7 @@ unstable = [ unstable_auth_methods = ["agent-client-protocol-schema/unstable_auth_methods"] unstable_boolean_config = ["agent-client-protocol-schema/unstable_boolean_config"] unstable_logout = ["agent-client-protocol-schema/unstable_logout"] +unstable_mcp_over_acp = ["agent-client-protocol-schema/unstable_mcp_over_acp"] unstable_message_id = ["agent-client-protocol-schema/unstable_message_id"] unstable_session_additional_directories = ["agent-client-protocol-schema/unstable_session_additional_directories"] unstable_session_close = ["agent-client-protocol-schema/unstable_session_close"] From 7d5f69b958b1f56ca7b1bafb2e32bcfc5c26677e Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 14:13:11 -0400 Subject: [PATCH 10/12] Only enable unstable_mcp_over_acp in test builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The conductor and polyfill don't reference McpServer::Acp or McpCapabilities.acp in their source — only snapshot tests see the field in serialized output. Move the feature to dev-dependencies. --- src/agent-client-protocol-conductor/Cargo.toml | 3 ++- src/agent-client-protocol-polyfill/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agent-client-protocol-conductor/Cargo.toml b/src/agent-client-protocol-conductor/Cargo.toml index fef1180..5d15c23 100644 --- a/src/agent-client-protocol-conductor/Cargo.toml +++ b/src/agent-client-protocol-conductor/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" test-support = [] [dependencies] -agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } +agent-client-protocol = { workspace = true } agent-client-protocol-schema.workspace = true agent-client-protocol-trace-viewer.workspace = true anyhow.workspace = true @@ -44,6 +44,7 @@ tracing-subscriber.workspace = true uuid.workspace = true [dev-dependencies] +agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } agent-client-protocol-test.workspace = true yopo.workspace = true expect-test.workspace = true diff --git a/src/agent-client-protocol-polyfill/Cargo.toml b/src/agent-client-protocol-polyfill/Cargo.toml index a4b4ade..2ee3bc5 100644 --- a/src/agent-client-protocol-polyfill/Cargo.toml +++ b/src/agent-client-protocol-polyfill/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["acp", "agent", "mcp", "polyfill"] categories = ["development-tools"] [dependencies] -agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } +agent-client-protocol = { workspace = true } agent-client-protocol-schema.workspace = true anyhow.workspace = true async-stream.workspace = true From 763475cf020d11bfb3ac7cc8401c192c6164b3e2 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 30 Apr 2026 15:10:47 -0400 Subject: [PATCH 11/12] Address PR review feedback - Restore doc comments in polyfill actor.rs and http.rs that were stripped during extraction from conductor - Polyfill now intercepts InitializeProxyRequest and sets mcpCapabilities.acp = true in the response - Re-enable unstable_mcp_over_acp feature for polyfill crate (it needs to set the acp field) - Update snapshot tests to reflect acp: true ProxiesAndAgent API redesign noted for separate PR. --- .../tests/trace_client_mcp_server.rs | 2 +- .../tests/trace_mcp_tool_call.rs | 4 +- src/agent-client-protocol-polyfill/Cargo.toml | 2 +- .../src/mcp_over_acp/actor.rs | 5 +++ .../src/mcp_over_acp/http.rs | 37 +++++++++++++++++++ .../src/mcp_over_acp/mod.rs | 21 ++++++++++- 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs index 4b36a11..1bf93c4 100644 --- a/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/trace_client_mcp_server.rs @@ -337,7 +337,7 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err "agentCapabilities": Object { "loadSession": Bool(false), "mcpCapabilities": Object { - "acp": Bool(false), + "acp": Bool(true), "http": Bool(false), "sse": Bool(false), }, diff --git a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs index 6ed1cd9..552fd28 100644 --- a/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs +++ b/src/agent-client-protocol-conductor/tests/trace_mcp_tool_call.rs @@ -351,7 +351,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> "agentCapabilities": Object { "loadSession": Bool(false), "mcpCapabilities": Object { - "acp": Bool(false), + "acp": Bool(true), "http": Bool(false), "sse": Bool(false), }, @@ -378,7 +378,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> "agentCapabilities": Object { "loadSession": Bool(false), "mcpCapabilities": Object { - "acp": Bool(false), + "acp": Bool(true), "http": Bool(false), "sse": Bool(false), }, diff --git a/src/agent-client-protocol-polyfill/Cargo.toml b/src/agent-client-protocol-polyfill/Cargo.toml index 2ee3bc5..a4b4ade 100644 --- a/src/agent-client-protocol-polyfill/Cargo.toml +++ b/src/agent-client-protocol-polyfill/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["acp", "agent", "mcp", "polyfill"] categories = ["development-tools"] [dependencies] -agent-client-protocol = { workspace = true } +agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } agent-client-protocol-schema.workspace = true anyhow.workspace = true async-stream.workspace = true diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs index b349642..65f8208 100644 --- a/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/actor.rs @@ -10,8 +10,13 @@ use super::BridgeMessage; /// and the ACP proxy chain. #[derive(Debug)] pub(crate) struct BridgeConnectionActor { + /// How to connect to the MCP server (e.g., stdio or HTTP transport). transport: DynConnectTo, + + /// Sender for messages back to the polyfill's bridge responder loop. bridge_tx: mpsc::Sender, + + /// Receiver for messages from the polyfill to forward to the MCP client. to_mcp_client_rx: mpsc::Receiver, } diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs index 26facef..fc01e40 100644 --- a/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/http.rs @@ -45,11 +45,14 @@ pub async fn run_http_listener( Ok(()) } +/// A component that receives HTTP requests/responses using the HTTP transport +/// defined by the MCP protocol. struct HttpMcpBridge { listener: tokio::net::TcpListener, } impl HttpMcpBridge { + /// Creates a new HTTP-MCP bridge from an existing TCP listener. fn new(listener: tokio::net::TcpListener) -> Self { Self { listener } } @@ -80,6 +83,7 @@ impl ConnectTo for HttpMcpBridge { } } +/// Error type for responding to malformed HTTP requests. #[derive(Debug, thiserror::Error)] #[error(transparent)] struct HttpError(#[from] agent_client_protocol::Error); @@ -97,10 +101,25 @@ impl IntoResponse for HttpError { } } +/// Run a webserver listening on `listener` for HTTP requests at `/` +/// and communicating those requests over `channel` to the JSON-RPC server. async fn run(listener: TcpListener, channel: Channel) -> Result<(), agent_client_protocol::Error> { let (registration_tx, registration_rx) = mpsc::unbounded(); let state = BridgeState { registration_tx }; + // The way that the MCP protocol works is a bit "special". + // + // Clients *POST* messages to `/`. Those are submitted to the MCP server. + // If the message is a REQUEST, then the client waits until it gets a reply. + // It expects the server to close the connection after responding. + // + // Clients can also issue a *GET* request. This will result in a stream of messages. + // + // Non-reply messages can be sent to any open stream (POST, GET, etc) but must be sent to + // exactly one. + // + // There are provisions for "resuming" from a blocked point by tagging each message in the SSE + // stream with an id, but we are not implementing that because I am lazy. async { let app = Router::new() .route("/", post(handle_post).get(handle_get)) @@ -114,36 +133,47 @@ async fn run(listener: TcpListener, channel: Channel) -> Result<(), agent_client .await } +/// The state we pass to our POST/GET handlers. struct BridgeState { + /// Where to send registration messages. registration_tx: mpsc::UnboundedSender, } +/// Messages from HTTP handlers to the bridge server. #[derive(Debug)] #[allow(dead_code)] enum HttpMessage { + /// A JSON-RPC request (has an id, expects a response via the channel). Request { http_request_id: uuid::Uuid, request: agent_client_protocol::jsonrpcmsg::Request, response_tx: mpsc::UnboundedSender, }, + /// A JSON-RPC notification (no id, no response expected). Notification { http_request_id: uuid::Uuid, request: agent_client_protocol::jsonrpcmsg::Request, }, + /// A JSON-RPC response from the client. Response { http_request_id: uuid::Uuid, response: agent_client_protocol::jsonrpcmsg::Response, }, + /// A GET request to open an SSE stream for server-initiated messages. Get { http_request_id: uuid::Uuid, response_tx: mpsc::UnboundedSender, }, } +/// Clone of `agent_client_protocol::jsonrpcmsg::Id` since it does not impl `Hash`. #[derive(Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Clone)] enum JsonRpcId { + /// String identifier. String(String), + /// Numeric identifier. Number(u64), + /// Null identifier (for notifications). Null, } @@ -172,6 +202,7 @@ impl RunningServer { } } + /// The main loop: listen for incoming HTTP messages and outgoing JSON-RPC messages. async fn run( mut self, mut channel: Channel, @@ -215,6 +246,7 @@ impl RunningServer { Ok(()) } + /// Handle an incoming HTTP message (request, notification, response, or GET). fn handle_http_message( &mut self, message: HttpMessage, @@ -339,6 +371,9 @@ impl RegisteredSession { } } +/// Accept a POST request carrying a JSON-RPC message from an MCP client. +/// For requests (messages with id), we return an SSE stream. +/// For notifications/responses (messages without id), we return 202 Accepted. async fn handle_post( State(state): State>, body: String, @@ -392,6 +427,8 @@ async fn handle_post( } } +/// Accept a GET request from an MCP client. +/// Opens an SSE stream for server-initiated messages. async fn handle_get( State(state): State>, ) -> Result>>, HttpError> { diff --git a/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs index 5da406f..308c81c 100644 --- a/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs +++ b/src/agent-client-protocol-polyfill/src/mcp_over_acp/mod.rs @@ -28,8 +28,8 @@ use std::collections::HashMap; use std::path::PathBuf; use agent_client_protocol::schema::{ - McpConnectRequest, McpConnectResponse, McpDisconnectNotification, McpOverAcpMessage, McpServer, - McpServerHttp, McpServerStdio, NewSessionRequest, + InitializeProxyRequest, McpConnectRequest, McpConnectResponse, McpDisconnectNotification, + McpOverAcpMessage, McpServer, McpServerHttp, McpServerStdio, NewSessionRequest, }; use agent_client_protocol::{ Agent, Client, Conductor, ConnectTo, ConnectionTo, Dispatch, Proxy, Role, @@ -149,6 +149,23 @@ impl ConnectTo for McpOverAcpPolyfill { bridge_rx, bridge_connections: HashMap::new(), }) + .on_receive_request_from( + Client, + async move |request: InitializeProxyRequest, + responder, + cx: ConnectionTo| { + // Forward initialize to successor, then set mcpCapabilities.acp = true + // in the response to advertise that we handle MCP-over-ACP. + cx.send_request_to(Agent, request.initialize) + .on_receiving_result(async move |result| { + responder.respond_with_result(result.map(|mut response| { + response.agent_capabilities.mcp_capabilities.acp = true; + response + })) + }) + }, + agent_client_protocol::on_receive_request!(), + ) .on_receive_request_from( Client, { From 3e99bee21f681d74210bf4becad8333c4baa13a1 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 1 May 2026 14:59:25 +0200 Subject: [PATCH 12/12] Trim unused deps --- src/agent-client-protocol-conductor/Cargo.toml | 3 --- src/agent-client-protocol-polyfill/Cargo.toml | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/agent-client-protocol-conductor/Cargo.toml b/src/agent-client-protocol-conductor/Cargo.toml index 1eac8a2..a584148 100644 --- a/src/agent-client-protocol-conductor/Cargo.toml +++ b/src/agent-client-protocol-conductor/Cargo.toml @@ -21,8 +21,6 @@ test-support = [] agent-client-protocol.workspace = true agent-client-protocol-trace-viewer.workspace = true anyhow.workspace = true -async-stream.workspace = true -axum.workspace = true chrono.workspace = true clap.workspace = true futures.workspace = true @@ -31,7 +29,6 @@ rustc-hash.workspace = true serde.workspace = true serde_json.workspace = true strip-ansi-escapes.workspace = true -thiserror.workspace = true tokio.workspace = true tokio-util.workspace = true tracing.workspace = true diff --git a/src/agent-client-protocol-polyfill/Cargo.toml b/src/agent-client-protocol-polyfill/Cargo.toml index a4b4ade..ebde699 100644 --- a/src/agent-client-protocol-polyfill/Cargo.toml +++ b/src/agent-client-protocol-polyfill/Cargo.toml @@ -12,14 +12,12 @@ categories = ["development-tools"] [dependencies] agent-client-protocol = { workspace = true, features = ["unstable_mcp_over_acp"] } -agent-client-protocol-schema.workspace = true anyhow.workspace = true async-stream.workspace = true axum.workspace = true futures.workspace = true futures-concurrency.workspace = true rustc-hash.workspace = true -serde.workspace = true serde_json.workspace = true thiserror = "2.0" tokio.workspace = true