From df7150b6875f0b38ee3d50c1d922b1a2f6583d01 Mon Sep 17 00:00:00 2001 From: john Date: Tue, 8 Sep 2026 21:37:27 +0800 Subject: [PATCH] feat(tkmind): add POST /agent/call_tool for sandbox page materialization Expose app-visible MCP tool dispatch so local smoke can verify write_file and edit_file without an LLM round trip. Co-authored-by: Cursor --- crates/goose/src/acp/tkmind/agent.rs | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/goose/src/acp/tkmind/agent.rs b/crates/goose/src/acp/tkmind/agent.rs index 3d14ca5d4..d6c4c88cc 100644 --- a/crates/goose/src/acp/tkmind/agent.rs +++ b/crates/goose/src/acp/tkmind/agent.rs @@ -8,11 +8,16 @@ use axum::{ Json, Router, }; use goose_providers::model::ModelConfig; +use rmcp::model::CallToolRequestParams; use serde::Deserialize; +use serde_json::Value; +use tokio_util::sync::CancellationToken; use tracing::error; use crate::agents::extension::ToolInfo; use crate::agents::extension_manager::get_parameter_names; +use crate::agents::reply_parts::is_tool_visible_to_app; +use crate::agents::tool_execution::ToolCallContext; use crate::agents::ExtensionLoadResult; use crate::agents::platform_extensions::{chatrecall, projectmemory, PLATFORM_EXTENSIONS}; use crate::agents::ExtensionConfig; @@ -94,6 +99,101 @@ pub struct RestartAgentResponse { pub extension_results: Vec, } +#[derive(Deserialize)] +pub struct CallToolRequest { + session_id: String, + name: String, + arguments: Value, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallToolResponse { + content: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + structured_content: Option, + is_error: bool, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "_meta")] + _meta: Option, +} + +async fn ensure_extensions_loaded(state: &Arc, session_id: &str) { + if let Ok(Some(_results)) = state.take_extension_loading_task(session_id).await { + tracing::debug!( + "Awaited background extension loading for session {session_id} before serving request" + ); + state.remove_extension_loading_task(session_id).await; + } +} + +async fn call_tool( + State(state): State>, + Json(payload): Json, +) -> Result, ErrorResponse> { + ensure_extensions_loaded(&state, &payload.session_id).await; + + let agent = state + .get_agent_for_route(payload.session_id.clone()) + .await + .map_err(|status| ErrorResponse { + message: "Failed to get agent for route".into(), + status, + })?; + + let tools = agent.list_tools(&payload.session_id, None).await; + if let Some(tool) = tools.iter().find(|t| *t.name == payload.name) { + if !is_tool_visible_to_app(tool) { + return Err(ErrorResponse { + message: format!("Tool '{}' cannot be called by the app", payload.name), + status: StatusCode::FORBIDDEN, + }); + } + } + + let arguments = match payload.arguments { + Value::Object(map) => Some(map), + _ => None, + }; + + let mut tool_call = CallToolRequestParams::new(payload.name); + if let Some(args) = arguments { + tool_call = tool_call.with_arguments(args); + } + + let ctx = ToolCallContext::new(payload.session_id.clone(), None, None); + let tool_result = agent + .extension_manager + .dispatch_tool_call(&ctx, tool_call, CancellationToken::default()) + .await + .map_err(|err| ErrorResponse { + message: err.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let result = tool_result.result.await.map_err(|err| ErrorResponse { + message: err.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let content = result + .content + .into_iter() + .map(serde_json::to_value) + .collect::, _>>() + .map_err(|err| ErrorResponse { + message: err.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + Ok(Json(CallToolResponse { + content, + structured_content: result.structured_content, + is_error: result.is_error.unwrap_or(false), + _meta: result.meta.and_then(|m| serde_json::to_value(m).ok()), + })) +} + #[derive(Deserialize)] pub struct StartAgentRequest { working_dir: String, @@ -562,6 +662,7 @@ pub fn routes(state: Arc) -> Router { Router::new() .route("/agent/start", post(start_agent)) .route("/agent/resume", post(resume_agent)) + .route("/agent/call_tool", post(call_tool)) .route("/agent/tools", get(get_tools)) .route("/agent/update_provider", post(update_agent_provider)) .route("/agent/update_session", post(update_session))