fix(mcp-server): Handle oneOf documents as operation input/output roots - #1345
Merged
Merged
Conversation
A document carrying the smithy.mcp#oneOf trait (a discriminated
polymorphic type) can appear as an operation's input or output in
bundled models, which are loaded with validation disabled. The
per-operation schema cache is shared across schema kinds and the input
schema is built before the output, so this failed in one of two
order-dependent ways:
- If a nested reference was rendered first, the cache held a
JsonOneOfSchema and createObjectSchema for the root then threw
ClassCastException, failing tool listing for the whole service.
- If the root was rendered first, the document produced an empty
object schema that was cached under the shape id, silently dropping
the oneOf variants from every nested reference.
Route oneOf documents in object positions through createOneOfSchema
(preserving the cached oneOf schema for other references) and re-shape
the result into an object-typed schema: JsonObjectSchema gains an
optional oneOf member, producing {"type": "object", "oneOf": [...]} as
the MCP spec requires for tool schemas. The guard is scoped to
ShapeType.DOCUMENT (the trait's selector), so any other shape kind
carrying the trait keeps its regular rendering, matching what runtime
input/output adaptation recognizes.
Two regression tests pin one ordering each within a single operation
(nested reference then document root, and document root then nested
reference); both fail on main and each fails only for its own
mechanism when that mechanism is mutated out.
psp65
force-pushed
the
mcp-oneof-root-schema
branch
from
September 14, 2026 17:46
d4e248f to
dda9ec9
Compare
adwsingh
requested changes
Sep 14, 2026
adwsingh
force-pushed
the
mcp-oneof-root-schema
branch
from
September 14, 2026 23:13
7206c85 to
a1c7a15
Compare
adwsingh
enabled auto-merge (rebase)
September 15, 2026 01:05
adwsingh
approved these changes
Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What behavior changes?
Tool schema generation (
McpSchemaFactory, formerlyMcpService) no longer fails (or silently degrades) when a shape carrying thesmithy.mcp#oneOftrait is used directly as an operation input or output.With a model like:
building the MCP service previously had two order-dependent failures, because the per-service schema cache is shared across schema kinds:
ProcessShape) was processed first, the cache held aJsonOneOfSchemafor the shape, andcreateObjectSchemafor theGetShapeoutput root then threwClassCastException: JsonOneOfSchema cannot be cast to JsonObjectSchema— failing construction of the entire service (every tool), not just the offending operation.createObjectSchematreated the document as a plain (member-less) shape and cached an emptyJsonObjectSchemaunder the shape id, which nested references then reused — silently dropping all oneOf variants.After this change both positions render correctly: the root position produces
{"type": "object", "oneOf": [...]}(the MCP spec requires tool input/output schemas to be object-typed; the oneOf variants are all objects, so the constraints compose), and nested references keep the cachedJsonOneOfSchemaexactly as before.Why is this change needed?
Bundled models (loaded through
ModelBundles, which assembles with validation disabled) can and do contain document-typed operation outputs: tooling that converts service models for MCP represents polymorphic type hierarchies as@oneOfdocuments, and a service with a polymorphic operation output currently cannot list tools at all. Observed in production as a deterministicClassCastExceptionatMcpSchemaFactory.createObjectSchema(formerlyMcpService.createJsonObjectSchema) for every such service.How was this validated?
StdioMcpServerTest. The schema cache is per operation and an operation's input schema is built before its output, so each test uses a single operation (in its own service) to pin one ordering:testOneOfDocumentAsOperationOutputRootWithCachedSchema—GetShape: nested reference in the input caches theJsonOneOfSchema, then the output requests the same shape as its root (theClassCastExceptionorder).testOneOfDocumentAsOperationInputRootBeforeNestedReference—PutShape: the document is the input root, then the output references it as a nested member (the order that silently dropped the variants).disableValidation()to mirrorModelBundles.mainwithout the fix (with theClassCastExceptionand the missing variants respectively), and that each fails only for its own mechanism when that mechanism is mutated out (blind cast restored / document guard removed)../gradlew :mcp:mcp-server:testpasses.What should reviewers focus on?
McpSchemaFactory#asJsonObjectSchema: the re-shaping of aJsonOneOfSchemainto an object-typed schema, and the new@oneOfguard increateObjectSchemathat routes trait-carrying shapes throughcreateOneOfSchemaso the cache keeps the full oneOf schema for other references.mcp-schemas/model/main.smithy:JsonObjectSchemagains an optionaloneOfmember so the object-typed root can carry the variants. The converted root preserves thetype: "object",oneOf, anddescriptionaJsonOneOfSchemawould serialize, plus the$schemaannotation that all object-typed roots already carry.createObjectSchemais scoped toShapeType.DOCUMENT(the trait's selector), so a non-document shape incorrectly carrying the trait in an unvalidated model keeps its existing rendering — matching what runtime input/output adaptation recognizes.Scope note
This change fixes schema generation (
tools/list). Invoking a tool whose input root is a@oneOfdocument is a separate, pre-existing limitation:SchemaGuidedDocumentBuilderrejects document roots during input adaptation. That behavior is unchanged here.Additional Links
None.