Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -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:
Expand Down
128 changes: 115 additions & 13 deletions docs/reference/server-json/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.*').",
Expand Down Expand Up @@ -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"
},
Expand Down
110 changes: 109 additions & 1 deletion tools/extract-server-schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}