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    /// Optional Server Card payload — advertised in the `initialize`
296    /// response's `serverInfo.card` field and exposed as a static
297    /// resource at the well-known URI `well-known://mcp-card`.
298    /// Populated by `harn mcp-serve --card path/to/card.json`.
299    server_card: Option<serde_json::Value>,
300}
301
302impl McpServer {
303    pub fn new(
304        server_name: String,
305        tools: Vec<McpToolDef>,
306        resources: Vec<McpResourceDef>,
307        resource_templates: Vec<McpResourceTemplateDef>,
308        prompts: Vec<McpPromptDef>,
309    ) -> Self {
310        Self {
311            server_name,
312            server_version: env!("CARGO_PKG_VERSION").to_string(),
313            tools,
314            resources,
315            resource_templates,
316            prompts,
317            log_level: RefCell::new("warning".to_string()),
318            server_card: None,
319        }
320    }
321
322    /// Attach a Server Card to be advertised over `initialize` and via
323    /// the `well-known://mcp-card` resource. Call on a freshly-built
324    /// `McpServer` before `run`.
325    pub fn with_server_card(mut self, card: serde_json::Value) -> Self {
326        self.server_card = Some(card);
327        self
328    }
329
330    /// Run the MCP server loop, reading JSON-RPC from stdin and writing to stdout.
331    pub async fn run(&self, vm: &mut Vm) -> Result<(), VmError> {
332        let stdin = BufReader::new(tokio::io::stdin());
333        let mut stdout = tokio::io::stdout();
334        let mut lines = stdin.lines();
335
336        while let Ok(Some(line)) = lines.next_line().await {
337            let trimmed = line.trim();
338            if trimmed.is_empty() {
339                continue;
340            }
341
342            let msg: serde_json::Value = match serde_json::from_str(trimmed) {
343                Ok(v) => v,
344                Err(_) => continue,
345            };
346
347            let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
348            let id = msg.get("id").cloned();
349            let params = msg.get("params").cloned().unwrap_or(serde_json::json!({}));
350
351            // Notifications have no id; ignore them silently.
352            if id.is_none() {
353                continue;
354            }
355            let id = id.unwrap();
356
357            let response = match method {
358                "initialize" => self.handle_initialize(&id),
359                "ping" => crate::jsonrpc::response(id.clone(), serde_json::json!({})),
360                "logging/setLevel" => self.handle_logging_set_level(&id, &params),
361                "tools/list" => self.handle_tools_list(&id, &params),
362                "tools/call" => self.handle_tools_call(&id, &params, vm).await,
363                "resources/list" => self.handle_resources_list(&id, &params),
364                "resources/read" => self.handle_resources_read(&id, &params, vm).await,
365                "resources/templates/list" => self.handle_resource_templates_list(&id, &params),
366                "prompts/list" => self.handle_prompts_list(&id, &params),
367                "prompts/get" => self.handle_prompts_get(&id, &params, vm).await,
368                _ => serde_json::json!({
369                    "jsonrpc": "2.0",
370                    "id": id,
371                    "error": {
372                        "code": -32601,
373                        "message": format!("Method not found: {method}")
374                    }
375                }),
376            };
377
378            let mut response_line = serde_json::to_string(&response)
379                .map_err(|e| VmError::Runtime(format!("MCP server serialization error: {e}")))?;
380            response_line.push('\n');
381            stdout
382                .write_all(response_line.as_bytes())
383                .await
384                .map_err(|e| VmError::Runtime(format!("MCP server write error: {e}")))?;
385            stdout
386                .flush()
387                .await
388                .map_err(|e| VmError::Runtime(format!("MCP server flush error: {e}")))?;
389        }
390
391        Ok(())
392    }
393
394    fn handle_initialize(&self, id: &serde_json::Value) -> serde_json::Value {
395        let mut capabilities = serde_json::Map::new();
396        if !self.tools.is_empty() {
397            capabilities.insert("tools".into(), serde_json::json!({}));
398        }
399        if !self.resources.is_empty()
400            || !self.resource_templates.is_empty()
401            || self.server_card.is_some()
402        {
403            capabilities.insert("resources".into(), serde_json::json!({}));
404        }
405        if !self.prompts.is_empty() {
406            capabilities.insert("prompts".into(), serde_json::json!({}));
407        }
408        capabilities.insert("logging".into(), serde_json::json!({}));
409
410        let mut server_info = serde_json::json!({
411            "name": self.server_name,
412            "version": self.server_version
413        });
414        if let Some(ref card) = self.server_card {
415            server_info["card"] = card.clone();
416        }
417
418        serde_json::json!({
419            "jsonrpc": "2.0",
420            "id": id,
421            "result": {
422                "protocolVersion": PROTOCOL_VERSION,
423                "capabilities": capabilities,
424                "serverInfo": server_info
425            }
426        })
427    }
428
429    fn handle_tools_list(
430        &self,
431        id: &serde_json::Value,
432        params: &serde_json::Value,
433    ) -> serde_json::Value {
434        let (offset, page_size) = parse_cursor(params);
435        let page_end = (offset + page_size).min(self.tools.len());
436        let tools: Vec<serde_json::Value> = self.tools[offset..page_end]
437            .iter()
438            .map(|t| {
439                let mut entry = serde_json::json!({
440                    "name": t.name,
441                    "description": t.description,
442                    "inputSchema": t.input_schema,
443                });
444                if let Some(ref title) = t.title {
445                    entry["title"] = serde_json::json!(title);
446                }
447                if let Some(ref output_schema) = t.output_schema {
448                    entry["outputSchema"] = output_schema.clone();
449                }
450                if let Some(ref annotations) = t.annotations {
451                    entry["annotations"] = annotations.clone();
452                }
453                entry
454            })
455            .collect();
456
457        let mut result = serde_json::json!({ "tools": tools });
458        if page_end < self.tools.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    async fn handle_tools_call(
470        &self,
471        id: &serde_json::Value,
472        params: &serde_json::Value,
473        vm: &mut Vm,
474    ) -> serde_json::Value {
475        let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
476
477        let tool = match self.tools.iter().find(|t| t.name == tool_name) {
478            Some(t) => t,
479            None => {
480                return serde_json::json!({
481                    "jsonrpc": "2.0",
482                    "id": id,
483                    "error": { "code": -32602, "message": format!("Unknown tool: {tool_name}") }
484                });
485            }
486        };
487
488        let arguments = params
489            .get("arguments")
490            .cloned()
491            .unwrap_or(serde_json::json!({}));
492        let args_vm = json_to_vm_value(&arguments);
493
494        let result = vm.call_closure_pub(&tool.handler, &[args_vm], &[]).await;
495
496        match result {
497            Ok(value) => {
498                let content = vm_value_to_content(&value);
499                let mut call_result = serde_json::json!({
500                    "content": content,
501                    "isError": false
502                });
503                if tool.output_schema.is_some() {
504                    let text = value.display();
505                    let structured = match serde_json::from_str::<serde_json::Value>(&text) {
506                        Ok(v) => v,
507                        _ => serde_json::json!(text),
508                    };
509                    call_result["structuredContent"] = structured;
510                }
511                serde_json::json!({
512                    "jsonrpc": "2.0",
513                    "id": id,
514                    "result": call_result
515                })
516            }
517            Err(e) => serde_json::json!({
518                "jsonrpc": "2.0",
519                "id": id,
520                "result": {
521                    "content": [{ "type": "text", "text": format!("{e}") }],
522                    "isError": true
523                }
524            }),
525        }
526    }
527
528    fn handle_resources_list(
529        &self,
530        id: &serde_json::Value,
531        params: &serde_json::Value,
532    ) -> serde_json::Value {
533        // Virtually prepend the Server Card as a static resource so
534        // clients that browse resources can discover the card without
535        // a separate well-known GET. Kept out of the underlying
536        // `self.resources` vec so cursor paging stays simple.
537        let card_entry = self.server_card.as_ref().map(|_| {
538            serde_json::json!({
539                "uri": "well-known://mcp-card",
540                "name": "Server Card",
541                "description": "MCP v2.1 Server Card advertising this server's identity and capabilities",
542                "mimeType": "application/json",
543            })
544        });
545
546        let (offset, page_size) = parse_cursor(params);
547        let page_end = (offset + page_size).min(self.resources.len());
548        let mut resources: Vec<serde_json::Value> = self.resources[offset..page_end]
549            .iter()
550            .map(|r| {
551                let mut entry = serde_json::json!({ "uri": r.uri, "name": r.name });
552                if let Some(ref title) = r.title {
553                    entry["title"] = serde_json::json!(title);
554                }
555                if let Some(ref desc) = r.description {
556                    entry["description"] = serde_json::json!(desc);
557                }
558                if let Some(ref mime) = r.mime_type {
559                    entry["mimeType"] = serde_json::json!(mime);
560                }
561                entry
562            })
563            .collect();
564        if offset == 0 {
565            if let Some(entry) = card_entry {
566                resources.insert(0, entry);
567            }
568        }
569
570        let mut result = serde_json::json!({ "resources": resources });
571        if page_end < self.resources.len() {
572            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
573        }
574
575        serde_json::json!({
576            "jsonrpc": "2.0",
577            "id": id,
578            "result": result
579        })
580    }
581
582    async fn handle_resources_read(
583        &self,
584        id: &serde_json::Value,
585        params: &serde_json::Value,
586        vm: &mut Vm,
587    ) -> serde_json::Value {
588        let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
589
590        // Expose the Server Card at the well-known URI. Matches the
591        // HTTP convention (.well-known/mcp-card) but routed through
592        // the stdio resource protocol.
593        if uri == "well-known://mcp-card" {
594            if let Some(ref card) = self.server_card {
595                let content = serde_json::json!({
596                    "uri": uri,
597                    "text": serde_json::to_string(card).unwrap_or_else(|_| "{}".to_string()),
598                    "mimeType": "application/json",
599                });
600                return serde_json::json!({
601                    "jsonrpc": "2.0",
602                    "id": id,
603                    "result": { "contents": [content] }
604                });
605            }
606        }
607
608        // Static resources take precedence over templates.
609        if let Some(resource) = self.resources.iter().find(|r| r.uri == uri) {
610            let mut content = serde_json::json!({ "uri": resource.uri, "text": resource.text });
611            if let Some(ref mime) = resource.mime_type {
612                content["mimeType"] = serde_json::json!(mime);
613            }
614            return serde_json::json!({
615                "jsonrpc": "2.0",
616                "id": id,
617                "result": { "contents": [content] }
618            });
619        }
620
621        for tmpl in &self.resource_templates {
622            if let Some(args) = match_uri_template(&tmpl.uri_template, uri) {
623                let args_vm = json_to_vm_value(&serde_json::json!(args));
624                let result = vm.call_closure_pub(&tmpl.handler, &[args_vm], &[]).await;
625                return match result {
626                    Ok(value) => {
627                        let mut content = serde_json::json!({
628                            "uri": uri,
629                            "text": value.display(),
630                        });
631                        if let Some(ref mime) = tmpl.mime_type {
632                            content["mimeType"] = serde_json::json!(mime);
633                        }
634                        serde_json::json!({
635                            "jsonrpc": "2.0",
636                            "id": id,
637                            "result": { "contents": [content] }
638                        })
639                    }
640                    Err(e) => serde_json::json!({
641                        "jsonrpc": "2.0",
642                        "id": id,
643                        "error": { "code": -32603, "message": format!("{e}") }
644                    }),
645                };
646            }
647        }
648
649        serde_json::json!({
650            "jsonrpc": "2.0",
651            "id": id,
652            "error": { "code": -32002, "message": format!("Resource not found: {uri}") }
653        })
654    }
655
656    fn handle_resource_templates_list(
657        &self,
658        id: &serde_json::Value,
659        params: &serde_json::Value,
660    ) -> serde_json::Value {
661        let (offset, page_size) = parse_cursor(params);
662        let page_end = (offset + page_size).min(self.resource_templates.len());
663        let templates: Vec<serde_json::Value> = self.resource_templates[offset..page_end]
664            .iter()
665            .map(|t| {
666                let mut entry =
667                    serde_json::json!({ "uriTemplate": t.uri_template, "name": t.name });
668                if let Some(ref title) = t.title {
669                    entry["title"] = serde_json::json!(title);
670                }
671                if let Some(ref desc) = t.description {
672                    entry["description"] = serde_json::json!(desc);
673                }
674                if let Some(ref mime) = t.mime_type {
675                    entry["mimeType"] = serde_json::json!(mime);
676                }
677                entry
678            })
679            .collect();
680
681        let mut result = serde_json::json!({ "resourceTemplates": templates });
682        if page_end < self.resource_templates.len() {
683            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
684        }
685
686        serde_json::json!({
687            "jsonrpc": "2.0",
688            "id": id,
689            "result": result
690        })
691    }
692
693    fn handle_prompts_list(
694        &self,
695        id: &serde_json::Value,
696        params: &serde_json::Value,
697    ) -> serde_json::Value {
698        let (offset, page_size) = parse_cursor(params);
699        let page_end = (offset + page_size).min(self.prompts.len());
700        let prompts: Vec<serde_json::Value> = self.prompts[offset..page_end]
701            .iter()
702            .map(|p| {
703                let mut entry = serde_json::json!({ "name": p.name });
704                if let Some(ref title) = p.title {
705                    entry["title"] = serde_json::json!(title);
706                }
707                if let Some(ref desc) = p.description {
708                    entry["description"] = serde_json::json!(desc);
709                }
710                if let Some(ref args) = p.arguments {
711                    let args_json: Vec<serde_json::Value> = args
712                        .iter()
713                        .map(|a| {
714                            let mut arg =
715                                serde_json::json!({ "name": a.name, "required": a.required });
716                            if let Some(ref desc) = a.description {
717                                arg["description"] = serde_json::json!(desc);
718                            }
719                            arg
720                        })
721                        .collect();
722                    entry["arguments"] = serde_json::json!(args_json);
723                }
724                entry
725            })
726            .collect();
727
728        let mut result = serde_json::json!({ "prompts": prompts });
729        if page_end < self.prompts.len() {
730            result["nextCursor"] = serde_json::json!(encode_cursor(page_end));
731        }
732
733        serde_json::json!({
734            "jsonrpc": "2.0",
735            "id": id,
736            "result": result
737        })
738    }
739
740    fn handle_logging_set_level(
741        &self,
742        id: &serde_json::Value,
743        params: &serde_json::Value,
744    ) -> serde_json::Value {
745        let level = params
746            .get("level")
747            .and_then(|l| l.as_str())
748            .unwrap_or("warning");
749        *self.log_level.borrow_mut() = level.to_string();
750        crate::jsonrpc::response(id.clone(), serde_json::json!({}))
751    }
752
753    async fn handle_prompts_get(
754        &self,
755        id: &serde_json::Value,
756        params: &serde_json::Value,
757        vm: &mut Vm,
758    ) -> serde_json::Value {
759        let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
760
761        let prompt = match self.prompts.iter().find(|p| p.name == name) {
762            Some(p) => p,
763            None => {
764                return serde_json::json!({
765                    "jsonrpc": "2.0",
766                    "id": id,
767                    "error": { "code": -32602, "message": format!("Unknown prompt: {name}") }
768                });
769            }
770        };
771
772        let arguments = params
773            .get("arguments")
774            .cloned()
775            .unwrap_or(serde_json::json!({}));
776        let args_vm = json_to_vm_value(&arguments);
777
778        let result = vm.call_closure_pub(&prompt.handler, &[args_vm], &[]).await;
779
780        match result {
781            Ok(value) => {
782                let messages = prompt_value_to_messages(&value);
783                serde_json::json!({
784                    "jsonrpc": "2.0",
785                    "id": id,
786                    "result": { "messages": messages }
787                })
788            }
789            Err(e) => serde_json::json!({
790                "jsonrpc": "2.0",
791                "id": id,
792                "error": { "code": -32603, "message": format!("{e}") }
793            }),
794        }
795    }
796}
797
798/// Encode an offset as a base64 cursor string.
799fn encode_cursor(offset: usize) -> String {
800    use base64::Engine;
801    base64::engine::general_purpose::STANDARD.encode(offset.to_string().as_bytes())
802}
803
804/// Decode a cursor from the request params, returning `(offset, page_size)`.
805fn parse_cursor(params: &serde_json::Value) -> (usize, usize) {
806    let offset = params
807        .get("cursor")
808        .and_then(|c| c.as_str())
809        .and_then(|c| {
810            use base64::Engine;
811            let bytes = base64::engine::general_purpose::STANDARD.decode(c).ok()?;
812            let s = String::from_utf8(bytes).ok()?;
813            s.parse::<usize>().ok()
814        })
815        .unwrap_or(0);
816    (offset, DEFAULT_PAGE_SIZE)
817}
818
819/// Convert a VmValue returned by a prompt handler into MCP messages.
820fn prompt_value_to_messages(value: &VmValue) -> Vec<serde_json::Value> {
821    match value {
822        VmValue::String(s) => {
823            vec![serde_json::json!({
824                "role": "user",
825                "content": { "type": "text", "text": &**s }
826            })]
827        }
828        VmValue::List(items) => items
829            .iter()
830            .map(|item| {
831                if let VmValue::Dict(d) = item {
832                    let role = d
833                        .get("role")
834                        .map(|v| v.display())
835                        .unwrap_or_else(|| "user".into());
836                    let content = d.get("content").map(|v| v.display()).unwrap_or_default();
837                    serde_json::json!({
838                        "role": role,
839                        "content": { "type": "text", "text": content }
840                    })
841                } else {
842                    serde_json::json!({
843                        "role": "user",
844                        "content": { "type": "text", "text": item.display() }
845                    })
846                }
847            })
848            .collect(),
849        _ => {
850            vec![serde_json::json!({
851                "role": "user",
852                "content": { "type": "text", "text": value.display() }
853            })]
854        }
855    }
856}
857
858/// Simple URI template matching (RFC 6570 Level 1 only).
859///
860/// Matches a URI against a template like `file:///{path}` and extracts named
861/// variables. Returns `None` if the URI doesn't match the template structure.
862fn match_uri_template(
863    template: &str,
864    uri: &str,
865) -> Option<std::collections::HashMap<String, String>> {
866    let mut vars = std::collections::HashMap::new();
867    let mut t_pos = 0;
868    let mut u_pos = 0;
869    let t_bytes = template.as_bytes();
870    let u_bytes = uri.as_bytes();
871
872    while t_pos < t_bytes.len() {
873        if t_bytes[t_pos] == b'{' {
874            // Find the closing brace
875            let close = template[t_pos..].find('}')? + t_pos;
876            let var_name = &template[t_pos + 1..close];
877            t_pos = close + 1;
878
879            // Capture everything up to the next literal in the template (or end)
880            let next_literal = if t_pos < t_bytes.len() {
881                // Find how much literal follows
882                let lit_start = t_pos;
883                let lit_end = template[t_pos..]
884                    .find('{')
885                    .map(|i| t_pos + i)
886                    .unwrap_or(t_bytes.len());
887                Some(&template[lit_start..lit_end])
888            } else {
889                None
890            };
891
892            let value_end = match next_literal {
893                Some(lit) if !lit.is_empty() => uri[u_pos..].find(lit).map(|i| u_pos + i)?,
894                _ => u_bytes.len(),
895            };
896
897            vars.insert(var_name.to_string(), uri[u_pos..value_end].to_string());
898            u_pos = value_end;
899        } else {
900            // Literal character must match
901            if u_pos >= u_bytes.len() || t_bytes[t_pos] != u_bytes[u_pos] {
902                return None;
903            }
904            t_pos += 1;
905            u_pos += 1;
906        }
907    }
908
909    if u_pos == u_bytes.len() {
910        Some(vars)
911    } else {
912        None
913    }
914}
915
916/// Convert a tool result VmValue into MCP content items.
917///
918/// Supports text, embedded resource, and resource_link content types.
919/// If the value is a list of dicts with a `type` field, each is treated as a
920/// content item. Otherwise, the whole value is serialized as a single text item.
921fn vm_value_to_content(value: &VmValue) -> Vec<serde_json::Value> {
922    if let VmValue::List(items) = value {
923        let mut content = Vec::new();
924        for item in items.iter() {
925            if let VmValue::Dict(d) = item {
926                let item_type = d.get("type").map(|v| v.display()).unwrap_or_default();
927                match item_type.as_str() {
928                    "resource" => {
929                        let mut entry = serde_json::json!({ "type": "resource" });
930                        if let Some(resource) = d.get("resource") {
931                            entry["resource"] = vm_value_to_json(resource);
932                        }
933                        content.push(entry);
934                    }
935                    "resource_link" => {
936                        let mut entry = serde_json::json!({ "type": "resource_link" });
937                        if let Some(uri) = d.get("uri") {
938                            entry["uri"] = serde_json::json!(uri.display());
939                        }
940                        if let Some(name) = d.get("name") {
941                            entry["name"] = serde_json::json!(name.display());
942                        }
943                        if let Some(desc) = d.get("description") {
944                            entry["description"] = serde_json::json!(desc.display());
945                        }
946                        if let Some(mime) = d.get("mimeType") {
947                            entry["mimeType"] = serde_json::json!(mime.display());
948                        }
949                        content.push(entry);
950                    }
951                    _ => {
952                        let text = d
953                            .get("text")
954                            .map(|v| v.display())
955                            .unwrap_or_else(|| item.display());
956                        content.push(serde_json::json!({ "type": "text", "text": text }));
957                    }
958                }
959            } else {
960                content.push(serde_json::json!({ "type": "text", "text": item.display() }));
961            }
962        }
963        if content.is_empty() {
964            vec![serde_json::json!({ "type": "text", "text": value.display() })]
965        } else {
966            content
967        }
968    } else {
969        vec![serde_json::json!({ "type": "text", "text": value.display() })]
970    }
971}
972
973/// Convert a VmValue to a serde_json::Value.
974fn vm_value_to_json(value: &VmValue) -> serde_json::Value {
975    match value {
976        VmValue::Nil => serde_json::Value::Null,
977        VmValue::Bool(b) => serde_json::json!(b),
978        VmValue::Int(n) => serde_json::json!(n),
979        VmValue::Float(f) => serde_json::json!(f),
980        VmValue::String(s) => serde_json::json!(&**s),
981        VmValue::List(items) => {
982            serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
983        }
984        VmValue::Dict(d) => {
985            let mut map = serde_json::Map::new();
986            for (k, v) in d.iter() {
987                map.insert(k.clone(), vm_value_to_json(v));
988            }
989            serde_json::Value::Object(map)
990        }
991        _ => serde_json::json!(value.display()),
992    }
993}
994
995/// Convert a VmValue annotations dict to a serde_json::Value with only the
996/// recognized MCP annotation fields.
997fn annotations_to_json(annotations: &VmValue) -> Option<serde_json::Value> {
998    let dict = match annotations {
999        VmValue::Dict(d) => d,
1000        _ => return None,
1001    };
1002
1003    let mut out = serde_json::Map::new();
1004    let str_keys = ["title"];
1005    let bool_keys = [
1006        "readOnlyHint",
1007        "destructiveHint",
1008        "idempotentHint",
1009        "openWorldHint",
1010    ];
1011
1012    for key in str_keys {
1013        if let Some(VmValue::String(s)) = dict.get(key) {
1014            out.insert(key.into(), serde_json::json!(&**s));
1015        }
1016    }
1017    for key in bool_keys {
1018        if let Some(VmValue::Bool(b)) = dict.get(key) {
1019            out.insert(key.into(), serde_json::json!(b));
1020        }
1021    }
1022
1023    if out.is_empty() {
1024        None
1025    } else {
1026        Some(serde_json::Value::Object(out))
1027    }
1028}
1029
1030/// Extract tools from a Harn tool_registry VmValue and convert to MCP tool definitions.
1031pub fn tool_registry_to_mcp_tools(registry: &VmValue) -> Result<Vec<McpToolDef>, VmError> {
1032    let dict = match registry {
1033        VmValue::Dict(d) => d,
1034        _ => {
1035            return Err(VmError::Runtime(
1036                "mcp_tools: argument must be a tool registry".into(),
1037            ));
1038        }
1039    };
1040
1041    match dict.get("_type") {
1042        Some(VmValue::String(t)) if &**t == "tool_registry" => {}
1043        _ => {
1044            return Err(VmError::Runtime(
1045                "mcp_tools: argument must be a tool registry (created with tool_registry())".into(),
1046            ));
1047        }
1048    }
1049
1050    let tools = match dict.get("tools") {
1051        Some(VmValue::List(list)) => list,
1052        _ => return Ok(Vec::new()),
1053    };
1054
1055    let mut mcp_tools = Vec::new();
1056    for tool in tools.iter() {
1057        if let VmValue::Dict(entry) = tool {
1058            let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1059            let title = entry.get("title").map(|v| v.display());
1060            let description = entry
1061                .get("description")
1062                .map(|v| v.display())
1063                .unwrap_or_default();
1064
1065            let handler = match entry.get("handler") {
1066                Some(VmValue::Closure(c)) => (**c).clone(),
1067                _ => {
1068                    return Err(VmError::Runtime(format!(
1069                        "mcp_tools: tool '{name}' has no handler closure"
1070                    )));
1071                }
1072            };
1073
1074            let input_schema = params_to_json_schema(entry.get("parameters"));
1075            let output_schema = entry.get("output_schema").and_then(|v| {
1076                if let VmValue::Dict(_) = v {
1077                    Some(vm_value_to_json(v))
1078                } else {
1079                    None
1080                }
1081            });
1082            let annotations = entry.get("annotations").and_then(annotations_to_json);
1083
1084            mcp_tools.push(McpToolDef {
1085                name,
1086                title,
1087                description,
1088                input_schema,
1089                output_schema,
1090                annotations,
1091                handler,
1092            });
1093        }
1094    }
1095
1096    Ok(mcp_tools)
1097}
1098
1099/// Convert Harn tool_define parameter definitions to JSON Schema for MCP inputSchema.
1100fn params_to_json_schema(params: Option<&VmValue>) -> serde_json::Value {
1101    let params_dict = match params {
1102        Some(VmValue::Dict(d)) => d,
1103        _ => {
1104            return serde_json::json!({ "type": "object", "properties": {} });
1105        }
1106    };
1107
1108    let mut properties = serde_json::Map::new();
1109    let mut required = Vec::new();
1110
1111    for (param_name, param_def) in params_dict.iter() {
1112        if let VmValue::Dict(def) = param_def {
1113            let mut prop = serde_json::Map::new();
1114            if let Some(VmValue::String(t)) = def.get("type") {
1115                prop.insert("type".into(), serde_json::Value::String(t.to_string()));
1116            }
1117            if let Some(VmValue::String(d)) = def.get("description") {
1118                prop.insert(
1119                    "description".into(),
1120                    serde_json::Value::String(d.to_string()),
1121                );
1122            }
1123            if matches!(def.get("required"), Some(VmValue::Bool(true))) {
1124                required.push(serde_json::Value::String(param_name.clone()));
1125            }
1126            properties.insert(param_name.clone(), serde_json::Value::Object(prop));
1127        } else if let VmValue::String(type_str) = param_def {
1128            let mut prop = serde_json::Map::new();
1129            prop.insert(
1130                "type".into(),
1131                serde_json::Value::String(type_str.to_string()),
1132            );
1133            properties.insert(param_name.clone(), serde_json::Value::Object(prop));
1134        }
1135    }
1136
1137    let mut schema = serde_json::Map::new();
1138    schema.insert("type".into(), serde_json::Value::String("object".into()));
1139    schema.insert("properties".into(), serde_json::Value::Object(properties));
1140    if !required.is_empty() {
1141        schema.insert("required".into(), serde_json::Value::Array(required));
1142    }
1143    serde_json::Value::Object(schema)
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148    use super::*;
1149    use std::collections::BTreeMap;
1150    use std::rc::Rc;
1151
1152    #[test]
1153    fn test_params_to_json_schema_empty() {
1154        let schema = params_to_json_schema(None);
1155        assert_eq!(
1156            schema,
1157            serde_json::json!({ "type": "object", "properties": {} })
1158        );
1159    }
1160
1161    #[test]
1162    fn test_params_to_json_schema_with_params() {
1163        let mut params = BTreeMap::new();
1164        let mut param_def = BTreeMap::new();
1165        param_def.insert("type".to_string(), VmValue::String(Rc::from("string")));
1166        param_def.insert(
1167            "description".to_string(),
1168            VmValue::String(Rc::from("A file path")),
1169        );
1170        param_def.insert("required".to_string(), VmValue::Bool(true));
1171        params.insert("path".to_string(), VmValue::Dict(Rc::new(param_def)));
1172
1173        let schema = params_to_json_schema(Some(&VmValue::Dict(Rc::new(params))));
1174        assert_eq!(
1175            schema,
1176            serde_json::json!({
1177                "type": "object",
1178                "properties": { "path": { "type": "string", "description": "A file path" } },
1179                "required": ["path"]
1180            })
1181        );
1182    }
1183
1184    #[test]
1185    fn test_params_to_json_schema_simple_form() {
1186        let mut params = BTreeMap::new();
1187        params.insert("query".to_string(), VmValue::String(Rc::from("string")));
1188        let schema = params_to_json_schema(Some(&VmValue::Dict(Rc::new(params))));
1189        assert_eq!(
1190            schema["properties"]["query"]["type"],
1191            serde_json::json!("string")
1192        );
1193    }
1194
1195    #[test]
1196    fn test_tool_registry_to_mcp_tools_invalid() {
1197        assert!(tool_registry_to_mcp_tools(&VmValue::Nil).is_err());
1198    }
1199
1200    #[test]
1201    fn test_tool_registry_to_mcp_tools_empty() {
1202        let mut registry = BTreeMap::new();
1203        registry.insert("_type".into(), VmValue::String(Rc::from("tool_registry")));
1204        registry.insert("tools".into(), VmValue::List(Rc::new(Vec::new())));
1205        let result = tool_registry_to_mcp_tools(&VmValue::Dict(Rc::new(registry)));
1206        assert!(result.unwrap().is_empty());
1207    }
1208
1209    #[test]
1210    fn test_prompt_value_to_messages_string() {
1211        let msgs = prompt_value_to_messages(&VmValue::String(Rc::from("hello")));
1212        assert_eq!(msgs.len(), 1);
1213        assert_eq!(msgs[0]["role"], "user");
1214        assert_eq!(msgs[0]["content"]["text"], "hello");
1215    }
1216
1217    #[test]
1218    fn test_prompt_value_to_messages_list() {
1219        let items = vec![
1220            VmValue::Dict(Rc::new({
1221                let mut d = BTreeMap::new();
1222                d.insert("role".into(), VmValue::String(Rc::from("user")));
1223                d.insert("content".into(), VmValue::String(Rc::from("hi")));
1224                d
1225            })),
1226            VmValue::Dict(Rc::new({
1227                let mut d = BTreeMap::new();
1228                d.insert("role".into(), VmValue::String(Rc::from("assistant")));
1229                d.insert("content".into(), VmValue::String(Rc::from("hello")));
1230                d
1231            })),
1232        ];
1233        let msgs = prompt_value_to_messages(&VmValue::List(Rc::new(items)));
1234        assert_eq!(msgs.len(), 2);
1235        assert_eq!(msgs[1]["role"], "assistant");
1236    }
1237
1238    #[test]
1239    fn test_match_uri_template_simple() {
1240        let vars = match_uri_template("file:///{path}", "file:///foo/bar.rs").unwrap();
1241        assert_eq!(vars["path"], "foo/bar.rs");
1242    }
1243
1244    #[test]
1245    fn test_match_uri_template_multiple() {
1246        let vars = match_uri_template("db://{schema}/{table}", "db://public/users").unwrap();
1247        assert_eq!(vars["schema"], "public");
1248        assert_eq!(vars["table"], "users");
1249    }
1250
1251    #[test]
1252    fn test_match_uri_template_no_match() {
1253        assert!(match_uri_template("file:///{path}", "http://example.com").is_none());
1254    }
1255
1256    #[test]
1257    fn test_annotations_to_json() {
1258        let mut d = BTreeMap::new();
1259        d.insert("title".into(), VmValue::String(Rc::from("My Tool")));
1260        d.insert("readOnlyHint".into(), VmValue::Bool(true));
1261        d.insert("destructiveHint".into(), VmValue::Bool(false));
1262        let json = annotations_to_json(&VmValue::Dict(Rc::new(d))).unwrap();
1263        assert_eq!(json["title"], "My Tool");
1264        assert_eq!(json["readOnlyHint"], true);
1265        assert_eq!(json["destructiveHint"], false);
1266    }
1267
1268    #[test]
1269    fn test_annotations_empty_returns_none() {
1270        let d = BTreeMap::new();
1271        assert!(annotations_to_json(&VmValue::Dict(Rc::new(d))).is_none());
1272    }
1273}