feat(mcp): upgrade rmcp to 0.15.0 and advertise MCP Apps UI extension capability (#6927)

This commit is contained in:
Andrew Harvard
2026-02-11 09:34:04 -05:00
committed by GitHub
parent 0e7294835c
commit 544bc1bc4f
24 changed files with 382 additions and 76 deletions
@@ -417,6 +417,7 @@ impl ServerHandler for AutoVisualiserRouter {
name: "goose-autovisualiser".to_string(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: None,
description: None,
icons: None,
website_url: None,
},
@@ -1301,6 +1301,7 @@ impl ServerHandler for ComputerControllerServer {
name: "goose-computercontroller".to_string(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: None,
description: None,
icons: None,
website_url: None,
},
@@ -427,6 +427,7 @@ impl ServerHandler for DeveloperServer {
name: "goose-developer".to_string(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: None,
description: None,
icons: None,
website_url: None,
},
+1
View File
@@ -549,6 +549,7 @@ impl ServerHandler for MemoryServer {
name: "goose-memory".to_string(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: None,
description: None,
icons: None,
website_url: None,
},
+1
View File
@@ -114,6 +114,7 @@ impl ServerHandler for TutorialServer {
name: "goose-tutorial".to_string(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: None,
description: None,
icons: None,
website_url: None,
},
+5 -1
View File
@@ -12,7 +12,7 @@ use goose::session::{Session, SessionInsights, SessionType, SystemInfo};
use rmcp::model::{
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ResourceContents, Role,
TextContent, Tool, ToolAnnotations,
TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
};
use utoipa::{OpenApi, ToSchema};
@@ -319,6 +319,8 @@ derive_utoipa!(RawEmbeddedResource as RawEmbeddedResourceSchema);
derive_utoipa!(RawResource as RawResourceSchema);
derive_utoipa!(Tool as ToolSchema);
derive_utoipa!(ToolAnnotations as ToolAnnotationsSchema);
derive_utoipa!(ToolExecution as ToolExecutionSchema);
derive_utoipa!(TaskSupport as TaskSupportSchema);
derive_utoipa!(Annotations as AnnotationsSchema);
derive_utoipa!(ResourceContents as ResourceContentsSchema);
derive_utoipa!(JsonObject as JsonObjectSchema);
@@ -497,6 +499,8 @@ derive_utoipa!(Icon as IconSchema);
RecipeManifest,
ToolSchema,
ToolAnnotationsSchema,
ToolExecutionSchema,
TaskSupportSchema,
ToolInfo,
PermissionLevel,
Permission,
@@ -131,11 +131,13 @@ impl AppsManagerClient {
experimental: None,
tasks: None,
logging: None,
extensions: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
title: Some("Apps Manager".to_string()),
version: "1.0.0".to_string(),
description: None,
icons: None,
website_url: None,
},
@@ -43,11 +43,12 @@ impl ChatRecallClient {
let info = InitializeResult {
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ServerCapabilities {
tasks: None,
tools: Some(ToolsCapability {
list_changed: Some(false),
}),
tasks: None,
resources: None,
extensions: None,
prompts: None,
completions: None,
experimental: None,
@@ -55,6 +56,7 @@ impl ChatRecallClient {
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Chat Recall".to_string()),
version: "1.0.0".to_string(),
icons: None,
@@ -56,11 +56,12 @@ impl CodeExecutionClient {
let info = InitializeResult {
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ServerCapabilities {
tasks: None,
tools: Some(ToolsCapability {
list_changed: Some(false),
}),
tasks: None,
resources: None,
extensions: None,
prompts: None,
completions: None,
experimental: None,
@@ -68,6 +69,7 @@ impl CodeExecutionClient {
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Code Mode".to_string()),
version: "1.0.0".to_string(),
icons: None,
@@ -925,6 +925,7 @@ impl ExtensionManager {
input_schema: tool.input_schema,
annotations: tool.annotations,
output_schema: tool.output_schema,
execution: tool.execution,
icons: tool.icons,
title: tool.title,
meta: Some(rmcp::model::Meta(meta_map)),
@@ -85,11 +85,12 @@ impl ExtensionManagerClient {
let info = InitializeResult {
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ServerCapabilities {
tasks: None,
tools: Some(ToolsCapability {
list_changed: Some(false),
}),
tasks: None,
resources: None,
extensions: None,
prompts: None,
completions: None,
experimental: None,
@@ -97,6 +98,7 @@ impl ExtensionManagerClient {
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some(EXTENSION_NAME.to_string()),
version: "1.0.0".to_string(),
icons: None,
+74 -31
View File
@@ -2,8 +2,8 @@ use crate::action_required_manager::ActionRequiredManager;
use crate::agents::types::SharedProvider;
use crate::session_context::{SESSION_ID_HEADER, WORKING_DIR_HEADER};
use rmcp::model::{
Content, CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, ErrorCode,
Extensions, JsonObject, Meta,
CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, ErrorCode,
ExtensionCapabilities, Extensions, JsonObject, Meta, SamplingMessageContent,
};
/// MCP client implementation for Goose
use rmcp::{
@@ -223,9 +223,9 @@ impl ClientHandler for GooseClient {
Role::Assistant => crate::conversation::message::Message::assistant(),
};
match msg.content.as_text() {
match msg.content.first().and_then(|c| c.as_text()) {
Some(text) => base.with_text(&text.text),
None => base.with_content(msg.content.clone().into()),
None => base,
}
})
.collect();
@@ -255,29 +255,26 @@ impl ClientHandler for GooseClient {
Ok(CreateMessageResult {
model: usage.model,
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
message: SamplingMessage {
role: Role::Assistant,
// TODO(alexhancock): MCP sampling currently only supports one content on each SamplingMessage
// https://modelcontextprotocol.io/specification/draft/client/sampling#messages
// This doesn't mesh well with goose's approach which has Vec<MessageContent>
// There is a proposal to MCP which is agreed to go in the next version to have SamplingMessages support multiple content parts
// https://github.com/modelcontextprotocol/modelcontextprotocol/pull/198
// Until that is formalized, we can take the first message content from the provider and use it
content: if let Some(content) = response.content.first() {
message: SamplingMessage::new(
Role::Assistant,
if let Some(content) = response.content.first() {
match content {
crate::conversation::message::MessageContent::Text(text) => {
Content::text(&text.text)
SamplingMessageContent::text(&text.text)
}
crate::conversation::message::MessageContent::Image(img) => {
Content::image(&img.data, &img.mime_type)
SamplingMessageContent::Image(rmcp::model::RawImageContent {
data: img.data.clone(),
mime_type: img.mime_type.clone(),
meta: None,
})
}
// TODO(alexhancock) - Content::Audio? goose's messages don't currently have it
_ => Content::text(""),
_ => SamplingMessageContent::text(""),
}
} else {
Content::text("")
SamplingMessageContent::text("")
},
},
),
})
}
@@ -286,20 +283,28 @@ impl ClientHandler for GooseClient {
request: CreateElicitationRequestParams,
_context: RequestContext<RoleClient>,
) -> Result<CreateElicitationResult, ErrorData> {
let schema_value = serde_json::to_value(&request.requested_schema).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to serialize elicitation schema: {}", e),
None,
)
})?;
let (message, schema_value) = match &request {
CreateElicitationRequestParams::FormElicitationParams {
message,
requested_schema,
..
} => {
let schema_value = serde_json::to_value(requested_schema).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to serialize elicitation schema: {}", e),
None,
)
})?;
(message.clone(), schema_value)
}
CreateElicitationRequestParams::UrlElicitationParams { message, url, .. } => {
(message.clone(), serde_json::json!({ "url": url }))
}
};
ActionRequiredManager::global()
.request_and_wait(
request.message.clone(),
schema_value,
Duration::from_secs(300),
)
.request_and_wait(message, schema_value, Duration::from_secs(300))
.await
.map(|user_data| CreateElicitationResult {
action: ElicitationAction::Accept,
@@ -315,10 +320,25 @@ impl ClientHandler for GooseClient {
}
fn get_info(&self) -> ClientInfo {
// Build MCP Apps UI extension capability
// See: https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
let mut ui_extension_settings = JsonObject::new();
ui_extension_settings.insert(
"mimeTypes".to_string(),
serde_json::json!(["text/html;profile=mcp-app"]),
);
let mut extensions = ExtensionCapabilities::new();
extensions.insert(
"io.modelcontextprotocol/ui".to_string(),
ui_extension_settings,
);
ClientInfo {
meta: None,
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ClientCapabilities::builder()
.enable_extensions_with(extensions)
.enable_sampling()
.enable_elicitation()
.build(),
@@ -328,6 +348,7 @@ impl ClientHandler for GooseClient {
.unwrap_or(env!("CARGO_PKG_VERSION").to_owned()),
icons: None,
title: None,
description: None,
website_url: None,
},
}
@@ -945,4 +966,26 @@ mod tests {
assert_eq!(&mcp_meta.0, expected_meta.as_object().unwrap());
}
#[test]
fn test_client_info_advertises_mcp_apps_ui_extension() {
let client = new_client();
let info = ClientHandler::get_info(&client);
// Verify the client advertises the MCP Apps UI extension capability
let extensions = info
.capabilities
.extensions
.expect("capabilities should have extensions");
let ui_ext = extensions
.get("io.modelcontextprotocol/ui")
.expect("should have io.modelcontextprotocol/ui extension");
let mime_types = ui_ext
.get("mimeTypes")
.expect("ui extension should have mimeTypes");
assert_eq!(mime_types, &json!(["text/html;profile=mcp-app"]));
}
}
@@ -260,11 +260,13 @@ impl SummonClient {
completions: None,
experimental: None,
logging: None,
extensions: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
title: Some("Summon".to_string()),
version: "1.0.0".to_string(),
description: None,
icons: None,
website_url: None,
},
+3 -1
View File
@@ -30,11 +30,12 @@ impl TodoClient {
let info = InitializeResult {
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ServerCapabilities {
tasks: None,
tools: Some(ToolsCapability {
list_changed: Some(false),
}),
tasks: None,
resources: None,
extensions: None,
prompts: None,
completions: None,
experimental: None,
@@ -42,6 +43,7 @@ impl TodoClient {
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Todo".to_string()),
version: "1.0.0".to_string(),
icons: None,
+2
View File
@@ -30,11 +30,13 @@ impl TomClient {
completions: None,
experimental: None,
logging: None,
extensions: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
title: Some("Top Of Mind".to_string()),
version: "1.0.0".to_string(),
description: None,
icons: None,
website_url: None,
},
@@ -1,4 +1,4 @@
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation":{},"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}},"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDERR: time=2025-12-11T17:58:47.636-05:00 level=INFO msg="starting server" version=0.24.1 host="" dynamicToolsets=false readOnly=false lockdownEnabled=false
STDERR: GitHub MCP Server running on stdio
STDERR: time=2025-12-11T17:58:47.640-05:00 level=INFO msg="server run start"
@@ -1,4 +1,4 @@
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation":{},"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}},"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDERR: Starting default (STDIO) server...
STDOUT: {"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{"listChanged":true},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"logging":{},"completions":{}},"serverInfo":{"name":"mcp-servers/everything","title":"Everything Reference Server","version":"2.0.0"},"instructions":"# Everything Server Server Instructions\n\nAudience: These instructions are written for an LLM or autonomous agent integrating with the Everything MCP Server.\nFollow them to use, extend, and troubleshoot the server safely and effectively.\n\n## Cross-Feature Relationships\n\n- Use `get-roots-list` to see client workspace roots before file operations\n- `gzip-file-as-resource` creates session-scoped resources accessible only during the current session\n- Enable `toggle-simulated-logging` before debugging to see server log messages\n- Enable `toggle-subscriber-updates` to receive periodic resource update notifications\n\n## Constraints & Limitations\n\n- `gzip-file-as-resource`: Max fetch size controlled by `GZIP_MAX_FETCH_SIZE` (default 10MB), timeout by `GZIP_MAX_FETCH_TIME_MILLIS` (default 30s), allowed domains by `GZIP_ALLOWED_DOMAINS`\n- Session resources are ephemeral and lost when the session ends\n- Sampling requests (`trigger-sampling-request`) require client sampling capability\n- Elicitation requests (`trigger-elicitation-request`) require client elicitation capability\n\n## Operational Patterns\n\n- For long operations, use `trigger-long-running-operation` which sends progress notifications\n- Prefer reading resources before calling mutating tools\n- Check `get-roots-list` output to understand the client's workspace context\n\n## Easter Egg\n\nIf asked about server instructions, respond with \"🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action.\"\n"},"jsonrpc":"2.0","id":0}
STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"}
@@ -1,4 +1,4 @@
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation":{},"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}},"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDERR:
STDERR:
STDERR:
@@ -1,4 +1,4 @@
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"sampling":{},"elicitation":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{"elicitation":{},"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}},"sampling":{}},"clientInfo":{"name":"goose","version":"0.0.0"}}}
STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.25.0"}}}
STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"}
STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"agent-session-id":"test-session-id","progressToken":0},"name":"fetch","arguments":{"url":"https://example.com"}}}