use escriba_command::CommandRegistry;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[must_use]
pub fn tool_catalog() -> Vec<McpTool> {
let mut out = Vec::new();
out.push(McpTool {
name: "get_spec".to_string(),
description: "Return the escriba OpenAPI 3.1 spec as JSON.".to_string(),
input_schema: json!({ "type": "object", "properties": {} }),
});
out.push(McpTool {
name: "list_commands".to_string(),
description: "Return every registered escriba command with its description.".to_string(),
input_schema: json!({ "type": "object", "properties": {} }),
});
out.push(McpTool {
name: "list_keymap".to_string(),
description: "Return every keybinding active in the current session.".to_string(),
input_schema: json!({ "type": "object", "properties": {} }),
});
let reg = CommandRegistry::default_set();
for c in reg.specs() {
out.push(McpTool {
name: format!("run_{}", c.name.replace('-', "_")),
description: c.description.clone(),
input_schema: json!({
"type": "object",
"properties": {
"args": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": false
}),
});
}
out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpRequest {
pub jsonrpc: String,
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResponse {
pub jsonrpc: String,
pub id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<McpError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpError {
pub code: i32,
pub message: String,
}
#[must_use]
pub fn handle(req: &McpRequest) -> McpResponse {
let result = match req.method.as_str() {
"initialize" => Ok(json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": { "name": "escriba-mcp", "version": env!("CARGO_PKG_VERSION") }
})),
"tools/list" => Ok(json!({ "tools": tool_catalog() })),
"tools/call" => dispatch_tool(&req.params),
_ => Err(McpError {
code: -32601,
message: format!("method not found: {}", req.method),
}),
};
match result {
Ok(result) => McpResponse {
jsonrpc: "2.0".into(),
id: req.id.clone(),
result: Some(result),
error: None,
},
Err(err) => McpResponse {
jsonrpc: "2.0".into(),
id: req.id.clone(),
result: None,
error: Some(err),
},
}
}
fn dispatch_tool(params: &Value) -> Result<Value, McpError> {
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| McpError {
code: -32602,
message: "missing tool name".into(),
})?;
match name {
"get_spec" => Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&escriba_spec::build_spec().0).unwrap_or_default()
}]
})),
"list_commands" => Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&CommandRegistry::default_set().specs()).unwrap_or_default()
}]
})),
"list_keymap" => Ok(json!({
"content": [{
"type": "text",
"text": "see the keymap subcommand on the CLI for a session-specific listing"
}]
})),
other if other.starts_with("run_") => Ok(json!({
"content": [{
"type": "text",
"text": format!("phase 2 wires live execution for tool '{other}'")
}]
})),
other => Err(McpError {
code: -32601,
message: format!("unknown tool: {other}"),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_includes_meta_plus_commands() {
let cat = tool_catalog();
assert!(cat.iter().any(|t| t.name == "get_spec"));
assert!(cat.iter().any(|t| t.name == "list_commands"));
assert!(cat.iter().any(|t| t.name == "run_save"));
assert!(cat.iter().any(|t| t.name == "run_quit"));
}
#[test]
fn initialize_returns_server_info() {
let req = McpRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "initialize".into(),
params: json!({}),
};
let resp = handle(&req);
let info = resp.result.unwrap();
assert_eq!(info["serverInfo"]["name"].as_str().unwrap(), "escriba-mcp");
}
#[test]
fn tools_list_returns_catalog() {
let req = McpRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "tools/list".into(),
params: json!({}),
};
let resp = handle(&req);
let tools = resp.result.unwrap();
assert!(tools["tools"].as_array().unwrap().len() >= 3);
}
#[test]
fn unknown_method_errors() {
let req = McpRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "not-a-method".into(),
params: json!({}),
};
let resp = handle(&req);
assert!(resp.error.is_some());
assert_eq!(resp.error.unwrap().code, -32601);
}
}