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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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<ExtensionLoadResult>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CallToolRequest {
|
||||
session_id: String,
|
||||
name: String,
|
||||
arguments: Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolResponse {
|
||||
content: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
structured_content: Option<Value>,
|
||||
is_error: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "_meta")]
|
||||
_meta: Option<Value>,
|
||||
}
|
||||
|
||||
async fn ensure_extensions_loaded(state: &Arc<AppState>, 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<Arc<AppState>>,
|
||||
Json(payload): Json<CallToolRequest>,
|
||||
) -> Result<Json<CallToolResponse>, 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::<Result<Vec<_>, _>>()
|
||||
.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<AppState>) -> 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))
|
||||
|
||||
Reference in New Issue
Block a user