Describe the desired outcome from the user's perspective
As an operator configuring mcp-openapi, I want an overlay to be written where the schema entry it annotates is, so the relationship between a spec's base document and its overlay is structural rather than a same-name convention across two separate config blocks.
Current shape
"specs": {
"patternProperties": {
"^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$": {
"properties": {
"server": { "type": "string" },
"catalog": {
"patternProperties": {
"^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$": {
"properties": { "subject": {...}, "version": {...} },
"required": ["subject"]
}
}
},
"overlay": {
"patternProperties": {
"^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$": {
"properties": { "subject": {...}, "version": {...} },
"required": ["subject"]
}
}
}
}
}
}
}
specs:
petstore:
server: https://api.example.com
catalog:
apicurio0:
subject: petstore-spec
version: latest
overlay:
apicurio0:
subject: petstore-mcp-overlay
version: latest
catalog and overlay are independent top-level patternProperties blocks. Each already uses the correct internal shape — a map keyed by catalog name, containing subject/version — and that internal shape is not what's changing here. The problem is only that overlay sits as a second, separate top-level block, correlated with catalog solely because both happen to use the same catalog name (apicurio0) as their key. Nothing in the schema requires that, and nothing would catch it if an operator added a new catalog entry and forgot the matching overlay entry, or added it under a mistyped name.
Proposed shape
"catalog": {
"patternProperties": {
"^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$": {
"properties": {
"subject": {...},
"version": {...},
"overlay": {
"type": "object",
"patternProperties": {
"^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$": {
"properties": { "subject": {...}, "version": {...} },
"required": ["subject"]
}
},
"additionalProperties": false,
"maxProperties": 1
}
},
"required": ["subject"]
}
}
}
specs:
petstore:
server: https://api.example.com
catalog:
apicurio0:
subject: petstore-spec
version: latest
overlay:
apicurio0:
subject: petstore-mcp-overlay
version: latest
Only the placement changes. overlay moves inside the catalog entry it annotates — a sibling of that entry's own subject/version — but keeps exactly the same internal shape it already has today: a map keyed by catalog name, containing subject/version. maxProperties: 1, since one overlay per base document is the intended cardinality. The catalog name under overlay does not have to match the enclosing entry's name (apicurio0 here happens to match, but a different named catalog is equally valid) — there is nothing to default or override, because the map key already says which catalog, exactly as it does everywhere else catalog: is used in this schema.
An earlier version of this issue proposed collapsing overlay to {subject, version} directly, with an optional flat catalog: string to name a different registry. That was wrong to introduce — it's a different, one-off shape found nowhere else in this schema, where every other catalog reference is a name-keyed map. Reverted; the fix here is placement only.
Why now rather than leave it
This schema was only added 2026-07-05 and has been actively reshaped since — #2128 closed options.* abstraction leaks and aligned terminology, #2269 unified route condition naming, #2272 switched to hyphenated type names. It's still in the phase where getting the shape right costs a schema patch, not a migration. Leaving the current placement in place only gets more expensive to fix the longer it ships.
Scope check — only this module
binding-mcp-schema-registry and binding-mcp-kafka-connect were checked and are unaffected: both explicitly set "catalog": false in their schema patch and have no overlay property anywhere in options (only server). Their overlays (karapace-schema-registry.overlay.json, kafka-connect.overlay.json) are bundled resource files applied internally by their composite generators — never exposed as zilla.yaml config, so there's no peer-vs-nested question for either of them.
Consistency with the schema-privacy overlay design
#2318 (schema overlay for PII field tagging, a different repo area entirely) independently arrived at the same nested-and-name-keyed shape — overlay: as a sibling of subject/version inside a catalog entry, itself a map keyed by catalog name. Fixing this now means both consumers of common-json's JsonOverlay mechanism share one placement and one internal-shape convention instead of diverging in either.
Acceptance criteria
- Update
mcp_openapi.schema.patch.json: move overlay from a peer of catalog to nested inside each catalog.<name> entry, keeping its existing catalog-name-keyed internal shape unchanged
- Update the config class(es) and adapter(s) that currently read
specs.<name>.overlay.<name> as a peer lookup, to instead read specs.<name>.catalog.<name>.overlay.<name>
- Update
McpOpenapiCompositeGenerator (and its test) wherever it currently resolves overlay by cross-referencing the catalog name between two separate maps
- Update existing k3po scripts and IT expectations in
specs/binding-mcp-openapi.spec that construct or validate the current peer-placement config shape
- Config validation: an
overlay catalog name (the map key) that doesn't correspond to any configured catalog produces a clear startup error, per this repo's existing test-first discipline for config validation
Additional context
This is a pre-existing binding-mcp-openapi config shape, not part of the PII epic (zilla-platform#383). No production behavior changes; this only reshapes where the same information is written in zilla.yaml.
Revised: overlay keeps its existing catalog-name-keyed internal shape; only its placement (nested vs. peer) changes. An earlier revision of this issue incorrectly flattened the internal shape too — reverted.
Describe the desired outcome from the user's perspective
As an operator configuring
mcp-openapi, I want an overlay to be written where the schema entry it annotates is, so the relationship between a spec's base document and its overlay is structural rather than a same-name convention across two separate config blocks.Current shape
catalogandoverlayare independent top-levelpatternPropertiesblocks. Each already uses the correct internal shape — a map keyed by catalog name, containingsubject/version— and that internal shape is not what's changing here. The problem is only thatoverlaysits as a second, separate top-level block, correlated withcatalogsolely because both happen to use the same catalog name (apicurio0) as their key. Nothing in the schema requires that, and nothing would catch it if an operator added a new catalog entry and forgot the matching overlay entry, or added it under a mistyped name.Proposed shape
Only the placement changes.
overlaymoves inside the catalog entry it annotates — a sibling of that entry's ownsubject/version— but keeps exactly the same internal shape it already has today: a map keyed by catalog name, containingsubject/version.maxProperties: 1, since one overlay per base document is the intended cardinality. The catalog name underoverlaydoes not have to match the enclosing entry's name (apicurio0here happens to match, but a different named catalog is equally valid) — there is nothing to default or override, because the map key already says which catalog, exactly as it does everywhere elsecatalog:is used in this schema.An earlier version of this issue proposed collapsing
overlayto{subject, version}directly, with an optional flatcatalog:string to name a different registry. That was wrong to introduce — it's a different, one-off shape found nowhere else in this schema, where every other catalog reference is a name-keyed map. Reverted; the fix here is placement only.Why now rather than leave it
This schema was only added
2026-07-05and has been actively reshaped since —#2128closedoptions.*abstraction leaks and aligned terminology,#2269unified route condition naming,#2272switched to hyphenated type names. It's still in the phase where getting the shape right costs a schema patch, not a migration. Leaving the current placement in place only gets more expensive to fix the longer it ships.Scope check — only this module
binding-mcp-schema-registryandbinding-mcp-kafka-connectwere checked and are unaffected: both explicitly set"catalog": falsein their schema patch and have nooverlayproperty anywhere inoptions(onlyserver). Their overlays (karapace-schema-registry.overlay.json,kafka-connect.overlay.json) are bundled resource files applied internally by their composite generators — never exposed as zilla.yaml config, so there's no peer-vs-nested question for either of them.Consistency with the schema-privacy overlay design
#2318(schema overlay for PII field tagging, a different repo area entirely) independently arrived at the same nested-and-name-keyed shape —overlay:as a sibling ofsubject/versioninside a catalog entry, itself a map keyed by catalog name. Fixing this now means both consumers ofcommon-json'sJsonOverlaymechanism share one placement and one internal-shape convention instead of diverging in either.Acceptance criteria
mcp_openapi.schema.patch.json: moveoverlayfrom a peer ofcatalogto nested inside eachcatalog.<name>entry, keeping its existing catalog-name-keyed internal shape unchangedspecs.<name>.overlay.<name>as a peer lookup, to instead readspecs.<name>.catalog.<name>.overlay.<name>McpOpenapiCompositeGenerator(and its test) wherever it currently resolves overlay by cross-referencing the catalog name between two separate mapsspecs/binding-mcp-openapi.specthat construct or validate the current peer-placement config shapeoverlaycatalog name (the map key) that doesn't correspond to any configured catalog produces a clear startup error, per this repo's existing test-first discipline for config validationAdditional context
This is a pre-existing
binding-mcp-openapiconfig shape, not part of the PII epic (zilla-platform#383). No production behavior changes; this only reshapes where the same information is written inzilla.yaml.Revised: overlay keeps its existing catalog-name-keyed internal shape; only its placement (nested vs. peer) changes. An earlier revision of this issue incorrectly flattened the internal shape too — reverted.