From 39eff428880c5e8a32adab0418adea69e2d22d59 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 15 Apr 2026 07:18:05 -0400 Subject: [PATCH] feat: integrate mcp apps acp surface Signed-off-by: Andrew Harvard --- crates/goose-acp/acp-meta.json | 20 ++ crates/goose-acp/acp-schema.json | 191 +++++++++++++ crates/goose-acp/src/server.rs | 275 +++++++++++++++++- crates/goose-sdk/src/custom_requests.rs | 73 +++++ crates/goose/src/agents/extension_manager.rs | 115 +++++++- crates/goose/src/agents/mcp_client.rs | 279 ++++++++++++++++++- crates/goose/src/lib.rs | 1 + crates/goose/src/observation.rs | 143 ++++++++++ ui/sdk/src/generated/client.gen.ts | 43 +++ ui/sdk/src/generated/index.ts | 23 +- ui/sdk/src/generated/types.gen.ts | 77 ++++- ui/sdk/src/generated/zod.gen.ts | 69 +++++ 12 files changed, 1288 insertions(+), 21 deletions(-) create mode 100644 crates/goose/src/observation.rs diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json index 7743ab6b33..6a67a364a5 100644 --- a/crates/goose-acp/acp-meta.json +++ b/crates/goose-acp/acp-meta.json @@ -15,11 +15,31 @@ "requestType": "GetToolsRequest", "responseType": "GetToolsResponse" }, + { + "method": "_goose/tools/call", + "requestType": "CallToolRequest", + "responseType": "CallToolResponse" + }, + { + "method": "_goose/resources/list", + "requestType": "ListResourcesRequest", + "responseType": "ListResourcesResponse" + }, + { + "method": "_goose/resources/templates/list", + "requestType": "ListResourceTemplatesRequest", + "responseType": "ListResourceTemplatesResponse" + }, { "method": "_goose/resource/read", "requestType": "ReadResourceRequest", "responseType": "ReadResourceResponse" }, + { + "method": "_goose/prompts/list", + "requestType": "ListPromptsRequest", + "responseType": "ListPromptsResponse" + }, { "method": "_goose/working_dir/update", "requestType": "UpdateWorkingDirRequest", diff --git a/crates/goose-acp/acp-schema.json b/crates/goose-acp/acp-schema.json index 590d8a871f..a3da17e7b5 100644 --- a/crates/goose-acp/acp-schema.json +++ b/crates/goose-acp/acp-schema.json @@ -73,6 +73,99 @@ "x-side": "agent", "x-method": "_goose/tools" }, + "CallToolRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "arguments": { + "default": null + } + }, + "required": [ + "sessionId", + "name" + ], + "description": "Call a tool from an active extension session.", + "x-side": "agent", + "x-method": "_goose/tools/call" + }, + "CallToolResponse": { + "type": "object", + "properties": { + "result": { + "description": "The tool call result from the extension (MCP CallToolResult).", + "default": null + } + }, + "description": "Tool call response.", + "x-side": "agent", + "x-method": "_goose/tools/call" + }, + "ListResourcesRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "extensionName": { + "type": "string" + } + }, + "required": [ + "sessionId", + "extensionName" + ], + "description": "List resources for an extension.", + "x-side": "agent", + "x-method": "_goose/resources/list" + }, + "ListResourcesResponse": { + "type": "object", + "properties": { + "result": { + "description": "The resource list result from the extension (MCP ListResourcesResult).", + "default": null + } + }, + "description": "Resource list response.", + "x-side": "agent", + "x-method": "_goose/resources/list" + }, + "ListResourceTemplatesRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "extensionName": { + "type": "string" + } + }, + "required": [ + "sessionId", + "extensionName" + ], + "description": "List resource templates for an extension.", + "x-side": "agent", + "x-method": "_goose/resources/templates/list" + }, + "ListResourceTemplatesResponse": { + "type": "object", + "properties": { + "result": { + "description": "The resource template list result from the extension (MCP ListResourceTemplatesResult).", + "default": null + } + }, + "description": "Resource template list response.", + "x-side": "agent", + "x-method": "_goose/resources/templates/list" + }, "ReadResourceRequest": { "type": "object", "properties": { @@ -107,6 +200,36 @@ "x-side": "agent", "x-method": "_goose/resource/read" }, + "ListPromptsRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "extensionName": { + "type": "string" + } + }, + "required": [ + "sessionId", + "extensionName" + ], + "description": "List prompts for an extension.", + "x-side": "agent", + "x-method": "_goose/prompts/list" + }, + "ListPromptsResponse": { + "type": "object", + "properties": { + "result": { + "description": "The prompt list result from the extension (MCP ListPromptsResult).", + "default": null + } + }, + "description": "Prompt list response.", + "x-side": "agent", + "x-method": "_goose/prompts/list" + }, "UpdateWorkingDirRequest": { "type": "object", "properties": { @@ -701,6 +824,33 @@ "description": "Params for _goose/tools", "title": "GetToolsRequest" }, + { + "allOf": [ + { + "$ref": "#/$defs/CallToolRequest" + } + ], + "description": "Params for _goose/tools/call", + "title": "CallToolRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListResourcesRequest" + } + ], + "description": "Params for _goose/resources/list", + "title": "ListResourcesRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListResourceTemplatesRequest" + } + ], + "description": "Params for _goose/resources/templates/list", + "title": "ListResourceTemplatesRequest" + }, { "allOf": [ { @@ -710,6 +860,15 @@ "description": "Params for _goose/resource/read", "title": "ReadResourceRequest" }, + { + "allOf": [ + { + "$ref": "#/$defs/ListPromptsRequest" + } + ], + "description": "Params for _goose/prompts/list", + "title": "ListPromptsRequest" + }, { "allOf": [ { @@ -918,6 +1077,30 @@ ], "title": "GetToolsResponse" }, + { + "allOf": [ + { + "$ref": "#/$defs/CallToolResponse" + } + ], + "title": "CallToolResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListResourcesResponse" + } + ], + "title": "ListResourcesResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListResourceTemplatesResponse" + } + ], + "title": "ListResourceTemplatesResponse" + }, { "allOf": [ { @@ -926,6 +1109,14 @@ ], "title": "ReadResourceResponse" }, + { + "allOf": [ + { + "$ref": "#/$defs/ListPromptsResponse" + } + ], + "title": "ListPromptsResponse" + }, { "allOf": [ { diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index bf902a9848..15d09b81a5 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -23,7 +23,9 @@ use goose::providers::base::Provider; use goose::session::session_manager::SessionType; use goose::session::{EnabledExtensionsState, Session, SessionManager}; use goose_acp_macros::custom_methods; -use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role}; +use rmcp::model::{ + CallToolRequestParams, CallToolResult, JsonObject, RawContent, ResourceContents, Role, +}; use sacp::schema::{ AgentCapabilities, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse, BlobResourceContents, CancelNotification, CloseSessionRequest, CloseSessionResponse, @@ -68,6 +70,9 @@ pub type AcpProviderFactory = Arc< const DEFAULT_PROVIDER_ID: &str = "goose"; const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)"; +const GOOSE2_META_KEY: &str = "com.block.goose2"; +const GOOSE2_MCP_UI_EXTENSION_KEY: &str = "io.modelcontextprotocol/ui"; +const GOOSE2_MCP_APP_MIME_TYPE: &str = "text/html;profile=mcp-app"; /// In-memory state for an active ACP session. /// @@ -114,12 +119,19 @@ struct AgentSetupRequest { resolved_provider: Option<(String, goose::model::ModelConfig)>, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct Goose2ClientCapabilities { + mcp_ui: bool, + structured_tool_result: bool, +} + pub struct GooseAcpAgent { sessions: Arc>>, provider_factory: AcpProviderFactory, builtins: Vec, client_fs_capabilities: OnceCell, client_terminal: OnceCell, + client_goose2_capabilities: OnceCell, config_dir: std::path::PathBuf, session_manager: Arc, thread_manager: Arc, @@ -373,6 +385,79 @@ fn summarize_tool_call(tool_name: &str, arguments: Option<&serde_json::Value>) - } } +fn parse_goose2_client_capabilities( + meta: Option<&serde_json::Map>, +) -> Goose2ClientCapabilities { + let goose2_meta = meta + .and_then(|meta| meta.get(GOOSE2_META_KEY)) + .and_then(serde_json::Value::as_object); + + let structured_tool_result = goose2_meta + .and_then(|meta| meta.get("structuredToolResult")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + let mcp_ui = goose2_meta + .and_then(|meta| meta.get("mcpClientCapabilities")) + .and_then(serde_json::Value::as_object) + .and_then(|caps| caps.get("extensions")) + .and_then(serde_json::Value::as_object) + .and_then(|extensions| extensions.get(GOOSE2_MCP_UI_EXTENSION_KEY)) + .and_then(serde_json::Value::as_object) + .and_then(|ui| ui.get("mimeTypes")) + .and_then(serde_json::Value::as_array) + .is_some_and(|mime_types| { + mime_types + .iter() + .any(|mime_type| mime_type.as_str() == Some(GOOSE2_MCP_APP_MIME_TYPE)) + }); + + Goose2ClientCapabilities { + mcp_ui, + structured_tool_result, + } +} + +fn build_tool_call_raw_output( + tool_result: &ToolResult, + extension_name: Option<&str>, +) -> Option { + let result = match tool_result { + Ok(result) if result.structured_content.is_some() || result.meta.is_some() => result, + _ => return None, + }; + + let content = serde_json::to_value(&result.content).ok()?; + let mut raw_output = serde_json::Map::from_iter([ + ("content".to_string(), content), + ( + "isError".to_string(), + serde_json::Value::Bool(result.is_error.unwrap_or(false)), + ), + ]); + + if let Some(structured_content) = &result.structured_content { + raw_output.insert("structuredContent".to_string(), structured_content.clone()); + } + + if let Some(extension_name) = extension_name { + raw_output.insert( + "extensionName".to_string(), + serde_json::Value::String(extension_name.to_string()), + ); + } + + if let Some(meta) = result + .meta + .as_ref() + .and_then(|meta| serde_json::to_value(meta).ok()) + { + raw_output.insert("_meta".to_string(), meta); + } + + Some(serde_json::Value::Object(raw_output)) +} + fn builtin_to_extension_config(name: &str) -> ExtensionConfig { if let Some(def) = PLATFORM_EXTENSIONS.get(name) { ExtensionConfig::Platform { @@ -621,6 +706,7 @@ impl GooseAcpAgent { builtins, client_fs_capabilities: OnceCell::new(), client_terminal: OnceCell::new(), + client_goose2_capabilities: OnceCell::new(), config_dir, session_manager, thread_manager, @@ -672,18 +758,28 @@ impl GooseAcpAgent { .cloned() .unwrap_or_default(); let client_terminal = self.client_terminal.get().copied().unwrap_or(false); + let goose2_capabilities = self + .client_goose2_capabilities + .get() + .copied() + .unwrap_or_default(); let provider_factory = Arc::clone(&self.provider_factory); let disable_session_naming = self.disable_session_naming; tokio::spawn(async move { let result: Result<(), String> = async { + let goose_platform = if goose2_capabilities.mcp_ui { + GoosePlatform::GooseDesktop + } else { + GoosePlatform::GooseCli + }; let agent = Arc::new(Agent::with_config(AgentConfig::new( session_manager, permission_manager, None, goose_mode, disable_session_naming, - GoosePlatform::GooseCli, + goose_platform, ))); let config_path = config_dir.join(CONFIG_YAML_NAME); @@ -956,6 +1052,16 @@ impl GooseAcpAgent { ), ))?; + if let Some(args_value) = args_value { + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + ToolCallId::new(tool_request.id.clone()), + ToolCallUpdateFields::new().raw_input(args_value), + )), + ))?; + } + if let Ok(tool_call) = &tool_request.tool_call { let agent = match &session.agent { AgentHandle::Ready(a) => a.clone(), @@ -1064,6 +1170,20 @@ impl GooseAcpAgent { }; let mut fields = ToolCallUpdateFields::new().status(status); + let goose2_capabilities = self + .client_goose2_capabilities + .get() + .copied() + .unwrap_or_default(); + let extension_name = session.tool_requests.get(&tool_response.id).and_then( + |tool_request| match &tool_request.tool_call { + Ok(tool_call) => tool_call + .name + .split_once("__") + .map(|(extension_name, _)| extension_name.to_string()), + Err(_) => None, + }, + ); if !tool_response .tool_result .as_ref() @@ -1084,6 +1204,14 @@ impl GooseAcpAgent { } } + if goose2_capabilities.structured_tool_result { + if let Some(raw_output) = + build_tool_call_raw_output(&tool_response.tool_result, extension_name.as_deref()) + { + fields = fields.raw_output(raw_output); + } + } + cx.send_notification(SessionNotification::new( session_id.clone(), SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( @@ -1236,6 +1364,11 @@ impl GooseAcpAgent { .client_fs_capabilities .set(args.client_capabilities.fs.clone()); let _ = self.client_terminal.set(args.client_capabilities.terminal); + let _ = self + .client_goose2_capabilities + .set(parse_goose2_client_capabilities( + args.client_capabilities.meta.as_ref(), + )); let capabilities = AgentCapabilities::new() .load_session(true) @@ -1559,6 +1692,20 @@ impl GooseAcpAgent { .status(ToolCallStatus::Pending), ), ))?; + + if let Ok(tool_call) = &tool_request.tool_call { + if let Some(arguments) = tool_call.arguments.as_ref() { + cx.send_notification(SessionNotification::new( + args.session_id.clone(), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + ToolCallId::new(tool_request.id.clone()), + ToolCallUpdateFields::new().raw_input( + serde_json::Value::Object(arguments.clone()), + ), + )), + ))?; + } + } } MessageContent::ToolResponse(tool_response) => { // Replay-only: emit the ToolCallUpdate notification, @@ -1571,6 +1718,21 @@ impl GooseAcpAgent { }; let mut fields = ToolCallUpdateFields::new().status(status); + let goose2_capabilities = self + .client_goose2_capabilities + .get() + .copied() + .unwrap_or_default(); + let extension_name = + replay_tool_requests + .get(&tool_response.id) + .and_then(|tool_request| match &tool_request.tool_call { + Ok(tool_call) => tool_call + .name + .split_once("__") + .map(|(extension_name, _)| extension_name.to_string()), + Err(_) => None, + }); if !tool_response .tool_result .as_ref() @@ -1594,6 +1756,15 @@ impl GooseAcpAgent { } } + if goose2_capabilities.structured_tool_result { + if let Some(raw_output) = build_tool_call_raw_output( + &tool_response.tool_result, + extension_name.as_deref(), + ) { + fields = fields.raw_output(raw_output); + } + } + cx.send_notification(SessionNotification::new( args.session_id.clone(), SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( @@ -2202,6 +2373,84 @@ impl GooseAcpAgent { Ok(GetToolsResponse { tools: tools_json }) } + #[custom_method(CallToolRequest)] + async fn on_call_tool(&self, req: CallToolRequest) -> Result { + let internal_id = self.internal_session_id(&req.session_id).await?; + let agent = self.get_session_agent(&req.session_id, None).await?; + let arguments = + match req.arguments { + serde_json::Value::Null => None, + value => Some(serde_json::from_value::(value).map_err(|e| { + sacp::Error::invalid_params().data(format!("bad arguments: {e}")) + })?), + }; + let tool_call = match arguments { + Some(arguments) => CallToolRequestParams::new(req.name).with_arguments(arguments), + None => CallToolRequestParams::new(req.name), + }; + let ctx = goose::agents::ToolCallContext::new(internal_id, None, None); + let result = agent + .extension_manager + .dispatch_tool_call(&ctx, tool_call, CancellationToken::new()) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let tool_result = result + .result + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let result_json = serde_json::to_value(&tool_result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(CallToolResponse { + result: result_json, + }) + } + + #[custom_method(ListResourcesRequest)] + async fn on_list_resources( + &self, + req: ListResourcesRequest, + ) -> Result { + let internal_id = self.internal_session_id(&req.session_id).await?; + let agent = self.get_session_agent(&req.session_id, None).await?; + let result = agent + .extension_manager + .list_resources_for_extension( + &internal_id, + &req.extension_name, + CancellationToken::new(), + ) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let result_json = serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(ListResourcesResponse { + result: result_json, + }) + } + + #[custom_method(ListResourceTemplatesRequest)] + async fn on_list_resource_templates( + &self, + req: ListResourceTemplatesRequest, + ) -> Result { + let internal_id = self.internal_session_id(&req.session_id).await?; + let agent = self.get_session_agent(&req.session_id, None).await?; + let result = agent + .extension_manager + .list_resource_templates_for_extension( + &internal_id, + &req.extension_name, + CancellationToken::new(), + ) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let result_json = serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(ListResourceTemplatesResponse { + result: result_json, + }) + } + #[custom_method(ReadResourceRequest)] async fn on_read_resource( &self, @@ -2222,6 +2471,28 @@ impl GooseAcpAgent { }) } + #[custom_method(ListPromptsRequest)] + async fn on_list_prompts( + &self, + req: ListPromptsRequest, + ) -> Result { + let internal_id = self.internal_session_id(&req.session_id).await?; + let agent = self.get_session_agent(&req.session_id, None).await?; + let prompts = agent + .extension_manager + .list_prompts_from_extension( + &internal_id, + &req.extension_name, + CancellationToken::new(), + ) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + let result_json = serde_json::json!({ "prompts": prompts }); + Ok(ListPromptsResponse { + result: result_json, + }) + } + #[custom_method(UpdateWorkingDirRequest)] async fn on_update_working_dir( &self, diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs index e1df7414bf..148936b36b 100644 --- a/crates/goose-sdk/src/custom_requests.rs +++ b/crates/goose-sdk/src/custom_requests.rs @@ -56,6 +56,62 @@ pub struct GetToolsResponse { pub tools: Vec, } +/// Call a tool from an active extension session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/tools/call", response = CallToolResponse)] +#[serde(rename_all = "camelCase")] +pub struct CallToolRequest { + pub session_id: String, + pub name: String, + #[serde(default)] + pub arguments: serde_json::Value, +} + +/// Tool call response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct CallToolResponse { + /// The tool call result from the extension (MCP CallToolResult). + #[serde(default)] + pub result: serde_json::Value, +} + +/// List resources for an extension. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/resources/list", response = ListResourcesResponse)] +#[serde(rename_all = "camelCase")] +pub struct ListResourcesRequest { + pub session_id: String, + pub extension_name: String, +} + +/// Resource list response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ListResourcesResponse { + /// The resource list result from the extension (MCP ListResourcesResult). + #[serde(default)] + pub result: serde_json::Value, +} + +/// List resource templates for an extension. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/resources/templates/list", + response = ListResourceTemplatesResponse +)] +#[serde(rename_all = "camelCase")] +pub struct ListResourceTemplatesRequest { + pub session_id: String, + pub extension_name: String, +} + +/// Resource template list response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ListResourceTemplatesResponse { + /// The resource template list result from the extension (MCP ListResourceTemplatesResult). + #[serde(default)] + pub result: serde_json::Value, +} + /// Read a resource from an extension. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "_goose/resource/read", response = ReadResourceResponse)] @@ -74,6 +130,23 @@ pub struct ReadResourceResponse { pub result: serde_json::Value, } +/// List prompts for an extension. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/prompts/list", response = ListPromptsResponse)] +#[serde(rename_all = "camelCase")] +pub struct ListPromptsRequest { + pub session_id: String, + pub extension_name: String, +} + +/// Prompt list response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ListPromptsResponse { + /// The prompt list result from the extension (MCP ListPromptsResult). + #[serde(default)] + pub result: serde_json::Value, +} + /// Update the working directory for a session. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "_goose/working_dir/update", response = EmptyResponse)] diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 6f80e976d1..b0b725ae86 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -34,7 +34,9 @@ use super::tool_execution::{ToolCallContext, ToolCallResult}; use super::types::SharedProvider; use crate::agents::extension::{Envs, ProcessExit}; use crate::agents::extension_malware_check; -use crate::agents::mcp_client::{GooseMcpClientCapabilities, McpClient, McpClientTrait}; +use crate::agents::mcp_client::{ + GooseMcpClientCapabilities, McpClient, McpClientTrait, McpTraceContext, +}; use crate::builtin_extension::get_builtin_extension; use crate::config::extensions::name_to_key; use crate::config::search_path::SearchPaths; @@ -249,6 +251,7 @@ async fn child_process_client( provider: SharedProvider, working_dir: &PathBuf, docker_container: Option, + trace_context: McpTraceContext, client_name: String, capabilities: GooseMcpClientCapabilities, ) -> ExtensionResult { @@ -286,6 +289,7 @@ async fn child_process_client( Duration::from_secs(resolve_timeout(*timeout)), provider, docker_container, + trace_context, client_name, capabilities, working_dir.clone(), @@ -455,6 +459,11 @@ async fn create_streamable_http_client( transport, timeout_duration, provider.clone(), + McpTraceContext { + server_name: name.to_string(), + transport_kind: "streamable-http".to_string(), + endpoint: Some(uri.to_string()), + }, client_name.clone(), capabilities.clone(), roots_dir.to_path_buf(), @@ -485,6 +494,11 @@ async fn create_streamable_http_client( transport, timeout_duration, provider, + McpTraceContext { + server_name: name.to_string(), + transport_kind: "streamable-http".to_string(), + endpoint: Some(uri.to_string()), + }, client_name, capabilities, roots_dir.to_path_buf(), @@ -676,6 +690,11 @@ impl ExtensionManager { self.provider.clone(), &effective_working_dir, Some(container_id.to_string()), + McpTraceContext { + server_name: normalized_name.clone(), + transport_kind: "stdio".to_string(), + endpoint: Some("docker exec goose".to_string()), + }, self.client_name.clone(), capabilities, ) @@ -695,6 +714,11 @@ impl ExtensionManager { (client_read, client_write), Duration::from_secs(timeout_secs), self.provider.clone(), + McpTraceContext { + server_name: normalized_name.clone(), + transport_kind: "in-process".to_string(), + endpoint: None, + }, self.client_name.clone(), capabilities, effective_working_dir.clone(), @@ -755,6 +779,11 @@ impl ExtensionManager { self.provider.clone(), &effective_working_dir, container.map(|c| c.id().to_string()), + McpTraceContext { + server_name: sanitized_name.clone(), + transport_kind: "stdio".to_string(), + endpoint: Some(cmd.to_string()), + }, self.client_name.clone(), capabilities, ) @@ -791,6 +820,11 @@ impl ExtensionManager { self.provider.clone(), &effective_working_dir, container.map(|c| c.id().to_string()), + McpTraceContext { + server_name: name.clone(), + transport_kind: "stdio".to_string(), + endpoint: Some("uvx".to_string()), + }, self.client_name.clone(), capabilities, ) @@ -986,11 +1020,18 @@ impl ExtensionManager { .lock() .await .iter() - .map(|(name, ext)| (name.clone(), ext.config.clone(), ext.get_client())) + .map(|(name, ext)| { + ( + name.clone(), + ext.config.clone(), + ext.server_info.clone(), + ext.get_client(), + ) + }) .collect(); let cancel_token = CancellationToken::default(); - let client_futures = clients.into_iter().map(|(name, config, client)| { + let client_futures = clients.into_iter().map(|(name, config, server_info, client)| { let cancel_token = cancel_token.clone(); let ext_name = name.clone(); async move { @@ -1026,6 +1067,13 @@ impl ExtensionManager { TOOL_EXTENSION_META_KEY.to_string(), serde_json::Value::String(name.clone()), ); + if let Some(server_info) = &server_info { + if let Ok(server_meta) = + serde_json::to_value(&server_info.server_info) + { + meta_map.insert("server".to_string(), server_meta); + } + } tool.name = public_name.into(); tool.meta = Some(rmcp::model::Meta(meta_map)); @@ -1202,6 +1250,67 @@ impl ExtensionManager { }) } + pub async fn list_resources_for_extension( + &self, + session_id: &str, + extension_name: &str, + cancellation_token: CancellationToken, + ) -> Result { + let client = self + .get_server_client(extension_name) + .await + .ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + format!("Extension {} is not valid", extension_name), + None, + ) + })?; + + client + .list_resources(session_id, None, cancellation_token) + .await + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Unable to list resources for {}, {:?}", extension_name, e), + None, + ) + }) + } + + pub async fn list_resource_templates_for_extension( + &self, + session_id: &str, + extension_name: &str, + cancellation_token: CancellationToken, + ) -> Result { + let client = self + .get_server_client(extension_name) + .await + .ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + format!("Extension {} is not valid", extension_name), + None, + ) + })?; + + client + .list_resource_templates(session_id, None, cancellation_token) + .await + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!( + "Unable to list resource templates for {}, {:?}", + extension_name, e + ), + None, + ) + }) + } + pub async fn get_ui_resources( &self, session_id: &str, diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index bae871de92..aa800928e9 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -1,6 +1,7 @@ use crate::action_required_manager::ActionRequiredManager; use crate::agents::tool_execution::ToolCallContext; use crate::agents::types::SharedProvider; +use crate::observation::{publish_jsonrpc_trace, TraceMetadata}; use crate::session_context::{SESSION_ID_HEADER, WORKING_DIR_HEADER}; use rmcp::model::{ CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, ErrorCode, @@ -13,10 +14,10 @@ use rmcp::{ CallToolRequestParams, CallToolResult, CancelledNotificationParam, ClientCapabilities, ClientInfo, ClientRequest, CreateMessageRequestParams, CreateMessageResult, GetPromptRequestParams, GetPromptResult, Implementation, InitializeRequestParams, - InitializeResult, ListPromptsResult, ListResourcesResult, ListToolsResult, Notification, - PaginatedRequestParams, ProtocolVersion, ReadResourceRequestParams, ReadResourceResult, - Request, RequestId, RequestOptionalParam, Role, SamplingMessage, ServerNotification, - ServerResult, + InitializeResult, ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, + ListToolsResult, Notification, PaginatedRequestParams, ProtocolVersion, + ReadResourceRequestParams, ReadResourceResult, Request, RequestId, RequestOptionalParam, + Role, SamplingMessage, ServerNotification, ServerResult, }, service::{ ClientInitializeError, PeerRequestOptions, RequestContext, RequestHandle, RunningService, @@ -37,6 +38,25 @@ pub type BoxError = Box; pub type Error = rmcp::ServiceError; +#[derive(Debug, Clone)] +pub struct McpTraceContext { + pub server_name: String, + pub transport_kind: String, + pub endpoint: Option, +} + +impl McpTraceContext { + fn metadata(&self, session_id: Option<&str>, working_dir: Option<&str>) -> TraceMetadata { + TraceMetadata { + session_id: session_id.map(|value| value.to_string()), + extension_name: Some(self.server_name.clone()), + transport_kind: Some(self.transport_kind.clone()), + endpoint: self.endpoint.clone(), + cwd: working_dir.map(|value| value.to_string()), + } + } +} + #[async_trait::async_trait] pub trait McpClientTrait: Send + Sync { async fn list_tools( @@ -74,6 +94,15 @@ pub trait McpClientTrait: Send + Sync { Err(Error::TransportClosed) } + async fn list_resource_templates( + &self, + _session_id: &str, + _next_cursor: Option, + _cancel_token: CancellationToken, + ) -> Result { + Err(Error::TransportClosed) + } + async fn list_prompts( &self, _session_id: &str, @@ -112,6 +141,7 @@ pub struct GooseClient { session_id: Mutex>, client_name: String, capabilities: GooseMcpClientCapabilities, + trace_context: McpTraceContext, working_dir: Arc>, } @@ -121,6 +151,7 @@ impl GooseClient { provider: SharedProvider, client_name: String, capabilities: GooseMcpClientCapabilities, + trace_context: McpTraceContext, working_dir: PathBuf, ) -> Self { GooseClient { @@ -129,6 +160,7 @@ impl GooseClient { session_id: Mutex::new(None), client_name, capabilities, + trace_context, working_dir: Arc::new(tokio::sync::RwLock::new(working_dir)), } } @@ -186,14 +218,31 @@ impl ClientHandler for GooseClient { params: rmcp::model::ProgressNotificationParam, context: rmcp::service::NotificationContext, ) { + let session_id = self.resolve_session_id(&context.extensions).await; + let working_dir = self.working_dir.read().await.display().to_string(); + let mut notification = Notification::new(params.clone()); + notification.extensions = context.extensions.clone(); + publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-notification", + serde_json::to_value(ServerNotification::ProgressNotification( + notification.clone(), + )) + .unwrap_or_else(|_| { + jsonrpc_notification_message("notifications/progress", Some(¶ms)) + }), + &self + .trace_context + .metadata(session_id.as_deref(), Some(&working_dir)), + ); self.notification_handlers .lock() .await .iter() .for_each(|handler| { - let mut not = Notification::new(params.clone()); - not.extensions = context.extensions.clone(); - let _ = handler.try_send(ServerNotification::ProgressNotification(not)); + let _ = handler.try_send(ServerNotification::ProgressNotification( + notification.clone(), + )); }); } @@ -202,15 +251,31 @@ impl ClientHandler for GooseClient { params: rmcp::model::LoggingMessageNotificationParam, context: rmcp::service::NotificationContext, ) { + let session_id = self.resolve_session_id(&context.extensions).await; + let working_dir = self.working_dir.read().await.display().to_string(); + let mut notification = LoggingMessageNotification::new(params.clone()); + notification.extensions = context.extensions.clone(); + publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-notification", + serde_json::to_value(ServerNotification::LoggingMessageNotification( + notification.clone(), + )) + .unwrap_or_else(|_| { + jsonrpc_notification_message("notifications/message", Some(¶ms)) + }), + &self + .trace_context + .metadata(session_id.as_deref(), Some(&working_dir)), + ); self.notification_handlers .lock() .await .iter() .for_each(|handler| { - let mut notification = LoggingMessageNotification::new(params.clone()); - notification.extensions = context.extensions.clone(); - let _ = - handler.try_send(ServerNotification::LoggingMessageNotification(notification)); + let _ = handler.try_send(ServerNotification::LoggingMessageNotification( + notification.clone(), + )); }); } @@ -386,6 +451,7 @@ pub struct McpClient { server_info: Option, timeout: std::time::Duration, docker_container: Option, + trace_context: McpTraceContext, } impl McpClient { @@ -393,6 +459,7 @@ impl McpClient { transport: T, timeout: std::time::Duration, provider: SharedProvider, + trace_context: McpTraceContext, client_name: String, capabilities: GooseMcpClientCapabilities, working_dir: PathBuf, @@ -406,6 +473,7 @@ impl McpClient { timeout, provider, None, + trace_context, client_name, capabilities, working_dir, @@ -418,6 +486,7 @@ impl McpClient { timeout: std::time::Duration, provider: SharedProvider, docker_container: Option, + trace_context: McpTraceContext, client_name: String, capabilities: GooseMcpClientCapabilities, working_dir: PathBuf, @@ -434,11 +503,39 @@ impl McpClient { provider, client_name.clone(), capabilities.clone(), + trace_context.clone(), working_dir, ); + let init_trace_id = Value::String(format!("init-{}", uuid::Uuid::new_v4())); + let init_params = client.get_info(); + publish_jsonrpc_trace( + "goose_to_mcp", + "mcp-request", + jsonrpc_request_message("initialize", init_trace_id.clone(), Some(&init_params)), + &trace_context.metadata(None, None), + ); let client: rmcp::service::RunningService = - client.serve(transport).await?; + match client.serve(transport).await { + Ok(client) => client, + Err(error) => { + publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-response", + jsonrpc_display_error_message(init_trace_id, &error), + &trace_context.metadata(None, None), + ); + return Err(error); + } + }; let server_info = client.peer_info().cloned(); + if let Some(server_info) = server_info.as_ref() { + publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-response", + jsonrpc_response_message(&init_trace_id, server_info), + &trace_context.metadata(None, None), + ); + } Ok(Self { client: Mutex::new(client), @@ -446,6 +543,7 @@ impl McpClient { server_info, timeout, docker_container, + trace_context, }) } @@ -469,6 +567,9 @@ impl McpClient { cancel_token: CancellationToken, ) -> Result { let request = inject_session_context_into_request(request, Some(session_id), working_dir); + let trace_metadata = self.trace_context.metadata(Some(session_id), working_dir); + let request_method = request.method().to_string(); + let request_params = request_params_from_request(&request); // The inner mutex is held only for the send; the actual response wait // happens outside the lock so concurrent calls can overlap. let handle = { @@ -478,11 +579,122 @@ impl McpClient { .send_cancellable_request(request, PeerRequestOptions::no_options()) .await }?; + let trace_id = request_id_value(&handle.id); + publish_jsonrpc_trace( + "goose_to_mcp", + "mcp-request", + jsonrpc_request_message_with_params(&request_method, trace_id.clone(), request_params), + &trace_metadata, + ); - await_response(handle, self.timeout, &cancel_token).await + let result = await_response(handle, self.timeout, &cancel_token).await; + match &result { + Ok(server_result) => publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-response", + jsonrpc_response_message(&trace_id, server_result), + &trace_metadata, + ), + Err(error) => publish_jsonrpc_trace( + "mcp_to_goose", + "mcp-response", + jsonrpc_error_message(trace_id, error), + &trace_metadata, + ), + } + + result } } +fn request_id_value(request_id: &RequestId) -> Value { + serde_json::to_value(request_id).unwrap_or_else(|_| Value::String(format!("{request_id:?}"))) +} + +fn jsonrpc_request_message( + method: &str, + id: Value, + params: Option<&T>, +) -> Value { + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("id".to_string(), id); + object.insert("method".to_string(), Value::String(method.to_string())); + if let Some(params) = params.and_then(|value| serde_json::to_value(value).ok()) { + object.insert("params".to_string(), params); + } + Value::Object(object) +} + +fn jsonrpc_request_message_with_params(method: &str, id: Value, params: Option) -> Value { + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("id".to_string(), id); + object.insert("method".to_string(), Value::String(method.to_string())); + if let Some(params) = params { + object.insert("params".to_string(), params); + } + Value::Object(object) +} + +fn jsonrpc_notification_message(method: &str, params: Option<&T>) -> Value { + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("method".to_string(), Value::String(method.to_string())); + if let Some(params) = params.and_then(|value| serde_json::to_value(value).ok()) { + object.insert("params".to_string(), params); + } + Value::Object(object) +} + +fn request_params_from_request(request: &ClientRequest) -> Option { + serde_json::to_value(request) + .ok() + .and_then(|value| value.as_object().and_then(|map| map.get("params")).cloned()) +} + +fn jsonrpc_response_message(id: &Value, result: &T) -> Value { + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("id".to_string(), id.clone()); + object.insert( + "result".to_string(), + serde_json::to_value(result).unwrap_or_else(|_| Value::String("".into())), + ); + Value::Object(object) +} + +fn jsonrpc_error_message(id: Value, error: &ServiceError) -> Value { + let mut error_object = serde_json::Map::new(); + error_object.insert("message".to_string(), Value::String(error.to_string())); + + if let ServiceError::McpError(error_data) = error { + if let Ok(code) = serde_json::to_value(&error_data.code) { + error_object.insert("code".to_string(), code); + } + if let Some(data) = error_data.data.clone() { + error_object.insert("data".to_string(), data); + } + } + + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("id".to_string(), id); + object.insert("error".to_string(), Value::Object(error_object)); + Value::Object(object) +} + +fn jsonrpc_display_error_message(id: Value, error: &impl std::fmt::Display) -> Value { + let mut error_object = serde_json::Map::new(); + error_object.insert("message".to_string(), Value::String(error.to_string())); + + let mut object = serde_json::Map::new(); + object.insert("jsonrpc".to_string(), Value::String("2.0".to_string())); + object.insert("id".to_string(), id); + object.insert("error".to_string(), Value::Object(error_object)); + Value::Object(object) +} + async fn await_response( handle: RequestHandle, timeout: Duration, @@ -569,6 +781,29 @@ impl McpClientTrait for McpClient { } } + async fn list_resource_templates( + &self, + session_id: &str, + cursor: Option, + cancel_token: CancellationToken, + ) -> Result { + let res = self + .send_request_with_context( + session_id, + None, + ClientRequest::ListResourceTemplatesRequest(RequestOptionalParam::with_param( + PaginatedRequestParams::default().with_cursor(cursor), + )), + cancel_token, + ) + .await?; + + match res { + ServerResult::ListResourceTemplatesResult(result) => Ok(result), + _ => Err(ServiceError::UnexpectedResponse), + } + } + async fn list_tools( &self, session_id: &str, @@ -737,6 +972,11 @@ fn inject_session_context_into_request( inject_session_context_into_extensions(req.extensions, session_id, working_dir); ClientRequest::ReadResourceRequest(req) } + ClientRequest::ListResourceTemplatesRequest(mut req) => { + req.extensions = + inject_session_context_into_extensions(req.extensions, session_id, working_dir); + ClientRequest::ListResourceTemplatesRequest(req) + } ClientRequest::ListToolsRequest(mut req) => { req.extensions = inject_session_context_into_extensions(req.extensions, session_id, working_dir); @@ -779,6 +1019,11 @@ mod tests { Arc::new(Mutex::new(None)), platform.to_string(), capabilities, + McpTraceContext { + server_name: "test".to_string(), + transport_kind: "test".to_string(), + endpoint: None, + }, std::env::current_dir().unwrap_or_default(), ) } @@ -787,6 +1032,7 @@ mod tests { match request { ClientRequest::ListResourcesRequest(req) => Some(&req.extensions), ClientRequest::ReadResourceRequest(req) => Some(&req.extensions), + ClientRequest::ListResourceTemplatesRequest(req) => Some(&req.extensions), ClientRequest::ListToolsRequest(req) => Some(&req.extensions), ClientRequest::CallToolRequest(req) => Some(&req.extensions), ClientRequest::ListPromptsRequest(req) => Some(&req.extensions), @@ -815,6 +1061,12 @@ mod tests { ClientRequest::ListToolsRequest(req) } + fn list_resource_templates_request(extensions: Extensions) -> ClientRequest { + let mut req = RequestOptionalParam::with_param(PaginatedRequestParams::default()); + req.extensions = extensions; + ClientRequest::ListResourceTemplatesRequest(req) + } + fn call_tool_request(extensions: Extensions) -> ClientRequest { let mut req = Request::new(CallToolRequestParams::new("tool".to_string())); req.extensions = extensions; @@ -875,6 +1127,7 @@ mod tests { #[test_case(list_resources_request; "list_resources")] #[test_case(read_resource_request; "read_resource")] + #[test_case(list_resource_templates_request; "list_resource_templates")] #[test_case(list_tools_request; "list_tools")] #[test_case(call_tool_request; "call_tool")] #[test_case(list_prompts_request; "list_prompts")] diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index cd60b4de8e..d2769bde08 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -23,6 +23,7 @@ pub mod logging; pub mod mcp_utils; pub mod model; pub mod oauth; +pub mod observation; #[cfg(feature = "otel")] pub mod otel; pub mod permission; diff --git a/crates/goose/src/observation.rs b/crates/goose/src/observation.rs new file mode 100644 index 0000000000..3bf6a7821e --- /dev/null +++ b/crates/goose/src/observation.rs @@ -0,0 +1,143 @@ +use once_cell::sync::OnceCell; +use serde_json::{json, Value}; +use std::io::Write; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +const INTERNAL_SEQUENCE_START: u64 = 1_000_000_000; + +#[derive(Debug, Clone, Default)] +pub struct TraceMetadata { + pub session_id: Option, + pub extension_name: Option, + pub transport_kind: Option, + pub endpoint: Option, + pub cwd: Option, +} + +struct ObservationTower { + host: String, + port: u16, + path: String, + host_header: String, + process_id: String, + child_pid: u32, + default_cwd: String, + sequence: AtomicU64, +} + +impl ObservationTower { + fn from_env() -> Option { + let raw_url = std::env::var("GOOSE_OBSERVATION_TOWER_URL").ok()?; + let raw_url = raw_url.trim(); + let without_scheme = raw_url.strip_prefix("http://")?; + let without_scheme = without_scheme.trim_end_matches('/'); + let (host_port, base_path) = match without_scheme.split_once('/') { + Some((host_port, path)) => (host_port, format!("/{}", path.trim_matches('/'))), + None => (without_scheme, String::new()), + }; + let (host, port) = match host_port.rsplit_once(':') { + Some((host, port)) => (host.to_string(), port.parse().ok()?), + None => (host_port.to_string(), 80), + }; + let path = if base_path.is_empty() { + "/api/events".to_string() + } else { + format!("{base_path}/api/events") + }; + let host_header = if port == 80 { + host.clone() + } else { + format!("{host}:{port}") + }; + let child_pid = std::process::id(); + let default_cwd = std::env::current_dir() + .ok() + .map(|path| path.display().to_string()) + .unwrap_or_default(); + + Some(Self { + host, + port, + path, + host_header, + process_id: format!("goose-serve-{child_pid}"), + child_pid, + default_cwd, + sequence: AtomicU64::new(INTERNAL_SEQUENCE_START), + }) + } + + fn global() -> Option<&'static Self> { + static OBSERVATION_TOWER: OnceCell> = OnceCell::new(); + OBSERVATION_TOWER.get_or_init(Self::from_env).as_ref() + } + + fn publish_jsonrpc( + &self, + direction: &str, + stream: &str, + json_value: Value, + metadata: &TraceMetadata, + ) { + let Ok(text) = serde_json::to_string(&json_value) else { + return; + }; + + let payload = json!({ + "id": uuid::Uuid::new_v4().to_string(), + "timestamp": SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs_f64()) + .unwrap_or_default(), + "sequence": self.sequence.fetch_add(1, Ordering::Relaxed), + "processId": self.process_id, + "childPid": self.child_pid, + "cwd": metadata.cwd.clone().unwrap_or_else(|| self.default_cwd.clone()), + "direction": direction, + "stream": stream, + "size": text.len(), + "text": text, + "json": json_value, + "protocol": "mcp", + "sessionId": metadata.session_id, + "extensionName": metadata.extension_name, + "transportKind": metadata.transport_kind, + "endpoint": metadata.endpoint, + }); + + let Ok(body) = serde_json::to_vec(&payload) else { + return; + }; + + let Ok(mut stream_socket) = std::net::TcpStream::connect((&*self.host, self.port)) else { + return; + }; + + let request = format!( + "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + self.path, + self.host_header, + body.len(), + ); + + if stream_socket.write_all(request.as_bytes()).is_err() { + return; + } + if stream_socket.write_all(&body).is_err() { + return; + } + let _ = stream_socket.flush(); + } +} + +pub fn publish_jsonrpc_trace( + direction: &str, + stream: &str, + json_value: Value, + metadata: &TraceMetadata, +) { + if let Some(tower) = ObservationTower::global() { + tower.publish_jsonrpc(direction, stream, json_value, metadata); + } +} diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index 693246052e..11bca05348 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -10,6 +10,8 @@ export interface ExtMethodProvider { import type { AddExtensionRequest, ArchiveSessionRequest, + CallToolRequest, + CallToolResponse, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, @@ -27,8 +29,14 @@ import type { GetToolsResponse, ImportSessionRequest, ImportSessionResponse, + ListPromptsRequest, + ListPromptsResponse, ListProvidersRequest, ListProvidersResponse, + ListResourceTemplatesRequest, + ListResourceTemplatesResponse, + ListResourcesRequest, + ListResourcesResponse, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, @@ -44,6 +52,7 @@ import type { UpsertSecretRequest, } from './types.gen.js'; import { + zCallToolResponse, zCheckSecretResponse, zExportSessionResponse, zGetExtensionsResponse, @@ -52,7 +61,10 @@ import { zGetSessionExtensionsResponse, zGetToolsResponse, zImportSessionResponse, + zListPromptsResponse, zListProvidersResponse, + zListResourceTemplatesResponse, + zListResourcesResponse, zReadConfigResponse, zReadResourceResponse, zUpdateProviderResponse, @@ -74,6 +86,30 @@ export class GooseExtClient { return zGetToolsResponse.parse(raw) as GetToolsResponse; } + async GooseToolsCall(params: CallToolRequest): Promise { + const raw = await this.conn.extMethod("_goose/tools/call", params); + return zCallToolResponse.parse(raw) as CallToolResponse; + } + + async GooseResourcesList( + params: ListResourcesRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/resources/list", params); + return zListResourcesResponse.parse(raw) as ListResourcesResponse; + } + + async GooseResourcesTemplatesList( + params: ListResourceTemplatesRequest, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/resources/templates/list", + params, + ); + return zListResourceTemplatesResponse.parse( + raw, + ) as ListResourceTemplatesResponse; + } + async GooseResourceRead( params: ReadResourceRequest, ): Promise { @@ -81,6 +117,13 @@ export class GooseExtClient { return zReadResourceResponse.parse(raw) as ReadResourceResponse; } + async GoosePromptsList( + params: ListPromptsRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/prompts/list", params); + return zListPromptsResponse.parse(raw) as ListPromptsResponse; + } + async GooseWorkingDirUpdate(params: UpdateWorkingDirRequest): Promise { await this.conn.extMethod("_goose/working_dir/update", params); } diff --git a/ui/sdk/src/generated/index.ts b/ui/sdk/src/generated/index.ts index f9190a0946..3dfd731abf 100644 --- a/ui/sdk/src/generated/index.ts +++ b/ui/sdk/src/generated/index.ts @@ -1,6 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListProvidersRequest, ListProvidersResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js'; +export type { AddExtensionRequest, ArchiveSessionRequest, CallToolRequest, CallToolResponse, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListPromptsRequest, ListPromptsResponse, ListProvidersRequest, ListProvidersResponse, ListResourcesRequest, ListResourcesResponse, ListResourceTemplatesRequest, ListResourceTemplatesResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js'; +export type { AddExtensionRequest, ArchiveSessionRequest, CallToolRequest, CallToolResponse, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListPromptsRequest, ListPromptsResponse, ListProvidersRequest, ListProvidersResponse, ListResourcesRequest, ListResourcesResponse, ListResourceTemplatesRequest, ListResourceTemplatesResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -18,11 +19,31 @@ export const GOOSE_EXT_METHODS = [ requestType: "GetToolsRequest", responseType: "GetToolsResponse", }, + { + method: "_goose/tools/call", + requestType: "CallToolRequest", + responseType: "CallToolResponse", + }, + { + method: "_goose/resources/list", + requestType: "ListResourcesRequest", + responseType: "ListResourcesResponse", + }, + { + method: "_goose/resources/templates/list", + requestType: "ListResourceTemplatesRequest", + responseType: "ListResourceTemplatesResponse", + }, { method: "_goose/resource/read", requestType: "ReadResourceRequest", responseType: "ReadResourceResponse", }, + { + method: "_goose/prompts/list", + requestType: "ListPromptsRequest", + responseType: "ListPromptsResponse", + }, { method: "_goose/working_dir/update", requestType: "UpdateWorkingDirRequest", diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index b89b8ced1b..5954c86ca0 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -44,6 +44,61 @@ export type GetToolsResponse = { tools: Array; }; +/** + * Call a tool from an active extension session. + */ +export type CallToolRequest = { + sessionId: string; + name: string; + arguments?: unknown; +}; + +/** + * Tool call response. + */ +export type CallToolResponse = { + /** + * The tool call result from the extension (MCP CallToolResult). + */ + result?: unknown; +}; + +/** + * List resources for an extension. + */ +export type ListResourcesRequest = { + sessionId: string; + extensionName: string; +}; + +/** + * Resource list response. + */ +export type ListResourcesResponse = { + /** + * The resource list result from the extension (MCP ListResourcesResult). + */ + result?: unknown; +}; + +/** + * List resource templates for an extension. + */ +export type ListResourceTemplatesRequest = { + sessionId: string; + extensionName: string; +}; + +/** + * Resource template list response. + */ +export type ListResourceTemplatesResponse = { + /** + * The resource template list result from the extension (MCP ListResourceTemplatesResult). + */ + result?: unknown; +}; + /** * Read a resource from an extension. */ @@ -63,6 +118,24 @@ export type ReadResourceResponse = { result?: unknown; }; +/** + * List prompts for an extension. + */ +export type ListPromptsRequest = { + sessionId: string; + extensionName: string; +}; + +/** + * Prompt list response. + */ +export type ListPromptsResponse = { + /** + * The prompt list result from the extension (MCP ListPromptsResult). + */ + result?: unknown; +}; + /** * Update the working directory for a session. */ @@ -307,14 +380,14 @@ export type UnarchiveSessionRequest = { export type ExtRequest = { id: string; method: string; - params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | UpdateProviderRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderModelsRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | { + params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | CallToolRequest | ListResourcesRequest | ListResourceTemplatesRequest | ReadResourceRequest | ListPromptsRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | UpdateProviderRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderModelsRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | { [key: string]: unknown; } | null; }; export type ExtResponse = { id: string; - result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | UpdateProviderResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderModelsResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | unknown; + result?: EmptyResponse | GetToolsResponse | CallToolResponse | ListResourcesResponse | ListResourceTemplatesResponse | ReadResourceResponse | ListPromptsResponse | GetExtensionsResponse | GetSessionExtensionsResponse | UpdateProviderResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderModelsResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | unknown; } | { error: { code: number; diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index 2bf62f021c..187297bcb6 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -37,6 +37,52 @@ export const zGetToolsResponse = z.object({ tools: z.array(z.unknown()) }); +/** + * Call a tool from an active extension session. + */ +export const zCallToolRequest = z.object({ + sessionId: z.string(), + name: z.string(), + arguments: z.unknown().optional().default(null) +}); + +/** + * Tool call response. + */ +export const zCallToolResponse = z.object({ + result: z.unknown().optional().default(null) +}); + +/** + * List resources for an extension. + */ +export const zListResourcesRequest = z.object({ + sessionId: z.string(), + extensionName: z.string() +}); + +/** + * Resource list response. + */ +export const zListResourcesResponse = z.object({ + result: z.unknown().optional().default(null) +}); + +/** + * List resource templates for an extension. + */ +export const zListResourceTemplatesRequest = z.object({ + sessionId: z.string(), + extensionName: z.string() +}); + +/** + * Resource template list response. + */ +export const zListResourceTemplatesResponse = z.object({ + result: z.unknown().optional().default(null) +}); + /** * Read a resource from an extension. */ @@ -53,6 +99,21 @@ export const zReadResourceResponse = z.object({ result: z.unknown().optional().default(null) }); +/** + * List prompts for an extension. + */ +export const zListPromptsRequest = z.object({ + sessionId: z.string(), + extensionName: z.string() +}); + +/** + * Prompt list response. + */ +export const zListPromptsResponse = z.object({ + result: z.unknown().optional().default(null) +}); + /** * Update the working directory for a session. */ @@ -306,7 +367,11 @@ export const zExtRequest = z.object({ zAddExtensionRequest, zRemoveExtensionRequest, zGetToolsRequest, + zCallToolRequest, + zListResourcesRequest, + zListResourceTemplatesRequest, zReadResourceRequest, + zListPromptsRequest, zUpdateWorkingDirRequest, zDeleteSessionRequest, zGetExtensionsRequest, @@ -340,7 +405,11 @@ export const zExtResponse = z.union([ z.union([ zEmptyResponse, zGetToolsResponse, + zCallToolResponse, + zListResourcesResponse, + zListResourceTemplatesResponse, zReadResourceResponse, + zListPromptsResponse, zGetExtensionsResponse, zGetSessionExtensionsResponse, zUpdateProviderResponse,