-
Notifications
You must be signed in to change notification settings - Fork 254
feat(router): typed custom scalars in MCP tool schemas with scalar_mappings overrides #3147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
asoorm
wants to merge
26
commits into
main
Choose a base branch
from
ahmet/eng-9903-mcp-custom-scalars-produce-untyped-json-schema-in-mcp-tool
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
0526d55
feat: add mcp.scalar_mappings config for custom scalar JSON schema types
asoorm 156bdf1
feat: support custom scalar schema overrides and defaulted-scalar rep…
asoorm 9ece97c
feat: plumb mcp scalar mappings into tool schema generation with star…
asoorm f74105d
test: cover custom scalar typing and scalar mapping overrides in mcp …
asoorm 8d9eb30
test: guard invalid scalar mappings at server construction and fix en…
asoorm 15770d7
docs: document mcp scalar_mappings and custom scalar type defaults
asoorm dd6857d
chore: use ascii punctuation in scalar mapping warning and comment
asoorm d23fe4d
feat: vendor graphql operation json schema generation as router-inter…
asoorm 90a6ad3
chore: restore released graphql-go-tools version in router and router…
asoorm f83c463
chore: rename stale enginejsonschema alias to internaljsonschema
asoorm cfc999c
chore: use jsonschema v6 in vendored schema tests to drop the v5 depe…
asoorm 90268f0
chore: merge main (mcp tools docs refactor, server discover) and reso…
asoorm 0c752fe
test: add opt-in live vendor schema acceptance tests for anthropic an…
asoorm 9f13b01
test: always skip live vendor schema tests instead of env-flag gating
asoorm dbe6671
test: assert the root-type rejection message verified against the liv…
asoorm ec46353
test: record live verification of openai schema acceptance probes
asoorm eb3a464
refactor: emit google/jsonschema-go schemas from the graphql walk
asoorm 88de1e4
docs: note true-schema marshaling and the canonical bytes contract
asoorm 042b563
fix: keep graphql nullability for object-mapped scalars and guard nil…
asoorm e2233a2
docs: state nullable schema forms for custom scalars and mappings
asoorm ee31ab5
refactor: validate mcp tool arguments with google jsonschema and pass…
asoorm b3e7fb4
fix: sanitize validator internals from tool input error text
asoorm fc64e43
docs: use an explicitly custom scalar name in scalar mapping examples
asoorm bf7a0aa
docs: state that the router does not generate the openai strict mode …
asoorm 7957dfe
docs: state why the anthropic probe pins api version 2023-06-01
asoorm 42b76e0
Merge branch 'main' into ahmet/eng-9903-mcp-custom-scalars-produce-un…
asoorm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
4 changes: 4 additions & 0 deletions
4
router-tests/protocol/testdata/mcp_operations_custom_scalar/UploadFile.graphql
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # This mutation uploads a single file. | ||
| mutation UploadFile($file: Upload!) { | ||
| singleUpload(file: $file) | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package jsonschema | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/santhosh-tekuri/jsonschema/v5" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" | ||
| ) | ||
|
|
||
| // TestNullableFieldsAreJSONSchema2020_12 verifies that the generator expresses | ||
| // nullability in the JSON Schema 2020-12 form rather than the OpenAPI 3.0 | ||
| // keyword `"nullable": true` (which standard validators silently ignore). | ||
| // | ||
| // Concretely: a payload that contains explicit `null` values for nullable | ||
| // scalar, enum, and recursive-ref fields must validate cleanly against the | ||
| // generated schema using a strict standard JSON Schema validator. | ||
| func TestNullableFieldsAreJSONSchema2020_12(t *testing.T) { | ||
| schemaSDL := scalarDefinitions + ` | ||
| schema { query: Query } | ||
|
|
||
| type Query { | ||
| processFormula(tree: FormulaNodeInput): Boolean | ||
| doThing(input: ThingInput): Boolean | ||
| } | ||
|
|
||
| input ThingInput { | ||
| name: String | ||
| count: Int | ||
| rating: Float | ||
| active: Boolean | ||
| status: Status | ||
| } | ||
|
|
||
| enum Status { ACTIVE INACTIVE } | ||
|
|
||
| input FormulaNodeInput { | ||
| nodeType: NodeType! | ||
| left: FormulaNodeInput | ||
| right: FormulaNodeInput | ||
| value: Float | ||
| } | ||
|
|
||
| enum NodeType { CONSTANT BINARY_OPERATION } | ||
| ` | ||
|
|
||
| operationSDL := ` | ||
| query Run($tree: FormulaNodeInput, $input: ThingInput) { | ||
| processFormula(tree: $tree) | ||
| doThing(input: $input) | ||
| } | ||
| ` | ||
|
|
||
| definitionDoc, report := astparser.ParseGraphqlDocumentString(schemaSDL) | ||
| require.False(t, report.HasErrors(), "schema parsing failed: %s", report.Error()) | ||
|
|
||
| operationDoc, report := astparser.ParseGraphqlDocumentString(operationSDL) | ||
| require.False(t, report.HasErrors(), "operation parsing failed: %s", report.Error()) | ||
|
|
||
| schema, err := BuildJsonSchema(&operationDoc, &definitionDoc) | ||
| require.NoError(t, err) | ||
|
|
||
| schemaJSON, err := json.Marshal(schema) | ||
| require.NoError(t, err) | ||
|
|
||
| compiled, err := jsonschema.CompileString("schema.json", string(schemaJSON)) | ||
| require.NoError(t, err, "generated JSON schema should compile") | ||
|
|
||
| // Nullable scalars and enum: explicit null values must be accepted. | ||
| t.Run("explicit nulls accepted for nullable scalar and enum fields", func(t *testing.T) { | ||
| const payloadJSON = `{ | ||
| "input": { | ||
| "name": null, | ||
| "count": null, | ||
| "rating": null, | ||
| "active": null, | ||
| "status": null | ||
| } | ||
| }` | ||
| var payload any | ||
| require.NoError(t, json.Unmarshal([]byte(payloadJSON), &payload)) | ||
| require.NoError(t, compiled.Validate(payload), | ||
| "nullable scalar/enum fields must accept explicit null per JSON Schema 2020-12") | ||
| }) | ||
|
|
||
| // Nullable recursive $ref: a leaf may explicitly set left/right to null | ||
| // (rather than omitting them) and the schema must accept it. | ||
| t.Run("explicit nulls accepted for nullable recursive ref fields", func(t *testing.T) { | ||
| const payloadJSON = `{ | ||
| "tree": { | ||
| "nodeType": "BINARY_OPERATION", | ||
| "left": { "nodeType": "CONSTANT", "value": 1, "left": null, "right": null }, | ||
| "right": null | ||
| } | ||
| }` | ||
| var payload any | ||
| require.NoError(t, json.Unmarshal([]byte(payloadJSON), &payload)) | ||
| require.NoError(t, compiled.Validate(payload), | ||
| "nullable recursive ref fields must accept explicit null per JSON Schema 2020-12") | ||
| }) | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package jsonschema | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/santhosh-tekuri/jsonschema/v5" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" | ||
| ) | ||
|
|
||
| // TestRecursiveInputAcceptsNestedPayload verifies that a self-recursive GraphQL | ||
| // input type produces a JSON Schema that accepts arbitrarily nested payloads. | ||
| // | ||
| // A self-recursive input type cannot be represented by inlining and truncating | ||
| // at a fixed recursion depth: the recursive fields get dropped from the schema, | ||
| // and because every object is emitted with `additionalProperties: false`, a | ||
| // valid nested payload is then rejected at the validation boundary with | ||
| // "additional properties '...' not allowed". The schema must instead reference | ||
| // the recursive type so that nesting is permitted to any depth. | ||
| func TestRecursiveInputAcceptsNestedPayload(t *testing.T) { | ||
| schemaSDL := scalarDefinitions + ` | ||
| schema { query: Query } | ||
|
|
||
| type Query { | ||
| createColumn(input: ColumnInput!): Boolean | ||
| } | ||
|
|
||
| input ColumnInput { | ||
| node: FormulaNodeInput! | ||
| } | ||
|
|
||
| input FormulaNodeInput { | ||
| nodeType: NodeType! | ||
| left: FormulaNodeInput | ||
| right: FormulaNodeInput | ||
| value: Float | ||
| } | ||
|
|
||
| enum NodeType { | ||
| CONSTANT | ||
| BINARY_OPERATION | ||
| } | ||
| ` | ||
|
|
||
| operationSDL := ` | ||
| query CreateColumn($input: ColumnInput!) { | ||
| createColumn(input: $input) | ||
| } | ||
| ` | ||
|
|
||
| definitionDoc, report := astparser.ParseGraphqlDocumentString(schemaSDL) | ||
| require.False(t, report.HasErrors(), "schema parsing failed: %s", report.Error()) | ||
|
|
||
| operationDoc, report := astparser.ParseGraphqlDocumentString(operationSDL) | ||
| require.False(t, report.HasErrors(), "operation parsing failed: %s", report.Error()) | ||
|
|
||
| schema, err := BuildJsonSchema(&operationDoc, &definitionDoc) | ||
| require.NoError(t, err) | ||
|
|
||
| schemaJSON, err := json.Marshal(schema) | ||
| require.NoError(t, err) | ||
|
|
||
| compiled, err := jsonschema.CompileString("schema.json", string(schemaJSON)) | ||
| require.NoError(t, err, "generated JSON schema should compile") | ||
|
|
||
| // A depth-2 expression tree: the inner BINARY_OPERATION node has its own | ||
| // left/right children, exercising recursion beyond a single level. | ||
| const payloadJSON = `{ | ||
| "input": { | ||
| "node": { | ||
| "nodeType": "BINARY_OPERATION", | ||
| "left": { | ||
| "nodeType": "BINARY_OPERATION", | ||
| "left": { "nodeType": "CONSTANT", "value": 1 }, | ||
| "right": { "nodeType": "CONSTANT", "value": 2 } | ||
| }, | ||
| "right": { "nodeType": "CONSTANT", "value": 3 } | ||
| } | ||
| } | ||
| }` | ||
|
|
||
| var payload any | ||
| require.NoError(t, json.Unmarshal([]byte(payloadJSON), &payload)) | ||
|
|
||
| err = compiled.Validate(payload) | ||
| require.NoError(t, err, "valid nested recursive payload must be accepted by the generated schema") | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.