Skip to main content

schemashim

Make MCP and agent tool schemas work on Gemini and OpenAI strict mode, then turn the model's arguments back into what the tool expects.

Gemini and OpenAI's strict: true function calling accept only a subset of JSON Schema, so one ordinary tool (a pydantic $ref, Optional[int], an array without items, an enum with "", an untyped field) makes every request fail with 400 INVALID_ARGUMENT or Invalid schema. schemashim rewrites a schema into the subset each provider accepts, says what it changed, and undoes the changes on the arguments the model sends back.

pip install schemashim

Python 3.9+, no dependencies.

Fix one schema

from schemashim import adapt

schema = {
    "type": "object",
    "$defs": {"Point": {"type": "object", "required": ["lat", "lon"],
                        "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}}},
    "properties": {
        "start": {"$ref": "#/$defs/Point"},
        "limit": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": None},
        "tags": {"type": "array"},
    },
    "required": ["start"],
}

shim = adapt(schema, "gemini")          # or "openai-strict"
shim.schema      # send this as the function's parameters
for change in shim.changes:
    print(change)
# start: inlined $ref #/$defs/Point (references are not supported)
# limit: null branch of anyOf changed to nullable
# tags: array without an items type: passed as a JSON string

# When the model calls the tool, map its arguments back before calling the real tool:
shim.restore({"start": {"lat": 1, "lon": 2}, "tags": '["a", "b"]'})
# {'start': {'lat': 1, 'lon': 2}, 'tags': ['a', 'b']}

shim.ok is True when the original was already accepted as is; check(schema, target) returns only the list of changes.

Bridge MCP tools to Gemini or OpenAI

from schemashim import adapt_tools

tools = (await session.list_tools()).tools        # mcp.types.Tool objects, or dicts
kit = adapt_tools(tools, "openai-strict")          # or "gemini"

response = client.chat.completions.create(model=..., messages=..., tools=kit.declarations)
for call in response.choices[0].message.tool_calls or []:
    name, args = kit.restore(call.function.name, json.loads(call.function.arguments))
    result = await session.call_tool(name, args)

For Gemini, kit.declarations are functionDeclarations: types.Tool(function_declarations=kit.declarations) in google-genai, or {"functionDeclarations": kit.declarations} over REST. For the OpenAI Responses API use [{"type": "function", **t.function} for t in kit].

Tool names are fixed too: github/search_issues becomes github_search_issues, names over 64 characters get a short hash, and two tools never end up with the same name. kit.restore maps the name back.

What it changes

Problem in the schema Gemini OpenAI strict
$ref / $defs / definitions inlined inlined
Recursive $ref (a tree) the recursive field is a JSON string same
anyOf: [X, {type: null}], type: [X, "null"], OpenAPI nullable nullable: true type: [X, "null"]
type: [A, B], oneOf anyOf anyOf
allOf merged merged
Optional properties kept optional made required and nullable (strict mode needs every property in required); restore drops the nulls
additionalProperties removed set to false on every object
Array without items, object without properties, untyped "any" field, tuple arrays sent as a JSON string; restore decodes it same
enum with "" or non-strings "" dropped; number enums move to the description kept
const one-value enum kept
Missing type inferred from properties, items, enum, bounds same
required naming undefined properties, "required": true on a property fixed fixed
Root not an object, root anyOf of objects made an object / merged same
format, exclusiveMinimum, multipleOf, minLength, default, regex lookarounds... that the target rejects written into the description so the model still sees them same
$schema, $id, not, if/then, patternProperties... removed removed

restore also turns 5.0 back into 5 for integer fields (Gemini sends numbers as floats).

In CI

schemashim check tools.json                     # exit 1 if Gemini or OpenAI strict would reject a tool
schemashim check tools.json --target gemini --json
schemashim fix tools.json --target openai-strict > tools.openai.json

The file can be a schema, one tool, a list of tools, a tools/list response or an mcptest snapshot, and - reads stdin. To check a running MCP server:

mcptest tools -- python my_server.py | schemashim check -

Limits

  • A JSON-string field is a workaround: the model sees "JSON-encoded value" instead of the real shape, so describe what goes in it. It only happens where the provider cannot express the shape at all.
  • Strict mode can't express "optional": the model sends null, and restore removes it. If a field was already nullable, a null is kept, because it may be meaningful.
  • Merging a root anyOf makes fields that are not in every choice optional; the tool must still validate which combination is valid.
  • Gemini's newer parametersJsonSchema field accepts more of JSON Schema. schemashim targets the classic parameters field that most clients and SDKs send, and its output is valid there too.
  • Only local #/$defs/... and #/definitions/... references are followed; remote ones become JSON-string fields.
  • Provider rules change. If a schema still gets rejected, open an issue with the error text.

Why this exists

Every MCP client and agent framework that talks to Gemini or OpenAI strict mode hits this and writes its own partial sanitizer. A few of the reports:

GitHub search finds over 300 issues titled "tool schema" + Gemini alone. The demand evidence is in demand/ (idea tool-schema-compat).

License

MIT

Metadata

Release files for schemashim 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for schemashim 0.1.0
File Size Uploaded Provenance Actions
schemashim-0.1.0.tar.gz 20.8 kB Without provenance Details for schemashim-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for schemashim 0.1.0
File Interpreter ABI Platform Actions
schemashim-0.1.0-py3-none-any.whl Python 3 none any Details for schemashim-0.1.0-py3-none-any.whl

Total release size: 38.1 kB

Release files / schemashim-0.1.0.tar.gz

Download URL schemashim-0.1.0.tar.gz
Size 20.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1aaa3292d77646736e816844d8cb3bdaf2b2759eb25c690f1d34e9e8e404dbeb
BLAKE2b-256 checksum
How to use checksums
70b1111b8a3f61e02fe0b747cf6a11854113bfc3e51781ee7d9fd75d7889218b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / schemashim-0.1.0-py3-none-any.whl

Download URL schemashim-0.1.0-py3-none-any.whl
Size 17.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a0b0bad6c86107b862787427672f774df95da7ca13b0394d6164665bd38bcdf
BLAKE2b-256 checksum
How to use checksums
255eaa81fa5ff5941ef56090b6c710a63074fdfb2d39875302f4986977c66f7a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for schemashim 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page