1mod common;
8mod http;
9mod sse;
10mod stdio;
11mod transport;
12
13pub use common::{call_tool_on, list_tools_on, tools_to_commands, McpClient};
14pub use http::connect_streamable;
15pub use sse::connect_sse;
16pub use stdio::{call_tool_stdio, connect_stdio, connect_stdio_with, fetch_mcp_tools_stdio};
17pub use transport::{
18 call_tool as call_tool_http, connect_http, fetch_mcp_tools as fetch_mcp_tools_http,
19 TransportMode,
20};
21
22use serde_json::Value;
23
24use crate::coerce::to_kebab;
25use crate::model::{schema_type_to_python, CommandDef, ParamDef, ParamLocation};
26
27pub fn extract_mcp_commands(tools: &[Value]) -> Vec<CommandDef> {
31 let mut commands = Vec::new();
32 for tool in tools {
33 let tool_name = tool
34 .get("name")
35 .and_then(|v| v.as_str())
36 .unwrap_or("unknown");
37 let name = to_kebab(tool_name);
38 let desc = tool
39 .get("description")
40 .and_then(|v| v.as_str())
41 .unwrap_or("")
42 .to_string();
43 let schema = tool
44 .get("inputSchema")
45 .filter(|v| !v.is_null())
46 .cloned()
47 .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
48
49 let required_fields: std::collections::HashSet<String> = schema
50 .get("required")
51 .and_then(|v| v.as_array())
52 .map(|arr| {
53 arr.iter()
54 .filter_map(|x| x.as_str().map(str::to_string))
55 .collect()
56 })
57 .unwrap_or_default();
58
59 let mut params = Vec::new();
60 if let Some(props) = schema.get("properties").and_then(|v| v.as_object()) {
61 for (prop_name, prop_schema) in props {
62 let (py_type, suffix) = schema_type_to_python(prop_schema);
63 let description = format!(
64 "{}{suffix}",
65 prop_schema
66 .get("description")
67 .and_then(|v| v.as_str())
68 .unwrap_or(prop_name)
69 );
70 let choices = prop_schema
71 .get("enum")
72 .and_then(|v| v.as_array())
73 .map(|arr| {
74 arr.iter()
75 .filter_map(|x| x.as_str().map(str::to_string))
76 .collect()
77 });
78 params.push(ParamDef {
79 name: to_kebab(prop_name),
80 original_name: prop_name.clone(),
81 python_type: py_type,
82 required: required_fields.contains(prop_name),
83 description,
84 choices,
85 location: ParamLocation::ToolInput,
86 schema: prop_schema.clone(),
87 });
88 }
89 }
90
91 let has_body = !params.is_empty();
92 commands.push(CommandDef {
93 name,
94 description: desc,
95 params,
96 has_body,
97 tool_name: Some(tool_name.to_string()),
98 ..Default::default()
99 });
100 }
101 commands
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use serde_json::json;
108
109 #[test]
110 fn extracts_and_handles_null_schema() {
111 let tools = vec![
112 json!({
113 "name": "list_tasks",
114 "description": "List tasks",
115 "inputSchema": {
116 "type": "object",
117 "properties": {
118 "status": {"type": "string", "enum": ["TO_DO", "DONE"]}
119 },
120 "required": ["status"]
121 }
122 }),
123 json!({
124 "name": "noop",
125 "description": "No inputs",
126 "inputSchema": null
127 }),
128 ];
129 let cmds = extract_mcp_commands(&tools);
130 assert_eq!(cmds.len(), 2);
131 assert_eq!(cmds[0].name, "list-tasks");
132 assert_eq!(cmds[0].params.len(), 1);
133 assert!(cmds[0].params[0].required);
134 assert_eq!(cmds[1].name, "noop");
135 assert!(cmds[1].params.is_empty());
136 }
137}