toolschema 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toolschema/__init__.py +29 -0
- toolschema/__main__.py +4 -0
- toolschema/_decorator.py +46 -0
- toolschema/_fields.py +65 -0
- toolschema/_init_scaffold.py +70 -0
- toolschema/_introspect.py +93 -0
- toolschema/_ir.py +61 -0
- toolschema/_schema_utils.py +11 -0
- toolschema/_standard.py +107 -0
- toolschema/_types.py +164 -0
- toolschema/_validate.py +289 -0
- toolschema/adapters/__init__.py +6 -0
- toolschema/adapters/_inline_refs.py +46 -0
- toolschema/adapters/anthropic.py +67 -0
- toolschema/adapters/gemini.py +58 -0
- toolschema/adapters/mcp.py +28 -0
- toolschema/adapters/openai.py +32 -0
- toolschema/cli.py +193 -0
- toolschema/integrations/__init__.py +21 -0
- toolschema/integrations/fastmcp.py +37 -0
- toolschema/integrations/langchain.py +26 -0
- toolschema/integrations/openai_agents.py +79 -0
- toolschema/integrations/pydantic_ai.py +44 -0
- toolschema/templates/__init__.py +0 -0
- toolschema/templates/mcp_server/README.md +47 -0
- toolschema/templates/mcp_server/claude_desktop_config.example.json +9 -0
- toolschema/templates/mcp_server/pyproject.toml +16 -0
- toolschema/templates/mcp_server/src/PACKAGE/__init__.py +1 -0
- toolschema/templates/mcp_server/src/PACKAGE/__main__.py +50 -0
- toolschema/templates/mcp_server/src/PACKAGE/tools.py +19 -0
- toolschema-1.0.0.dist-info/METADATA +428 -0
- toolschema-1.0.0.dist-info/RECORD +35 -0
- toolschema-1.0.0.dist-info/WHEEL +4 -0
- toolschema-1.0.0.dist-info/entry_points.txt +2 -0
- toolschema-1.0.0.dist-info/licenses/LICENSE +21 -0
toolschema/_validate.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ValidationIssueKind(str, Enum):
|
|
10
|
+
REQUIRED = "required"
|
|
11
|
+
TYPE = "type"
|
|
12
|
+
ENUM = "enum"
|
|
13
|
+
CONSTRAINT = "constraint"
|
|
14
|
+
ADDITIONAL_PROPERTY = "additional_property"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ValidationIssue:
|
|
19
|
+
message: str
|
|
20
|
+
path: tuple[str | int, ...] = ()
|
|
21
|
+
kind: ValidationIssueKind = ValidationIssueKind.TYPE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ValidationSuccess:
|
|
26
|
+
value: dict[str, Any]
|
|
27
|
+
issues: None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ValidationFailure:
|
|
32
|
+
value: None = None
|
|
33
|
+
issues: tuple[ValidationIssue, ...] = ()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ValidationResult = ValidationSuccess | ValidationFailure
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _issue(
|
|
40
|
+
message: str,
|
|
41
|
+
*,
|
|
42
|
+
path: tuple[str | int, ...] = (),
|
|
43
|
+
kind: ValidationIssueKind = ValidationIssueKind.TYPE,
|
|
44
|
+
) -> ValidationIssue:
|
|
45
|
+
return ValidationIssue(message=message, path=path, kind=kind)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _type_name(value: Any) -> str:
|
|
49
|
+
if value is None:
|
|
50
|
+
return "null"
|
|
51
|
+
if isinstance(value, bool):
|
|
52
|
+
return "boolean"
|
|
53
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
54
|
+
return "integer"
|
|
55
|
+
if isinstance(value, float):
|
|
56
|
+
return "number"
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
return "string"
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return "array"
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
return "object"
|
|
63
|
+
return type(value).__name__
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _matches_type(value: Any, expected: str) -> bool:
|
|
67
|
+
if expected == "null":
|
|
68
|
+
return value is None
|
|
69
|
+
if expected == "boolean":
|
|
70
|
+
return isinstance(value, bool)
|
|
71
|
+
if expected == "integer":
|
|
72
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
73
|
+
if expected == "number":
|
|
74
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
75
|
+
if expected == "string":
|
|
76
|
+
return isinstance(value, str)
|
|
77
|
+
if expected == "array":
|
|
78
|
+
return isinstance(value, list)
|
|
79
|
+
if expected == "object":
|
|
80
|
+
return isinstance(value, dict)
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _validate_constraints(
|
|
85
|
+
value: Any, schema: dict[str, Any], path: tuple[str | int, ...]
|
|
86
|
+
) -> list[ValidationIssue]:
|
|
87
|
+
issues: list[ValidationIssue] = []
|
|
88
|
+
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
min_length = schema.get("minLength")
|
|
91
|
+
max_length = schema.get("maxLength")
|
|
92
|
+
pattern = schema.get("pattern")
|
|
93
|
+
if min_length is not None and len(value) < min_length:
|
|
94
|
+
issues.append(
|
|
95
|
+
_issue(
|
|
96
|
+
f"String too short (minLength {min_length})",
|
|
97
|
+
path=path,
|
|
98
|
+
kind=ValidationIssueKind.CONSTRAINT,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
if max_length is not None and len(value) > max_length:
|
|
102
|
+
issues.append(
|
|
103
|
+
_issue(
|
|
104
|
+
f"String too long (maxLength {max_length})",
|
|
105
|
+
path=path,
|
|
106
|
+
kind=ValidationIssueKind.CONSTRAINT,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
if pattern is not None and re.search(pattern, value) is None:
|
|
110
|
+
issues.append(
|
|
111
|
+
_issue(
|
|
112
|
+
f"String does not match pattern {pattern!r}",
|
|
113
|
+
path=path,
|
|
114
|
+
kind=ValidationIssueKind.CONSTRAINT,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
119
|
+
minimum = schema.get("minimum")
|
|
120
|
+
maximum = schema.get("maximum")
|
|
121
|
+
exclusive_minimum = schema.get("exclusiveMinimum")
|
|
122
|
+
exclusive_maximum = schema.get("exclusiveMaximum")
|
|
123
|
+
if minimum is not None and value < minimum:
|
|
124
|
+
issues.append(
|
|
125
|
+
_issue(
|
|
126
|
+
f"Value must be >= {minimum}", path=path, kind=ValidationIssueKind.CONSTRAINT
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
if maximum is not None and value > maximum:
|
|
130
|
+
issues.append(
|
|
131
|
+
_issue(
|
|
132
|
+
f"Value must be <= {maximum}", path=path, kind=ValidationIssueKind.CONSTRAINT
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
if exclusive_minimum is not None and value <= exclusive_minimum:
|
|
136
|
+
issues.append(
|
|
137
|
+
_issue(
|
|
138
|
+
f"Value must be > {exclusive_minimum}",
|
|
139
|
+
path=path,
|
|
140
|
+
kind=ValidationIssueKind.CONSTRAINT,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
if exclusive_maximum is not None and value >= exclusive_maximum:
|
|
144
|
+
issues.append(
|
|
145
|
+
_issue(
|
|
146
|
+
f"Value must be < {exclusive_maximum}",
|
|
147
|
+
path=path,
|
|
148
|
+
kind=ValidationIssueKind.CONSTRAINT,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return issues
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _validate_value(
|
|
156
|
+
value: Any, schema: dict[str, Any], path: tuple[str | int, ...] = ()
|
|
157
|
+
) -> list[ValidationIssue]:
|
|
158
|
+
if "anyOf" in schema:
|
|
159
|
+
branch_issues = [_validate_value(value, branch, path) for branch in schema["anyOf"]]
|
|
160
|
+
if any(not branch for branch in branch_issues):
|
|
161
|
+
return []
|
|
162
|
+
return [
|
|
163
|
+
_issue(
|
|
164
|
+
f"Value {_type_name(value)!r} does not match anyOf",
|
|
165
|
+
path=path,
|
|
166
|
+
kind=ValidationIssueKind.TYPE,
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
if "enum" in schema and value not in schema["enum"]:
|
|
171
|
+
return [
|
|
172
|
+
_issue(
|
|
173
|
+
f"Value {value!r} is not in enum {schema['enum']!r}",
|
|
174
|
+
path=path,
|
|
175
|
+
kind=ValidationIssueKind.ENUM,
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
json_type = schema.get("type")
|
|
180
|
+
if json_type and not _matches_type(value, json_type):
|
|
181
|
+
return [
|
|
182
|
+
_issue(
|
|
183
|
+
f"Expected {json_type}, got {_type_name(value)}",
|
|
184
|
+
path=path,
|
|
185
|
+
kind=ValidationIssueKind.TYPE,
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
issues = _validate_constraints(value, schema, path)
|
|
190
|
+
|
|
191
|
+
if json_type == "array" and "items" in schema and isinstance(value, list):
|
|
192
|
+
for index, item in enumerate(value):
|
|
193
|
+
issues.extend(_validate_value(item, schema["items"], path + (index,)))
|
|
194
|
+
|
|
195
|
+
if json_type == "object" and isinstance(value, dict):
|
|
196
|
+
properties = schema.get("properties", {})
|
|
197
|
+
required = set(schema.get("required", []))
|
|
198
|
+
|
|
199
|
+
if schema.get("additionalProperties") is False:
|
|
200
|
+
extra = set(value) - set(properties)
|
|
201
|
+
for key in sorted(extra):
|
|
202
|
+
issues.append(
|
|
203
|
+
_issue(
|
|
204
|
+
f"Additional property {key!r} is not allowed",
|
|
205
|
+
path=path + (key,),
|
|
206
|
+
kind=ValidationIssueKind.ADDITIONAL_PROPERTY,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
for key in sorted(required):
|
|
211
|
+
if key not in value:
|
|
212
|
+
prop_schema = properties.get(key, {})
|
|
213
|
+
if "default" not in prop_schema:
|
|
214
|
+
issues.append(
|
|
215
|
+
_issue(
|
|
216
|
+
f"Missing required property {key!r}",
|
|
217
|
+
path=path + (key,),
|
|
218
|
+
kind=ValidationIssueKind.REQUIRED,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for key, prop_schema in properties.items():
|
|
223
|
+
if key in value:
|
|
224
|
+
issues.extend(_validate_value(value[key], prop_schema, path + (key,)))
|
|
225
|
+
|
|
226
|
+
additional = schema.get("additionalProperties")
|
|
227
|
+
if isinstance(additional, dict):
|
|
228
|
+
for key, item in value.items():
|
|
229
|
+
if key not in properties:
|
|
230
|
+
issues.extend(_validate_value(item, additional, path + (key,)))
|
|
231
|
+
|
|
232
|
+
return issues
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def validate_arguments(args: Any, parameters_schema: dict[str, Any]) -> ValidationResult:
|
|
236
|
+
"""Validate tool arguments against a JSON Schema parameters object."""
|
|
237
|
+
if not isinstance(args, dict):
|
|
238
|
+
return ValidationFailure(
|
|
239
|
+
issues=(
|
|
240
|
+
_issue(
|
|
241
|
+
f"Arguments must be an object, got {_type_name(args)}",
|
|
242
|
+
kind=ValidationIssueKind.TYPE,
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if parameters_schema.get("type") != "object":
|
|
248
|
+
issues = _validate_value(args, parameters_schema)
|
|
249
|
+
return ValidationFailure(issues=tuple(issues)) if issues else ValidationSuccess(value=args)
|
|
250
|
+
|
|
251
|
+
properties = parameters_schema.get("properties", {})
|
|
252
|
+
required = set(parameters_schema.get("required", []))
|
|
253
|
+
issues: list[ValidationIssue] = []
|
|
254
|
+
|
|
255
|
+
for key in sorted(required):
|
|
256
|
+
if key not in args:
|
|
257
|
+
prop_schema = properties.get(key, {})
|
|
258
|
+
if "default" not in prop_schema:
|
|
259
|
+
issues.append(
|
|
260
|
+
_issue(
|
|
261
|
+
f"Missing required property {key!r}",
|
|
262
|
+
path=(key,),
|
|
263
|
+
kind=ValidationIssueKind.REQUIRED,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
normalized = dict(args)
|
|
268
|
+
for key, prop_schema in properties.items():
|
|
269
|
+
if key not in normalized and isinstance(prop_schema, dict) and "default" in prop_schema:
|
|
270
|
+
normalized[key] = prop_schema["default"]
|
|
271
|
+
|
|
272
|
+
if parameters_schema.get("additionalProperties") is False:
|
|
273
|
+
extra = set(normalized) - set(properties)
|
|
274
|
+
for key in sorted(extra):
|
|
275
|
+
issues.append(
|
|
276
|
+
_issue(
|
|
277
|
+
f"Additional property {key!r} is not allowed",
|
|
278
|
+
path=(key,),
|
|
279
|
+
kind=ValidationIssueKind.ADDITIONAL_PROPERTY,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
for key, prop_schema in properties.items():
|
|
284
|
+
if key in normalized:
|
|
285
|
+
issues.extend(_validate_value(normalized[key], prop_schema, (key,)))
|
|
286
|
+
|
|
287
|
+
if issues:
|
|
288
|
+
return ValidationFailure(issues=tuple(issues))
|
|
289
|
+
return ValidationSuccess(value=normalized)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _resolve_ref(ref: str, defs: dict[str, Any]) -> dict[str, Any]:
|
|
8
|
+
if not ref.startswith("#/$defs/"):
|
|
9
|
+
raise ValueError(f"Unsupported $ref format: {ref!r}")
|
|
10
|
+
key = ref.removeprefix("#/$defs/")
|
|
11
|
+
if key not in defs:
|
|
12
|
+
raise ValueError(f"Unresolved $ref: {ref!r}")
|
|
13
|
+
return copy.deepcopy(defs[key])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _inline_node(node: Any, defs: dict[str, Any], resolving: set[str]) -> Any:
|
|
17
|
+
if isinstance(node, dict):
|
|
18
|
+
if "$ref" in node:
|
|
19
|
+
ref = node["$ref"]
|
|
20
|
+
key = ref.removeprefix("#/$defs/")
|
|
21
|
+
if key in resolving:
|
|
22
|
+
raise ValueError(f"Circular $ref detected: {ref!r}")
|
|
23
|
+
resolving = resolving | {key}
|
|
24
|
+
resolved = _inline_node(_resolve_ref(ref, defs), defs, resolving)
|
|
25
|
+
extras = {k: v for k, v in node.items() if k != "$ref"}
|
|
26
|
+
if extras:
|
|
27
|
+
if not isinstance(resolved, dict):
|
|
28
|
+
return resolved
|
|
29
|
+
return {**resolved, **extras}
|
|
30
|
+
return resolved
|
|
31
|
+
|
|
32
|
+
return {k: _inline_node(v, defs, resolving) for k, v in node.items()}
|
|
33
|
+
|
|
34
|
+
if isinstance(node, list):
|
|
35
|
+
return [_inline_node(item, defs, resolving) for item in node]
|
|
36
|
+
|
|
37
|
+
return node
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def inline_refs(schema: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
"""Flatten JSON Schema $ref pointers using local $defs."""
|
|
42
|
+
cloned = copy.deepcopy(schema)
|
|
43
|
+
defs = cloned.pop("$defs", {})
|
|
44
|
+
if not defs:
|
|
45
|
+
return cloned
|
|
46
|
+
return _inline_node(cloned, defs, set())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from toolschema._ir import ToolDefinition
|
|
7
|
+
from toolschema._schema_utils import strip_canonical_meta
|
|
8
|
+
|
|
9
|
+
_CONSTRAINT_KEYS = frozenset(
|
|
10
|
+
{
|
|
11
|
+
"minLength",
|
|
12
|
+
"maxLength",
|
|
13
|
+
"minimum",
|
|
14
|
+
"maximum",
|
|
15
|
+
"exclusiveMinimum",
|
|
16
|
+
"exclusiveMaximum",
|
|
17
|
+
"pattern",
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _constraint_to_description(prop: dict[str, Any]) -> str | None:
|
|
23
|
+
parts: list[str] = []
|
|
24
|
+
if "minLength" in prop:
|
|
25
|
+
parts.append(f"min length {prop['minLength']}")
|
|
26
|
+
if "maxLength" in prop:
|
|
27
|
+
parts.append(f"max length {prop['maxLength']}")
|
|
28
|
+
if "minimum" in prop:
|
|
29
|
+
parts.append(f"minimum {prop['minimum']}")
|
|
30
|
+
if "maximum" in prop:
|
|
31
|
+
parts.append(f"maximum {prop['maximum']}")
|
|
32
|
+
if "exclusiveMinimum" in prop:
|
|
33
|
+
parts.append(f"exclusive minimum {prop['exclusiveMinimum']}")
|
|
34
|
+
if "exclusiveMaximum" in prop:
|
|
35
|
+
parts.append(f"exclusive maximum {prop['exclusiveMaximum']}")
|
|
36
|
+
if "pattern" in prop:
|
|
37
|
+
parts.append(f"pattern {prop['pattern']!r}")
|
|
38
|
+
return "; ".join(parts) if parts else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _adapt_property(prop: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
result = copy.deepcopy(prop)
|
|
43
|
+
constraint_desc = _constraint_to_description(result)
|
|
44
|
+
if constraint_desc:
|
|
45
|
+
existing = result.get("description", "")
|
|
46
|
+
suffix = f" ({constraint_desc})"
|
|
47
|
+
result["description"] = f"{existing}{suffix}" if existing else constraint_desc
|
|
48
|
+
for key in _CONSTRAINT_KEYS:
|
|
49
|
+
result.pop(key, None)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _adapt_input_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
result = strip_canonical_meta(schema)
|
|
55
|
+
properties = result.get("properties")
|
|
56
|
+
if isinstance(properties, dict):
|
|
57
|
+
result["properties"] = {k: _adapt_property(v) for k, v in properties.items()}
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def to_anthropic(tool: ToolDefinition) -> dict[str, Any]:
|
|
62
|
+
"""Convert ToolDefinition to Anthropic tool format."""
|
|
63
|
+
return {
|
|
64
|
+
"name": tool.name,
|
|
65
|
+
"description": tool.description,
|
|
66
|
+
"input_schema": _adapt_input_schema(tool.parameters),
|
|
67
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from toolschema._ir import ToolDefinition
|
|
7
|
+
|
|
8
|
+
_TYPE_MAP = {
|
|
9
|
+
"string": "STRING",
|
|
10
|
+
"integer": "INTEGER",
|
|
11
|
+
"number": "NUMBER",
|
|
12
|
+
"boolean": "BOOLEAN",
|
|
13
|
+
"array": "ARRAY",
|
|
14
|
+
"object": "OBJECT",
|
|
15
|
+
"null": "NULL",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _strip_canonical_meta(schema: dict[str, Any]) -> dict[str, Any]:
|
|
20
|
+
result = copy.deepcopy(schema)
|
|
21
|
+
result.pop("$schema", None)
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _convert_type(schema: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
result: dict[str, Any] = {}
|
|
27
|
+
for key, value in schema.items():
|
|
28
|
+
if key == "type" and isinstance(value, str):
|
|
29
|
+
result["type"] = _TYPE_MAP.get(value, value.upper())
|
|
30
|
+
elif key == "properties" and isinstance(value, dict):
|
|
31
|
+
result["properties"] = {k: _convert_schema(v) for k, v in value.items()}
|
|
32
|
+
elif key == "items" and isinstance(value, dict):
|
|
33
|
+
result["items"] = _convert_schema(value)
|
|
34
|
+
elif key == "additionalProperties" and isinstance(value, dict):
|
|
35
|
+
result["additionalProperties"] = _convert_schema(value)
|
|
36
|
+
elif key == "anyOf" and isinstance(value, list):
|
|
37
|
+
result["anyOf"] = [_convert_schema(item) for item in value]
|
|
38
|
+
else:
|
|
39
|
+
result[key] = value
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _convert_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
return _convert_type(copy.deepcopy(schema))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_gemini(tool: ToolDefinition) -> dict[str, Any]:
|
|
48
|
+
"""Convert ToolDefinition to Gemini FunctionDeclaration format."""
|
|
49
|
+
parameters = _convert_schema(_strip_canonical_meta(tool.parameters))
|
|
50
|
+
if "type" not in parameters:
|
|
51
|
+
parameters["type"] = "OBJECT"
|
|
52
|
+
|
|
53
|
+
result: dict[str, Any] = {
|
|
54
|
+
"name": tool.name,
|
|
55
|
+
"description": tool.description,
|
|
56
|
+
"parameters": parameters,
|
|
57
|
+
}
|
|
58
|
+
return result
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from toolschema._ir import ToolDefinition
|
|
6
|
+
from toolschema._schema_utils import strip_canonical_meta
|
|
7
|
+
from toolschema.adapters._inline_refs import inline_refs as resolve_inline_refs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_mcp(tool: ToolDefinition, *, inline_refs: bool = True) -> dict[str, Any]:
|
|
11
|
+
"""Convert ToolDefinition to MCP tools/list format."""
|
|
12
|
+
input_schema = strip_canonical_meta(tool.parameters)
|
|
13
|
+
if inline_refs:
|
|
14
|
+
input_schema = resolve_inline_refs(input_schema)
|
|
15
|
+
|
|
16
|
+
result: dict[str, Any] = {
|
|
17
|
+
"name": tool.name,
|
|
18
|
+
"description": tool.description,
|
|
19
|
+
"inputSchema": input_schema,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if tool.output is not None:
|
|
23
|
+
output_schema = strip_canonical_meta(tool.output)
|
|
24
|
+
if inline_refs:
|
|
25
|
+
output_schema = resolve_inline_refs(output_schema)
|
|
26
|
+
result["outputSchema"] = output_schema
|
|
27
|
+
|
|
28
|
+
return result
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from toolschema._ir import ToolDefinition
|
|
7
|
+
from toolschema._schema_utils import strip_canonical_meta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _apply_strict(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
11
|
+
params = copy.deepcopy(parameters)
|
|
12
|
+
params["additionalProperties"] = False
|
|
13
|
+
properties = params.get("properties", {})
|
|
14
|
+
if properties:
|
|
15
|
+
params["required"] = list(properties.keys())
|
|
16
|
+
return params
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_openai(tool: ToolDefinition, *, strict: bool = False) -> dict[str, Any]:
|
|
20
|
+
"""Convert ToolDefinition to OpenAI function-calling tool format."""
|
|
21
|
+
parameters = strip_canonical_meta(tool.parameters)
|
|
22
|
+
if strict:
|
|
23
|
+
parameters = _apply_strict(parameters)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"type": "function",
|
|
27
|
+
"function": {
|
|
28
|
+
"name": tool.name,
|
|
29
|
+
"description": tool.description,
|
|
30
|
+
"parameters": parameters,
|
|
31
|
+
},
|
|
32
|
+
}
|