feat: integrate mcp apps acp surface

Signed-off-by: Andrew Harvard <aharvard@squareup.com>
This commit is contained in:
Andrew Harvard
2026-04-15 07:18:05 -04:00
parent fd93865951
commit 39eff42888
12 changed files with 1288 additions and 21 deletions
+20
View File
@@ -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",
+191
View File
@@ -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": [
{
+273 -2
View File
@@ -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<Mutex<HashMap<String, GooseAcpSession>>>,
provider_factory: AcpProviderFactory,
builtins: Vec<String>,
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
client_terminal: OnceCell<bool>,
client_goose2_capabilities: OnceCell<Goose2ClientCapabilities>,
config_dir: std::path::PathBuf,
session_manager: Arc<SessionManager>,
thread_manager: Arc<goose::session::ThreadManager>,
@@ -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<String, serde_json::Value>>,
) -> 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<CallToolResult>,
extension_name: Option<&str>,
) -> Option<serde_json::Value> {
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<CallToolResponse, sacp::Error> {
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::<JsonObject>(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<ListResourcesResponse, sacp::Error> {
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<ListResourceTemplatesResponse, sacp::Error> {
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<ListPromptsResponse, sacp::Error> {
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,
+73
View File
@@ -56,6 +56,62 @@ pub struct GetToolsResponse {
pub tools: Vec<serde_json::Value>,
}
/// 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)]
+112 -3
View File
@@ -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<String>,
trace_context: McpTraceContext,
client_name: String,
capabilities: GooseMcpClientCapabilities,
) -> ExtensionResult<McpClient> {
@@ -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<rmcp::model::ListResourcesResult, ErrorData> {
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<rmcp::model::ListResourceTemplatesResult, ErrorData> {
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,
+266 -13
View File
@@ -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<dyn std::error::Error + Sync + Send>;
pub type Error = rmcp::ServiceError;
#[derive(Debug, Clone)]
pub struct McpTraceContext {
pub server_name: String,
pub transport_kind: String,
pub endpoint: Option<String>,
}
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<String>,
_cancel_token: CancellationToken,
) -> Result<ListResourceTemplatesResult, Error> {
Err(Error::TransportClosed)
}
async fn list_prompts(
&self,
_session_id: &str,
@@ -112,6 +141,7 @@ pub struct GooseClient {
session_id: Mutex<Option<String>>,
client_name: String,
capabilities: GooseMcpClientCapabilities,
trace_context: McpTraceContext,
working_dir: Arc<tokio::sync::RwLock<PathBuf>>,
}
@@ -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<rmcp::RoleClient>,
) {
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(&params))
}),
&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<rmcp::RoleClient>,
) {
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(&params))
}),
&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<InitializeResult>,
timeout: std::time::Duration,
docker_container: Option<String>,
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<String>,
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<rmcp::RoleClient, GooseClient> =
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<ServerResult, Error> {
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<T: serde::Serialize>(
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>) -> 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<T: serde::Serialize>(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<Value> {
serde_json::to_value(request)
.ok()
.and_then(|value| value.as_object().and_then(|map| map.get("params")).cloned())
}
fn jsonrpc_response_message<T: serde::Serialize>(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("<unserializable>".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<RoleClient>,
timeout: Duration,
@@ -569,6 +781,29 @@ impl McpClientTrait for McpClient {
}
}
async fn list_resource_templates(
&self,
session_id: &str,
cursor: Option<String>,
cancel_token: CancellationToken,
) -> Result<ListResourceTemplatesResult, Error> {
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")]
+1
View File
@@ -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;
+143
View File
@@ -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<String>,
pub extension_name: Option<String>,
pub transport_kind: Option<String>,
pub endpoint: Option<String>,
pub cwd: Option<String>,
}
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<Self> {
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<Option<ObservationTower>> = 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);
}
}
+43
View File
@@ -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<CallToolResponse> {
const raw = await this.conn.extMethod("_goose/tools/call", params);
return zCallToolResponse.parse(raw) as CallToolResponse;
}
async GooseResourcesList(
params: ListResourcesRequest,
): Promise<ListResourcesResponse> {
const raw = await this.conn.extMethod("_goose/resources/list", params);
return zListResourcesResponse.parse(raw) as ListResourcesResponse;
}
async GooseResourcesTemplatesList(
params: ListResourceTemplatesRequest,
): Promise<ListResourceTemplatesResponse> {
const raw = await this.conn.extMethod(
"_goose/resources/templates/list",
params,
);
return zListResourceTemplatesResponse.parse(
raw,
) as ListResourceTemplatesResponse;
}
async GooseResourceRead(
params: ReadResourceRequest,
): Promise<ReadResourceResponse> {
@@ -81,6 +117,13 @@ export class GooseExtClient {
return zReadResourceResponse.parse(raw) as ReadResourceResponse;
}
async GoosePromptsList(
params: ListPromptsRequest,
): Promise<ListPromptsResponse> {
const raw = await this.conn.extMethod("_goose/prompts/list", params);
return zListPromptsResponse.parse(raw) as ListPromptsResponse;
}
async GooseWorkingDirUpdate(params: UpdateWorkingDirRequest): Promise<void> {
await this.conn.extMethod("_goose/working_dir/update", params);
}
+22 -1
View File
@@ -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",
+75 -2
View File
@@ -44,6 +44,61 @@ export type GetToolsResponse = {
tools: Array<unknown>;
};
/**
* 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;
+69
View File
@@ -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,