From d56d3c57bf7425eeac46fa267bc9de6b7a48a185 Mon Sep 17 00:00:00 2001 From: Bob Dickinson Date: Tue, 7 Oct 2025 11:08:12 -0700 Subject: [PATCH 1/2] Added simulated discrimintated union for transports and arguments to avoid "anyOf" fan out schema validation errors (where all templates fail when none match) --- docs/reference/server-json/server.schema.json | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/docs/reference/server-json/server.schema.json b/docs/reference/server-json/server.schema.json index 7b7c7644..66446241 100644 --- a/docs/reference/server-json/server.schema.json +++ b/docs/reference/server-json/server.schema.json @@ -87,17 +87,21 @@ ] }, "transport": { - "anyOf": [ - { - "$ref": "#/definitions/StdioTransport" - }, - { - "$ref": "#/definitions/StreamableHttpTransport" - }, - { - "$ref": "#/definitions/SseTransport" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["stdio", "streamable-http", "sse"] } - ], + }, + "required": ["type"], + "if": {"properties": {"type": {"const": "stdio"}}}, + "then": {"$ref": "#/definitions/StdioTransport"}, + "else": { + "if": {"properties": {"type": {"const": "streamable-http"}}}, + "then": {"$ref": "#/definitions/StreamableHttpTransport"}, + "else": {"$ref": "#/definitions/SseTransport"} + }, "description": "Transport protocol configuration for the package" }, "runtimeArguments": { @@ -292,14 +296,17 @@ }, "Argument": { "description": "Warning: Arguments construct command-line parameters that may contain user-provided input. This creates potential command injection risks if clients execute commands in a shell environment. For example, a malicious argument value like ';rm -rf ~/Development' could execute dangerous commands. Clients should prefer non-shell execution methods (e.g., posix_spawn) when possible to eliminate injection risks entirely. Where not possible, clients should obtain consent from users or agents to run the resolved command before execution.", - "anyOf": [ - { - "$ref": "#/definitions/PositionalArgument" - }, - { - "$ref": "#/definitions/NamedArgument" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["positional", "named"] } - ] + }, + "required": ["type"], + "if": {"properties": {"type": {"const": "positional"}}}, + "then": {"$ref": "#/definitions/PositionalArgument"}, + "else": {"$ref": "#/definitions/NamedArgument"} }, "StdioTransport": { "type": "object", @@ -334,6 +341,7 @@ }, "url": { "type": "string", + "pattern": "^https?://[^\\s]+$", "description": "URL template for the streamable-http transport. Variables in {curly_braces} reference argument valueHints, argument names, or environment variable names. After variable substitution, this should produce a valid URI.", "example": "https://api.example.com/mcp" }, @@ -363,8 +371,8 @@ }, "url": { "type": "string", - "format": "uri", - "description": "Server-Sent Events endpoint URL", + "pattern": "^https?://[^\\s]+$", + "description": "URL template for the sse transport. Variables in {curly_braces} reference argument valueHints, argument names, or environment variable names. After variable substitution, this should produce a valid URI.", "example": "https://mcp-fs.example.com/sse" }, "headers": { @@ -490,14 +498,17 @@ "remotes": { "type": "array", "items": { - "anyOf": [ - { - "$ref": "#/definitions/StreamableHttpTransport" - }, - { - "$ref": "#/definitions/SseTransport" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["streamable-http", "sse"] } - ] + }, + "required": ["type"], + "if": {"properties": {"type": {"const": "streamable-http"}}}, + "then": {"$ref": "#/definitions/StreamableHttpTransport"}, + "else": {"$ref": "#/definitions/SseTransport"} } }, "_meta": { From ea17bae4f31b12c53b38b7a06e89642bce439f40 Mon Sep 17 00:00:00 2001 From: Bob Dickinson Date: Thu, 16 Oct 2025 14:44:55 -0700 Subject: [PATCH 2/2] Auto-generate discriminated unions from OpenAPI discriminators - Add discriminators to OpenAPI spec for transport and Argument types - Enhance schema generation tool to auto-convert discriminators to allOf with if/then blocks - Replace manual discriminated union patterns with auto-generated schema - Ensures schema stays in sync with OpenAPI spec and produces cleaner validation errors --- docs/reference/api/openapi.yaml | 23 +++- docs/reference/server-json/server.schema.json | 128 ++++++++++++++++-- tools/extract-server-schema/main.go | 110 ++++++++++++++- 3 files changed, 243 insertions(+), 18 deletions(-) diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 2680619d..d404a93b 100644 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -347,11 +347,16 @@ components: description: A hint to help clients determine the appropriate runtime for the package. This field should be provided when `runtimeArguments` are present. examples: [npx, uvx, docker, dnx] transport: - anyOf: + discriminator: + propertyName: type + mapping: + stdio: '#/components/schemas/StdioTransport' + streamable-http: '#/components/schemas/StreamableHttpTransport' + sse: '#/components/schemas/SseTransport' + oneOf: - $ref: '#/components/schemas/StdioTransport' - $ref: '#/components/schemas/StreamableHttpTransport' - $ref: '#/components/schemas/SseTransport' - description: Transport protocol configuration for the package runtimeArguments: type: array description: A list of arguments to be passed to the package's runtime command (such as docker or npx). The `runtimeHint` field should be provided when `runtimeArguments` are present. @@ -478,7 +483,12 @@ components: Argument: description: "Warning: Arguments construct command-line parameters that may contain user-provided input. This creates potential command injection risks if clients execute commands in a shell environment. For example, a malicious argument value like ';rm -rf ~/Development' could execute dangerous commands. Clients should prefer non-shell execution methods (e.g., posix_spawn) when possible to eliminate injection risks entirely. Where not possible, clients should obtain consent from users or agents to run the resolved command before execution." - anyOf: + discriminator: + propertyName: type + mapping: + positional: '#/components/schemas/PositionalArgument' + named: '#/components/schemas/NamedArgument' + oneOf: - $ref: '#/components/schemas/PositionalArgument' - $ref: '#/components/schemas/NamedArgument' @@ -624,7 +634,12 @@ components: remotes: type: array items: - anyOf: + discriminator: + propertyName: type + mapping: + streamable-http: '#/components/schemas/StreamableHttpTransport' + sse: '#/components/schemas/SseTransport' + oneOf: - $ref: '#/components/schemas/StreamableHttpTransport' - $ref: '#/components/schemas/SseTransport' _meta: diff --git a/docs/reference/server-json/server.schema.json b/docs/reference/server-json/server.schema.json index 8022347a..705373b4 100644 --- a/docs/reference/server-json/server.schema.json +++ b/docs/reference/server-json/server.schema.json @@ -5,15 +5,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Argument": { - "anyOf": [ + "allOf": [ { - "$ref": "#/definitions/PositionalArgument" + "if": { + "properties": { + "type": { + "const": "positional" + } + } + }, + "then": { + "$ref": "#/definitions/PositionalArgument" + } }, { - "$ref": "#/definitions/NamedArgument" + "if": { + "properties": { + "type": { + "const": "named" + } + } + }, + "then": { + "$ref": "#/definitions/NamedArgument" + } + } + ], + "description": "Warning: Arguments construct command-line parameters that may contain user-provided input. This creates potential command injection risks if clients execute commands in a shell environment. For example, a malicious argument value like ';rm -rf ~/Development' could execute dangerous commands. Clients should prefer non-shell execution methods (e.g., posix_spawn) when possible to eliminate injection risks entirely. Where not possible, clients should obtain consent from users or agents to run the resolved command before execution.", + "properties": { + "type": { + "enum": [ + "positional", + "named" + ], + "type": "string" } + }, + "required": [ + "type" ], - "description": "Warning: Arguments construct command-line parameters that may contain user-provided input. This creates potential command injection risks if clients execute commands in a shell environment. For example, a malicious argument value like ';rm -rf ~/Development' could execute dangerous commands. Clients should prefer non-shell execution methods (e.g., posix_spawn) when possible to eliminate injection risks entirely. Where not possible, clients should obtain consent from users or agents to run the resolved command before execution." + "type": "object" }, "Icon": { "description": "An optionally-sized icon that can be displayed in a user interface.", @@ -262,18 +293,58 @@ "type": "string" }, "transport": { - "anyOf": [ + "allOf": [ { - "$ref": "#/definitions/StdioTransport" + "if": { + "properties": { + "type": { + "const": "stdio" + } + } + }, + "then": { + "$ref": "#/definitions/StdioTransport" + } }, { - "$ref": "#/definitions/StreamableHttpTransport" + "if": { + "properties": { + "type": { + "const": "streamable-http" + } + } + }, + "then": { + "$ref": "#/definitions/StreamableHttpTransport" + } }, { - "$ref": "#/definitions/SseTransport" + "if": { + "properties": { + "type": { + "const": "sse" + } + } + }, + "then": { + "$ref": "#/definitions/SseTransport" + } } ], - "description": "Transport protocol configuration for the package" + "properties": { + "type": { + "enum": [ + "stdio", + "streamable-http", + "sse" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" }, "version": { "description": "Package version. Must be a specific version. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '\u003e=1.2.3', '1.x', '1.*').", @@ -427,14 +498,45 @@ }, "remotes": { "items": { - "anyOf": [ + "allOf": [ { - "$ref": "#/definitions/StreamableHttpTransport" + "if": { + "properties": { + "type": { + "const": "streamable-http" + } + } + }, + "then": { + "$ref": "#/definitions/StreamableHttpTransport" + } }, { - "$ref": "#/definitions/SseTransport" + "if": { + "properties": { + "type": { + "const": "sse" + } + } + }, + "then": { + "$ref": "#/definitions/SseTransport" + } } - ] + ], + "properties": { + "type": { + "enum": [ + "streamable-http", + "sse" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" }, "type": "array" }, diff --git a/tools/extract-server-schema/main.go b/tools/extract-server-schema/main.go index a554e252..abb5e95f 100644 --- a/tools/extract-server-schema/main.go +++ b/tools/extract-server-schema/main.go @@ -99,7 +99,10 @@ func main() { "definitions": definitions, } - // Replace all #/components/schemas/ references with #/definitions/ + // Convert OpenAPI discriminators to JSON Schema if/then/else patterns first + jsonSchema = convertDiscriminators(jsonSchema).(map[string]interface{}) + + // Then replace all #/components/schemas/ references with #/definitions/ jsonSchema = replaceComponentRefs(jsonSchema).(map[string]interface{}) // Convert to JSON @@ -191,3 +194,108 @@ func replaceComponentRefs(obj interface{}) interface{} { return obj } } + +// convertDiscriminators converts OpenAPI discriminators to JSON Schema if/then/else patterns +func convertDiscriminators(obj interface{}) interface{} { + switch v := obj.(type) { + case map[string]interface{}: + // Check if this object has a discriminator with oneOf + if discriminator, hasDiscriminator := v["discriminator"].(map[string]interface{}); hasDiscriminator { + if oneOf, hasOneOf := v["oneOf"].([]interface{}); hasOneOf { + // Extract discriminator property name and mapping + propertyName, _ := discriminator["propertyName"].(string) + mapping, _ := discriminator["mapping"].(map[string]interface{}) + + if propertyName != "" && mapping != nil && len(oneOf) > 0 { + // Get description if present + description, _ := v["description"].(string) + + // Build the allOf with if/then blocks for discriminated union + result := buildDiscriminatedUnion(propertyName, mapping, oneOf, description) + + // Recursively convert discriminators in the result + return convertDiscriminators(result) + } + } + } + + // Recursively convert discriminators in nested objects + result := make(map[string]interface{}) + for key, value := range v { + result[key] = convertDiscriminators(value) + } + return result + + case []interface{}: + result := make([]interface{}, len(v)) + for i, item := range v { + result[i] = convertDiscriminators(item) + } + return result + + default: + return obj + } +} + +// buildDiscriminatedUnion builds an allOf structure with separate if/then blocks for each discriminator value +func buildDiscriminatedUnion(propertyName string, mapping map[string]interface{}, oneOf []interface{}, description string) map[string]interface{} { + // Build a sorted list of mapping entries by extracting from oneOf order + mappingList := make([]struct{ key, ref string }, 0, len(oneOf)) + for _, item := range oneOf { + if refMap, ok := item.(map[string]interface{}); ok { + if ref, ok := refMap["$ref"].(string); ok { + // Find the key in mapping that matches this ref + for key, value := range mapping { + if refValue, ok := value.(string); ok && refValue == ref { + mappingList = append(mappingList, struct{ key, ref string }{key, ref}) + break + } + } + } + } + } + + // Extract enum values in the same order + enumValues := make([]interface{}, 0, len(mappingList)) + for _, item := range mappingList { + enumValues = append(enumValues, item.key) + } + + // Build allOf array with separate if/then for each type + allOfItems := make([]interface{}, 0, len(mappingList)) + for _, item := range mappingList { + allOfItems = append(allOfItems, map[string]interface{}{ + "if": map[string]interface{}{ + "properties": map[string]interface{}{ + propertyName: map[string]interface{}{ + "const": item.key, + }, + }, + }, + "then": map[string]interface{}{ + "$ref": item.ref, + }, + }) + } + + // Build result as regular map (will be alphabetically sorted by Go's json.Marshal) + result := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + propertyName: map[string]interface{}{ + "type": "string", + "enum": enumValues, + }, + }, + "required": []interface{}{propertyName}, + "allOf": allOfItems, + } + + // Add description if present + if description != "" { + result["description"] = description + } + + return result +}