1use 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 static MCP_SERVE_REGISTRY: RefCell<Option<VmValue>> = const { RefCell::new(None) };
20 static MCP_SERVE_RESOURCES: RefCell<Vec<McpResourceDef>> = const { RefCell::new(Vec::new()) };
22 static MCP_SERVE_RESOURCE_TEMPLATES: RefCell<Vec<McpResourceTemplateDef>> = const { RefCell::new(Vec::new()) };
24 static MCP_SERVE_PROMPTS: RefCell<Vec<McpPromptDef>> = const { RefCell::new(Vec::new()) };
26}
27
28pub fn register_mcp_server_builtins(vm: &mut Vm) {
34 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) = ®istry {
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 vm.register_builtin("mcp_serve", |args, _out| register_tools_impl(args));
63
64 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 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 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
224pub 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
244const PROTOCOL_VERSION: &str = "2025-11-25";
246
247const DEFAULT_PAGE_SIZE: usize = 50;
249
250pub 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
265pub 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
275pub 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
285pub struct McpPromptArgDef {
287 pub name: String,
288 pub description: Option<String>,
289 pub required: bool,
290}
291
292pub 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
301pub 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 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 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, ¶ms),
366 "tools/list" => self.handle_tools_list(&id, ¶ms),
367 "tools/call" => self.handle_tools_call(&id, ¶ms, vm).await,
368 "resources/list" => self.handle_resources_list(&id, ¶ms),
369 "resources/read" => self.handle_resources_read(&id, ¶ms, vm).await,
370 "resources/templates/list" => self.handle_resource_templates_list(&id, ¶ms),
371 "prompts/list" => self.handle_prompts_list(&id, ¶ms),
372 "prompts/get" => self.handle_prompts_get(&id, ¶ms, 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 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 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 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 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 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 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
776fn encode_cursor(offset: usize) -> String {
782 use base64::Engine;
783 base64::engine::general_purpose::STANDARD.encode(offset.to_string().as_bytes())
784}
785
786fn 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
801fn 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
840fn 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 let close = template[t_pos..].find('}')? + t_pos;
858 let var_name = &template[t_pos + 1..close];
859 t_pos = close + 1;
860
861 let next_literal = if t_pos < t_bytes.len() {
863 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 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
898fn 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
955fn 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
977fn 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
1012pub 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
1085fn 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}