From 97cefd6fffa644b2099e324390d37d4be2f28708 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Sat, 19 Sep 2026 01:45:05 +0000 Subject: [PATCH] feat(mcp)!: upgrade rmcp to 2.x Move the mcp-server feature from rmcp 1.6 to 2.2. rmcp 2 aligns its model types with MCP 2025-11-25: Content/RawContent become the flat ContentBlock enum and Resource is a flat struct, so tool results and resource listings are built with ContentBlock::text and Resource::new(uri, name). BREAKING CHANGE: rmcp types are part of the public MCP API (CliFrameworkHandler implements rmcp::ServerHandler; dispatch_tool_call*, list_resources_result and read_resource_uri take/return rmcp model types), so consumers must move to rmcp 2 together with this release. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 15 +++++++++++++++ Cargo.toml | 2 +- src/mcp/mod.rs | 23 ++++++++++++----------- tests/unit/mcp_dispatch.rs | 4 ++-- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ce71b..663781a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +### Breaking + +- The `mcp-server` feature now depends on `rmcp` 2.x (was 1.x). rmcp types + are part of cli-framework's public MCP API: `CliFrameworkHandler` implements + `rmcp::ServerHandler`, `dispatch_tool_call*` take `rmcp::model::JsonObject` + and return `rmcp::model::CallToolResult` / `ErrorData`, and + `list_resources_result` / `read_resource_uri` return rmcp result types. + Consumers that name these types or wrap `CliFrameworkHandler` in their own + `ServerHandler` must move to `rmcp = "2"` at the same time. rmcp 2 aligns its + model with MCP 2025-11-25: `Content`/`RawContent` become the flat + `ContentBlock` enum (match `result.content[0]` directly instead of + `.raw`), and `Resource` is a flat struct (`Resource::new(uri, name)` instead + of `Resource::new(RawResource::new(..), None)`). cli-framework's own + signatures and wire output are unchanged. + ### Added - `self-install` feature (ADR 0080, phase 1): `AppBuilder::with_self_install` diff --git a/Cargo.toml b/Cargo.toml index e36e939..06db50a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ indicatif = { version = "0.17", optional = true } # Optional MCP server dependencies # - transport-io: server-side stdio transport (Phase 2 rollout #50) # - client: enabled so integration tests can exercise stdio-style transports without TCP -rmcp = { version = "1.6", features = ["server", "client", "transport-streamable-http-server", "transport-io"], optional = true } +rmcp = { version = "2.2", features = ["server", "client", "transport-streamable-http-server", "transport-io"], optional = true } axum = { version = "0.8", optional = true } # `http` (not `axum`'s re-export) so the per-request identity seam's public # signature (`&http::HeaderMap`) names a crate consumers can depend on directly. diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 3cc8647..5778895 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -19,9 +19,10 @@ pub use banner::BannerSettings; #[cfg(feature = "mcp-server")] use rmcp::{ model::{ - CallToolRequestParams, CallToolResult, Content, ErrorData, JsonObject, ListResourcesResult, - ListToolsResult, Meta, PaginatedRequestParams, RawResource, ReadResourceRequestParams, - ReadResourceResult, Resource, ResourceContents, ServerCapabilities, ServerInfo, Tool, + CallToolRequestParams, CallToolResult, ContentBlock, ErrorData, JsonObject, + ListResourcesResult, ListToolsResult, Meta, PaginatedRequestParams, + ReadResourceRequestParams, ReadResourceResult, Resource, ResourceContents, + ServerCapabilities, ServerInfo, Tool, }, service::RequestContext, RoleServer, ServerHandler, @@ -373,7 +374,7 @@ fn make_rmcp_tool(desc: &McpToolDescriptor) -> Tool { Arc::new(input_schema), ); - // rmcp 1.6 `Tool` carries a per-tool `_meta` passthrough (`Tool::meta`, + // rmcp 2 `Tool` carries a per-tool `_meta` passthrough (`Tool::meta`, // serialized as `_meta`) but has NO `visibility` field. We therefore merge // the command's opaque `_meta` value AND the `visibility` tags into a single // `_meta` object so both survive on the wire (see R1). The opaque `_meta` @@ -484,10 +485,10 @@ impl CliFrameworkHandler { .listings() .into_iter() .map(|listing| { - let mut raw = RawResource::new(listing.uri, listing.name); - raw.description = listing.description; - raw.mime_type = listing.mime_type; - Resource::new(raw, None) + let mut resource = Resource::new(listing.uri, listing.name); + resource.description = listing.description; + resource.mime_type = listing.mime_type; + resource }) .collect(); ListResourcesResult { @@ -702,7 +703,7 @@ pub async fn dispatch_tool_call_with_identity( // CF-7: a command may attach a `structuredContent` value distinct // from the `content` text (e.g. server-rendered View HTML), kept out // of the model's text context. - let mut result = CallToolResult::success(vec![Content::text(text)]); + let mut result = CallToolResult::success(vec![ContentBlock::text(text)]); result.structured_content = output.structured; Ok(result) } @@ -1162,8 +1163,8 @@ mod cf7_structured_content_tests { // structuredContent carries the HTML; the model-facing text does not. let structured = result.structured_content.expect("structured content set"); assert_eq!(structured["html"], "
hi
"); - let text = match &result.content[0].raw { - rmcp::model::RawContent::Text(t) => t.text.clone(), + let text = match &result.content[0] { + rmcp::model::ContentBlock::Text(t) => t.text.clone(), other => panic!("expected text content, got {other:?}"), }; assert_eq!(text, "text fallback for the model\n"); diff --git a/tests/unit/mcp_dispatch.rs b/tests/unit/mcp_dispatch.rs index 8e4cf11..d4a0129 100644 --- a/tests/unit/mcp_dispatch.rs +++ b/tests/unit/mcp_dispatch.rs @@ -349,8 +349,8 @@ fn whoami_cmd() -> Command { } fn call_result_text(result: &rmcp::model::CallToolResult) -> String { - match &result.content[0].raw { - rmcp::model::RawContent::Text(t) => t.text.clone(), + match &result.content[0] { + rmcp::model::ContentBlock::Text(t) => t.text.clone(), other => panic!("expected text content, got {other:?}"), } }