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
@@ -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,
},