schemashim 0.1.0__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ node_modules/
11
+ coverage/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-10)
4
+
5
+ First release.
6
+
7
+ - `adapt(schema, "gemini" | "openai-strict")`: rewrites a tool's JSON Schema into the subset the
8
+ provider accepts and lists every change. Inlines `$ref`/`$defs`, turns null branches and type
9
+ lists into `nullable` (Gemini) or `[T, "null"]` (OpenAI), merges `allOf`, changes `oneOf` to
10
+ `anyOf`, closes objects and makes every property required for strict mode, infers missing
11
+ types, fixes `required` lists, drops empty enum values, and moves constraints the provider
12
+ rejects (`format`, `exclusiveMinimum`, `minLength`, `default`, regex lookarounds...) into the
13
+ description. Shapes the provider can't express (recursive schemas, free-form objects, arrays
14
+ without `items`) become JSON-string fields.
15
+ - `Shim.restore(args)`: maps the model's arguments back: decodes JSON-string fields, drops the
16
+ nulls strict mode forces for optional fields, turns `5.0` into `5` for integers.
17
+ - `adapt_tools(tools, target)` / `ToolKit`: adapts MCP `tools/list` entries or `mcp.types.Tool`
18
+ objects into Gemini `functionDeclarations` or OpenAI `tools`, with valid, unique tool names,
19
+ and maps calls back with `kit.restore(name, args)`.
20
+ - `tool_name(name, target)`: valid tool names for each provider.
21
+ - CLI: `schemashim check FILE` (exit 1 on problems, `--json`) and `schemashim fix FILE --target`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sri Krishna Lingamaneni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.5
2
+ Name: schemashim
3
+ Version: 0.1.0
4
+ Summary: Fix MCP and agent tool JSON Schemas that Gemini and OpenAI strict mode reject ($ref, anyOf null, type lists, arrays without items, enums), and map the model's arguments back
5
+ Project-URL: Homepage, https://github.com/srikrishnaL/kitforge/tree/main/packages/schemashim
6
+ Project-URL: Source, https://github.com/srikrishnaL/kitforge
7
+ Project-URL: Issues, https://github.com/srikrishnaL/kitforge/issues
8
+ Project-URL: Changelog, https://github.com/srikrishnaL/kitforge/blob/main/packages/schemashim/CHANGELOG.md
9
+ Author: Sri Krishna Lingamaneni
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,function calling,function declarations,gemini,invalid_argument,json schema,llm,mcp,model context protocol,openai,strict mode,structured outputs,tool calling,vertex ai
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.9
21
+ Provides-Extra: test
22
+ Requires-Dist: pydantic>=2; extra == 'test'
23
+ Requires-Dist: pytest>=7; extra == 'test'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # schemashim
27
+
28
+ Make MCP and agent tool schemas work on Gemini and OpenAI strict mode, then turn the model's
29
+ arguments back into what the tool expects.
30
+
31
+ Gemini and OpenAI's `strict: true` function calling accept only a subset of JSON Schema, so one
32
+ ordinary tool (a pydantic `$ref`, `Optional[int]`, an array without `items`, an `enum` with `""`,
33
+ an untyped field) makes **every** request fail with `400 INVALID_ARGUMENT` or `Invalid schema`.
34
+ schemashim rewrites a schema into the subset each provider accepts, says what it changed, and
35
+ undoes the changes on the arguments the model sends back.
36
+
37
+ ```bash
38
+ pip install schemashim
39
+ ```
40
+
41
+ Python 3.9+, no dependencies.
42
+
43
+ ## Fix one schema
44
+
45
+ ```python
46
+ from schemashim import adapt
47
+
48
+ schema = {
49
+ "type": "object",
50
+ "$defs": {"Point": {"type": "object", "required": ["lat", "lon"],
51
+ "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}}},
52
+ "properties": {
53
+ "start": {"$ref": "#/$defs/Point"},
54
+ "limit": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": None},
55
+ "tags": {"type": "array"},
56
+ },
57
+ "required": ["start"],
58
+ }
59
+
60
+ shim = adapt(schema, "gemini") # or "openai-strict"
61
+ shim.schema # send this as the function's parameters
62
+ for change in shim.changes:
63
+ print(change)
64
+ # start: inlined $ref #/$defs/Point (references are not supported)
65
+ # limit: null branch of anyOf changed to nullable
66
+ # tags: array without an items type: passed as a JSON string
67
+
68
+ # When the model calls the tool, map its arguments back before calling the real tool:
69
+ shim.restore({"start": {"lat": 1, "lon": 2}, "tags": '["a", "b"]'})
70
+ # {'start': {'lat': 1, 'lon': 2}, 'tags': ['a', 'b']}
71
+ ```
72
+
73
+ `shim.ok` is `True` when the original was already accepted as is; `check(schema, target)`
74
+ returns only the list of changes.
75
+
76
+ ## Bridge MCP tools to Gemini or OpenAI
77
+
78
+ ```python
79
+ from schemashim import adapt_tools
80
+
81
+ tools = (await session.list_tools()).tools # mcp.types.Tool objects, or dicts
82
+ kit = adapt_tools(tools, "openai-strict") # or "gemini"
83
+
84
+ response = client.chat.completions.create(model=..., messages=..., tools=kit.declarations)
85
+ for call in response.choices[0].message.tool_calls or []:
86
+ name, args = kit.restore(call.function.name, json.loads(call.function.arguments))
87
+ result = await session.call_tool(name, args)
88
+ ```
89
+
90
+ For Gemini, `kit.declarations` are `functionDeclarations`:
91
+ `types.Tool(function_declarations=kit.declarations)` in `google-genai`, or
92
+ `{"functionDeclarations": kit.declarations}` over REST. For the OpenAI Responses API use
93
+ `[{"type": "function", **t.function} for t in kit]`.
94
+
95
+ Tool names are fixed too: `github/search_issues` becomes `github_search_issues`, names over 64
96
+ characters get a short hash, and two tools never end up with the same name. `kit.restore` maps
97
+ the name back.
98
+
99
+ ## What it changes
100
+
101
+ | Problem in the schema | Gemini | OpenAI strict |
102
+ | --- | --- | --- |
103
+ | `$ref` / `$defs` / `definitions` | inlined | inlined |
104
+ | Recursive `$ref` (a tree) | the recursive field is a JSON string | same |
105
+ | `anyOf: [X, {type: null}]`, `type: [X, "null"]`, OpenAPI `nullable` | `nullable: true` | `type: [X, "null"]` |
106
+ | `type: [A, B]`, `oneOf` | `anyOf` | `anyOf` |
107
+ | `allOf` | merged | merged |
108
+ | Optional properties | kept optional | made required and nullable (strict mode needs every property in `required`); `restore` drops the nulls |
109
+ | `additionalProperties` | removed | set to `false` on every object |
110
+ | Array without `items`, object without `properties`, untyped "any" field, tuple arrays | sent as a JSON string; `restore` decodes it | same |
111
+ | `enum` with `""` or non-strings | `""` dropped; number enums move to the description | kept |
112
+ | `const` | one-value `enum` | kept |
113
+ | Missing `type` | inferred from `properties`, `items`, `enum`, bounds | same |
114
+ | `required` naming undefined properties, `"required": true` on a property | fixed | fixed |
115
+ | Root not an object, root `anyOf` of objects | made an object / merged | same |
116
+ | `format`, `exclusiveMinimum`, `multipleOf`, `minLength`, `default`, regex lookarounds... that the target rejects | written into the description so the model still sees them | same |
117
+ | `$schema`, `$id`, `not`, `if`/`then`, `patternProperties`... | removed | removed |
118
+
119
+ `restore` also turns `5.0` back into `5` for integer fields (Gemini sends numbers as floats).
120
+
121
+ ## In CI
122
+
123
+ ```bash
124
+ schemashim check tools.json # exit 1 if Gemini or OpenAI strict would reject a tool
125
+ schemashim check tools.json --target gemini --json
126
+ schemashim fix tools.json --target openai-strict > tools.openai.json
127
+ ```
128
+
129
+ The file can be a schema, one tool, a list of tools, a `tools/list` response or an
130
+ [mcptest](../mcptest/) snapshot, and `-` reads stdin. To check a running MCP server:
131
+
132
+ ```bash
133
+ mcptest tools -- python my_server.py | schemashim check -
134
+ ```
135
+
136
+ ## Limits
137
+
138
+ - A JSON-string field is a workaround: the model sees "JSON-encoded value" instead of the real
139
+ shape, so describe what goes in it. It only happens where the provider cannot express the
140
+ shape at all.
141
+ - Strict mode can't express "optional": the model sends `null`, and `restore` removes it. If a
142
+ field was already nullable, a `null` is kept, because it may be meaningful.
143
+ - Merging a root `anyOf` makes fields that are not in every choice optional; the tool must still
144
+ validate which combination is valid.
145
+ - Gemini's newer `parametersJsonSchema` field accepts more of JSON Schema. schemashim targets the
146
+ classic `parameters` field that most clients and SDKs send, and its output is valid there too.
147
+ - Only local `#/$defs/...` and `#/definitions/...` references are followed; remote ones become
148
+ JSON-string fields.
149
+ - Provider rules change. If a schema still gets rejected, open an issue with the error text.
150
+
151
+ ## Why this exists
152
+
153
+ Every MCP client and agent framework that talks to Gemini or OpenAI strict mode hits this and
154
+ writes its own partial sanitizer. A few of the reports:
155
+
156
+ - [google-gemini/gemini-cli#13326](https://github.com/google-gemini/gemini-cli/issues/13326): Gemini API rejects MCP tool schemas with `$defs` references
157
+ - [modelcontextprotocol/servers#1624](https://github.com/modelcontextprotocol/servers/issues/1624): mcp-server-fetch's schema can't be used with Gemini 2.5 Pro
158
+ - [Portkey-AI/gateway#1203](https://github.com/Portkey-AI/gateway/issues/1203): not converting JSON Schema to Gemini compatible tool definitions
159
+ - [chatboxai/chatbox#2606](https://github.com/chatboxai/chatbox/issues/2606): Gemini tool integration fails with `INVALID_ARGUMENT`
160
+ - [langchain-ai/langchain-google#535](https://github.com/langchain-ai/langchain-google/issues/535): "should be non-empty for OBJECT type"
161
+ - [BerriAI/litellm#18442](https://github.com/BerriAI/litellm/issues/18442): "functionDeclaration parameters schema should be of type OBJECT"
162
+ - [Kilo-Org/kilocode#15512](https://github.com/Kilo-Org/kilocode/issues/15512), [cline/cline#11162](https://github.com/cline/cline/issues/11162), [CherryHQ/cherry-studio#18890](https://github.com/CherryHQ/cherry-studio/issues/18890), [anomalyco/opencode#48073](https://github.com/anomalyco/opencode/issues/48073): empty enums, undefined required properties, arrays without items, nullable arrays
163
+ - [openclaw/openclaw#166877](https://github.com/openclaw/openclaw/issues/166877), [NousResearch/hermes-agent#104796](https://github.com/NousResearch/hermes-agent/issues/104796), [cloudflare/mcp-server-cloudflare#378](https://github.com/cloudflare/mcp-server-cloudflare/issues/378): one MCP tool breaks every strict-mode request
164
+
165
+ GitHub search finds over 300 issues titled "tool schema" + Gemini alone. The demand evidence
166
+ is in [`demand/`](../../demand/) (idea `tool-schema-compat`).
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,145 @@
1
+ # schemashim
2
+
3
+ Make MCP and agent tool schemas work on Gemini and OpenAI strict mode, then turn the model's
4
+ arguments back into what the tool expects.
5
+
6
+ Gemini and OpenAI's `strict: true` function calling accept only a subset of JSON Schema, so one
7
+ ordinary tool (a pydantic `$ref`, `Optional[int]`, an array without `items`, an `enum` with `""`,
8
+ an untyped field) makes **every** request fail with `400 INVALID_ARGUMENT` or `Invalid schema`.
9
+ schemashim rewrites a schema into the subset each provider accepts, says what it changed, and
10
+ undoes the changes on the arguments the model sends back.
11
+
12
+ ```bash
13
+ pip install schemashim
14
+ ```
15
+
16
+ Python 3.9+, no dependencies.
17
+
18
+ ## Fix one schema
19
+
20
+ ```python
21
+ from schemashim import adapt
22
+
23
+ schema = {
24
+ "type": "object",
25
+ "$defs": {"Point": {"type": "object", "required": ["lat", "lon"],
26
+ "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}}},
27
+ "properties": {
28
+ "start": {"$ref": "#/$defs/Point"},
29
+ "limit": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": None},
30
+ "tags": {"type": "array"},
31
+ },
32
+ "required": ["start"],
33
+ }
34
+
35
+ shim = adapt(schema, "gemini") # or "openai-strict"
36
+ shim.schema # send this as the function's parameters
37
+ for change in shim.changes:
38
+ print(change)
39
+ # start: inlined $ref #/$defs/Point (references are not supported)
40
+ # limit: null branch of anyOf changed to nullable
41
+ # tags: array without an items type: passed as a JSON string
42
+
43
+ # When the model calls the tool, map its arguments back before calling the real tool:
44
+ shim.restore({"start": {"lat": 1, "lon": 2}, "tags": '["a", "b"]'})
45
+ # {'start': {'lat': 1, 'lon': 2}, 'tags': ['a', 'b']}
46
+ ```
47
+
48
+ `shim.ok` is `True` when the original was already accepted as is; `check(schema, target)`
49
+ returns only the list of changes.
50
+
51
+ ## Bridge MCP tools to Gemini or OpenAI
52
+
53
+ ```python
54
+ from schemashim import adapt_tools
55
+
56
+ tools = (await session.list_tools()).tools # mcp.types.Tool objects, or dicts
57
+ kit = adapt_tools(tools, "openai-strict") # or "gemini"
58
+
59
+ response = client.chat.completions.create(model=..., messages=..., tools=kit.declarations)
60
+ for call in response.choices[0].message.tool_calls or []:
61
+ name, args = kit.restore(call.function.name, json.loads(call.function.arguments))
62
+ result = await session.call_tool(name, args)
63
+ ```
64
+
65
+ For Gemini, `kit.declarations` are `functionDeclarations`:
66
+ `types.Tool(function_declarations=kit.declarations)` in `google-genai`, or
67
+ `{"functionDeclarations": kit.declarations}` over REST. For the OpenAI Responses API use
68
+ `[{"type": "function", **t.function} for t in kit]`.
69
+
70
+ Tool names are fixed too: `github/search_issues` becomes `github_search_issues`, names over 64
71
+ characters get a short hash, and two tools never end up with the same name. `kit.restore` maps
72
+ the name back.
73
+
74
+ ## What it changes
75
+
76
+ | Problem in the schema | Gemini | OpenAI strict |
77
+ | --- | --- | --- |
78
+ | `$ref` / `$defs` / `definitions` | inlined | inlined |
79
+ | Recursive `$ref` (a tree) | the recursive field is a JSON string | same |
80
+ | `anyOf: [X, {type: null}]`, `type: [X, "null"]`, OpenAPI `nullable` | `nullable: true` | `type: [X, "null"]` |
81
+ | `type: [A, B]`, `oneOf` | `anyOf` | `anyOf` |
82
+ | `allOf` | merged | merged |
83
+ | Optional properties | kept optional | made required and nullable (strict mode needs every property in `required`); `restore` drops the nulls |
84
+ | `additionalProperties` | removed | set to `false` on every object |
85
+ | Array without `items`, object without `properties`, untyped "any" field, tuple arrays | sent as a JSON string; `restore` decodes it | same |
86
+ | `enum` with `""` or non-strings | `""` dropped; number enums move to the description | kept |
87
+ | `const` | one-value `enum` | kept |
88
+ | Missing `type` | inferred from `properties`, `items`, `enum`, bounds | same |
89
+ | `required` naming undefined properties, `"required": true` on a property | fixed | fixed |
90
+ | Root not an object, root `anyOf` of objects | made an object / merged | same |
91
+ | `format`, `exclusiveMinimum`, `multipleOf`, `minLength`, `default`, regex lookarounds... that the target rejects | written into the description so the model still sees them | same |
92
+ | `$schema`, `$id`, `not`, `if`/`then`, `patternProperties`... | removed | removed |
93
+
94
+ `restore` also turns `5.0` back into `5` for integer fields (Gemini sends numbers as floats).
95
+
96
+ ## In CI
97
+
98
+ ```bash
99
+ schemashim check tools.json # exit 1 if Gemini or OpenAI strict would reject a tool
100
+ schemashim check tools.json --target gemini --json
101
+ schemashim fix tools.json --target openai-strict > tools.openai.json
102
+ ```
103
+
104
+ The file can be a schema, one tool, a list of tools, a `tools/list` response or an
105
+ [mcptest](../mcptest/) snapshot, and `-` reads stdin. To check a running MCP server:
106
+
107
+ ```bash
108
+ mcptest tools -- python my_server.py | schemashim check -
109
+ ```
110
+
111
+ ## Limits
112
+
113
+ - A JSON-string field is a workaround: the model sees "JSON-encoded value" instead of the real
114
+ shape, so describe what goes in it. It only happens where the provider cannot express the
115
+ shape at all.
116
+ - Strict mode can't express "optional": the model sends `null`, and `restore` removes it. If a
117
+ field was already nullable, a `null` is kept, because it may be meaningful.
118
+ - Merging a root `anyOf` makes fields that are not in every choice optional; the tool must still
119
+ validate which combination is valid.
120
+ - Gemini's newer `parametersJsonSchema` field accepts more of JSON Schema. schemashim targets the
121
+ classic `parameters` field that most clients and SDKs send, and its output is valid there too.
122
+ - Only local `#/$defs/...` and `#/definitions/...` references are followed; remote ones become
123
+ JSON-string fields.
124
+ - Provider rules change. If a schema still gets rejected, open an issue with the error text.
125
+
126
+ ## Why this exists
127
+
128
+ Every MCP client and agent framework that talks to Gemini or OpenAI strict mode hits this and
129
+ writes its own partial sanitizer. A few of the reports:
130
+
131
+ - [google-gemini/gemini-cli#13326](https://github.com/google-gemini/gemini-cli/issues/13326): Gemini API rejects MCP tool schemas with `$defs` references
132
+ - [modelcontextprotocol/servers#1624](https://github.com/modelcontextprotocol/servers/issues/1624): mcp-server-fetch's schema can't be used with Gemini 2.5 Pro
133
+ - [Portkey-AI/gateway#1203](https://github.com/Portkey-AI/gateway/issues/1203): not converting JSON Schema to Gemini compatible tool definitions
134
+ - [chatboxai/chatbox#2606](https://github.com/chatboxai/chatbox/issues/2606): Gemini tool integration fails with `INVALID_ARGUMENT`
135
+ - [langchain-ai/langchain-google#535](https://github.com/langchain-ai/langchain-google/issues/535): "should be non-empty for OBJECT type"
136
+ - [BerriAI/litellm#18442](https://github.com/BerriAI/litellm/issues/18442): "functionDeclaration parameters schema should be of type OBJECT"
137
+ - [Kilo-Org/kilocode#15512](https://github.com/Kilo-Org/kilocode/issues/15512), [cline/cline#11162](https://github.com/cline/cline/issues/11162), [CherryHQ/cherry-studio#18890](https://github.com/CherryHQ/cherry-studio/issues/18890), [anomalyco/opencode#48073](https://github.com/anomalyco/opencode/issues/48073): empty enums, undefined required properties, arrays without items, nullable arrays
138
+ - [openclaw/openclaw#166877](https://github.com/openclaw/openclaw/issues/166877), [NousResearch/hermes-agent#104796](https://github.com/NousResearch/hermes-agent/issues/104796), [cloudflare/mcp-server-cloudflare#378](https://github.com/cloudflare/mcp-server-cloudflare/issues/378): one MCP tool breaks every strict-mode request
139
+
140
+ GitHub search finds over 300 issues titled "tool schema" + Gemini alone. The demand evidence
141
+ is in [`demand/`](../../demand/) (idea `tool-schema-compat`).
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "schemashim"
7
+ version = "0.1.0"
8
+ description = "Fix MCP and agent tool JSON Schemas that Gemini and OpenAI strict mode reject ($ref, anyOf null, type lists, arrays without items, enums), and map the model's arguments back"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Sri Krishna Lingamaneni" }]
14
+ keywords = [
15
+ "json schema", "gemini", "vertex ai", "openai", "structured outputs", "strict mode",
16
+ "function calling", "tool calling", "mcp", "model context protocol", "invalid_argument",
17
+ "function declarations", "llm", "agents",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Topic :: Software Development :: Libraries",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ ]
28
+ dependencies = []
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=7", "pydantic>=2"]
32
+
33
+ [project.scripts]
34
+ schemashim = "schemashim.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/srikrishnaL/kitforge/tree/main/packages/schemashim"
38
+ Source = "https://github.com/srikrishnaL/kitforge"
39
+ Issues = "https://github.com/srikrishnaL/kitforge/issues"
40
+ Changelog = "https://github.com/srikrishnaL/kitforge/blob/main/packages/schemashim/CHANGELOG.md"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/schemashim"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,12 @@
1
+ """schemashim: make MCP and agent tool schemas work on Gemini and OpenAI strict mode."""
2
+
3
+ from .core import GEMINI, OPENAI_STRICT, TARGETS, Change, Shim, adapt, check
4
+ from .names import tool_name
5
+ from .tools import AdaptedTool, ToolKit, adapt_tool, adapt_tools
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ __all__ = [
10
+ "GEMINI", "OPENAI_STRICT", "TARGETS", "Change", "Shim", "adapt", "check", "tool_name",
11
+ "AdaptedTool", "ToolKit", "adapt_tool", "adapt_tools", "__version__",
12
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ """schemashim check / fix: find and fix tool schemas a provider would reject."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any, List, Optional
9
+
10
+ from . import __version__
11
+ from .core import TARGETS, adapt
12
+ from .tools import ToolKit
13
+
14
+
15
+ def _load(path: str) -> Any:
16
+ try:
17
+ if path == "-":
18
+ return json.load(sys.stdin)
19
+ with open(path, encoding="utf-8") as f:
20
+ return json.load(f)
21
+ except OSError as exc:
22
+ raise SystemExit(f"schemashim: can't read {path}: {exc.strerror or exc}")
23
+ except ValueError as exc:
24
+ raise SystemExit(f"schemashim: {path} is not valid JSON: {exc}")
25
+
26
+
27
+ def _tools(data: Any) -> Optional[list]:
28
+ """The list of tools in ``data``, or None when ``data`` is a bare schema."""
29
+ if isinstance(data, list):
30
+ return data
31
+ if isinstance(data, dict):
32
+ result = data.get("result") if isinstance(data.get("result"), dict) else data
33
+ if isinstance(result.get("tools"), list):
34
+ return result["tools"]
35
+ if isinstance(result.get("tools"), dict): # mcptest snapshot: {"tools": {name: tool}}
36
+ return [{"name": k, **v} for k, v in result["tools"].items() if isinstance(v, dict)]
37
+ if "name" in data and ("inputSchema" in data or "input_schema" in data):
38
+ return [data]
39
+ return None
40
+
41
+
42
+ def main(argv: Optional[List[str]] = None) -> int:
43
+ parser = argparse.ArgumentParser(
44
+ prog="schemashim",
45
+ description="Find and fix JSON Schemas that Gemini or OpenAI strict mode reject. "
46
+ "FILE is a schema, a tool, a list of tools or a tools/list result "
47
+ "('-' reads stdin).")
48
+ parser.add_argument("--version", action="version", version=f"schemashim {__version__}")
49
+ sub = parser.add_subparsers(dest="command", required=True)
50
+ c = sub.add_parser("check", help="list what each target would reject; exit 1 if anything")
51
+ c.add_argument("file")
52
+ c.add_argument("--target", choices=TARGETS + ("all",), default="all")
53
+ c.add_argument("--json", action="store_true", help="print the findings as JSON")
54
+ f = sub.add_parser("fix", help="print the adapted schema or tool declarations")
55
+ f.add_argument("file")
56
+ f.add_argument("--target", choices=TARGETS, required=True)
57
+ args = parser.parse_args(argv)
58
+
59
+ data = _load(args.file)
60
+ tools = _tools(data)
61
+ try:
62
+ if args.command == "fix":
63
+ if tools is None:
64
+ out: Any = adapt(data, args.target).schema
65
+ else:
66
+ out = ToolKit(tools, args.target).declarations
67
+ json.dump(out, sys.stdout, indent=2)
68
+ sys.stdout.write("\n")
69
+ return 0
70
+
71
+ targets = TARGETS if args.target == "all" else (args.target,)
72
+ findings = []
73
+ for target in targets:
74
+ if tools is None:
75
+ findings += [{"target": target, "tool": None, "path": c.path,
76
+ "message": c.message} for c in adapt(data, target).changes]
77
+ else:
78
+ for tool in ToolKit(tools, target):
79
+ findings += [{"target": target, "tool": tool.original_name, "path": c.path,
80
+ "message": c.message} for c in tool.changes]
81
+ except (TypeError, ValueError) as exc:
82
+ print(f"schemashim: {exc}", file=sys.stderr)
83
+ return 2
84
+
85
+ if args.json:
86
+ json.dump(findings, sys.stdout, indent=2)
87
+ sys.stdout.write("\n")
88
+ else:
89
+ for item in findings:
90
+ where = f"{item['tool']}: " if item["tool"] else ""
91
+ print(f"[{item['target']}] {where}{item['path'] or '(root)'}: {item['message']}")
92
+ if findings:
93
+ n = len(findings)
94
+ print(f"{n} problem{'s' if n != 1 else ''} found; `schemashim fix` rewrites them.")
95
+ else:
96
+ print("OK: accepted as is by " + ", ".join(targets))
97
+ return 1 if findings else 0