@@ -16,11 +16,9 @@ import (
1616// MustOutputSchema infers an output schema for T, panicking during package
1717// initialization if inference fails.
1818//
19- // Unlike input schemas, an output schema root need not be `{"type":"object"}`:
20- // from protocol version 2026-07-28 (SEP-2106) it may be any valid JSON Schema
21- // 2020-12, including a bare array or a bare anyOf. Schemas whose root is not
22- // an object are stripped per-request for older clients — see
23- // inventory.OutputSchemaVersionGate — so inferring one here is safe.
19+ // Unlike input schemas, an output schema root need not be `{"type":"object"}`,
20+ // so T may be a slice or a scalar. Non-object roots are stripped per-request
21+ // for pre-2026-07-28 clients by inventory.OutputSchemaVersionGate.
2422func MustOutputSchema [T any ]() * jsonschema.Schema {
2523 schema , err := jsonschema.For [T ](nil )
2624 if err != nil {
@@ -32,36 +30,21 @@ func MustOutputSchema[T any]() *jsonschema.Schema {
3230
3331// AnyOfSchema builds a union output schema over the given branches.
3432//
35- // It deliberately emits `anyOf` and never `oneOf`. `oneOf` requires that
36- // EXACTLY ONE branch match, which is wrong for essentially every tool in this
37- // package whose output shape varies by method:
38- //
39- // - Branches are frequently structurally identical. All four of
40- // actions_run_trigger's non-run_workflow methods return the same
41- // {message, run_id, status, status_code} map, so a oneOf over them matches
42- // four branches and therefore always fails.
43- // - Empty collections are ambiguous. issue_read method=get_comments on an
44- // issue with no comments returns [], which vacuously satisfies every array
45- // branch.
46- // - Optional fields overlap. issue_read method=get on a sub-issue populates
47- // `parent`, which also satisfies the get_parent branch.
48- //
49- // anyOf ("at least one") accepts all three while still rejecting values that
50- // match no branch, which is the useful half of the validation.
33+ // Always anyOf, never oneOf: oneOf requires EXACTLY ONE branch to match, and
34+ // these unions have structurally identical branches, empty arrays that satisfy
35+ // every array branch, and overlapping optional fields. anyOf still rejects
36+ // values matching no branch, which is the useful half of the validation.
37+ // TestAnyOfAcceptsAmbiguousPayloadsButStillRejectsGarbage demonstrates both.
5138func AnyOfSchema (branches ... * jsonschema.Schema ) * jsonschema.Schema {
5239 return & jsonschema.Schema {AnyOf : branches }
5340}
5441
55- // MustRawOutputSchema wraps a hand-authored JSON Schema document, panicking
56- // during package initialization if it does not parse or does not resolve.
57- //
58- // Hand-authored schemas are kept as json.RawMessage rather than
59- // *jsonschema.Schema so they round-trip byte-for-byte to the client and
60- // produce deterministic toolsnaps output, independent of jsonschema-go's
61- // struct field coverage and marshaling order.
42+ // MustRawOutputSchema wraps a hand-authored JSON Schema document as raw bytes,
43+ // so it reaches the client exactly as written rather than through
44+ // jsonschema-go's struct coverage and field ordering.
6245//
63- // Resolution is checked here ( not just parsing) so a dangling $ref fails the
64- // build rather than the request.
46+ // Resolution is checked, not just parsing, so a dangling $ref fails the build
47+ // rather than a request.
6548func MustRawOutputSchema (raw string ) json.RawMessage {
6649 var parsed jsonschema.Schema
6750 if err := json .Unmarshal ([]byte (raw ), & parsed ); err != nil {
@@ -73,9 +56,8 @@ func MustRawOutputSchema(raw string) json.RawMessage {
7356 return json .RawMessage (raw )
7457}
7558
76- // outputSchemasEnabled reports whether the output_schemas feature is on for
77- // this request. This is the rollout gate only; see canSendStructuredContent
78- // for the protocol-legality gate.
59+ // outputSchemasEnabled is the rollout gate; canSendStructuredContent is the
60+ // separate protocol-legality gate.
7961func outputSchemasEnabled (ctx context.Context , deps ToolDependencies ) bool {
8062 return deps .IsFeatureEnabled (ctx , FeatureFlagOutputSchemas )
8163}
@@ -85,14 +67,10 @@ func isJSONObject(marshaled []byte) bool {
8567 return bytes .HasPrefix (bytes .TrimLeft (marshaled , " \t \r \n " ), []byte ("{" ))
8668}
8769
88- // canSendStructuredContent reports whether a structuredContent value of the
89- // given marshaled shape may legally be sent to this client.
90- //
91- // Under 2025-11-25 and earlier, structuredContent is typed
92- // `{ [key: string]: unknown }` — a JSON object. 2026-07-28 widened it to
93- // `unknown`, explicitly "any JSON value (object, array, string, number,
94- // boolean, or null)". So an object is always safe; anything else requires the
95- // newer protocol. req may be nil in tests, in which case only objects are sent.
70+ // canSendStructuredContent reports whether a structuredContent value of this
71+ // shape may legally be sent to this client. An object is always safe; anything
72+ // else needs 2026-07-28, which widened the field from an object to any JSON
73+ // value. A nil req (tests) sends objects only.
9674func canSendStructuredContent (req * mcp.CallToolRequest , marshaled []byte ) bool {
9775 if isJSONObject (marshaled ) {
9876 return true
@@ -103,15 +81,10 @@ func canSendStructuredContent(req *mcp.CallToolRequest, marshaled []byte) bool {
10381 return req .ProtocolVersion () >= inventory .ProtocolVersionNonObjectOutputSchemas
10482}
10583
106- // structuredTextResult builds a tool result whose text content is the
107- // serialized textValue — byte-identical to what the tool returned before
108- // output schemas existed — and which additionally carries structured as
109- // structuredContent when both gates allow it.
110- //
111- // The text block is always populated regardless of the gates. The spec calls
112- // for this independently: "For backwards compatibility, a tool that returns
113- // structured content SHOULD also return the serialized JSON in a TextContent
114- // block."
84+ // structuredTextResult returns a result whose text content is the serialized
85+ // textValue — byte-identical to the pre-output-schema behaviour, and populated
86+ // regardless of the gates — plus structured as structuredContent when both the
87+ // feature flag and the client's protocol version allow it.
11588func structuredTextResult (ctx context.Context , deps ToolDependencies , req * mcp.CallToolRequest , textValue , structured any ) (* mcp.CallToolResult , error ) {
11689 data , err := json .Marshal (textValue )
11790 if err != nil {
0 commit comments