Skip to main content

bn/mcp/
server.rs

1//! MCP stdio server: reads JSON-RPC 2.0 from stdin, writes responses to stdout.
2
3use std::io::{self, BufRead, Write};
4use std::path::Path;
5
6use serde_json::{json, Value};
7
8use crate::mcp::protocol::{
9    JsonRpcRequest, JsonRpcResponse, INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND, PARSE_ERROR,
10};
11use crate::mcp::resources;
12use crate::mcp::tools;
13
14/// Protocol version we support.
15const PROTOCOL_VERSION: &str = "2024-11-05";
16
17/// Run the MCP server loop on stdin/stdout.
18///
19/// Reads newline-delimited JSON-RPC 2.0 messages from stdin,
20/// dispatches to the appropriate handler, and writes responses
21/// to stdout. Notifications (no `id`) do not get responses.
22pub fn run(beans_dir: &Path) -> anyhow::Result<()> {
23    let stdin = io::stdin();
24    let stdout = io::stdout();
25    let reader = stdin.lock();
26    let mut writer = stdout.lock();
27
28    eprintln!("beans MCP server started (protocol {})", PROTOCOL_VERSION);
29
30    for line in reader.lines() {
31        let line = match line {
32            Ok(l) => l,
33            Err(e) => {
34                eprintln!("stdin read error: {}", e);
35                break;
36            }
37        };
38
39        let trimmed = line.trim();
40        if trimmed.is_empty() {
41            continue;
42        }
43
44        let request: JsonRpcRequest = match serde_json::from_str(trimmed) {
45            Ok(r) => r,
46            Err(e) => {
47                // Parse error — respond with error if we can extract an id
48                let error_response =
49                    JsonRpcResponse::error(Value::Null, PARSE_ERROR, format!("Parse error: {}", e));
50                write_response(&mut writer, &error_response)?;
51                continue;
52            }
53        };
54
55        // Notifications (no id) don't get responses
56        let id = match request.id {
57            Some(id) => id,
58            None => {
59                // Handle notification silently
60                handle_notification(&request.method);
61                continue;
62            }
63        };
64
65        let response = dispatch(&request.method, &request.params, id.clone(), beans_dir);
66        write_response(&mut writer, &response)?;
67    }
68
69    eprintln!("beans MCP server shutting down");
70    Ok(())
71}
72
73/// Dispatch a JSON-RPC request to the appropriate handler.
74fn dispatch(method: &str, params: &Option<Value>, id: Value, beans_dir: &Path) -> JsonRpcResponse {
75    match method {
76        "initialize" => handle_initialize(params, id),
77        "tools/list" => handle_tools_list(id),
78        "tools/call" => handle_tools_call(params, id, beans_dir),
79        "resources/list" => handle_resources_list(id),
80        "resources/read" => handle_resources_read(params, id, beans_dir),
81        "ping" => JsonRpcResponse::success(id, json!({})),
82        _ => JsonRpcResponse::error(id, METHOD_NOT_FOUND, format!("Unknown method: {}", method)),
83    }
84}
85
86/// Handle notifications (no response needed).
87fn handle_notification(method: &str) {
88    match method {
89        "notifications/initialized" => {
90            eprintln!("Client initialized");
91        }
92        "notifications/cancelled" => {
93            eprintln!("Request cancelled by client");
94        }
95        _ => {
96            eprintln!("Unknown notification: {}", method);
97        }
98    }
99}
100
101/// Write a JSON-RPC response as a single line to stdout.
102fn write_response(writer: &mut impl Write, response: &JsonRpcResponse) -> anyhow::Result<()> {
103    let json = serde_json::to_string(response)?;
104    writeln!(writer, "{}", json)?;
105    writer.flush()?;
106    Ok(())
107}
108
109// ---------------------------------------------------------------------------
110// MCP Method Handlers
111// ---------------------------------------------------------------------------
112
113fn handle_initialize(params: &Option<Value>, id: Value) -> JsonRpcResponse {
114    let _client_info = params
115        .as_ref()
116        .and_then(|p| p.get("clientInfo"))
117        .and_then(|c| c.get("name"))
118        .and_then(|n| n.as_str())
119        .unwrap_or("unknown");
120
121    eprintln!("Initializing with client: {}", _client_info);
122
123    JsonRpcResponse::success(
124        id,
125        json!({
126            "protocolVersion": PROTOCOL_VERSION,
127            "capabilities": {
128                "tools": {},
129                "resources": {}
130            },
131            "serverInfo": {
132                "name": "beans",
133                "version": env!("CARGO_PKG_VERSION")
134            }
135        }),
136    )
137}
138
139fn handle_tools_list(id: Value) -> JsonRpcResponse {
140    let tool_defs = tools::tool_definitions();
141    let tools_json: Vec<Value> = tool_defs
142        .iter()
143        .map(|t| {
144            json!({
145                "name": t.name,
146                "description": t.description,
147                "inputSchema": t.input_schema,
148            })
149        })
150        .collect();
151
152    JsonRpcResponse::success(id, json!({ "tools": tools_json }))
153}
154
155fn handle_tools_call(params: &Option<Value>, id: Value, beans_dir: &Path) -> JsonRpcResponse {
156    let params = match params {
157        Some(p) => p,
158        None => {
159            return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing params");
160        }
161    };
162
163    let name = match params.get("name").and_then(|n| n.as_str()) {
164        Some(n) => n,
165        None => {
166            return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing tool name");
167        }
168    };
169
170    let args = params.get("arguments").cloned().unwrap_or(json!({}));
171
172    eprintln!("Tool call: {}", name);
173    let result = tools::handle_tool_call(name, &args, beans_dir);
174    JsonRpcResponse::success(id, result)
175}
176
177fn handle_resources_list(id: Value) -> JsonRpcResponse {
178    let resource_defs = resources::resource_definitions();
179    let resources_json: Vec<Value> = resource_defs
180        .iter()
181        .map(|r| {
182            let mut obj = json!({
183                "uri": r.uri,
184                "name": r.name,
185            });
186            if let Some(ref desc) = r.description {
187                obj["description"] = json!(desc);
188            }
189            if let Some(ref mime) = r.mime_type {
190                obj["mimeType"] = json!(mime);
191            }
192            obj
193        })
194        .collect();
195
196    JsonRpcResponse::success(id, json!({ "resources": resources_json }))
197}
198
199fn handle_resources_read(params: &Option<Value>, id: Value, beans_dir: &Path) -> JsonRpcResponse {
200    let params = match params {
201        Some(p) => p,
202        None => {
203            return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing params");
204        }
205    };
206
207    let uri = match params.get("uri").and_then(|u| u.as_str()) {
208        Some(u) => u,
209        None => {
210            return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing resource URI");
211        }
212    };
213
214    match resources::handle_resource_read(uri, beans_dir) {
215        Ok(contents) => {
216            let contents_json: Vec<Value> = contents
217                .iter()
218                .map(|c| {
219                    let mut obj = json!({
220                        "uri": c.uri,
221                        "text": c.text,
222                    });
223                    if let Some(ref mime) = c.mime_type {
224                        obj["mimeType"] = json!(mime);
225                    }
226                    obj
227                })
228                .collect();
229            JsonRpcResponse::success(id, json!({ "contents": contents_json }))
230        }
231        Err(e) => JsonRpcResponse::error(id, INTERNAL_ERROR, format!("Resource error: {}", e)),
232    }
233}