Skip to content

feat: add SEP-2106 structuredContent wire-shape scenario (complements #295)#308

Draft
olaservo wants to merge 1 commit into
modelcontextprotocol:mainfrom
olaservo:sep-2106-structured-content-wire-shape
Draft

feat: add SEP-2106 structuredContent wire-shape scenario (complements #295)#308
olaservo wants to merge 1 commit into
modelcontextprotocol:mainfrom
olaservo:sep-2106-structured-content-wire-shape

Conversation

@olaservo
Copy link
Copy Markdown
Member

Summary

Complements #295 by adding the second half of SEP-2106's wire-format changes that the keyword-preservation checks don't reach: the loosened outputSchema (non-object types at the root) and the widened structuredContent (any JSON value, not just records). These are exactly the shapes SEP-2106's motivation section is built around (the weather-forecast and get-count examples).

cc @pcarleton — heads-up that this picks up where #295 left off; happy to fold differently if you'd prefer the checks live inside JsonSchema2020_12Scenario.

What #295 covered vs. what this adds

SEP-2106 surface Covered by #295? Covered here?
inputSchema keeps type: \"object\" plus 2020-12 vocabulary survives tools/list
outputSchema may be type: \"array\" at the root, advertised in tools/list
outputSchema may be a primitive (type: \"number\") at the root
tools/call returns an array directly in structuredContent (on the wire)
tools/call returns a primitive directly in structuredContent (on the wire)
Client MUST NOT auto-deref network $ref (SEP security section) ✅ (json-schema-ref-no-deref)

New scenario: sep-2106-structured-content

Six checks across two test tools:

check ID what it asserts
sep-2106-array-output-tool-found tool advertised
sep-2106-array-output-schema-preserved outputSchema.type === 'array' survives tools/list (SDK didn't wrap it in {type:'object'})
sep-2106-array-structured-content tools/call returns array directly in structuredContent
sep-2106-primitive-output-tool-found tool advertised
sep-2106-primitive-output-schema-preserved outputSchema.type === 'number' survives
sep-2106-primitive-structured-content tools/call returns raw number in structuredContent

All emit FAILURE (capability-test framing, same as SEP-1613 / #295 — no new RFC 2119 sentences in the spec diff, so the keyword-mapping rule doesn't constrain severity). The scenario lives in pendingClientScenariosList only — the in-repo everything-server can't satisfy these checks until the SDK widens CallToolResultSchema.structuredContent to unknown. Once that lands, the pending entry comes out.

Why raw HTTP + a non-SDK reference server

Pre-SEP-2106, both sides of the SDK reject non-object structuredContent:

  • The SDK Client response validator rejects the tools/list response (outputSchema.type !== 'object') and the tools/call response (structuredContent not a record). The scenario uses raw http.request to bypass that and inspect the actual wire bytes.
  • The SDK Server validates outgoing tools/call results against the same CallToolResultSchema and returns JSON-RPC -32602 instead of letting array/primitive structuredContent through. That means the only way to demonstrate a fully-compliant server today is to skip the SDK entirely, which is what examples/servers/typescript/sep-2106-compliant-server.ts does (bare Express, ~170 lines).

The compliant server is the positive-test target. A vitest in negative.test.ts (port 3009) spawns it and asserts all six checks SUCCESS — without it, there's no way to prove the scenario isn't just emitting FAILURE everywhere.

Files

  • src/scenarios/server/sep-2106-structured-content.ts — new scenario (raw HTTP, ~440 lines incl. helper)
  • examples/servers/typescript/sep-2106-compliant-server.ts — non-SDK reference server (raw Express, ~170 lines)
  • src/scenarios/server/negative.test.ts — positive vitest case against the compliant server
  • src/scenarios/index.ts — register in pendingClientScenariosList + allClientScenariosList, comment mirrors SEP-1613 framing

Test plan

  • npm run typecheck — clean
  • npm run lint — clean
  • npm test215/215 passing (was 207 pre-change; +1 positive case = 208? — actually pre-push hook reports 215 because the scenario emits checks that the existing soft suite counters also see; the only new vitest block is the compliant-server positive case)
  • Reviewer to check whether the wire-shape concerns belong in their own scenario (this PR) or folded into JsonSchema2020_12Scenario alongside SEP-2106: traceability YAML + JSON Schema 2020-12 conformance checks #295's checks — I went with separate because the soft version gate (SKIPPED/FAILURE) doesn't quite fit here (the SDK can't return the new shape at any negotiated version), but happy to fold.
  • Once the SDK ships SEP-2106's CallToolResultSchema widening: remove the pendingClientScenariosList entry; the in-repo everything-server can then be extended with these tools and the scenario goes green against it.

Notes for review


Note: implementation assisted by Claude Code 🦉 (full transcript available on request); design choices and the comparison to #295 are mine.

…odelcontextprotocol#295)

modelcontextprotocol#295 added SEP-2106 conformance checks for JSON Schema 2020-12 keyword
preservation in inputSchema (composition/conditional/$anchor surviving
tools/list), folded into the existing JsonSchema2020_12Scenario. This
PR covers the other half of SEP-2106: the loosened wire format for
outputSchema and structuredContent.

Sep2106StructuredContentScenario emits six checks across two test tools:
  - sep_2106_array_output_tool: outputSchema.type === 'array' at the
    root advertised in tools/list; tools/call returns a JSON array
    directly in structuredContent.
  - sep_2106_primitive_output_tool: outputSchema.type === 'number' at
    the root; tools/call returns a raw number in structuredContent.

Both shapes are what SEP-2106's motivation section uses as the
worked examples (weather forecast / get-count) -- the wire-side
behaviour the SEP exists to enable -- and neither is exercised by the
keyword-preservation checks. The scenario uses raw HTTP because both
the SDK Client and SDK Server validators currently reject non-object
outputSchema/structuredContent: the SDK Client refuses to parse the
list response, and the SDK Server returns JSON-RPC -32602 instead of
emitting the call result. Raw HTTP bypasses both so the scenario can
inspect what is actually on the wire.

The scenario is registered in pendingClientScenariosList (and the all
list, mirroring SEP-1613) so it does not run in the active suite
against the in-repo everything-server, which cannot satisfy these
checks until the SDK widens CallToolResultSchema.structuredContent to
unknown. Once that lands, the pending entry can be removed.

Positive verification target ships as
examples/servers/typescript/sep-2106-compliant-server.ts: a bare-bones
raw-Express server that speaks the SEP-2106 wire format end-to-end
without an SDK in the path. A vitest in negative.test.ts spawns it and
asserts every check is SUCCESS, which is what proves the scenario is
not just emitting FAILURE everywhere.

Assisted by Claude Code 🦉

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 23, 2026

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@308

commit: 7d4b97d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant