mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 08:39:24 +00:00
Fix MCP tool calls, approval loop, shutdown, and improve web UI
- Fix MCP tool schema deserialization: rename input_schema to match protocol's camelCase inputSchema, so models receive actual parameter schemas instead of empty defaults - Fix conversation history: add tool_calls field to ChatMessage and include assistant message with tool_calls before tool results, as required by OpenAI-compatible APIs - Fix approval loop: pass resume_after_tool flag to run_agentic_loop so the "force tool use" heuristic doesn't re-trigger after approval - Fix shutdown: add Submission::Quit, Ctrl+C signal handler, and graceful shutdown flow - Fix MCP activate button: auto-attempt auth flow when activation fails due to missing authentication - Add inline approval cards in chat via SSE ApprovalNeeded events - Add markdown rendering in chat (marked.js) with proper streaming - Add structured fields to log entries (key=value pairs from tracing) - Collapse log entries to single line with click-to-expand Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2cdd04a359
commit
bf3b8b339f
@@ -226,6 +226,38 @@ impl Tool for ToolAuthTool {
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
|
||||
|
||||
// Auto-activate after successful auth so tools are available immediately
|
||||
if result.status == "authenticated" {
|
||||
match self.manager.activate(name).await {
|
||||
Ok(activate_result) => {
|
||||
let output = serde_json::json!({
|
||||
"status": "authenticated_and_activated",
|
||||
"name": name,
|
||||
"tools_loaded": activate_result.tools_loaded,
|
||||
"message": activate_result.message,
|
||||
});
|
||||
return Ok(ToolOutput::success(output, start.elapsed()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Extension '{}' authenticated but activation failed: {}",
|
||||
name,
|
||||
e
|
||||
);
|
||||
let output = serde_json::json!({
|
||||
"status": "authenticated",
|
||||
"name": name,
|
||||
"activation_error": e.to_string(),
|
||||
"message": format!(
|
||||
"Authenticated but activation failed: {}. Try tool_activate.",
|
||||
e
|
||||
),
|
||||
});
|
||||
return Ok(ToolOutput::success(output, start.elapsed()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = serde_json::to_value(&result)
|
||||
.unwrap_or_else(|_| serde_json::json!({"error": "serialization failed"}));
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ pub struct McpTool {
|
||||
pub description: String,
|
||||
/// JSON Schema for input parameters.
|
||||
/// Defaults to empty object schema if not provided.
|
||||
#[serde(default = "default_input_schema")]
|
||||
/// MCP protocol uses camelCase `inputSchema`.
|
||||
#[serde(
|
||||
default = "default_input_schema",
|
||||
rename = "inputSchema",
|
||||
alias = "input_schema"
|
||||
)]
|
||||
pub input_schema: serde_json::Value,
|
||||
/// Optional annotations from the MCP server.
|
||||
#[serde(default)]
|
||||
@@ -285,3 +290,95 @@ impl ContentBlock {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_deserialize_camel_case_input_schema() {
|
||||
// MCP protocol uses camelCase "inputSchema"
|
||||
let json = serde_json::json!({
|
||||
"name": "list_issues",
|
||||
"description": "List GitHub issues",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": { "type": "string" },
|
||||
"repo": { "type": "string" }
|
||||
},
|
||||
"required": ["owner", "repo"]
|
||||
}
|
||||
});
|
||||
|
||||
let tool: McpTool = serde_json::from_value(json).expect("deserialize McpTool");
|
||||
assert_eq!(tool.name, "list_issues");
|
||||
assert_eq!(tool.description, "List GitHub issues");
|
||||
|
||||
// The schema must have the properties, not the empty default
|
||||
let props = tool.input_schema.get("properties").expect("has properties");
|
||||
assert!(props.get("owner").is_some());
|
||||
assert!(props.get("repo").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_deserialize_snake_case_alias() {
|
||||
// Also accept snake_case "input_schema" for flexibility
|
||||
let json = serde_json::json!({
|
||||
"name": "search",
|
||||
"description": "Search",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let tool: McpTool = serde_json::from_value(json).expect("deserialize McpTool");
|
||||
let props = tool.input_schema.get("properties").expect("has properties");
|
||||
assert!(props.get("query").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_missing_schema_gets_default() {
|
||||
let json = serde_json::json!({
|
||||
"name": "ping",
|
||||
"description": "Ping"
|
||||
});
|
||||
|
||||
let tool: McpTool = serde_json::from_value(json).expect("deserialize McpTool");
|
||||
assert_eq!(tool.input_schema["type"], "object");
|
||||
assert!(tool.input_schema["properties"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_roundtrip_preserves_schema() {
|
||||
// Simulate what list_tools returns from a real MCP server
|
||||
let server_response = serde_json::json!({
|
||||
"tools": [{
|
||||
"name": "github-copilot_list_issues",
|
||||
"description": "List issues for a repository",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"owner": { "type": "string", "description": "Repository owner" },
|
||||
"repo": { "type": "string", "description": "Repository name" },
|
||||
"state": { "type": "string", "enum": ["open", "closed", "all"] }
|
||||
},
|
||||
"required": ["owner", "repo"]
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let result: ListToolsResult =
|
||||
serde_json::from_value(server_response).expect("deserialize ListToolsResult");
|
||||
assert_eq!(result.tools.len(), 1);
|
||||
|
||||
let tool = &result.tools[0];
|
||||
assert_eq!(tool.name, "github-copilot_list_issues");
|
||||
|
||||
let required = tool.input_schema.get("required").expect("has required");
|
||||
assert!(required.as_array().expect("is array").len() == 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user