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