gestura_core/tools/
schemas.rs1pub use gestura_core_tools::schemas::{
14 ProviderToolSchemas, build_provider_tool_schemas, normalize_openai_parameters_schema,
15};
16
17pub fn build_mcp_tool_schemas(
24 server_tools: &[(String, Vec<crate::mcp::types::Tool>)],
25) -> ProviderToolSchemas {
26 let mut out = ProviderToolSchemas::default();
27
28 for (server_name, tools) in server_tools {
29 for tool in tools {
30 let namespaced = format!("mcp__{}__{}", server_name, tool.name);
31 let description = tool
32 .description
33 .clone()
34 .unwrap_or_else(|| format!("MCP tool {}/{}", server_name, tool.name));
35
36 let openai = serde_json::json!({
37 "type": "function",
38 "function": {
39 "name": namespaced,
40 "description": description,
41 "parameters": normalize_openai_parameters_schema(tool.input_schema.clone())
42 }
43 });
44 let openai_responses = serde_json::json!({
45 "type": "function",
46 "name": namespaced,
47 "description": description,
48 "parameters": normalize_openai_parameters_schema(tool.input_schema.clone())
49 });
50 let anthropic = serde_json::json!({
51 "name": namespaced,
52 "description": description,
53 "input_schema": tool.input_schema
54 });
55 let gemini = serde_json::json!({
56 "name": namespaced,
57 "description": description,
58 "parameters": tool.input_schema
59 });
60
61 out.openai.push(openai);
62 out.openai_responses.push(openai_responses);
63 out.anthropic.push(anthropic);
64 out.gemini.push(gemini);
65 }
66 }
67
68 out
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::mcp::types::Tool;
75 use crate::tools::registry::find_tool;
76
77 #[test]
78 fn builds_shell_schema_for_all_providers() {
79 let shell = find_tool("shell").unwrap();
80 let schemas = build_provider_tool_schemas(&[shell]);
81 assert_eq!(schemas.openai.len(), 1);
82 assert_eq!(schemas.openai_responses.len(), 1);
83 assert_eq!(schemas.anthropic.len(), 1);
84 assert_eq!(schemas.gemini.len(), 1);
85
86 assert_eq!(schemas.openai[0]["function"]["name"], "shell");
88 assert!(
89 schemas.openai[0]["function"]["parameters"]["required"]
90 .as_array()
91 .unwrap()
92 .iter()
93 .any(|v| v == "command")
94 );
95 assert_eq!(
96 schemas.openai[0]["function"]["parameters"]["properties"]["allow_long_running"]["type"],
97 "boolean"
98 );
99 assert_eq!(schemas.openai_responses[0]["name"], "shell");
100 assert_eq!(
101 schemas.openai_responses[0]["parameters"]["properties"]["allow_long_running"]["type"],
102 "boolean"
103 );
104
105 assert_eq!(schemas.gemini[0]["name"], "shell");
107 assert!(
108 schemas.gemini[0]["parameters"]["required"]
109 .as_array()
110 .unwrap()
111 .iter()
112 .any(|v| v == "command")
113 );
114 assert_eq!(
115 schemas.gemini[0]["parameters"]["properties"]["stall_timeout_secs"]["type"],
116 "integer"
117 );
118 }
119
120 #[test]
121 fn mcp_openai_schemas_strip_top_level_combinators() {
122 let schemas = build_mcp_tool_schemas(&[(
123 "demo".to_string(),
124 vec![Tool {
125 name: "inspect".to_string(),
126 description: Some("Inspect demo state".to_string()),
127 input_schema: serde_json::json!({
128 "type": "object",
129 "properties": {
130 "path": {"type": "string"}
131 },
132 "oneOf": [
133 {"required": ["path"]}
134 ],
135 "additionalProperties": false
136 }),
137 annotations: None,
138 }],
139 )]);
140
141 assert!(
142 schemas.openai[0]["function"]["parameters"]
143 .get("oneOf")
144 .is_none()
145 );
146 assert!(
147 schemas.openai_responses[0]["parameters"]
148 .get("oneOf")
149 .is_none()
150 );
151 assert_eq!(
152 schemas.openai[0]["function"]["parameters"]["type"],
153 serde_json::json!("object")
154 );
155 assert!(schemas.anthropic[0]["input_schema"]["oneOf"].is_array());
156 }
157}