Skip to main content

batuta/serve/banco/
mcp.rs

1//! Model Context Protocol (MCP) handler — JSON-RPC 2.0 endpoint.
2//!
3//! Exposes Banco's tools as MCP tools, enabling Claude Desktop, Cursor,
4//! and other MCP-compatible clients to connect via HTTP SSE transport.
5//!
6//! Implements: initialize, tools/list, tools/call, resources/list, prompts/list.
7
8use serde::{Deserialize, Serialize};
9
10/// MCP JSON-RPC 2.0 request.
11#[derive(Debug, Clone, Deserialize)]
12pub struct McpRequest {
13    pub jsonrpc: String,
14    pub id: Option<serde_json::Value>,
15    pub method: String,
16    #[serde(default)]
17    pub params: serde_json::Value,
18}
19
20/// MCP JSON-RPC 2.0 response.
21#[derive(Debug, Clone, Serialize)]
22pub struct McpResponse {
23    pub jsonrpc: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub id: Option<serde_json::Value>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub result: Option<serde_json::Value>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub error: Option<McpError>,
30}
31
32/// MCP JSON-RPC error.
33#[derive(Debug, Clone, Serialize)]
34pub struct McpError {
35    pub code: i32,
36    pub message: String,
37}
38
39impl McpResponse {
40    pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
41        Self { jsonrpc: "2.0".to_string(), id, result: Some(result), error: None }
42    }
43
44    pub fn error(id: Option<serde_json::Value>, code: i32, message: impl Into<String>) -> Self {
45        Self {
46            jsonrpc: "2.0".to_string(),
47            id,
48            result: None,
49            error: Some(McpError { code, message: message.into() }),
50        }
51    }
52}
53
54/// MCP server info returned by initialize.
55#[derive(Debug, Clone, Serialize)]
56pub struct McpServerInfo {
57    pub name: String,
58    pub version: String,
59}
60
61/// MCP tool definition (MCP protocol format).
62#[derive(Debug, Clone, Serialize)]
63pub struct McpTool {
64    pub name: String,
65    pub description: String,
66    #[serde(rename = "inputSchema")]
67    pub input_schema: serde_json::Value,
68}
69
70/// MCP resource definition.
71#[derive(Debug, Clone, Serialize)]
72pub struct McpResource {
73    pub uri: String,
74    pub name: String,
75    pub description: String,
76    #[serde(rename = "mimeType")]
77    pub mime_type: String,
78}
79
80/// MCP prompt definition.
81#[derive(Debug, Clone, Serialize)]
82pub struct McpPrompt {
83    pub name: String,
84    pub description: String,
85}
86
87/// Process an MCP JSON-RPC request using the Banco tool registry.
88pub fn handle_mcp_request(
89    request: &McpRequest,
90    tools: &super::tools::ToolRegistry,
91    prompts: &super::prompts::PromptStore,
92) -> McpResponse {
93    match request.method.as_str() {
94        "initialize" => handle_initialize(request),
95        "tools/list" => handle_tools_list(request, tools),
96        "tools/call" => handle_tools_call(request, tools),
97        "resources/list" => handle_resources_list(request),
98        "prompts/list" => handle_prompts_list(request, prompts),
99        "ping" => McpResponse::success(request.id.clone(), serde_json::json!({})),
100        _ => McpResponse::error(
101            request.id.clone(),
102            -32601,
103            format!("Method not found: {}", request.method),
104        ),
105    }
106}
107
108fn handle_initialize(request: &McpRequest) -> McpResponse {
109    McpResponse::success(
110        request.id.clone(),
111        serde_json::json!({
112            "protocolVersion": "2024-11-05",
113            "capabilities": {
114                "tools": {},
115                "resources": {},
116                "prompts": {}
117            },
118            "serverInfo": {
119                "name": "banco",
120                "version": env!("CARGO_PKG_VERSION")
121            }
122        }),
123    )
124}
125
126fn handle_tools_list(request: &McpRequest, tools: &super::tools::ToolRegistry) -> McpResponse {
127    let mcp_tools: Vec<McpTool> = tools
128        .list()
129        .into_iter()
130        .filter(|t| t.enabled)
131        .map(|t| McpTool { name: t.name, description: t.description, input_schema: t.parameters })
132        .collect();
133
134    McpResponse::success(request.id.clone(), serde_json::json!({ "tools": mcp_tools }))
135}
136
137fn handle_tools_call(request: &McpRequest, tools: &super::tools::ToolRegistry) -> McpResponse {
138    let name = request.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
139    let arguments = request.params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
140
141    if tools.get(name).is_none() {
142        return McpResponse::error(request.id.clone(), -32602, format!("Unknown tool: {name}"));
143    }
144
145    let call = super::tools::ToolCall {
146        id: format!("mcp-{}", epoch_secs()),
147        name: name.to_string(),
148        arguments,
149    };
150
151    let result = tools.execute(&call);
152
153    if let Some(err) = &result.error {
154        McpResponse::success(
155            request.id.clone(),
156            serde_json::json!({
157                "content": [{"type": "text", "text": err}],
158                "isError": true
159            }),
160        )
161    } else {
162        McpResponse::success(
163            request.id.clone(),
164            serde_json::json!({
165                "content": [{"type": "text", "text": result.content}]
166            }),
167        )
168    }
169}
170
171fn handle_resources_list(request: &McpRequest) -> McpResponse {
172    let resources = vec![
173        McpResource {
174            uri: "banco://system".to_string(),
175            name: "System Info".to_string(),
176            description: "Banco system status and configuration".to_string(),
177            mime_type: "application/json".to_string(),
178        },
179        McpResource {
180            uri: "banco://models".to_string(),
181            name: "Models".to_string(),
182            description: "Available and loaded models".to_string(),
183            mime_type: "application/json".to_string(),
184        },
185    ];
186    McpResponse::success(request.id.clone(), serde_json::json!({ "resources": resources }))
187}
188
189fn handle_prompts_list(request: &McpRequest, prompts: &super::prompts::PromptStore) -> McpResponse {
190    let mcp_prompts: Vec<McpPrompt> = prompts
191        .list()
192        .into_iter()
193        .map(|p| McpPrompt { name: p.name, description: p.content })
194        .collect();
195
196    McpResponse::success(request.id.clone(), serde_json::json!({ "prompts": mcp_prompts }))
197}
198
199fn epoch_secs() -> u64 {
200    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()
201}