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 +}