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