From 77b6d68bf8f87a2fa3faa8abef15920c8910a688 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:49:01 +0000 Subject: [PATCH 1/2] build(deps): bump rmcp from 2.2.0 to 3.4.0 Bumps [rmcp](https://github.com/modelcontextprotocol/rust-sdk) from 2.2.0 to 3.4.0. - [Release notes](https://github.com/modelcontextprotocol/rust-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/rust-sdk/blob/main/release-plz.toml) - [Commits](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v2.2.0...rmcp-v3.4.0) --- updated-dependencies: - dependency-name: rmcp dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Cargo.lock | 75 ++++++++++++------------------------------------------ Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e2bdbf..4ce55f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,17 +373,6 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - [[package]] name = "async-tungstenite" version = "0.35.0" @@ -778,37 +767,14 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - [[package]] name = "darling" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.24.1", - "darling_macro 0.24.1", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", + "darling_core", + "darling_macro", ] [[package]] @@ -824,24 +790,13 @@ dependencies = [ "syn 3.0.6", ] -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.119", -] - [[package]] name = "darling_macro" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.24.1", + "darling_core", "quote", "syn 3.0.6", ] @@ -1958,9 +1913,9 @@ dependencies = [ [[package]] name = "process-wrap" -version = "9.1.0" +version = "10.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +checksum = "1f21b97672d2dc848e7b25701ab4535618b92f4861c13cc3f7f7bed52ad3c8da" dependencies = [ "futures", "indexmap 2.14.2", @@ -2208,15 +2163,16 @@ dependencies = [ [[package]] name = "rmcp" -version = "2.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14db48ee17a9ba61810ab1a9c1beb7d06d8136ae39ac25a1137f10d357af01af" +checksum = "b23c62fe489ac1d401ab32688cfacac3737a8978dc3343e5361464c7724fd3cb" dependencies = [ - "async-trait", - "base64 0.22.1", + "base64 0.23.1", + "bytes", "chrono", "futures", "http", + "indexmap 2.14.2", "pastey", "pin-project-lite", "process-wrap", @@ -2231,19 +2187,20 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "2.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783d787bf21813b285f13019adc49e11af501c658890c1e519f31f937c68b7e3" +checksum = "cd740c45d66ceb87e5579082abc27bd771665e464e9660a17a048c721b2a6025" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "serde_json", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -2551,7 +2508,7 @@ version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" dependencies = [ - "darling 0.24.1", + "darling", "proc-macro2", "quote", "syn 3.0.6", diff --git a/Cargo.toml b/Cargo.toml index ee82a78..e62bf28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } # MCP SDK -rmcp = { version = "2.1.0", features = ["server", "transport-io", "schemars"] } +rmcp = { version = "3.4.0", features = ["server", "transport-io", "schemars"] } # CLI parsing clap = { version = "4.5", features = ["derive"] } From d290dbba03ae4dcbf5d336ec34b6b060a3235847 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 24 Sep 2026 13:11:19 +0200 Subject: [PATCH 2/2] feat(rmcp)!: migrate integration to rmcp 3.4 Adapt tool responses and configuration types, retain existing default-protocol behavior, and verify real modern MCP requests and MRTR through the adapter. BREAKING CHANGE: rmcp is a public dependency of agent-client-protocol-rmcp. Supplied services must migrate to rmcp 3.x; release the integration as 4.x while keeping the core ACP SDK on 2.x. --- .release-plz.toml | 4 +- md/SUMMARY.md | 1 + md/migration-rmcp-v4.md | 61 +++++ .../tests/scoped_mcp_server.rs | 4 +- .../tests/standalone_mcp_server.rs | 6 +- .../tests/test_tool_fn.rs | 2 +- .../tests/trace_client_mcp_server.rs | 2 +- .../tests/trace_mcp_tool_call.rs | 8 +- src/agent-client-protocol-cookbook/src/lib.rs | 4 +- src/agent-client-protocol-rmcp/CHANGELOG.md | 16 ++ src/agent-client-protocol-rmcp/README.md | 7 + .../examples/with_mcp_server.rs | 4 +- src/agent-client-protocol-rmcp/src/builder.rs | 34 ++- .../tests/modern_mcp.rs | 218 ++++++++++++++++++ .../src/bin/mcp_echo_server.rs | 4 +- 15 files changed, 345 insertions(+), 30 deletions(-) create mode 100644 md/migration-rmcp-v4.md create mode 100644 src/agent-client-protocol-rmcp/tests/modern_mcp.rs diff --git a/.release-plz.toml b/.release-plz.toml index 2af3fdd..d79c4e0 100644 --- a/.release-plz.toml +++ b/.release-plz.toml @@ -35,7 +35,9 @@ version_group = "acp" # major release here. Keep it outside the `acp` version group because its # version need not equal the core version, but verify dependency-only release # proposals manually: core 1.x -> 2.x requires rmcp 2.x -> 3.x even if -# release-plz initially classifies the manifest update as a patch. +# release-plz initially classifies the manifest update as a patch. Upgrading +# the public rmcp dependency from 2.x -> 3.x similarly requires this integration +# crate to move from 3.x -> 4.x; it does not require a core ACP major release. [[package]] name = "agent-client-protocol-rmcp" diff --git a/md/SUMMARY.md b/md/SUMMARY.md index e9fd0e8..c51ce8a 100644 --- a/md/SUMMARY.md +++ b/md/SUMMARY.md @@ -31,6 +31,7 @@ # Reference +- [Migrating the rmcp Integration to v4](./migration-rmcp-v4.md) - [Migrating to v2.0](./migration_v2.0.md) - [Migrating to v0.11](./migration_v0.11.x.md) diff --git a/md/migration-rmcp-v4.md b/md/migration-rmcp-v4.md new file mode 100644 index 0000000..1de6f43 --- /dev/null +++ b/md/migration-rmcp-v4.md @@ -0,0 +1,61 @@ +# Migrating the rmcp Integration to v4 + +The next major release of `agent-client-protocol-rmcp` upgrades its public +`rmcp` dependency from 2.x to 3.4. This is a breaking change for integrations +that pass rmcp services or types across the crate boundary. The core +`agent-client-protocol` SDK remains on 2.x, and the minimum supported Rust +version remains 1.88. + +| Integration crate | Core ACP SDK | MCP SDK | +| --- | --- | --- | +| `agent-client-protocol-rmcp` 4.x (unreleased) | 2.x | `rmcp` 3.x | +| `agent-client-protocol-rmcp` 3.x | 2.x | `rmcp` 2.x | + +Upgrade the application's rmcp dependency together with the integration crate. +A service implementing rmcp 2.x's `Service` cannot be passed to the new +`McpServer::from_rmcp`, even when it provides the same tools. + +## Custom services and tools + +- Use `ServerConfig` and `ClientConfig` instead of the deprecated `ServerInfo` + and `ClientInfo` aliases. +- A manual `ServerHandler::call_tool` implementation now returns + `Result`. Convert a completed `CallToolResult` + with `.into()`. The response enum also represents MRTR input-required + results and task-extension results; do not assume every response is a + completed tool result. +- Functions registered with rmcp's `#[tool]` macro can still return + `CallToolResult`. The ACP integration's `tool` and `tool_fn` builder APIs also + remain available, and their results become completed tool responses. +- `ToolExecution` and `Tool::with_execution` are no longer part of the tool + model. Do not add the old task-execution marker to tool definitions. + +For example, a manual handler that previously returned +`Ok(CallToolResult::structured(value))` returns +`Ok(CallToolResult::structured(value).into())` under its new +`CallToolResponse` return type. + +## Modern MCP is now available to supplied services + +rmcp 3.4 implements discovery, per-request metadata, modern result shapes, +MRTR, and subscription APIs for MCP 2026-07-28. The adapter preserves those +requests and results when using `McpServer::from_rmcp`; a supplied service is +still responsible for its advertised capabilities and handlers. + +The integration tests exercise both the built-in tool server and a supplied +rmcp service with actual 2026-07-28 requests, without sending `initialize`. +They cover direct tool calls before discovery, discovery itself, per-request +version errors, and an MRTR retry with fresh request metadata. + +Do not treat the version-string constant or dependency upgrade alone as +protocol selection. rmcp 3.4's `ProtocolVersion::LATEST` still defaults to +2025-11-25. A modern client must select 2026-07-28 explicitly and include +the required request metadata. + +## MCP-over-ACP remains a separate draft + +This upgrade does not change ACP's unstable `mcp/connect`, `mcp/message`, or +`mcp/disconnect` envelopes. Redesigning that transport around stateless, +server-addressed requests is separate work. The new transport's latest-only +target does not require removing existing rmcp behavior from this prerequisite +dependency upgrade. 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 f63d19b..d0e3a64 100644 --- a/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/scoped_mcp_server.rs @@ -39,7 +39,7 @@ async fn test_scoped_mcp_server_through_proxy() -> Result<(), agent_client_proto .await?; expect_test::expect![[r#" - "OK: CallToolResult { content: [Text(TextContent { text: \"2\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" + "OK: CallToolResult { result_type: None, content: [Text(TextContent { text: \"2\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" "#]].assert_debug_eq(&result); Ok(()) @@ -84,7 +84,7 @@ async fn test_scoped_mcp_server_through_session() -> Result<(), agent_client_pro .await?; expect_test::expect![[r#" - "OK: CallToolResult { content: [Text(TextContent { text: \"2\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" + "OK: CallToolResult { result_type: None, content: [Text(TextContent { text: \"2\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" "#]].assert_debug_eq(&result); Ok(()) diff --git a/src/agent-client-protocol-conductor/tests/standalone_mcp_server.rs b/src/agent-client-protocol-conductor/tests/standalone_mcp_server.rs index a07b5dc..8de40f5 100644 --- a/src/agent-client-protocol-conductor/tests/standalone_mcp_server.rs +++ b/src/agent-client-protocol-conductor/tests/standalone_mcp_server.rs @@ -7,7 +7,7 @@ use agent_client_protocol::{ ByteStreams, ConnectTo, RunWithConnectionTo, mcp_server::McpServer, role::mcp, util::run_until, }; use agent_client_protocol_rmcp::McpServerExt as _; -use rmcp::{ClientHandler, ServiceExt, model::ClientInfo}; +use rmcp::{ClientHandler, ServiceExt, model::ClientConfig}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; @@ -64,8 +64,8 @@ fn create_test_server() -> McpServer ClientInfo { - ClientInfo::default() + fn get_info(&self) -> ClientConfig { + ClientConfig::default() } } 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 74ec86a..5520341 100644 --- a/src/agent-client-protocol-conductor/tests/test_tool_fn.rs +++ b/src/agent-client-protocol-conductor/tests/test_tool_fn.rs @@ -74,7 +74,7 @@ async fn test_tool_fn_greet() -> Result<(), agent_client_protocol::Error> { .await?; expect_test::expect![[r#" - "OK: CallToolResult { content: [Text(TextContent { text: \"\\\"Hello, World!\\\"\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" + "OK: CallToolResult { result_type: None, content: [Text(TextContent { text: \"\\\"Hello, World!\\\"\", meta: None, annotations: None })], structured_content: None, is_error: Some(false), meta: None }" "#]].assert_debug_eq(&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 f350fee..3987983 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 @@ -500,7 +500,7 @@ async fn test_trace_client_mcp_server() -> Result<(), agent_client_protocol::Err "sessionUpdate": String("agent_message_chunk"), "content": Object { "type": String("text"), - "text": String("OK: CallToolResult { content: [Text(TextContent { text: \"{\\\"echoed\\\":\\\"Client echoes: Hello from client test!\\\",\\\"call_number\\\":1}\", meta: None, annotations: None })], structured_content: Some(Object {\"echoed\": String(\"Client echoes: Hello from client test!\"), \"call_number\": Number(1)}), is_error: Some(false), meta: None }"), + "text": String("OK: CallToolResult { result_type: None, content: [Text(TextContent { text: \"{\\\"echoed\\\":\\\"Client echoes: Hello from client test!\\\",\\\"call_number\\\":1}\", meta: None, annotations: None })], structured_content: Some(Object {\"echoed\": String(\"Client echoes: Hello from client test!\"), \"call_number\": Number(1)}), is_error: Some(false), meta: None }"), }, "messageId": String("testy-message-end-turn-1"), }, 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 a777ddb..48366eb 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 @@ -670,7 +670,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> "capabilities": Object {}, "clientInfo": Object { "name": String("rmcp"), - "version": String("2.2.0"), + "version": String("3.4.0"), }, }, }, @@ -689,7 +689,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> }, "serverInfo": Object { "name": String("rmcp"), - "version": String("2.2.0"), + "version": String("3.4.0"), }, "instructions": String("A simple test MCP server with an echo tool"), }, @@ -761,7 +761,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> "sessionUpdate": String("agent_message_chunk"), "content": Object { "type": String("text"), - "text": String("OK: CallToolResult { content: [Text(TextContent { 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 }"), + "text": String("OK: CallToolResult { result_type: None, content: [Text(TextContent { 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 }"), }, "messageId": String("testy-message-end-turn-1"), }, @@ -794,7 +794,7 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error> "sessionUpdate": String("agent_message_chunk"), "content": Object { "type": String("text"), - "text": String("OK: CallToolResult { content: [Text(TextContent { 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 }"), + "text": String("OK: CallToolResult { result_type: None, content: [Text(TextContent { 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 }"), }, "messageId": String("testy-message-end-turn-1"), }, diff --git a/src/agent-client-protocol-cookbook/src/lib.rs b/src/agent-client-protocol-cookbook/src/lib.rs index fa248e6..58fba61 100644 --- a/src/agent-client-protocol-cookbook/src/lib.rs +++ b/src/agent-client-protocol-cookbook/src/lib.rs @@ -720,8 +720,8 @@ pub mod global_mcp_server { //! //! #[tool_handler] //! impl ServerHandler for MyMcpServer { - //! fn get_info(&self) -> ServerInfo { - //! ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + //! fn get_info(&self) -> ServerConfig { + //! ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) //! .with_protocol_version(ProtocolVersion::V_2024_11_05) //! .with_server_info(Implementation::from_build_env()) //! } diff --git a/src/agent-client-protocol-rmcp/CHANGELOG.md b/src/agent-client-protocol-rmcp/CHANGELOG.md index b0b9e9f..0a25138 100644 --- a/src/agent-client-protocol-rmcp/CHANGELOG.md +++ b/src/agent-client-protocol-rmcp/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Breaking changes + +- Upgrade the public `rmcp` dependency from 2.x to 3.4. This requires the next + major release of `agent-client-protocol-rmcp` (4.x), while the core ACP SDK + remains on 2.x. Services supplied to `McpServer::from_rmcp` must use rmcp 3.x. +- Adapt tool handlers to rmcp's `CallToolResponse`, use `ServerConfig` in place + of the deprecated `ServerInfo` alias, and remove legacy tool-execution + metadata. See the [migration guide](https://agentclientprotocol.github.io/rust-sdk/migration-rmcp-v4.html). + +### Added + +- Integration coverage for MCP 2026-07-28 requests without initialization, + discovery, per-request metadata/version validation, and MRTR results from + caller-supplied rmcp services. This dependency upgrade does not replace the + current unstable MCP-over-ACP wire lifecycle. + ## [3.1.1](https://github.com/agentclientprotocol/rust-sdk/compare/agent-client-protocol-rmcp-v3.1.0...agent-client-protocol-rmcp-v3.1.1) - 2026-09-18 ### Other diff --git a/src/agent-client-protocol-rmcp/README.md b/src/agent-client-protocol-rmcp/README.md index f361405..4b12c96 100644 --- a/src/agent-client-protocol-rmcp/README.md +++ b/src/agent-client-protocol-rmcp/README.md @@ -67,10 +67,17 @@ release of either dependency therefore requires a major release of this crate. | agent-client-protocol-rmcp | agent-client-protocol | rmcp | | -------------------------- | --------------------- | ---- | +| 4.x (unreleased) | 2.x | 3.x | | 3.x | 2.x | 2.x | | 2.x | 1.x | 2.x | | 1.x | 1.x | 1.x | +See the [rmcp 4.x integration migration guide](https://agentclientprotocol.github.io/rust-sdk/migration-rmcp-v4.html) +for adapting services to rmcp 3.x. The minimum supported Rust version remains +1.88. The dependency upgrade does not itself change the unstable ACP transport +envelopes or require applications to use modern MCP instead of rmcp's default +protocol version. + ## Related Crates - **[agent-client-protocol](../agent-client-protocol/)** — Core ACP protocol types and traits diff --git a/src/agent-client-protocol-rmcp/examples/with_mcp_server.rs b/src/agent-client-protocol-rmcp/examples/with_mcp_server.rs index 36b5d7e..c8dcc4e 100644 --- a/src/agent-client-protocol-rmcp/examples/with_mcp_server.rs +++ b/src/agent-client-protocol-rmcp/examples/with_mcp_server.rs @@ -66,8 +66,8 @@ impl ExampleMcpServer { #[allow(unknown_lints, clippy::unused_async_trait_impl)] #[tool_handler] impl ServerHandler for ExampleMcpServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + fn get_info(&self) -> ServerConfig { + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(Implementation::new("example-mcp-server", "0.1.0")) .with_protocol_version(ProtocolVersion::V_2024_11_05) .with_instructions("A simple example MCP server with an echo tool") diff --git a/src/agent-client-protocol-rmcp/src/builder.rs b/src/agent-client-protocol-rmcp/src/builder.rs index a41da1e..585cd94 100644 --- a/src/agent-client-protocol-rmcp/src/builder.rs +++ b/src/agent-client-protocol-rmcp/src/builder.rs @@ -6,7 +6,10 @@ use futures::future::{BoxFuture, Either}; use futures_concurrency::future::TryJoin; use rmcp::{ ErrorData, ServerHandler, - model::{CallToolResult, ListToolsResult, Tool}, + model::{ + CacheScope, CallToolResponse, CallToolResult, ListToolsResult, ProtocolVersion, + ServerConfig, Tool, + }, }; use schemars::JsonSchema; use serde::{Serialize, de::DeserializeOwned}; @@ -303,7 +306,7 @@ impl ServerHandler for McpServerConnection { &self, request: rmcp::model::CallToolRequestParams, context: rmcp::service::RequestContext, - ) -> Result { + ) -> Result { // Lookup the tool definition, erroring if not found or disabled let Some(registered) = self.data.enabled_tool(&request.name) else { return Err(rmcp::model::ErrorData::invalid_params( @@ -328,11 +331,10 @@ impl ServerHandler for McpServerConnection { Ok(result) => { // Use structured output only if the tool declared an output_schema if has_structured_output { - Ok(CallToolResult::structured(result)) + Ok(CallToolResult::structured(result).into()) } else { - Ok(CallToolResult::success(vec![ - rmcp::model::ContentBlock::text(result.to_string()), - ])) + let content = rmcp::model::ContentBlock::text(result.to_string()); + Ok(CallToolResult::success(vec![content]).into()) } } Err(error) => Err(to_rmcp_error(error)), @@ -348,7 +350,7 @@ impl ServerHandler for McpServerConnection { fn list_tools( &self, _request: Option, - _context: rmcp::service::RequestContext, + context: rmcp::service::RequestContext, ) -> impl Future> + Send { // Return only enabled tools let tools: Vec<_> = self @@ -356,12 +358,21 @@ impl ServerHandler for McpServerConnection { .enabled_tools() .map(|tool| make_tool_model(tool.metadata())) .collect(); - std::future::ready(Ok(ListToolsResult::with_all_items(tools))) + let mut result = ListToolsResult::with_all_items(tools); + if context + .protocol_version() + .is_some_and(|version| version >= ProtocolVersion::V_2026_07_28) + { + // Modern MCP requires cache metadata. Avoid sharing tool catalogs + // across authorization contexts or promising a stale cache lifetime. + result = result.with_ttl_ms(0).with_cache_scope(CacheScope::Private); + } + std::future::ready(Ok(result)) } - fn get_info(&self) -> rmcp::model::ServerInfo { + fn get_info(&self) -> ServerConfig { // Basic server info - let base = rmcp::model::ServerInfo::new( + let base = ServerConfig::new( rmcp::model::ServerCapabilities::builder() .enable_tools() .build(), @@ -383,8 +394,7 @@ fn make_tool_model(metadata: &McpToolMetadata) -> Tool { metadata.name().to_string(), metadata.description().to_string(), metadata.input_schema().clone(), - ) - .with_execution(rmcp::model::ToolExecution::new()); + ); if let Some(title) = metadata.title() { tool = tool.with_title(title.to_string()); diff --git a/src/agent-client-protocol-rmcp/tests/modern_mcp.rs b/src/agent-client-protocol-rmcp/tests/modern_mcp.rs new file mode 100644 index 0000000..4f102be --- /dev/null +++ b/src/agent-client-protocol-rmcp/tests/modern_mcp.rs @@ -0,0 +1,218 @@ +//! Exercise the adapter with modern MCP requests, without an initialization handshake. + +use std::{future::Future, time::Duration}; + +use agent_client_protocol::{ + ConnectionTo, Error, UntypedMessage, mcp_server::McpServer, role::mcp, +}; +use agent_client_protocol_rmcp::McpServerExt; +use rmcp::{ + ErrorData, RoleServer, ServerHandler, + model::{ + CallToolRequestParams, CallToolResponse, CallToolResult, InputRequiredResult, + ServerCapabilities, ServerConfig, + }, + service::RequestContext, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; + +const TIMEOUT: Duration = Duration::from_secs(10); +const MODERN_VERSION: &str = "2026-07-28"; + +fn params(mut params: Value, marker: &str) -> Value { + params["_meta"] = json!({ + "io.modelcontextprotocol/protocolVersion": MODERN_VERSION, + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": { + "name": "modern-adapter-test", + "version": "1" + }, + "example/marker": marker + }); + params +} + +async fn request( + connection: &ConnectionTo, + method: &str, + params: Value, +) -> Result { + connection + .send_request(UntypedMessage::new(method, params)?) + .block_task() + .await +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +struct Echo { + message: String, +} + +#[tokio::test] +async fn built_server_handles_modern_calls_before_discovery() -> Result<(), Error> { + tokio::time::timeout(TIMEOUT, async { + let server = McpServer::::builder("modern-tools") + .tool_fn( + "echo", + "Echo a message", + async |input: Echo, _cx| Ok(input), + agent_client_protocol_rmcp::tool_fn!(), + ) + .build(); + + mcp::Client + .builder() + .connect_with(server, async |connection| { + // Neither MCP initialize nor discovery is a prerequisite for a tool call. + let result = request( + &connection, + "tools/call", + params( + json!({"name": "echo", "arguments": {"message": "hello"}}), + "call", + ), + ) + .await?; + assert_eq!(result["resultType"], "complete"); + assert_eq!(result["structuredContent"], json!({"message": "hello"})); + + let discovery = request( + &connection, + "server/discover", + params(json!({}), "discover"), + ) + .await?; + assert!( + discovery["supportedVersions"] + .as_array() + .expect("discovery should list supported protocol versions") + .contains(&json!(MODERN_VERSION)) + ); + assert!(discovery["capabilities"]["tools"].is_object()); + + let tools = request(&connection, "tools/list", params(json!({}), "list")).await?; + assert_eq!(tools["resultType"], "complete"); + assert_eq!(tools["ttlMs"], 0); + assert_eq!(tools["cacheScope"], "private"); + assert_eq!(tools["tools"][0]["name"], "echo"); + + let mut legacy = params(json!({}), "legacy"); + legacy["_meta"]["io.modelcontextprotocol/protocolVersion"] = json!("2025-11-25"); + let tools = request(&connection, "tools/list", legacy).await?; + assert!(tools.get("ttlMs").is_none()); + assert!(tools.get("cacheScope").is_none()); + + let mut unsupported = params(json!({}), "unsupported"); + unsupported["_meta"]["io.modelcontextprotocol/protocolVersion"] = + json!("1900-01-01"); + let error = request(&connection, "tools/list", unsupported) + .await + .expect_err("the server must reject an unsupported per-request version"); + assert_eq!(serde_json::to_value(error)?["code"], -32022); + + // A failed request must not change the context for the next request. + let tools = + request(&connection, "tools/list", params(json!({}), "after-error")).await?; + assert_eq!(tools["tools"][0]["name"], "echo"); + Ok(()) + }) + .await + }) + .await + .expect("modern adapter test timed out") +} + +struct RetryServer; + +impl ServerHandler for RetryServer { + fn call_tool( + &self, + request: CallToolRequestParams, + context: RequestContext, + ) -> impl Future> + Send { + std::future::ready(match request.request_state.as_deref() { + None => { + let input_requests = serde_json::from_value(json!({ + "confirmation": { + "method": "elicitation/create", + "params": { + "mode": "form", + "message": "Confirm this test call", + "requestedSchema": { + "type": "object", + "properties": {"approved": {"type": "boolean"}}, + "required": ["approved"] + } + } + } + })) + .expect("the test elicitation request should deserialize"); + Ok(InputRequiredResult::new( + Some(input_requests), + Some("opaque-test-state".to_owned()), + ) + .into()) + } + Some("opaque-test-state") => Ok(CallToolResult::structured(json!({ + "marker": context.meta.get("example/marker"), + "inputResponses": request.input_responses + })) + .into()), + Some(_) => Err(ErrorData::invalid_params("unexpected retry state", None)), + }) + } + + fn get_info(&self) -> ServerConfig { + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) + } +} + +#[tokio::test] +async fn supplied_rmcp_service_preserves_mrtr_and_per_request_metadata() -> Result<(), Error> { + tokio::time::timeout(TIMEOUT, async { + let server = McpServer::::from_rmcp("retry-tools", || RetryServer); + mcp::Client + .builder() + .connect_with(server, async |connection| { + let mut first_params = params(json!({"name": "retry", "arguments": {}}), "first"); + first_params["_meta"]["io.modelcontextprotocol/clientCapabilities"] = + json!({"elicitation": {"form": {}}}); + let first = request(&connection, "tools/call", first_params).await?; + assert_eq!(first["resultType"], "input_required"); + assert_eq!(first["requestState"], "opaque-test-state"); + assert_eq!( + first["inputRequests"]["confirmation"]["method"], + "elicitation/create" + ); + + // This is a fresh JSON-RPC request with its own metadata, not a reverse RPC. + let input_responses = json!({ + "confirmation": {"action": "accept", "content": {"approved": true}} + }); + let mut retry_params = params( + json!({ + "name": "retry", + "arguments": {}, + "requestState": first["requestState"], + "inputResponses": input_responses + }), + "second", + ); + retry_params["_meta"]["io.modelcontextprotocol/clientCapabilities"] = + json!({"elicitation": {"form": {}}}); + let second = request(&connection, "tools/call", retry_params).await?; + assert_eq!(second["resultType"], "complete"); + assert_eq!(second["structuredContent"]["marker"], "second"); + assert_eq!( + second["structuredContent"]["inputResponses"], + input_responses + ); + Ok(()) + }) + .await + }) + .await + .expect("MRTR adapter test timed out") +} diff --git a/src/agent-client-protocol-test/src/bin/mcp_echo_server.rs b/src/agent-client-protocol-test/src/bin/mcp_echo_server.rs index 73462ae..ed089b1 100644 --- a/src/agent-client-protocol-test/src/bin/mcp_echo_server.rs +++ b/src/agent-client-protocol-test/src/bin/mcp_echo_server.rs @@ -51,8 +51,8 @@ impl EchoServer { #[allow(unknown_lints, clippy::unused_async_trait_impl)] #[tool_handler] impl ServerHandler for EchoServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + fn get_info(&self) -> ServerConfig { + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(Implementation::new("mcp-echo-server", "1.0.0")) .with_protocol_version(ProtocolVersion::V_2024_11_05) .with_instructions("A simple MCP server with an echo tool for testing")