Skip to content

[MCP/OpenAPI] The two schema type mappers disagree: unrecognized types, nested enum/format/const, and nullability #1942

Description

@kylebernhardy

Follow-up from the docs pass for #1923 (documentation#605). Writing the fragment-key reference table required documenting the same key twice, once per surface, because the two mappers diverge in three places. Each divergence is defensible in isolation; together they mean an author cannot reason about static properties without knowing which consumer is reading.

#1921 already extracted JSON_SCHEMA_SCALAR_TYPES into resources/jsonSchemaTypes.ts so the two type mappers could not drift on recognized scalars. These are the cases that shared set doesn't cover.

1. Unrecognized type names diverge silently

A type name in neither vocabulary — not a DATA_TYPES key, not a JSON Schema scalar ('Text', 'Object', 'Array') — resolves differently:

  • MCP: components/mcp/tools/schemas/derive.ts:77 default:{ type: 'string' }
  • OpenAPI: resources/openApi.ts:187new Type(DATA_TYPES['Text'] /* undefined */, 'Text'){}

One coerces to a wrong-but-confident type, the other emits untyped. Neither warns. A typo in a static properties fragment produces two different wrong answers with no signal to the author.

Suggested: share one resolution helper, and log a warn-once at registration naming the property and the unrecognized type — the author almost certainly meant one of the two vocabularies.

2. enum / format / const / description are dropped by OpenAPI below the top level

openApi.ts:190-198 attaches these only to the top-level props[name]. Nested objects route through attributeToFragment, which deliberately omits enum/format/const (jsonSchemaTypes.ts:121-124), and primitive array items route through new Type(...), which carries only type — dropping description too. MCP's attributeToProperty (derive.ts:113-115) recurses and keeps all of them.

So the same fragment yields a documented, constrained nested property for an LLM and a bare typed one for Swagger UI. description in particular is a plain loss for the OpenAPI consumer.

3. Nullability has three different representations

An author writes nullable: true or type: ['string','null']; both fold to the same internal flag (jsonSchemaTypes.ts:159-165). Emission:

  • MCP re-expands to { "type": ["string","null"] } (derive.ts:103-109) — never emits a nullable keyword.
  • OpenAPI drops it entirely for a top-level property — openApi.ts:148 reads nullable only to compute required, and never writes it onto the property schema.
  • OpenAPI does emit nullable: true for a property inside a nested object, via attributeToFragment (jsonSchemaTypes.ts:120).

The top-level OpenAPI drop is the actual bug: OpenAPI 3.0.3 has a nullable keyword and we have the information, so a nullable property is currently advertised as non-nullable. The nested/top-level inconsistency within OpenAPI itself is the clearest evidence this wasn't deliberate.

Suggested approach

Route both emit paths through one projector that takes a target dialect (json-schema-2020 for MCP, openapi-3.0.3 for OpenAPI) and handles the two legitimate dialect differences — union-with-null vs nullable: true — in one place, so everything else stays identical by construction. That is a bigger change than #1921 made; splitting item 3 (the top-level nullable drop) out as a standalone fix is reasonable if the unification slips.

Each item is separately observable, so unit coverage should assert MCP and OpenAPI output for the same fragment side by side — the convergence test added in #1921 is the natural home.

Once fixed, the per-surface caveats in the fragment-key table in reference/resources/resource-api.md collapse back to single rows.

Issue generated by kAIle (Claude Opus 4.8).

Metadata

Metadata

Assignees

Labels

area:mcpModel Context Protocol (MCP) server: protocol, profiles, stdio CLI

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions