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
258 changes: 224 additions & 34 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ uninlined_format_args = "allow"
string_slice = "warn"

[workspace.dependencies]
rmcp = { version = "0.14.0", features = ["schemars", "auth"] }
rmcp = { version = "0.15.0", features = ["schemars", "auth"] }
anyhow = "1.0"
futures = "0.3"
regex = "1.12"
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/autovisualiser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/computercontroller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/developer/rmcp_developer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/tutorial/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
6 changes: 5 additions & 1 deletion crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
Comment on lines 321 to +323
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schemars→utoipa conversion used by derive_utoipa! currently ignores JSON Schema enum values, so rmcp enums like TaskSupport degrade into meaningless oneOf entries of plain string (as seen in the generated OpenAPI), which in turn makes the generated TypeScript type just string. Extend the converter (e.g., in convert_typed_schema/convert_json_object_to_utoipa) to preserve enum (and collapse redundant oneOf) so the OpenAPI accurately constrains allowed values.

Copilot uses AI. Check for mistakes.
derive_utoipa!(Annotations as AnnotationsSchema);
derive_utoipa!(ResourceContents as ResourceContentsSchema);
derive_utoipa!(JsonObject as JsonObjectSchema);
Expand Down Expand Up @@ -497,6 +499,8 @@ derive_utoipa!(Icon as IconSchema);
RecipeManifest,
ToolSchema,
ToolAnnotationsSchema,
ToolExecutionSchema,
TaskSupportSchema,
ToolInfo,
PermissionLevel,
Permission,
Expand Down
2 changes: 2 additions & 0 deletions crates/goose/src/agents/apps_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
4 changes: 3 additions & 1 deletion crates/goose/src/agents/chatrecall_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,20 @@ 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,
logging: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Chat Recall".to_string()),
version: "1.0.0".to_string(),
icons: None,
Expand Down
4 changes: 3 additions & 1 deletion crates/goose/src/agents/code_execution_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,20 @@ 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,
logging: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Code Mode".to_string()),
version: "1.0.0".to_string(),
icons: None,
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/agents/extension_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
4 changes: 3 additions & 1 deletion crates/goose/src/agents/extension_manager_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,20 @@ 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,
logging: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some(EXTENSION_NAME.to_string()),
version: "1.0.0".to_string(),
icons: None,
Expand Down
105 changes: 74 additions & 31 deletions crates/goose/src/agents/mcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,
}
Comment on lines +226 to 229
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_message drops non-text sampling message content (and even text beyond the first part), producing provider messages with empty content when the incoming MCP message contains images/resources; this is a behavioral regression from the previous with_content(msg.content.clone().into()) path and can lead to invalid/empty prompts being sent to providers. Convert each SamplingMessageContent item into MessageContent (text/image/resource) and add it to the base message (or at least preserve the first non-text part via a placeholder) instead of returning base unchanged.

Copilot uses AI. Check for mistakes.
})
.collect();
Expand Down Expand Up @@ -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("")
},
},
),
})
}

Expand All @@ -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,
Expand All @@ -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(),
Expand All @@ -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,
},
}
Expand Down Expand Up @@ -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"]));
}
}
2 changes: 2 additions & 0 deletions crates/goose/src/agents/summon_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
4 changes: 3 additions & 1 deletion crates/goose/src/agents/todo_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,20 @@ 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,
logging: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
description: None,
title: Some("Todo".to_string()),
version: "1.0.0".to_string(),
icons: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/goose/src/agents/tom_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/tests/mcp_replays/github-mcp-serverstdio
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}}}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers (in Python 3).

Copilot uses AI. Check for mistakes.
STDERR:
STDERR:
STDERR: ╭──────────────────────────────────────────────────────────────────────────────╮
Expand Down
Loading
Loading