mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: integrate mcp apps acp surface
Signed-off-by: Andrew Harvard <aharvard@squareup.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¶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<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(¶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<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")]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user