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