HarperFast / HarperFast/harper
[MCP/OpenAPI] The two schema type mappers disagree: unrecognized types, nested enum/format/const, and nullability
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
Follow-up from the docs pass for #1923 ([documentation#605](https://github.com/HarperFast/documentation/pull/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:187` → `new 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)._
Contributor guide
Assessment
This issue has not been assessed yet.