Skip to main content

harn_vm/mcp_server/
server.rs

1use std::cell::RefCell;
2
3use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
4
5use crate::stdlib::json_to_vm_value;
6use crate::value::VmError;
7use crate::vm::Vm;
8
9use super::convert::{prompt_value_to_messages, vm_value_to_content};
10use super::defs::{McpPromptDef, McpResourceDef, McpResourceTemplateDef, McpToolDef};
11use super::pagination::{encode_cursor, parse_cursor};
12use super::uri::match_uri_template;
13use super::PROTOCOL_VERSION;
14
15/// MCP server that exposes Harn tools, resources, and prompts over stdio JSON-RPC.
16pub struct McpServer {
17    server_name: String,
18    server_version: String,
19    tools: Vec<McpToolDef>,
20    resources: Vec<McpResourceDef>,
21    resource_templates: Vec<McpResourceTemplateDef>,
22    prompts: Vec<McpPromptDef>,
23    log_level: RefCell<String>,
24    /// Optional Server Card payload — advertised in the `initialize`
25    /// response's `serverInfo.card` field and exposed as a static
26    /// resource at the well-known URI `well-known://mcp-card`.
27    /// Populated by `harn mcp-serve --card path/to/card.json`.
28    server_card: Option<serde_json::Value>,
29}
30
31impl McpServer {
32    pub fn new(
33        server_name: String,
34        tools: Vec<McpToolDef>,
35        resources: Vec<McpResourceDef>,
36        resource_templates: Vec<McpResourceTemplateDef>,
37        prompts: Vec<McpPromptDef>,
38    ) -> Self {
39        Self {
40            server_name,
41            server_version: env!("CARGO_PKG_VERSION").to_string(),
42            tools,
43            resources,
44            resource_templates,
45            prompts,
46            log_level: RefCell::new("warning".to_string()),
47            server_card: None,
48        }
49    }
50
51    /// Attach a Server Card to be advertised over `initialize` and via
52    /// the `well-known://mcp-card` resource. Call on a freshly-built
53    /// `McpServer` before `run`.
54    pub fn with_server_card(mut self, card: serde_json::Value) -> Self {
55        self.server_card = Some(card);
56        self
57    }
58
59    /// Run the MCP server loop, reading JSON-RPC from stdin and writing to stdout.
60    pub async fn run(&self, vm: &mut Vm) -> Result<(), VmError> {
61        let stdin = BufReader::new(tokio::io::stdin());
62        let mut stdout = tokio::io::stdout();
63        let mut lines = stdin.lines();
64
65        while let Ok(Some(line)) = lines.next_line().await {
66            let trimmed = line.trim();
67            if trimmed.is_empty() {
68                continue;
69            }
70
71            let msg: serde_json::Value = match serde_json::from_str(trimmed) {
72                Ok(v) => v,
73                Err(_) => continue,
74            };
75
76            let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
77            let id = msg.get("id").cloned();
78            let params = msg.get("params").cloned().unwrap_or(serde_json::json!({}));
79
80            // Notifications have no id; ignore them silently.
81            if id.is_none() {
82                continue;
83            }
84            let id = id.unwrap();
85
86            let response = match method {
87                "initialize" => self.handle_initialize(&id),
88                "ping" => crate::jsonrpc::response(id.clone(), serde_json::json!({})),
89                "logging/setLevel" => self.handle_logging_set_level(&id, &params),
90                "tools/list" => self.handle_tools_list(&id, &params),
91                "tools/call" => self.handle_tools_call(&id, &params, vm).await,
92                "resources/list" => self.handle_resources_list(&id, &params),
93                "resources/read" => self.handle_resources_read(&id, &params, vm).await,
94                "resources/templates/list" => self.handle_resource_templates_list(&id, &params),
95                "prompts/list" => self.handle_prompts_list(&id, &params),
96                "prompts/get" => self.handle_prompts_get(&id, &params, vm).await,
97                _ => serde_json::json!({
98                    "jsonrpc": "2.0",
99                    "id": id,
100                    "error": {
101                        "code": -32601,
102                        "message": format!("Method not found: {method}")
103                    }
104                }),
105            };
106
107            let mut response_line = serde_json::to_string(&response)
108                .map_err(|e| VmError::Runtime(format!("MCP server serialization error: {e}")))?;
109            response_line.push('\n');
110            stdout
111                .write_all(response_line.as_bytes())
112                .await
113                .map_err(|e| VmError::Runtime(format!("MCP server write error: {e}")))?;
114            stdout
115                .flush()
116                .await
117                .map_err(|e| VmError::Runtime(format!("MCP server flush error: {e}")))?;
118        }
119
120        Ok(())
121    }
122
123    fn handle_initialize(&self, id: &serde_json::Value) -> serde_json::Value {
124        let mut capabilities = serde_json::Map::new();
125        if !self.tools.is_empty() {
126            capabilities.insert("tools".into(), serde_json::json!({}));
127        }
128        if !self.resources.is_empty()
129            || !self.resource_templates.is_empty()
130            || self.server_card.is_some()
131        {
132            capabilities.insert("resources".into(), serde_json::json!({}));
133        }
134        if !self.prompts.is_empty() {
135            capabilities.insert("prompts".into(), serde_json::json!({}));
136        }
137        capabilities.insert("logging".into(), serde_json::json!({}));
138
139        let mut server_info = serde_json::json!({
140            "name": self.server_name,
141            "version": self.server_version
142        });
143        if let Some(ref card) = self.server_card {
144            server_info["card"] = card.clone();
145        }
146
147        serde_json::json!({
148            "jsonrpc": "2.0",
149            "id": id,
150            "result": {
151                "protocolVersion": PROTOCOL_VERSION,
152                "capabilities": capabilities,
153                "serverInfo": server_info
154            }
155        })
156    }
157
158    fn handle_tools_list(
159        &self,
160        id: &serde_json::Value,
161        params: &serde_json::Value,
162    ) -> serde_json::Value {
163        let (offset, page_size) = parse_cursor(params);
164        let page_end = (offset + page_size).min(self.tools.len());
165        let tools: Vec<serde_json::Value> = self.tools[offset..page_end]
166            .iter()
167            .map(|t| {
168                let mut entry = serde_json::json!({
169                    "name": t.name,
170                    "description": t.description,
171                    "inputSchema": t.input_schema,
172                });
173                if let Some(ref title) = t.title {
174                    entry["title"] = serde_json::json!(title);
175                }
176                if let Some(ref output_schema) = t.output_schema {
177                    entry["outputSchema"] = output_schema.clone();
178                }
179                if let Some(ref annotations) = t.annotations {
180                    entry["annotations"] = annotations.clone();
181                }
182                entry
183            })
184            .collect();
185
186        let mut result = serde_json::json!({ "tools": tools });
187        if page_end < self.tools.len() {
188            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
189        }
190
191        serde_json::json!({
192            "jsonrpc": "2.0",
193            "id": id,
194            "result": result
195        })
196    }
197
198    async fn handle_tools_call(
199        &self,
200        id: &serde_json::Value,
201        params: &serde_json::Value,
202        vm: &mut Vm,
203    ) -> serde_json::Value {
204        let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
205
206        let tool = match self.tools.iter().find(|t| t.name == tool_name) {
207            Some(t) => t,
208            None => {
209                return serde_json::json!({
210                    "jsonrpc": "2.0",
211                    "id": id,
212                    "error": { "code": -32602, "message": format!("Unknown tool: {tool_name}") }
213                });
214            }
215        };
216
217        let arguments = params
218            .get("arguments")
219            .cloned()
220            .unwrap_or(serde_json::json!({}));
221        let args_vm = json_to_vm_value(&arguments);
222
223        let result = vm.call_closure_pub(&tool.handler, &[args_vm], &[]).await;
224
225        match result {
226            Ok(value) => {
227                let content = vm_value_to_content(&value);
228                let mut call_result = serde_json::json!({
229                    "content": content,
230                    "isError": false
231                });
232                if tool.output_schema.is_some() {
233                    let text = value.display();
234                    let structured = match serde_json::from_str::<serde_json::Value>(&text) {
235                        Ok(v) => v,
236                        _ => serde_json::json!(text),
237                    };
238                    call_result["structuredContent"] = structured;
239                }
240                serde_json::json!({
241                    "jsonrpc": "2.0",
242                    "id": id,
243                    "result": call_result
244                })
245            }
246            Err(e) => serde_json::json!({
247                "jsonrpc": "2.0",
248                "id": id,
249                "result": {
250                    "content": [{ "type": "text", "text": format!("{e}") }],
251                    "isError": true
252                }
253            }),
254        }
255    }
256
257    fn handle_resources_list(
258        &self,
259        id: &serde_json::Value,
260        params: &serde_json::Value,
261    ) -> serde_json::Value {
262        // Virtually prepend the Server Card as a static resource so
263        // clients that browse resources can discover the card without
264        // a separate well-known GET. Kept out of the underlying
265        // `self.resources` vec so cursor paging stays simple.
266        let card_entry = self.server_card.as_ref().map(|_| {
267            serde_json::json!({
268                "uri": "well-known://mcp-card",
269                "name": "Server Card",
270                "description": "MCP v2.1 Server Card advertising this server's identity and capabilities",
271                "mimeType": "application/json",
272            })
273        });
274
275        let (offset, page_size) = parse_cursor(params);
276        let page_end = (offset + page_size).min(self.resources.len());
277        let mut resources: Vec<serde_json::Value> = self.resources[offset..page_end]
278            .iter()
279            .map(|r| {
280                let mut entry = serde_json::json!({ "uri": r.uri, "name": r.name });
281                if let Some(ref title) = r.title {
282                    entry["title"] = serde_json::json!(title);
283                }
284                if let Some(ref desc) = r.description {
285                    entry["description"] = serde_json::json!(desc);
286                }
287                if let Some(ref mime) = r.mime_type {
288                    entry["mimeType"] = serde_json::json!(mime);
289                }
290                entry
291            })
292            .collect();
293        if offset == 0 {
294            if let Some(entry) = card_entry {
295                resources.insert(0, entry);
296            }
297        }
298
299        let mut result = serde_json::json!({ "resources": resources });
300        if page_end < self.resources.len() {
301            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
302        }
303
304        serde_json::json!({
305            "jsonrpc": "2.0",
306            "id": id,
307            "result": result
308        })
309    }
310
311    async fn handle_resources_read(
312        &self,
313        id: &serde_json::Value,
314        params: &serde_json::Value,
315        vm: &mut Vm,
316    ) -> serde_json::Value {
317        let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
318
319        // Expose the Server Card at the well-known URI. Matches the
320        // HTTP convention (.well-known/mcp-card) but routed through
321        // the stdio resource protocol.
322        if uri == "well-known://mcp-card" {
323            if let Some(ref card) = self.server_card {
324                let content = serde_json::json!({
325                    "uri": uri,
326                    "text": serde_json::to_string(card).unwrap_or_else(|_| "{}".to_string()),
327                    "mimeType": "application/json",
328                });
329                return serde_json::json!({
330                    "jsonrpc": "2.0",
331                    "id": id,
332                    "result": { "contents": [content] }
333                });
334            }
335        }
336
337        // Static resources take precedence over templates.
338        if let Some(resource) = self.resources.iter().find(|r| r.uri == uri) {
339            let mut content = serde_json::json!({ "uri": resource.uri, "text": resource.text });
340            if let Some(ref mime) = resource.mime_type {
341                content["mimeType"] = serde_json::json!(mime);
342            }
343            return serde_json::json!({
344                "jsonrpc": "2.0",
345                "id": id,
346                "result": { "contents": [content] }
347            });
348        }
349
350        for tmpl in &self.resource_templates {
351            if let Some(args) = match_uri_template(&tmpl.uri_template, uri) {
352                let args_vm = json_to_vm_value(&serde_json::json!(args));
353                let result = vm.call_closure_pub(&tmpl.handler, &[args_vm], &[]).await;
354                return match result {
355                    Ok(value) => {
356                        let mut content = serde_json::json!({
357                            "uri": uri,
358                            "text": value.display(),
359                        });
360                        if let Some(ref mime) = tmpl.mime_type {
361                            content["mimeType"] = serde_json::json!(mime);
362                        }
363                        serde_json::json!({
364                            "jsonrpc": "2.0",
365                            "id": id,
366                            "result": { "contents": [content] }
367                        })
368                    }
369                    Err(e) => serde_json::json!({
370                        "jsonrpc": "2.0",
371                        "id": id,
372                        "error": { "code": -32603, "message": format!("{e}") }
373                    }),
374                };
375            }
376        }
377
378        serde_json::json!({
379            "jsonrpc": "2.0",
380            "id": id,
381            "error": { "code": -32002, "message": format!("Resource not found: {uri}") }
382        })
383    }
384
385    fn handle_resource_templates_list(
386        &self,
387        id: &serde_json::Value,
388        params: &serde_json::Value,
389    ) -> serde_json::Value {
390        let (offset, page_size) = parse_cursor(params);
391        let page_end = (offset + page_size).min(self.resource_templates.len());
392        let templates: Vec<serde_json::Value> = self.resource_templates[offset..page_end]
393            .iter()
394            .map(|t| {
395                let mut entry =
396                    serde_json::json!({ "uriTemplate": t.uri_template, "name": t.name });
397                if let Some(ref title) = t.title {
398                    entry["title"] = serde_json::json!(title);
399                }
400                if let Some(ref desc) = t.description {
401                    entry["description"] = serde_json::json!(desc);
402                }
403                if let Some(ref mime) = t.mime_type {
404                    entry["mimeType"] = serde_json::json!(mime);
405                }
406                entry
407            })
408            .collect();
409
410        let mut result = serde_json::json!({ "resourceTemplates": templates });
411        if page_end < self.resource_templates.len() {
412            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
413        }
414
415        serde_json::json!({
416            "jsonrpc": "2.0",
417            "id": id,
418            "result": result
419        })
420    }
421
422    fn handle_prompts_list(
423        &self,
424        id: &serde_json::Value,
425        params: &serde_json::Value,
426    ) -> serde_json::Value {
427        let (offset, page_size) = parse_cursor(params);
428        let page_end = (offset + page_size).min(self.prompts.len());
429        let prompts: Vec<serde_json::Value> = self.prompts[offset..page_end]
430            .iter()
431            .map(|p| {
432                let mut entry = serde_json::json!({ "name": p.name });
433                if let Some(ref title) = p.title {
434                    entry["title"] = serde_json::json!(title);
435                }
436                if let Some(ref desc) = p.description {
437                    entry["description"] = serde_json::json!(desc);
438                }
439                if let Some(ref args) = p.arguments {
440                    let args_json: Vec<serde_json::Value> = args
441                        .iter()
442                        .map(|a| {
443                            let mut arg =
444                                serde_json::json!({ "name": a.name, "required": a.required });
445                            if let Some(ref desc) = a.description {
446                                arg["description"] = serde_json::json!(desc);
447                            }
448                            arg
449                        })
450                        .collect();
451                    entry["arguments"] = serde_json::json!(args_json);
452                }
453                entry
454            })
455            .collect();
456
457        let mut result = serde_json::json!({ "prompts": prompts });
458        if page_end < self.prompts.len() {
459            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
460        }
461
462        serde_json::json!({
463            "jsonrpc": "2.0",
464            "id": id,
465            "result": result
466        })
467    }
468
469    fn handle_logging_set_level(
470        &self,
471        id: &serde_json::Value,
472        params: &serde_json::Value,
473    ) -> serde_json::Value {
474        let level = params
475            .get("level")
476            .and_then(|l| l.as_str())
477            .unwrap_or("warning");
478        *self.log_level.borrow_mut() = level.to_string();
479        crate::jsonrpc::response(id.clone(), serde_json::json!({}))
480    }
481
482    async fn handle_prompts_get(
483        &self,
484        id: &serde_json::Value,
485        params: &serde_json::Value,
486        vm: &mut Vm,
487    ) -> serde_json::Value {
488        let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
489
490        let prompt = match self.prompts.iter().find(|p| p.name == name) {
491            Some(p) => p,
492            None => {
493                return serde_json::json!({
494                    "jsonrpc": "2.0",
495                    "id": id,
496                    "error": { "code": -32602, "message": format!("Unknown prompt: {name}") }
497                });
498            }
499        };
500
501        let arguments = params
502            .get("arguments")
503            .cloned()
504            .unwrap_or(serde_json::json!({}));
505        let args_vm = json_to_vm_value(&arguments);
506
507        let result = vm.call_closure_pub(&prompt.handler, &[args_vm], &[]).await;
508
509        match result {
510            Ok(value) => {
511                let messages = prompt_value_to_messages(&value);
512                serde_json::json!({
513                    "jsonrpc": "2.0",
514                    "id": id,
515                    "result": { "messages": messages }
516                })
517            }
518            Err(e) => serde_json::json!({
519                "jsonrpc": "2.0",
520                "id": id,
521                "error": { "code": -32603, "message": format!("{e}") }
522            }),
523        }
524    }
525}