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