Skip to main content

authy/mcp/
mod.rs

1//! MCP (Model Context Protocol) server — stdio JSON-RPC 2.0.
2//!
3//! Implements the minimal MCP handshake (`initialize`, `notifications/initialized`,
4//! `tools/list`, `tools/call`, `ping`) over line-delimited JSON on stdin/stdout.
5
6pub mod tools;
7
8use std::io::{BufRead, Write};
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::api::AuthyClient;
14
15// ── JSON-RPC 2.0 types ──────────────────────────────────────────
16
17#[derive(Debug, Deserialize)]
18pub struct JsonRpcRequest {
19    #[allow(dead_code)]
20    pub jsonrpc: String,
21    pub id: Option<Value>,
22    pub method: String,
23    #[serde(default)]
24    pub params: Value,
25}
26
27#[derive(Debug, Serialize)]
28pub struct JsonRpcResponse {
29    pub jsonrpc: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub id: Option<Value>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub result: Option<Value>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub error: Option<JsonRpcError>,
36}
37
38#[derive(Debug, Serialize)]
39pub struct JsonRpcError {
40    pub code: i64,
41    pub message: String,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub data: Option<Value>,
44}
45
46impl JsonRpcResponse {
47    fn success(id: Option<Value>, result: Value) -> Self {
48        Self {
49            jsonrpc: "2.0".to_string(),
50            id,
51            result: Some(result),
52            error: None,
53        }
54    }
55
56    fn error(id: Option<Value>, code: i64, message: impl Into<String>) -> Self {
57        Self {
58            jsonrpc: "2.0".to_string(),
59            id,
60            result: None,
61            error: Some(JsonRpcError {
62                code,
63                message: message.into(),
64                data: None,
65            }),
66        }
67    }
68}
69
70// ── MCP Server ───────────────────────────────────────────────────
71
72/// MCP server that dispatches JSON-RPC requests to the AuthyClient API.
73pub struct McpServer {
74    client: Option<AuthyClient>,
75}
76
77impl McpServer {
78    pub fn new(client: Option<AuthyClient>) -> Self {
79        Self { client }
80    }
81
82    /// Run the server read loop on the given reader/writer pair.
83    ///
84    /// Reads line-delimited JSON-RPC from `reader`, dispatches, and writes
85    /// responses to `writer`. Returns when the reader reaches EOF.
86    pub fn run<R: BufRead, W: Write>(&self, reader: R, mut writer: W) -> std::io::Result<()> {
87        for line in reader.lines() {
88            let line = line?;
89            let line = line.trim();
90            if line.is_empty() {
91                continue;
92            }
93
94            let request: JsonRpcRequest = match serde_json::from_str(line) {
95                Ok(r) => r,
96                Err(e) => {
97                    let resp =
98                        JsonRpcResponse::error(None, -32700, format!("Parse error: {}", e));
99                    Self::write_response(&mut writer, &resp)?;
100                    continue;
101                }
102            };
103
104            // Notifications (no id) produce no response
105            let is_notification = request.id.is_none();
106            let response = self.dispatch(&request);
107
108            if !is_notification {
109                if let Some(resp) = response {
110                    Self::write_response(&mut writer, &resp)?;
111                }
112            }
113        }
114        Ok(())
115    }
116
117    fn write_response<W: Write>(writer: &mut W, resp: &JsonRpcResponse) -> std::io::Result<()> {
118        let json = serde_json::to_string(resp)
119            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
120        writeln!(writer, "{}", json)?;
121        writer.flush()
122    }
123
124    fn dispatch(&self, req: &JsonRpcRequest) -> Option<JsonRpcResponse> {
125        match req.method.as_str() {
126            "initialize" => Some(self.handle_initialize(req)),
127            "notifications/initialized" => None,
128            "ping" => Some(self.handle_ping(req)),
129            "tools/list" => Some(self.handle_tools_list(req)),
130            "tools/call" => Some(self.handle_tools_call(req)),
131            _ => Some(JsonRpcResponse::error(
132                req.id.clone(),
133                -32601,
134                format!("Method not found: {}", req.method),
135            )),
136        }
137    }
138
139    fn handle_initialize(&self, req: &JsonRpcRequest) -> JsonRpcResponse {
140        let result = serde_json::json!({
141            "protocolVersion": "2024-11-05",
142            "capabilities": {
143                "tools": {}
144            },
145            "serverInfo": {
146                "name": "authy",
147                "version": env!("CARGO_PKG_VERSION")
148            }
149        });
150        JsonRpcResponse::success(req.id.clone(), result)
151    }
152
153    fn handle_ping(&self, req: &JsonRpcRequest) -> JsonRpcResponse {
154        JsonRpcResponse::success(req.id.clone(), serde_json::json!({}))
155    }
156
157    fn handle_tools_list(&self, req: &JsonRpcRequest) -> JsonRpcResponse {
158        let tool_defs = tools::tool_definitions();
159        let result = serde_json::json!({ "tools": tool_defs });
160        JsonRpcResponse::success(req.id.clone(), result)
161    }
162
163    fn handle_tools_call(&self, req: &JsonRpcRequest) -> JsonRpcResponse {
164        let tool_name = req
165            .params
166            .get("name")
167            .and_then(|v| v.as_str())
168            .unwrap_or("");
169        let arguments = req
170            .params
171            .get("arguments")
172            .cloned()
173            .unwrap_or(Value::Object(Default::default()));
174
175        let client = match &self.client {
176            Some(c) => c,
177            None => {
178                let result = tools::error_result(
179                    "No credentials configured. Set AUTHY_KEYFILE or AUTHY_PASSPHRASE.",
180                );
181                return JsonRpcResponse::success(req.id.clone(), result);
182            }
183        };
184
185        let result = tools::dispatch(client, tool_name, &arguments);
186        JsonRpcResponse::success(req.id.clone(), result)
187    }
188}