1use 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
14const PROTOCOL_VERSION: &str = "2024-11-05";
16
17pub 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 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 let id = match request.id {
57 Some(id) => id,
58 None => {
59 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
73fn 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
86fn 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
101fn 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
109fn 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}