feat(mcp): upgrade rmcp to 0.15.0 and advertise MCP Apps UI extension capability (#6927)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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"}}}
|
||||
|
||||
Reference in New Issue
Block a user