Skip to main content

harn_vm/
mcp_server.rs

1//! MCP server mode: expose Harn tools, resources, resource templates, and
2//! prompts as MCP capabilities over stdio.
3//!
4//! This is the mirror of `mcp.rs` (the client). A Harn pipeline registers
5//! capabilities with `mcp_tools()`, `mcp_resource()`, `mcp_resource_template()`,
6//! and `mcp_prompt()`, then the CLI's `mcp-serve` command starts this server,
7//! making them callable by Claude Desktop, Cursor, or any MCP client.
8
9use std::cell::RefCell;
10
11use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
12
13use crate::stdlib::json_to_vm_value;
14use crate::value::{VmClosure, VmError, VmValue};
15use crate::vm::Vm;
16
17thread_local! {
18    /// Stores the tool registry set by `mcp_tools` / `mcp_serve`.
19    static MCP_SERVE_REGISTRY: RefCell<Option<VmValue>> = const { RefCell::new(None) };
20    /// Static resources registered by `mcp_resource`.
21    static MCP_SERVE_RESOURCES: RefCell<Vec<McpResourceDef>> = const { RefCell::new(Vec::new()) };
22    /// Resource templates registered by `mcp_resource_template`.
23    static MCP_SERVE_RESOURCE_TEMPLATES: RefCell<Vec<McpResourceTemplateDef>> = const { RefCell::new(Vec::new()) };
24    /// Prompts registered by `mcp_prompt`.
25    static MCP_SERVE_PROMPTS: RefCell<Vec<McpPromptDef>> = const { RefCell::new(Vec::new()) };
26}
27
28/// Register all MCP server builtins on a VM.
29pub fn register_mcp_server_builtins(vm: &mut Vm) {
30    fn register_tools_impl(args: &[VmValue]) -> Result<VmValue, VmError> {
31        let registry = args.first().cloned().ok_or_else(|| {
32            VmError::Runtime("mcp_tools: requires a tool_registry argument".into())
33        })?;
34        if let VmValue::Dict(d) = &registry {
35            match d.get("_type") {
36                Some(VmValue::String(t)) if &**t == "tool_registry" => {}
37                _ => {
38                    return Err(VmError::Runtime(
39                        "mcp_tools: argument must be a tool registry (created with tool_registry())"
40                            .into(),
41                    ));
42                }
43            }
44        } else {
45            return Err(VmError::Runtime(
46                "mcp_tools: argument must be a tool registry".into(),
47            ));
48        }
49        MCP_SERVE_REGISTRY.with(|cell| {
50            *cell.borrow_mut() = Some(registry);
51        });
52        Ok(VmValue::Nil)
53    }
54
55    vm.register_builtin("mcp_tools", |args, _out| register_tools_impl(args));
56    // `mcp_serve` is the old name; kept as an alias.
57    vm.register_builtin("mcp_serve", |args, _out| register_tools_impl(args));
58
59    // mcp_resource({uri, name, text, description?, mime_type?}) -> nil
60    vm.register_builtin("mcp_resource", |args, _out| {
61        let dict = match args.first() {
62            Some(VmValue::Dict(d)) => d,
63            _ => {
64                return Err(VmError::Runtime(
65                    "mcp_resource: argument must be a dict with {uri, name, text}".into(),
66                ));
67            }
68        };
69
70        let uri = dict
71            .get("uri")
72            .map(|v| v.display())
73            .ok_or_else(|| VmError::Runtime("mcp_resource: 'uri' is required".into()))?;
74        let name = dict
75            .get("name")
76            .map(|v| v.display())
77            .ok_or_else(|| VmError::Runtime("mcp_resource: 'name' is required".into()))?;
78        let title = dict.get("title").map(|v| v.display());
79        let description = dict.get("description").map(|v| v.display());
80        let mime_type = dict.get("mime_type").map(|v| v.display());
81        let text = dict
82            .get("text")
83            .map(|v| v.display())
84            .ok_or_else(|| VmError::Runtime("mcp_resource: 'text' is required".into()))?;
85
86        MCP_SERVE_RESOURCES.with(|cell| {
87            cell.borrow_mut().push(McpResourceDef {
88                uri,
89                name,
90                title,
91                description,
92                mime_type,
93                text,
94            });
95        });
96
97        Ok(VmValue::Nil)
98    });
99
100    // mcp_resource_template({uri_template, name, handler, description?, mime_type?}) -> nil
101    // The handler receives a dict of URI template arguments and returns a string.
102    vm.register_builtin("mcp_resource_template", |args, _out| {
103        let dict = match args.first() {
104            Some(VmValue::Dict(d)) => d,
105            _ => {
106                return Err(VmError::Runtime(
107                    "mcp_resource_template: argument must be a dict".into(),
108                ));
109            }
110        };
111
112        let uri_template = dict
113            .get("uri_template")
114            .map(|v| v.display())
115            .ok_or_else(|| {
116                VmError::Runtime("mcp_resource_template: 'uri_template' is required".into())
117            })?;
118        let name = dict
119            .get("name")
120            .map(|v| v.display())
121            .ok_or_else(|| VmError::Runtime("mcp_resource_template: 'name' is required".into()))?;
122        let title = dict.get("title").map(|v| v.display());
123        let description = dict.get("description").map(|v| v.display());
124        let mime_type = dict.get("mime_type").map(|v| v.display());
125        let handler = match dict.get("handler") {
126            Some(VmValue::Closure(c)) => (**c).clone(),
127            _ => {
128                return Err(VmError::Runtime(
129                    "mcp_resource_template: 'handler' closure is required".into(),
130                ));
131            }
132        };
133
134        MCP_SERVE_RESOURCE_TEMPLATES.with(|cell| {
135            cell.borrow_mut().push(McpResourceTemplateDef {
136                uri_template,
137                name,
138                title,
139                description,
140                mime_type,
141                handler,
142            });
143        });
144
145        Ok(VmValue::Nil)
146    });
147
148    // mcp_prompt({name, handler, description?, arguments?}) -> nil
149    vm.register_builtin("mcp_prompt", |args, _out| {
150        let dict = match args.first() {
151            Some(VmValue::Dict(d)) => d,
152            _ => {
153                return Err(VmError::Runtime(
154                    "mcp_prompt: argument must be a dict with {name, handler}".into(),
155                ));
156            }
157        };
158
159        let name = dict
160            .get("name")
161            .map(|v| v.display())
162            .ok_or_else(|| VmError::Runtime("mcp_prompt: 'name' is required".into()))?;
163        let title = dict.get("title").map(|v| v.display());
164        let description = dict.get("description").map(|v| v.display());
165
166        let handler = match dict.get("handler") {
167            Some(VmValue::Closure(c)) => (**c).clone(),
168            _ => {
169                return Err(VmError::Runtime(
170                    "mcp_prompt: 'handler' closure is required".into(),
171                ));
172            }
173        };
174
175        let arguments = dict.get("arguments").and_then(|v| {
176            if let VmValue::List(list) = v {
177                let args: Vec<McpPromptArgDef> = list
178                    .iter()
179                    .filter_map(|item| {
180                        if let VmValue::Dict(d) = item {
181                            Some(McpPromptArgDef {
182                                name: d.get("name").map(|v| v.display()).unwrap_or_default(),
183                                description: d.get("description").map(|v| v.display()),
184                                required: matches!(d.get("required"), Some(VmValue::Bool(true))),
185                            })
186                        } else {
187                            None
188                        }
189                    })
190                    .collect();
191                if args.is_empty() {
192                    None
193                } else {
194                    Some(args)
195                }
196            } else {
197                None
198            }
199        });
200
201        MCP_SERVE_PROMPTS.with(|cell| {
202            cell.borrow_mut().push(McpPromptDef {
203                name,
204                title,
205                description,
206                arguments,
207                handler,
208            });
209        });
210
211        Ok(VmValue::Nil)
212    });
213}
214
215// Thread-local accessors used by the CLI after pipeline execution.
216
217pub fn take_mcp_serve_registry() -> Option<VmValue> {
218    MCP_SERVE_REGISTRY.with(|cell| cell.borrow_mut().take())
219}
220
221pub fn take_mcp_serve_resources() -> Vec<McpResourceDef> {
222    MCP_SERVE_RESOURCES.with(|cell| cell.borrow_mut().drain(..).collect())
223}
224
225pub fn take_mcp_serve_resource_templates() -> Vec<McpResourceTemplateDef> {
226    MCP_SERVE_RESOURCE_TEMPLATES.with(|cell| cell.borrow_mut().drain(..).collect())
227}
228
229pub fn take_mcp_serve_prompts() -> Vec<McpPromptDef> {
230    MCP_SERVE_PROMPTS.with(|cell| cell.borrow_mut().drain(..).collect())
231}
232
233/// MCP protocol version.
234const PROTOCOL_VERSION: &str = "2025-11-25";
235
236/// Default page size for cursor-based pagination.
237const DEFAULT_PAGE_SIZE: usize = 50;
238
239/// A tool extracted from a Harn tool_registry, ready to serve over MCP.
240pub struct McpToolDef {
241    pub name: String,
242    pub title: Option<String>,
243    pub description: String,
244    pub input_schema: serde_json::Value,
245    pub output_schema: Option<serde_json::Value>,
246    pub annotations: Option<serde_json::Value>,
247    pub handler: VmClosure,
248}
249
250/// A static resource to serve over MCP.
251pub struct McpResourceDef {
252    pub uri: String,
253    pub name: String,
254    pub title: Option<String>,
255    pub description: Option<String>,
256    pub mime_type: Option<String>,
257    pub text: String,
258}
259
260/// A parameterized resource template (RFC 6570 URI template).
261pub struct McpResourceTemplateDef {
262    pub uri_template: String,
263    pub name: String,
264    pub title: Option<String>,
265    pub description: Option<String>,
266    pub mime_type: Option<String>,
267    pub handler: VmClosure,
268}
269
270/// A prompt argument definition.
271pub struct McpPromptArgDef {
272    pub name: String,
273    pub description: Option<String>,
274    pub required: bool,
275}
276
277/// A prompt template to serve over MCP.
278pub struct McpPromptDef {
279    pub name: String,
280    pub title: Option<String>,
281    pub description: Option<String>,
282    pub arguments: Option<Vec<McpPromptArgDef>>,
283    pub handler: VmClosure,
284}
285
286/// MCP server that exposes Harn tools, resources, and prompts over stdio JSON-RPC.
287pub struct McpServer {
288    server_name: String,
289    server_version: String,
290    tools: Vec<McpToolDef>,
291    resources: Vec<McpResourceDef>,
292    resource_templates: Vec<McpResourceTemplateDef>,
293    prompts: Vec<McpPromptDef>,
294    log_level: RefCell<String>,
295}
296
297impl McpServer {
298    pub fn new(
299        server_name: String,
300        tools: Vec<McpToolDef>,
301        resources: Vec<McpResourceDef>,
302        resource_templates: Vec<McpResourceTemplateDef>,
303        prompts: Vec<McpPromptDef>,
304    ) -> Self {
305        Self {
306            server_name,
307            server_version: env!("CARGO_PKG_VERSION").to_string(),
308            tools,
309            resources,
310            resource_templates,
311            prompts,
312            log_level: RefCell::new("warning".to_string()),
313        }
314    }
315
316    /// Run the MCP server loop, reading JSON-RPC from stdin and writing to stdout.
317    pub async fn run(&self, vm: &mut Vm) -> Result<(), VmError> {
318        let stdin = BufReader::new(tokio::io::stdin());
319        let mut stdout = tokio::io::stdout();
320        let mut lines = stdin.lines();
321
322        while let Ok(Some(line)) = lines.next_line().await {
323            let trimmed = line.trim();
324            if trimmed.is_empty() {
325                continue;
326            }
327
328            let msg: serde_json::Value = match serde_json::from_str(trimmed) {
329                Ok(v) => v,
330                Err(_) => continue,
331            };
332
333            let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
334            let id = msg.get("id").cloned();
335            let params = msg.get("params").cloned().unwrap_or(serde_json::json!({}));
336
337            // Notifications have no id; ignore them silently.
338            if id.is_none() {
339                continue;
340            }
341            let id = id.unwrap();
342
343            let response = match method {
344                "initialize" => self.handle_initialize(&id),
345                "ping" => crate::jsonrpc::response(id.clone(), serde_json::json!({})),
346                "logging/setLevel" => self.handle_logging_set_level(&id, &params),
347                "tools/list" => self.handle_tools_list(&id, &params),
348                "tools/call" => self.handle_tools_call(&id, &params, vm).await,
349                "resources/list" => self.handle_resources_list(&id, &params),
350                "resources/read" => self.handle_resources_read(&id, &params, vm).await,
351                "resources/templates/list" => self.handle_resource_templates_list(&id, &params),
352                "prompts/list" => self.handle_prompts_list(&id, &params),
353                "prompts/get" => self.handle_prompts_get(&id, &params, vm).await,
354                _ => serde_json::json!({
355                    "jsonrpc": "2.0",
356                    "id": id,
357                    "error": {
358                        "code": -32601,
359                        "message": format!("Method not found: {method}")
360                    }
361                }),
362            };
363
364            let mut response_line = serde_json::to_string(&response)
365                .map_err(|e| VmError::Runtime(format!("MCP server serialization error: {e}")))?;
366            response_line.push('\n');
367            stdout
368                .write_all(response_line.as_bytes())
369                .await
370                .map_err(|e| VmError::Runtime(format!("MCP server write error: {e}")))?;
371            stdout
372                .flush()
373                .await
374                .map_err(|e| VmError::Runtime(format!("MCP server flush error: {e}")))?;
375        }
376
377        Ok(())
378    }
379
380    fn handle_initialize(&self, id: &serde_json::Value) -> serde_json::Value {
381        let mut capabilities = serde_json::Map::new();
382        if !self.tools.is_empty() {
383            capabilities.insert("tools".into(), serde_json::json!({}));
384        }
385        if !self.resources.is_empty() || !self.resource_templates.is_empty() {
386            capabilities.insert("resources".into(), serde_json::json!({}));
387        }
388        if !self.prompts.is_empty() {
389            capabilities.insert("prompts".into(), serde_json::json!({}));
390        }
391        capabilities.insert("logging".into(), serde_json::json!({}));
392
393        serde_json::json!({
394            "jsonrpc": "2.0",
395            "id": id,
396            "result": {
397                "protocolVersion": PROTOCOL_VERSION,
398                "capabilities": capabilities,
399                "serverInfo": {
400                    "name": self.server_name,
401                    "version": self.server_version
402                }
403            }
404        })
405    }
406
407    fn handle_tools_list(
408        &self,
409        id: &serde_json::Value,
410        params: &serde_json::Value,
411    ) -> serde_json::Value {
412        let (offset, page_size) = parse_cursor(params);
413        let page_end = (offset + page_size).min(self.tools.len());
414        let tools: Vec<serde_json::Value> = self.tools[offset..page_end]
415            .iter()
416            .map(|t| {
417                let mut entry = serde_json::json!({
418                    "name": t.name,
419                    "description": t.description,
420                    "inputSchema": t.input_schema,
421                });
422                if let Some(ref title) = t.title {
423                    entry["title"] = serde_json::json!(title);
424                }
425                if let Some(ref output_schema) = t.output_schema {
426                    entry["outputSchema"] = output_schema.clone();
427                }
428                if let Some(ref annotations) = t.annotations {
429                    entry["annotations"] = annotations.clone();
430                }
431                entry
432            })
433            .collect();
434
435        let mut result = serde_json::json!({ "tools": tools });
436        if page_end < self.tools.len() {
437            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
438        }
439
440        serde_json::json!({
441            "jsonrpc": "2.0",
442            "id": id,
443            "result": result
444        })
445    }
446
447    async fn handle_tools_call(
448        &self,
449        id: &serde_json::Value,
450        params: &serde_json::Value,
451        vm: &mut Vm,
452    ) -> serde_json::Value {
453        let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
454
455        let tool = match self.tools.iter().find(|t| t.name == tool_name) {
456            Some(t) => t,
457            None => {
458                return serde_json::json!({
459                    "jsonrpc": "2.0",
460                    "id": id,
461                    "error": { "code": -32602, "message": format!("Unknown tool: {tool_name}") }
462                });
463            }
464        };
465
466        let arguments = params
467            .get("arguments")
468            .cloned()
469            .unwrap_or(serde_json::json!({}));
470        let args_vm = json_to_vm_value(&arguments);
471
472        let result = vm.call_closure_pub(&tool.handler, &[args_vm], &[]).await;
473
474        match result {
475            Ok(value) => {
476                let content = vm_value_to_content(&value);
477                let mut call_result = serde_json::json!({
478                    "content": content,
479                    "isError": false
480                });
481                if tool.output_schema.is_some() {
482                    let text = value.display();
483                    let structured = match serde_json::from_str::<serde_json::Value>(&text) {
484                        Ok(v) => v,
485                        _ => serde_json::json!(text),
486                    };
487                    call_result["structuredContent"] = structured;
488                }
489                serde_json::json!({
490                    "jsonrpc": "2.0",
491                    "id": id,
492                    "result": call_result
493                })
494            }
495            Err(e) => serde_json::json!({
496                "jsonrpc": "2.0",
497                "id": id,
498                "result": {
499                    "content": [{ "type": "text", "text": format!("{e}") }],
500                    "isError": true
501                }
502            }),
503        }
504    }
505
506    fn handle_resources_list(
507        &self,
508        id: &serde_json::Value,
509        params: &serde_json::Value,
510    ) -> serde_json::Value {
511        let (offset, page_size) = parse_cursor(params);
512        let page_end = (offset + page_size).min(self.resources.len());
513        let resources: Vec<serde_json::Value> = self.resources[offset..page_end]
514            .iter()
515            .map(|r| {
516                let mut entry = serde_json::json!({ "uri": r.uri, "name": r.name });
517                if let Some(ref title) = r.title {
518                    entry["title"] = serde_json::json!(title);
519                }
520                if let Some(ref desc) = r.description {
521                    entry["description"] = serde_json::json!(desc);
522                }
523                if let Some(ref mime) = r.mime_type {
524                    entry["mimeType"] = serde_json::json!(mime);
525                }
526                entry
527            })
528            .collect();
529
530        let mut result = serde_json::json!({ "resources": resources });
531        if page_end < self.resources.len() {
532            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
533        }
534
535        serde_json::json!({
536            "jsonrpc": "2.0",
537            "id": id,
538            "result": result
539        })
540    }
541
542    async fn handle_resources_read(
543        &self,
544        id: &serde_json::Value,
545        params: &serde_json::Value,
546        vm: &mut Vm,
547    ) -> serde_json::Value {
548        let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
549
550        // Static resources take precedence over templates.
551        if let Some(resource) = self.resources.iter().find(|r| r.uri == uri) {
552            let mut content = serde_json::json!({ "uri": resource.uri, "text": resource.text });
553            if let Some(ref mime) = resource.mime_type {
554                content["mimeType"] = serde_json::json!(mime);
555            }
556            return serde_json::json!({
557                "jsonrpc": "2.0",
558                "id": id,
559                "result": { "contents": [content] }
560            });
561        }
562
563        for tmpl in &self.resource_templates {
564            if let Some(args) = match_uri_template(&tmpl.uri_template, uri) {
565                let args_vm = json_to_vm_value(&serde_json::json!(args));
566                let result = vm.call_closure_pub(&tmpl.handler, &[args_vm], &[]).await;
567                return match result {
568                    Ok(value) => {
569                        let mut content = serde_json::json!({
570                            "uri": uri,
571                            "text": value.display(),
572                        });
573                        if let Some(ref mime) = tmpl.mime_type {
574                            content["mimeType"] = serde_json::json!(mime);
575                        }
576                        serde_json::json!({
577                            "jsonrpc": "2.0",
578                            "id": id,
579                            "result": { "contents": [content] }
580                        })
581                    }
582                    Err(e) => serde_json::json!({
583                        "jsonrpc": "2.0",
584                        "id": id,
585                        "error": { "code": -32603, "message": format!("{e}") }
586                    }),
587                };
588            }
589        }
590
591        serde_json::json!({
592            "jsonrpc": "2.0",
593            "id": id,
594            "error": { "code": -32002, "message": format!("Resource not found: {uri}") }
595        })
596    }
597
598    fn handle_resource_templates_list(
599        &self,
600        id: &serde_json::Value,
601        params: &serde_json::Value,
602    ) -> serde_json::Value {
603        let (offset, page_size) = parse_cursor(params);
604        let page_end = (offset + page_size).min(self.resource_templates.len());
605        let templates: Vec<serde_json::Value> = self.resource_templates[offset..page_end]
606            .iter()
607            .map(|t| {
608                let mut entry =
609                    serde_json::json!({ "uriTemplate": t.uri_template, "name": t.name });
610                if let Some(ref title) = t.title {
611                    entry["title"] = serde_json::json!(title);
612                }
613                if let Some(ref desc) = t.description {
614                    entry["description"] = serde_json::json!(desc);
615                }
616                if let Some(ref mime) = t.mime_type {
617                    entry["mimeType"] = serde_json::json!(mime);
618                }
619                entry
620            })
621            .collect();
622
623        let mut result = serde_json::json!({ "resourceTemplates": templates });
624        if page_end < self.resource_templates.len() {
625            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
626        }
627
628        serde_json::json!({
629            "jsonrpc": "2.0",
630            "id": id,
631            "result": result
632        })
633    }
634
635    fn handle_prompts_list(
636        &self,
637        id: &serde_json::Value,
638        params: &serde_json::Value,
639    ) -> serde_json::Value {
640        let (offset, page_size) = parse_cursor(params);
641        let page_end = (offset + page_size).min(self.prompts.len());
642        let prompts: Vec<serde_json::Value> = self.prompts[offset..page_end]
643            .iter()
644            .map(|p| {
645                let mut entry = serde_json::json!({ "name": p.name });
646                if let Some(ref title) = p.title {
647                    entry["title"] = serde_json::json!(title);
648                }
649                if let Some(ref desc) = p.description {
650                    entry["description"] = serde_json::json!(desc);
651                }
652                if let Some(ref args) = p.arguments {
653                    let args_json: Vec<serde_json::Value> = args
654                        .iter()
655                        .map(|a| {
656                            let mut arg =
657                                serde_json::json!({ "name": a.name, "required": a.required });
658                            if let Some(ref desc) = a.description {
659                                arg["description"] = serde_json::json!(desc);
660                            }
661                            arg
662                        })
663                        .collect();
664                    entry["arguments"] = serde_json::json!(args_json);
665                }
666                entry
667            })
668            .collect();
669
670        let mut result = serde_json::json!({ "prompts": prompts });
671        if page_end < self.prompts.len() {
672            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
673        }
674
675        serde_json::json!({
676            "jsonrpc": "2.0",
677            "id": id,
678            "result": result
679        })
680    }
681
682    fn handle_logging_set_level(
683        &self,
684        id: &serde_json::Value,
685        params: &serde_json::Value,
686    ) -> serde_json::Value {
687        let level = params
688            .get("level")
689            .and_then(|l| l.as_str())
690            .unwrap_or("warning");
691        *self.log_level.borrow_mut() = level.to_string();
692        crate::jsonrpc::response(id.clone(), serde_json::json!({}))
693    }
694
695    async fn handle_prompts_get(
696        &self,
697        id: &serde_json::Value,
698        params: &serde_json::Value,
699        vm: &mut Vm,
700    ) -> serde_json::Value {
701        let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
702
703        let prompt = match self.prompts.iter().find(|p| p.name == name) {
704            Some(p) => p,
705            None => {
706                return serde_json::json!({
707                    "jsonrpc": "2.0",
708                    "id": id,
709                    "error": { "code": -32602, "message": format!("Unknown prompt: {name}") }
710                });
711            }
712        };
713
714        let arguments = params
715            .get("arguments")
716            .cloned()
717            .unwrap_or(serde_json::json!({}));
718        let args_vm = json_to_vm_value(&arguments);
719
720        let result = vm.call_closure_pub(&prompt.handler, &[args_vm], &[]).await;
721
722        match result {
723            Ok(value) => {
724                let messages = prompt_value_to_messages(&value);
725                serde_json::json!({
726                    "jsonrpc": "2.0",
727                    "id": id,
728                    "result": { "messages": messages }
729                })
730            }
731            Err(e) => serde_json::json!({
732                "jsonrpc": "2.0",
733                "id": id,
734                "error": { "code": -32603, "message": format!("{e}") }
735            }),
736        }
737    }
738}
739
740/// Encode an offset as a base64 cursor string.
741fn encode_cursor(offset: usize) -> String {
742    use base64::Engine;
743    base64::engine::general_purpose::STANDARD.encode(offset.to_string().as_bytes())
744}
745
746/// Decode a cursor from the request params, returning `(offset, page_size)`.
747fn parse_cursor(params: &serde_json::Value) -> (usize, usize) {
748    let offset = params
749        .get("cursor")
750        .and_then(|c| c.as_str())
751        .and_then(|c| {
752            use base64::Engine;
753            let bytes = base64::engine::general_purpose::STANDARD.decode(c).ok()?;
754            let s = String::from_utf8(bytes).ok()?;
755            s.parse::<usize>().ok()
756        })
757        .unwrap_or(0);
758    (offset, DEFAULT_PAGE_SIZE)
759}
760
761/// Convert a VmValue returned by a prompt handler into MCP messages.
762fn prompt_value_to_messages(value: &VmValue) -> Vec<serde_json::Value> {
763    match value {
764        VmValue::String(s) => {
765            vec![serde_json::json!({
766                "role": "user",
767                "content": { "type": "text", "text": &**s }
768            })]
769        }
770        VmValue::List(items) => items
771            .iter()
772            .map(|item| {
773                if let VmValue::Dict(d) = item {
774                    let role = d
775                        .get("role")
776                        .map(|v| v.display())
777                        .unwrap_or_else(|| "user".into());
778                    let content = d.get("content").map(|v| v.display()).unwrap_or_default();
779                    serde_json::json!({
780                        "role": role,
781                        "content": { "type": "text", "text": content }
782                    })
783                } else {
784                    serde_json::json!({
785                        "role": "user",
786                        "content": { "type": "text", "text": item.display() }
787                    })
788                }
789            })
790            .collect(),
791        _ => {
792            vec![serde_json::json!({
793                "role": "user",
794                "content": { "type": "text", "text": value.display() }
795            })]
796        }
797    }
798}
799
800/// Simple URI template matching (RFC 6570 Level 1 only).
801///
802/// Matches a URI against a template like `file:///{path}` and extracts named
803/// variables. Returns `None` if the URI doesn't match the template structure.
804fn match_uri_template(
805    template: &str,
806    uri: &str,
807) -> Option<std::collections::HashMap<String, String>> {
808    let mut vars = std::collections::HashMap::new();
809    let mut t_pos = 0;
810    let mut u_pos = 0;
811    let t_bytes = template.as_bytes();
812    let u_bytes = uri.as_bytes();
813
814    while t_pos < t_bytes.len() {
815        if t_bytes[t_pos] == b'{' {
816            // Find the closing brace
817            let close = template[t_pos..].find('}')? + t_pos;
818            let var_name = &template[t_pos + 1..close];
819            t_pos = close + 1;
820
821            // Capture everything up to the next literal in the template (or end)
822            let next_literal = if t_pos < t_bytes.len() {
823                // Find how much literal follows
824                let lit_start = t_pos;
825                let lit_end = template[t_pos..]
826                    .find('{')
827                    .map(|i| t_pos + i)
828                    .unwrap_or(t_bytes.len());
829                Some(&template[lit_start..lit_end])
830            } else {
831                None
832            };
833
834            let value_end = match next_literal {
835                Some(lit) if !lit.is_empty() => uri[u_pos..].find(lit).map(|i| u_pos + i)?,
836                _ => u_bytes.len(),
837            };
838
839            vars.insert(var_name.to_string(), uri[u_pos..value_end].to_string());
840            u_pos = value_end;
841        } else {
842            // Literal character must match
843            if u_pos >= u_bytes.len() || t_bytes[t_pos] != u_bytes[u_pos] {
844                return None;
845            }
846            t_pos += 1;
847            u_pos += 1;
848        }
849    }
850
851    if u_pos == u_bytes.len() {
852        Some(vars)
853    } else {
854        None
855    }
856}
857
858/// Convert a tool result VmValue into MCP content items.
859///
860/// Supports text, embedded resource, and resource_link content types.
861/// If the value is a list of dicts with a `type` field, each is treated as a
862/// content item. Otherwise, the whole value is serialized as a single text item.
863fn vm_value_to_content(value: &VmValue) -> Vec<serde_json::Value> {
864    if let VmValue::List(items) = value {
865        let mut content = Vec::new();
866        for item in items.iter() {
867            if let VmValue::Dict(d) = item {
868                let item_type = d.get("type").map(|v| v.display()).unwrap_or_default();
869                match item_type.as_str() {
870                    "resource" => {
871                        let mut entry = serde_json::json!({ "type": "resource" });
872                        if let Some(resource) = d.get("resource") {
873                            entry["resource"] = vm_value_to_json(resource);
874                        }
875                        content.push(entry);
876                    }
877                    "resource_link" => {
878                        let mut entry = serde_json::json!({ "type": "resource_link" });
879                        if let Some(uri) = d.get("uri") {
880                            entry["uri"] = serde_json::json!(uri.display());
881                        }
882                        if let Some(name) = d.get("name") {
883                            entry["name"] = serde_json::json!(name.display());
884                        }
885                        if let Some(desc) = d.get("description") {
886                            entry["description"] = serde_json::json!(desc.display());
887                        }
888                        if let Some(mime) = d.get("mimeType") {
889                            entry["mimeType"] = serde_json::json!(mime.display());
890                        }
891                        content.push(entry);
892                    }
893                    _ => {
894                        let text = d
895                            .get("text")
896                            .map(|v| v.display())
897                            .unwrap_or_else(|| item.display());
898                        content.push(serde_json::json!({ "type": "text", "text": text }));
899                    }
900                }
901            } else {
902                content.push(serde_json::json!({ "type": "text", "text": item.display() }));
903            }
904        }
905        if content.is_empty() {
906            vec![serde_json::json!({ "type": "text", "text": value.display() })]
907        } else {
908            content
909        }
910    } else {
911        vec![serde_json::json!({ "type": "text", "text": value.display() })]
912    }
913}
914
915/// Convert a VmValue to a serde_json::Value.
916fn vm_value_to_json(value: &VmValue) -> serde_json::Value {
917    match value {
918        VmValue::Nil => serde_json::Value::Null,
919        VmValue::Bool(b) => serde_json::json!(b),
920        VmValue::Int(n) => serde_json::json!(n),
921        VmValue::Float(f) => serde_json::json!(f),
922        VmValue::String(s) => serde_json::json!(&**s),
923        VmValue::List(items) => {
924            serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
925        }
926        VmValue::Dict(d) => {
927            let mut map = serde_json::Map::new();
928            for (k, v) in d.iter() {
929                map.insert(k.clone(), vm_value_to_json(v));
930            }
931            serde_json::Value::Object(map)
932        }
933        _ => serde_json::json!(value.display()),
934    }
935}
936
937/// Convert a VmValue annotations dict to a serde_json::Value with only the
938/// recognized MCP annotation fields.
939fn annotations_to_json(annotations: &VmValue) -> Option<serde_json::Value> {
940    let dict = match annotations {
941        VmValue::Dict(d) => d,
942        _ => return None,
943    };
944
945    let mut out = serde_json::Map::new();
946    let str_keys = ["title"];
947    let bool_keys = [
948        "readOnlyHint",
949        "destructiveHint",
950        "idempotentHint",
951        "openWorldHint",
952    ];
953
954    for key in str_keys {
955        if let Some(VmValue::String(s)) = dict.get(key) {
956            out.insert(key.into(), serde_json::json!(&**s));
957        }
958    }
959    for key in bool_keys {
960        if let Some(VmValue::Bool(b)) = dict.get(key) {
961            out.insert(key.into(), serde_json::json!(b));
962        }
963    }
964
965    if out.is_empty() {
966        None
967    } else {
968        Some(serde_json::Value::Object(out))
969    }
970}
971
972/// Extract tools from a Harn tool_registry VmValue and convert to MCP tool definitions.
973pub fn tool_registry_to_mcp_tools(registry: &VmValue) -> Result<Vec<McpToolDef>, VmError> {
974    let dict = match registry {
975        VmValue::Dict(d) => d,
976        _ => {
977            return Err(VmError::Runtime(
978                "mcp_tools: argument must be a tool registry".into(),
979            ));
980        }
981    };
982
983    match dict.get("_type") {
984        Some(VmValue::String(t)) if &**t == "tool_registry" => {}
985        _ => {
986            return Err(VmError::Runtime(
987                "mcp_tools: argument must be a tool registry (created with tool_registry())".into(),
988            ));
989        }
990    }
991
992    let tools = match dict.get("tools") {
993        Some(VmValue::List(list)) => list,
994        _ => return Ok(Vec::new()),
995    };
996
997    let mut mcp_tools = Vec::new();
998    for tool in tools.iter() {
999        if let VmValue::Dict(entry) = tool {
1000            let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1001            let title = entry.get("title").map(|v| v.display());
1002            let description = entry
1003                .get("description")
1004                .map(|v| v.display())
1005                .unwrap_or_default();
1006
1007            let handler = match entry.get("handler") {
1008                Some(VmValue::Closure(c)) => (**c).clone(),
1009                _ => {
1010                    return Err(VmError::Runtime(format!(
1011                        "mcp_tools: tool '{name}' has no handler closure"
1012                    )));
1013                }
1014            };
1015
1016            let input_schema = params_to_json_schema(entry.get("parameters"));
1017            let output_schema = entry.get("output_schema").and_then(|v| {
1018                if let VmValue::Dict(_) = v {
1019                    Some(vm_value_to_json(v))
1020                } else {
1021                    None
1022                }
1023            });
1024            let annotations = entry.get("annotations").and_then(annotations_to_json);
1025
1026            mcp_tools.push(McpToolDef {
1027                name,
1028                title,
1029                description,
1030                input_schema,
1031                output_schema,
1032                annotations,
1033                handler,
1034            });
1035        }
1036    }
1037
1038    Ok(mcp_tools)
1039}
1040
1041/// Convert Harn tool_define parameter definitions to JSON Schema for MCP inputSchema.
1042fn params_to_json_schema(params: Option<&VmValue>) -> serde_json::Value {
1043    let params_dict = match params {
1044        Some(VmValue::Dict(d)) => d,
1045        _ => {
1046            return serde_json::json!({ "type": "object", "properties": {} });
1047        }
1048    };
1049
1050    let mut properties = serde_json::Map::new();
1051    let mut required = Vec::new();
1052
1053    for (param_name, param_def) in params_dict.iter() {
1054        if let VmValue::Dict(def) = param_def {
1055            let mut prop = serde_json::Map::new();
1056            if let Some(VmValue::String(t)) = def.get("type") {
1057                prop.insert("type".into(), serde_json::Value::String(t.to_string()));
1058            }
1059            if let Some(VmValue::String(d)) = def.get("description") {
1060                prop.insert(
1061                    "description".into(),
1062                    serde_json::Value::String(d.to_string()),
1063                );
1064            }
1065            if matches!(def.get("required"), Some(VmValue::Bool(true))) {
1066                required.push(serde_json::Value::String(param_name.clone()));
1067            }
1068            properties.insert(param_name.clone(), serde_json::Value::Object(prop));
1069        } else if let VmValue::String(type_str) = param_def {
1070            let mut prop = serde_json::Map::new();
1071            prop.insert(
1072                "type".into(),
1073                serde_json::Value::String(type_str.to_string()),
1074            );
1075            properties.insert(param_name.clone(), serde_json::Value::Object(prop));
1076        }
1077    }
1078
1079    let mut schema = serde_json::Map::new();
1080    schema.insert("type".into(), serde_json::Value::String("object".into()));
1081    schema.insert("properties".into(), serde_json::Value::Object(properties));
1082    if !required.is_empty() {
1083        schema.insert("required".into(), serde_json::Value::Array(required));
1084    }
1085    serde_json::Value::Object(schema)
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091    use std::collections::BTreeMap;
1092    use std::rc::Rc;
1093
1094    #[test]
1095    fn test_params_to_json_schema_empty() {
1096        let schema = params_to_json_schema(None);
1097        assert_eq!(
1098            schema,
1099            serde_json::json!({ "type": "object", "properties": {} })
1100        );
1101    }
1102
1103    #[test]
1104    fn test_params_to_json_schema_with_params() {
1105        let mut params = BTreeMap::new();
1106        let mut param_def = BTreeMap::new();
1107        param_def.insert("type".to_string(), VmValue::String(Rc::from("string")));
1108        param_def.insert(
1109            "description".to_string(),
1110            VmValue::String(Rc::from("A file path")),
1111        );
1112        param_def.insert("required".to_string(), VmValue::Bool(true));
1113        params.insert("path".to_string(), VmValue::Dict(Rc::new(param_def)));
1114
1115        let schema = params_to_json_schema(Some(&VmValue::Dict(Rc::new(params))));
1116        assert_eq!(
1117            schema,
1118            serde_json::json!({
1119                "type": "object",
1120                "properties": { "path": { "type": "string", "description": "A file path" } },
1121                "required": ["path"]
1122            })
1123        );
1124    }
1125
1126    #[test]
1127    fn test_params_to_json_schema_simple_form() {
1128        let mut params = BTreeMap::new();
1129        params.insert("query".to_string(), VmValue::String(Rc::from("string")));
1130        let schema = params_to_json_schema(Some(&VmValue::Dict(Rc::new(params))));
1131        assert_eq!(
1132            schema["properties"]["query"]["type"],
1133            serde_json::json!("string")
1134        );
1135    }
1136
1137    #[test]
1138    fn test_tool_registry_to_mcp_tools_invalid() {
1139        assert!(tool_registry_to_mcp_tools(&VmValue::Nil).is_err());
1140    }
1141
1142    #[test]
1143    fn test_tool_registry_to_mcp_tools_empty() {
1144        let mut registry = BTreeMap::new();
1145        registry.insert("_type".into(), VmValue::String(Rc::from("tool_registry")));
1146        registry.insert("tools".into(), VmValue::List(Rc::new(Vec::new())));
1147        let result = tool_registry_to_mcp_tools(&VmValue::Dict(Rc::new(registry)));
1148        assert!(result.unwrap().is_empty());
1149    }
1150
1151    #[test]
1152    fn test_prompt_value_to_messages_string() {
1153        let msgs = prompt_value_to_messages(&VmValue::String(Rc::from("hello")));
1154        assert_eq!(msgs.len(), 1);
1155        assert_eq!(msgs[0]["role"], "user");
1156        assert_eq!(msgs[0]["content"]["text"], "hello");
1157    }
1158
1159    #[test]
1160    fn test_prompt_value_to_messages_list() {
1161        let items = vec![
1162            VmValue::Dict(Rc::new({
1163                let mut d = BTreeMap::new();
1164                d.insert("role".into(), VmValue::String(Rc::from("user")));
1165                d.insert("content".into(), VmValue::String(Rc::from("hi")));
1166                d
1167            })),
1168            VmValue::Dict(Rc::new({
1169                let mut d = BTreeMap::new();
1170                d.insert("role".into(), VmValue::String(Rc::from("assistant")));
1171                d.insert("content".into(), VmValue::String(Rc::from("hello")));
1172                d
1173            })),
1174        ];
1175        let msgs = prompt_value_to_messages(&VmValue::List(Rc::new(items)));
1176        assert_eq!(msgs.len(), 2);
1177        assert_eq!(msgs[1]["role"], "assistant");
1178    }
1179
1180    #[test]
1181    fn test_match_uri_template_simple() {
1182        let vars = match_uri_template("file:///{path}", "file:///foo/bar.rs").unwrap();
1183        assert_eq!(vars["path"], "foo/bar.rs");
1184    }
1185
1186    #[test]
1187    fn test_match_uri_template_multiple() {
1188        let vars = match_uri_template("db://{schema}/{table}", "db://public/users").unwrap();
1189        assert_eq!(vars["schema"], "public");
1190        assert_eq!(vars["table"], "users");
1191    }
1192
1193    #[test]
1194    fn test_match_uri_template_no_match() {
1195        assert!(match_uri_template("file:///{path}", "http://example.com").is_none());
1196    }
1197
1198    #[test]
1199    fn test_annotations_to_json() {
1200        let mut d = BTreeMap::new();
1201        d.insert("title".into(), VmValue::String(Rc::from("My Tool")));
1202        d.insert("readOnlyHint".into(), VmValue::Bool(true));
1203        d.insert("destructiveHint".into(), VmValue::Bool(false));
1204        let json = annotations_to_json(&VmValue::Dict(Rc::new(d))).unwrap();
1205        assert_eq!(json["title"], "My Tool");
1206        assert_eq!(json["readOnlyHint"], true);
1207        assert_eq!(json["destructiveHint"], false);
1208    }
1209
1210    #[test]
1211    fn test_annotations_empty_returns_none() {
1212        let d = BTreeMap::new();
1213        assert!(annotations_to_json(&VmValue::Dict(Rc::new(d))).is_none());
1214    }
1215}