Skip to content

v1.5.2 - Schema dialect normalization for proxied tools

Choose a tag to compare

@pacnpal pacnpal released this 20 Aug 19:13
· 9 commits to main since this release
9ec188a

Schema dialect normalization for proxied tools

The MCP TypeScript SDK hardcodes "$schema": "http://json-schema.org/draft-07/schema#"
into every generated tool's inputSchema/outputSchema, with no config option for the
upstream server to change it. A strict client whose validator only accepts 2020-12 (what
the MCP spec itself targets) refuses every tool such a server advertises through
mcpelevator — even though the schema itself usually has no draft-07-specific keywords,
only the wrong dialect pointer. 1.5.2 adds an opt-in per-server toggle that rewrites the
dialect in place when it's safe to, and separately fixes a connection-pool exhaustion bug
that could take the whole control plane offline, login included.


Normalize schema dialect toggle (#124, closes #123)

New per-server toggle, normalize_schema_dialect (Server form → Exposure → "Normalize
schema dialect"). When on, the bridge rewrites $schema from draft-07 to 2020-12 on a
proxied tool's parameters/output schema — nothing else about the schema moves, and a
schema declaring no $schema has none injected.

It's applied inside _ToolTransform._scrub (app/bridge/host.py), the same place that
already strips the reserved identity key — so it reaches every surface that resolves
tools through this proxy (MCP tools/list, REST/OpenAPI, the group hub), composes with
hide/rename policy, and is part of config_hash (toggling it restarts the bridge to
re-apply). Plumbed end-to-end: Server.normalize_schema_dialect (forward-migrated
column) → every runner's ProcessSpec → the bridge spec JSON → _tool_transform
ServerCreate/ServerUpdate/ServerDetail → the create/edit form and server-detail page.

The compatibility guard

The rewrite is refused whenever relabeling would mean something other than renaming the
dialect. _has_incompatible_draft07_construct dispatches over four hazard families,
checked recursively but only through positions the JSON Schema grammar declares as
sub-schemas:

  • Removed or changed constructs — tuple-form items, dependencies (except beside a
    $ref, where draft-07 was already ignoring it as a sibling and no 2020-12 vocabulary
    evaluates it either, so nothing is lost by relabeling).
  • Dormant assertions — anything added after draft-07 that asserts
    (unevaluatedProperties, dependentRequired, prefixItems, $dynamicRef, …).
    draft-07 ignores what it doesn't recognize; the relabel switches them on. minContains/
    maxContains are conditional: inert without a sibling contains, so those normalize.
  • Reference resolution$anchor/$dynamicAnchor/$vocabulary; $id in a
    subschema, beside a $ref, or with a non-empty fragment; a $ref whose target sits
    somewhere the walk doesn't independently inspect (only #/$defs/… and
    #/definitions/… targets are trusted, since those containers are walked
    unconditionally).
  • Non-portable values — a keyword 2020-12 knows and draft-07 doesn't, carrying a value
    2020-12's meta-schema rejects ({"contentSchema": 7}, {"$defs": {"T": 7}},
    {"deprecated": "yes"}, {"$recursiveAnchor": true}, …). draft-07 never inspects an
    unknown keyword's value, so the wrong shape is free until the relabel makes the name
    meaningful.

Every shape was verified against the published 2020-12 meta-schemas rather than assumed.
Two deliberate scope calls, documented at their definitions:

  • format is not guarded. 2020-12's default meta-schema requires the
    format-annotation vocabulary, so a client that asserted formats under draft-07 may
    stop. It's excluded because draft-07 never guaranteed assertion (its own spec makes
    it optional and opt-outable), and because guarding it would refuse most real
    TypeScript-SDK tool schemas — the population this toggle exists for.
  • $ref targets are not resolved. A full JSON Pointer resolver would mean cycle
    handling, escaping, and external documents in the tools/list path, for a shape
    generated schemas don't emit.

Two availability bugs were also closed here: a malformed non-string $schema raised
TypeError, and a deeply nested schema (~500+ levels) raised RecursionError — either
would have taken tools/list down for the whole server once the toggle was on. Both now
fail to the safe answer (leave the schema under draft-07) instead of raising.


Fixes and hardening

  • Stopped long-lived requests from exhausting the DB connection pool (#122). Every
    /api request could 500 with sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached — including login, since the failure surfaced at the allowlist
    middleware, the first DB touch of any control-plane request. The real cause: a request
    holds its session for its whole lifetime, and the SSE log stream
    (GET /api/servers/{id}/logs) holds one for as long as a viewer leaves the tab open. A
    handful of open log views (or concurrent playground tool calls, which can run up to
    300s) could park every one of the pool's 15 connections and block the next request for
    the full 30s timeout. Fixed by dropping the connection pool for the SQLite engine
    (opening a local SQLite connection costs microseconds; the pool bought nothing and
    imposed a hard ceiling) and by releasing the log stream's session before it starts
    streaming, since the stream re-validates visibility on its own short-lived sessions
    afterward. No API, schema, or dependency changes.

Upgrade notes

  • No migration and no new environment variables. normalize_schema_dialect arrives
    via the existing forward-only ADD COLUMN path and is nullable; a pre-column row reads
    as off and hashes as off, so no bridge restarts on upgrade.
  • Nothing changes until you opt in. The toggle is off by default; existing servers
    keep advertising their upstream's declared dialect exactly as before.
  • The DB pool fix is transparent — no config to set, no behavior change beyond no
    longer exhausting under sustained SSE/playground load.

Full Changelog: v1.5.1...v1.5.2

License

MIT © pacnpal