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