Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 12 additions & 11 deletions src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"], "<article>hi</article>");
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");
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/mcp_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}"),
}
}
Expand Down
Loading