Enhanced server.json validation (phase 1) (#636)
Added schema validation and support for exhaustive and more detailed validation to existing validators. Added new `mcp-publisher validate` command. No change to any existing tests or behavior other than the new command. ## Motivation and Context Many currently published servers fail schema validation. Of those that pass, many have semantic/logical errors or other errors that make them impossible to configure, or their configuration when applied doesn't generate correct MCP server configuration (per their own documentation). To address this, we need better tooling to validate servers and to inform server publishers of errors, issues, and best practices in a way that is clear and actionable, both during server development and at time of publishing. I have started a discussion about the broader topic: https://github.com/modelcontextprotocol/registry/discussions/635 There is also a fairly detailed document describing the design, architecture, implementation, future plans, etc. See [Enhanced Validation Design](docs/explanations/proposed-enhanced-validation.md) This PR is the first step in the process of improving validation. It adds schema validation and updates all existing validators to use a new model that allows them to track context and return rich and exhaustive results. This has been done in a way that is backward compatible (ValidateServerJSON is unchanged as are all existing validation unit tests). There is a new `mcp-publisher` command called `validate` that is currenty the only place that schema validation and enhanced (exhaustive/rich) validation results are exposed. ### Changes Internal Validation improvements - Schema validation has been implemented and is available as an option in validation methods (defaults to off) - Existing validators track the JSON path (context) of the attribute they're evaluating, passing it to any child validators - Existing validators are exhaustive (return all issues, not failing fast on the first error) - Existing validators return rich validation results, including: - type (schema, semantic, linter) - severity (error, warn, info) - path (JSON path to server.json element) - reference (to the schema element or rule that triggered the result) - message (currently the same as previous error message) A new `validate` command has been added to `mcp-publisher` so publishers can evaluate their server.json before publishing - Includes new schema validation and previous semantic validation - Shows full details of all issues encountered during validation ### Usage Given a server.json that looks like this: ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.BobDickinson/registry", "description": "An MCP server that provides [describe what your server does]", "repository": { "url": "", "source": "" }, "version": "=1.0.0", "packages": [ { "registryType": "oci", "registryBaseUrl": "https://docker.io", "identifier": "registry", "version": "1.0.0", "transport": { "type": "sse" }, "packageArguments": [ { "type": "named", "name": "--mode", "description": "Operation mode", "format": "foo" } ], "environmentVariables": [ { "description": "Your API key for the service", "isRequired": true, "format": "string", "isSecret": true, "name": "YOUR_API_KEY" } ] } ] } ``` Run the command: `mcp-publisher validate server.json` Which will produce the output: ```bash ❌ Validation failed with 5 issue(s): 1. [error] packages.0.packageArguments.0.format (schema) value must be one of "string", "number", "boolean", "filepath" Reference: #/definitions/Input/properties/format/enum from: [#/definitions/ServerDetail]/properties/packages/items/[#/definitions/Package]/properties/packageArguments/items/[#/definitions/Argument]/else/[#/definitions/NamedArgument]/allOf/0/[#/definitions/InputWithVariables]/allOf/0/[#/definitions/Input]/properties/format/enum 2. [error] packages.0.transport (schema) missing required fields: 'url' Reference: #/definitions/SseTransport/required from: [#/definitions/ServerDetail]/properties/packages/items/[#/definitions/Package]/properties/transport/else/else/[#/definitions/SseTransport]/required 3. [error] repository.url (schema) '' has invalid format 'uri' Reference: #/definitions/Repository/properties/url/format from: [#/definitions/ServerDetail]/properties/repository/[#/definitions/Repository]/properties/url/format 4. [error] version (semantic) version must be a specific version, not a range: "=1.0.0" Reference: version-looks-like-range 5. [error] packages[0].transport.url (semantic) url is required for sse transport type Reference: streamable-transport-url-required Error: validation failed ``` Note in the results above: - All errors contain the JSON path of the triggering element from server.json. - The first three errors are schema errors detected during schema validation. The `Reference` provided contains an absolute path to the schema rule that was violated, followed by the full schema path that enforced that rule (with $ref redirect values substited and enclosed in square brackets). - The error messages provided by the existing semantic validators are exactly the same as the previous error messages those validators produced. In the final solution we would remove semantic validators that are redundant to schema validation (as in number 5 above). We have the option to use the structured data produced by schema validation to replace the generic schema validation message with a more descriptive message if that is an issue (in particular, if the only argument for an ad-hoc validator is that is produces a better/cleaner message, we would just move that message creation code to the schema error result formatter and special case it as opposed to having a redundant validator). ### What's Next If this general direction is supported and this PR is accepted, a fast follow would include: - Add new validators (infliuenced by observed issues) for both semantic errors and best practices - Require passing schema validation (in addition to improved semantic validation) in order to publish - Remove existing validation code that is redundant to schema validation (ensuring error messages remain intelligible) - Update validation client code and tests to handle the new rich return types natively ### Issues Schema validation requires embedding the `server.schema.json` file into the validators package via `go:embed`, which is restricted from embedding files outside of the package. For this reason we copy the schema file into a schema subdir (via a prep target in the makefile) and we .gitignore it. I tried cleaning up the copy in the make clean target, but then the linter would complain about the embed target being missing if it wasn't there during development. I also considered just checking in the schema file and assuming a separate workflow for updating the embedded schema file, but it's not clear what that workflow would be or how it would be maintained. I'm open to a different approach, but the schema file does need to be embedded somehow for schema validation to work. ## How Has This Been Tested? All existing validation tests pass, new tests implemented and all pass. ## Breaking Changes No breaking changes, no behavior change except new `validate` command ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [X] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [X] My code follows the repository's style guidelines - [X] New and existing tests pass locally - [X] I have added appropriate error handling - [X] I have added or updated documentation as needed --------- Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
parent
d3d137e170
commit
fc88cf0870
14 changed files with 6107 additions and 179 deletions
476
internal/validators/schema.go
Normal file
476
internal/validators/schema.go
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
package validators
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
||||
"github.com/modelcontextprotocol/registry/pkg/model"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
//go:embed schemas/*.json
|
||||
var schemaFS embed.FS
|
||||
|
||||
// extractVersionFromSchemaURL extracts the version identifier from a schema URL
|
||||
// e.g., "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json" -> "2025-10-17"
|
||||
// e.g., "https://static.modelcontextprotocol.io/schemas/draft/server.schema.json" -> "draft"
|
||||
// Version identifier can contain: A-Z, a-z, 0-9, hyphen (-), underscore (_), tilde (~), and period (.)
|
||||
func extractVersionFromSchemaURL(schemaURL string) (string, error) {
|
||||
// Pattern: /schemas/{identifier}/server.schema.json
|
||||
// Identifier allowed characters: A-Z, a-z, 0-9, -, _, ~, .
|
||||
re := regexp.MustCompile(`/schemas/([A-Za-z0-9_~.-]+)/server\.schema\.json`)
|
||||
matches := re.FindStringSubmatch(schemaURL)
|
||||
if len(matches) < 2 {
|
||||
return "", fmt.Errorf("invalid schema URL format: %s", schemaURL)
|
||||
}
|
||||
return matches[1], nil
|
||||
}
|
||||
|
||||
// loadSchemaByVersion loads a schema file from the embedded filesystem by version
|
||||
func loadSchemaByVersion(version string) ([]byte, error) {
|
||||
filename := fmt.Sprintf("schemas/%s.json", version)
|
||||
data, err := schemaFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schema version %s not found in embedded schemas: %w", version, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetCurrentSchemaVersion returns the current schema URL from constants
|
||||
func GetCurrentSchemaVersion() (string, error) {
|
||||
return model.CurrentSchemaURL, nil
|
||||
}
|
||||
|
||||
// validateServerJSONSchema validates the server JSON against the schema version specified in $schema using jsonschema
|
||||
// Empty/missing schema always produces an error.
|
||||
// If performValidation is true, performs full JSON Schema validation.
|
||||
// If performValidation is false, only checks for empty schema (always an error) and handles non-current schemas per policy.
|
||||
// nonCurrentPolicy determines how non-current (but valid) schema versions are handled when performValidation is true.
|
||||
func validateServerJSONSchema(serverJSON *apiv0.ServerJSON, performValidation bool, nonCurrentPolicy SchemaVersionPolicy) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
ctx := &ValidationContext{}
|
||||
|
||||
// Empty/missing schema is always an error
|
||||
if serverJSON.Schema == "" {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("schema").String(),
|
||||
"$schema field is required",
|
||||
ValidationIssueSeverityError,
|
||||
"schema-field-required",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Extract version from the schema URL
|
||||
version, err := extractVersionFromSchemaURL(serverJSON.Schema)
|
||||
if err != nil {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("failed to extract schema version from URL: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-version-extraction-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Check if the schema version is the current one and handle based on policy
|
||||
currentSchemaURL, err := GetCurrentSchemaVersion()
|
||||
if err == nil && serverJSON.Schema != currentSchemaURL {
|
||||
// Extract current version for the message
|
||||
currentVersion, _ := extractVersionFromSchemaURL(currentSchemaURL)
|
||||
|
||||
switch nonCurrentPolicy {
|
||||
case SchemaVersionPolicyError:
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("schema version %s is not the current version (%s). Use the current schema version", version, currentVersion),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-version-deprecated",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
case SchemaVersionPolicyWarn:
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("schema version %s is not the current version (%s). Consider updating to the latest schema version", version, currentVersion),
|
||||
ValidationIssueSeverityWarning,
|
||||
"schema-version-deprecated",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
case SchemaVersionPolicyAllow:
|
||||
// No issue added - allow non-current schemas silently
|
||||
}
|
||||
}
|
||||
|
||||
// Load the appropriate schema file to verify it exists (required for schema version validation)
|
||||
// This ensures that the specified schema version is available, even when not performing full validation
|
||||
schemaData, err := loadSchemaByVersion(version)
|
||||
if err != nil {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("schema version %s not available: %v", version, err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-version-not-available",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// If not performing validation, return after performing schema version checks (done above)
|
||||
if !performValidation {
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse the schema
|
||||
var schema map[string]any
|
||||
if err := json.Unmarshal(schemaData, &schema); err != nil {
|
||||
// If we can't parse the schema, return an error
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("failed to parse schema file: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-parse-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Convert the server JSON to a map for validation
|
||||
serverData, err := json.Marshal(serverJSON)
|
||||
if err != nil {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeJSON,
|
||||
"",
|
||||
fmt.Sprintf("failed to marshal server JSON for schema validation: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"json-marshal-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
var serverMap map[string]any
|
||||
if err := json.Unmarshal(serverData, &serverMap); err != nil {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeJSON,
|
||||
"",
|
||||
fmt.Sprintf("failed to unmarshal server JSON for schema validation: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"json-unmarshal-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Get the schema $id for proper reference resolution
|
||||
// Schema files must have $id (required by JSON Schema spec and verified by sync process)
|
||||
// However, we check here in case a schema file exists but is malformed or missing $id
|
||||
schemaID, ok := schema["$id"].(string)
|
||||
if !ok {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("schema file for version %s exists but is missing or has invalid $id field (required by JSON Schema spec)", version),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-missing-id",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Validate against schema using jsonschema library
|
||||
compiler := jsonschema.NewCompiler()
|
||||
if err := compiler.AddResource(schemaID, bytes.NewReader(schemaData)); err != nil {
|
||||
// If we can't add the schema resource, return an error
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
ctx.Field("schema").String(),
|
||||
fmt.Sprintf("failed to add schema resource: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-resource-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
schemaInstance, err := compiler.Compile(schemaID)
|
||||
if err != nil {
|
||||
// If we can't compile the schema, return an error
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
"",
|
||||
fmt.Sprintf("failed to compile schema: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-compile-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Perform validation
|
||||
if err := schemaInstance.Validate(serverMap); err != nil {
|
||||
// Convert validation error to our issue format
|
||||
var validationErr *jsonschema.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
// Process the validation error and its causes
|
||||
addValidationError(result, validationErr, schema)
|
||||
} else {
|
||||
// Fallback for other error types
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
"",
|
||||
fmt.Sprintf("schema validation failed: %v", err),
|
||||
ValidationIssueSeverityError,
|
||||
"schema-validation-error",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// addValidationError processes validation errors and extracts useful information
|
||||
func addValidationError(result *ValidationResult, validationErr *jsonschema.ValidationError, schema map[string]any) {
|
||||
// Use DetailedOutput to get the nested error details
|
||||
detailed := validationErr.DetailedOutput()
|
||||
|
||||
// Process the detailed error structure
|
||||
|
||||
addDetailedErrors(result, detailed, schema)
|
||||
}
|
||||
|
||||
// ConvertJSONPointerToBracketNotation converts a JSON Pointer path (RFC 6901) to bracket notation
|
||||
// format to match the format used by semantic validation (ValidationContext).
|
||||
// The transformation includes:
|
||||
// 1. Remove leading slash from JSON Pointer format
|
||||
// 2. Convert path separators from "/" to "."
|
||||
// 3. Convert numeric array indices from dot notation to bracket notation
|
||||
// Example: "/packages/0/transport" -> "packages[0].transport"
|
||||
// Example: "/0/name" -> "[0].name"
|
||||
// Example: "/packages/0/transport/1/url" -> "packages[0].transport[1].url"
|
||||
func ConvertJSONPointerToBracketNotation(jsonPointer string) string {
|
||||
if jsonPointer == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Step 1: Convert JSON Pointer to dot notation (remove leading slash, convert / to .)
|
||||
path := strings.TrimPrefix(jsonPointer, "/")
|
||||
path = strings.ReplaceAll(path, "/", ".")
|
||||
|
||||
// Step 2: Convert dot notation array indices to bracket notation
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(path, ".")
|
||||
var result strings.Builder
|
||||
|
||||
for i, part := range parts {
|
||||
// Check if part is a pure number (array index)
|
||||
if _, err := strconv.Atoi(part); err == nil {
|
||||
// It's a numeric index - use bracket notation
|
||||
result.WriteString(fmt.Sprintf("[%s]", part))
|
||||
// Add dot after bracket if next part exists and is a field name (not a number)
|
||||
if i < len(parts)-1 {
|
||||
nextPart := parts[i+1]
|
||||
if _, err := strconv.Atoi(nextPart); err != nil {
|
||||
// Next part is a field name, add dot separator
|
||||
result.WriteString(".")
|
||||
}
|
||||
// If next part is a number, no dot needed (brackets will connect: [0][1])
|
||||
}
|
||||
} else {
|
||||
// It's a field name
|
||||
// Add dot separator before field name if previous part was also a field name
|
||||
if i > 0 {
|
||||
prevPart := parts[i-1]
|
||||
if _, err := strconv.Atoi(prevPart); err != nil {
|
||||
// Previous was not a number (it's a field), need dot separator
|
||||
result.WriteString(".")
|
||||
}
|
||||
// If previous was a number, brackets already written, dot added after bracket above
|
||||
}
|
||||
result.WriteString(part)
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// addDetailedErrors recursively processes detailed validation errors
|
||||
func addDetailedErrors(result *ValidationResult, detailed jsonschema.Detailed, schema map[string]any) {
|
||||
// Only process errors that have specific field paths and meaningful messages
|
||||
if detailed.InstanceLocation != "" && detailed.Error != "" {
|
||||
// Convert JSON Pointer format to bracket notation to match semantic validation format
|
||||
path := ConvertJSONPointerToBracketNotation(detailed.InstanceLocation)
|
||||
|
||||
// Clean up the error message
|
||||
message := detailed.Error
|
||||
|
||||
// Make messages more user-friendly
|
||||
if strings.Contains(message, "missing properties:") {
|
||||
message = strings.ReplaceAll(message, "missing properties:", "missing required fields:")
|
||||
}
|
||||
if strings.Contains(message, "is not valid") {
|
||||
message = strings.ReplaceAll(message, "is not valid", "has invalid format")
|
||||
}
|
||||
|
||||
// Build the full resolved reference path
|
||||
reference := buildResolvedReference(detailed.KeywordLocation, detailed.AbsoluteKeywordLocation, schema)
|
||||
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSchema,
|
||||
path,
|
||||
message,
|
||||
ValidationIssueSeverityError,
|
||||
reference, // cleaned schema rule path for deterministic mapping
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Process nested errors
|
||||
for _, nested := range detailed.Errors {
|
||||
addDetailedErrors(result, nested, schema)
|
||||
}
|
||||
}
|
||||
|
||||
// buildResolvedReference extracts the resolved reference path by resolving $ref segments
|
||||
func buildResolvedReference(keywordLocation, absoluteKeywordLocation string, schema map[string]any) string {
|
||||
if keywordLocation == "" || absoluteKeywordLocation == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Clean up the absolute location by removing file:// prefix
|
||||
absolute := absoluteKeywordLocation
|
||||
if strings.HasPrefix(absolute, "file://") {
|
||||
absolute = strings.TrimPrefix(absolute, "file://")
|
||||
if idx := strings.Index(absolute, "#"); idx != -1 {
|
||||
absolute = absolute[idx:] // Keep only the #/path part
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the keyword location to understand the $ref chain
|
||||
keyword := strings.TrimPrefix(keywordLocation, "/")
|
||||
keywordParts := strings.Split(keyword, "/")
|
||||
|
||||
// Build the path showing $ref resolution
|
||||
pathSegments := make([]string, 0)
|
||||
|
||||
// Track the resolved path so far (starts empty, gets built up as we resolve $refs)
|
||||
resolvedPath := ""
|
||||
|
||||
// Process each part of the keyword path
|
||||
for i, part := range keywordParts {
|
||||
if part == "" {
|
||||
continue // Skip empty parts
|
||||
}
|
||||
|
||||
if part == "$ref" {
|
||||
// This is a $ref - we need to look up what it resolves to
|
||||
// For the first $ref, use the path from the root
|
||||
// For subsequent $refs, use the resolved path from the previous $ref plus the current segment
|
||||
var refPath string
|
||||
if resolvedPath == "" {
|
||||
// First $ref - use the path from the root
|
||||
refPath = strings.Join(keywordParts[:i+1], "/")
|
||||
refPath = "/" + refPath
|
||||
} else {
|
||||
// Subsequent $ref - use the resolved path plus the current segment
|
||||
refPath = resolvedPath + "/" + part
|
||||
}
|
||||
|
||||
// Look up the $ref value in the schema
|
||||
refValue := resolveRefInSchema(schema, refPath)
|
||||
|
||||
if refValue != "" {
|
||||
pathSegments = append(pathSegments, fmt.Sprintf("[%s]", refValue))
|
||||
// Update the resolved path for the next $ref
|
||||
resolvedPath = refValue
|
||||
} else {
|
||||
pathSegments = append(pathSegments, "[$ref]")
|
||||
}
|
||||
} else {
|
||||
// Regular path segment
|
||||
pathSegments = append(pathSegments, part)
|
||||
// Add this segment to the resolved path for the next $ref
|
||||
if resolvedPath != "" {
|
||||
resolvedPath = resolvedPath + "/" + part
|
||||
} else {
|
||||
resolvedPath = part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the final reference string
|
||||
if len(pathSegments) > 0 {
|
||||
pathStr := strings.Join(pathSegments, "/")
|
||||
return fmt.Sprintf("%s from: %s", absolute, pathStr)
|
||||
}
|
||||
|
||||
// Fallback: return the absolute location with context
|
||||
return absolute + " (from: " + keywordLocation + ")"
|
||||
}
|
||||
|
||||
// resolveRefInSchema looks up a $ref value in the schema
|
||||
func resolveRefInSchema(schema map[string]any, refPath string) string {
|
||||
// Handle the # prefix - it indicates the root of the schema JSON
|
||||
refPath = strings.TrimPrefix(refPath, "#")
|
||||
|
||||
// Parse the JSON pointer path
|
||||
pathParts := strings.Split(strings.TrimPrefix(refPath, "/"), "/")
|
||||
|
||||
// Navigate through the schema to find the $ref value
|
||||
var current any = schema
|
||||
for _, part := range pathParts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if part == "$ref" {
|
||||
// We've reached the $ref, return its value
|
||||
if currentMap, ok := current.(map[string]any); ok {
|
||||
if refValue, ok := currentMap["$ref"].(string); ok {
|
||||
return refValue
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Navigate to the next level
|
||||
// Check if this is an array index
|
||||
if index, err := strconv.Atoi(part); err == nil {
|
||||
// This is an array index - check if current element is an array
|
||||
if arr, ok := current.([]any); ok && index < len(arr) {
|
||||
current = arr[index]
|
||||
} else {
|
||||
// Current element is not an array or index out of bounds
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
// This is a map key
|
||||
if currentMap, ok := current.(map[string]any); ok {
|
||||
current = currentMap[part]
|
||||
} else {
|
||||
// Current element is not a map
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
121
internal/validators/schema_test.go
Normal file
121
internal/validators/schema_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/validators"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertJSONPointerToBracketNotation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonPointer string
|
||||
expectedOutput string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "single array index in middle",
|
||||
jsonPointer: "/packages/0/transport",
|
||||
expectedOutput: "packages[0].transport",
|
||||
description: "JSON Pointer with single array index converts to bracket notation",
|
||||
},
|
||||
{
|
||||
name: "multiple array indices",
|
||||
jsonPointer: "/packages/0/transport/1/url",
|
||||
expectedOutput: "packages[0].transport[1].url",
|
||||
description: "JSON Pointer with multiple array indices converts correctly",
|
||||
},
|
||||
{
|
||||
name: "leading array index",
|
||||
jsonPointer: "/0/name",
|
||||
expectedOutput: "[0].name",
|
||||
description: "JSON Pointer starting with array index converts to leading bracket",
|
||||
},
|
||||
{
|
||||
name: "trailing array index",
|
||||
jsonPointer: "/packages/0",
|
||||
expectedOutput: "packages[0]",
|
||||
description: "JSON Pointer ending with array index converts correctly",
|
||||
},
|
||||
{
|
||||
name: "no array indices",
|
||||
jsonPointer: "/name/version",
|
||||
expectedOutput: "name.version",
|
||||
description: "JSON Pointer without array indices converts to dot notation only",
|
||||
},
|
||||
{
|
||||
name: "complex nested path",
|
||||
jsonPointer: "/packages/0/runtimeArguments/1/name",
|
||||
expectedOutput: "packages[0].runtimeArguments[1].name",
|
||||
description: "Complex nested JSON Pointer with multiple indices converts correctly",
|
||||
},
|
||||
{
|
||||
name: "multiple consecutive indices",
|
||||
jsonPointer: "/a/0/1/2",
|
||||
expectedOutput: "a[0][1][2]",
|
||||
description: "JSON Pointer with consecutive array indices converts to consecutive brackets",
|
||||
},
|
||||
{
|
||||
name: "single character path with index",
|
||||
jsonPointer: "/a/0",
|
||||
expectedOutput: "a[0]",
|
||||
description: "Simple JSON Pointer with single field and index converts correctly",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
jsonPointer: "",
|
||||
expectedOutput: "",
|
||||
description: "Empty JSON Pointer returns empty string",
|
||||
},
|
||||
{
|
||||
name: "root path",
|
||||
jsonPointer: "/",
|
||||
expectedOutput: "",
|
||||
description: "Root JSON Pointer (just slash) converts to empty string",
|
||||
},
|
||||
{
|
||||
name: "only index",
|
||||
jsonPointer: "/0",
|
||||
expectedOutput: "[0]",
|
||||
description: "JSON Pointer with only array index converts to bracket notation",
|
||||
},
|
||||
{
|
||||
name: "two digit index",
|
||||
jsonPointer: "/packages/10/transport",
|
||||
expectedOutput: "packages[10].transport",
|
||||
description: "JSON Pointer with multi-digit array index converts correctly",
|
||||
},
|
||||
{
|
||||
name: "three digit index",
|
||||
jsonPointer: "/packages/123/transport",
|
||||
expectedOutput: "packages[123].transport",
|
||||
description: "JSON Pointer with three-digit array index converts correctly",
|
||||
},
|
||||
{
|
||||
name: "remotes array index",
|
||||
jsonPointer: "/remotes/0/url",
|
||||
expectedOutput: "remotes[0].url",
|
||||
description: "JSON Pointer for remotes array converts correctly",
|
||||
},
|
||||
{
|
||||
name: "package arguments nested",
|
||||
jsonPointer: "/packages/0/packageArguments/0/format",
|
||||
expectedOutput: "packages[0].packageArguments[0].format",
|
||||
description: "JSON Pointer with nested array structures converts correctly",
|
||||
},
|
||||
{
|
||||
name: "repository url",
|
||||
jsonPointer: "/repository/url",
|
||||
expectedOutput: "repository.url",
|
||||
description: "JSON Pointer without array indices converts to simple dot notation",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validators.ConvertJSONPointerToBracketNotation(tt.jsonPointer)
|
||||
assert.Equal(t, tt.expectedOutput, result, "%s: JSON Pointer format should convert to bracket notation", tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
478
internal/validators/schemas/2025-07-09.json
Normal file
478
internal/validators/schemas/2025-07-09.json
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
|
||||
"title": "MCP Server Detail",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"definitions": {
|
||||
"Repository": {
|
||||
"type": "object",
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/<owner>/<repo> --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
|
||||
},
|
||||
"subfolder": {
|
||||
"type": "string",
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Server": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"minLength": 3,
|
||||
"maxLength": 200
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "deprecated", "deleted"],
|
||||
"default": "active",
|
||||
"description": "Server lifecycle status. 'deprecated' indicates the server is no longer recommended for new usage. 'deleted' indicates the server should never be installed and existing installations should be uninstalled - this is rare, and usually indicates malware or a legal takedown."
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"example": "1.0.2",
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*')."
|
||||
},
|
||||
"website_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Package": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"registry_type",
|
||||
"identifier",
|
||||
"version",
|
||||
"transport"
|
||||
],
|
||||
"properties": {
|
||||
"registry_type": {
|
||||
"type": "string",
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": ["npm", "pypi", "oci", "nuget", "mcpb"]
|
||||
},
|
||||
"registry_base_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": ["https://registry.npmjs.org", "https://pypi.org", "https://docker.io", "https://api.nuget.org", "https://github.com", "https://gitlab.com"]
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": ["@modelcontextprotocol/server-brave-search", "https://github.com/example/releases/download/v1.0.0/package.mcpb"]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Package version. Must be a specific version. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*').",
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"example": "1.0.2",
|
||||
"minLength": 1
|
||||
},
|
||||
"file_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce"
|
||||
},
|
||||
"runtime_hint": {
|
||||
"type": "string",
|
||||
"description": "A hint to help clients determine the appropriate runtime for the package. This field should be provided when `runtime_arguments` are present.",
|
||||
"examples": [
|
||||
"npx",
|
||||
"uvx",
|
||||
"docker",
|
||||
"dnx"
|
||||
]
|
||||
},
|
||||
"transport": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
],
|
||||
"description": "Transport protocol configuration for the package"
|
||||
},
|
||||
"runtime_arguments": {
|
||||
"type": "array",
|
||||
"description": "A list of arguments to be passed to the package's runtime command (such as docker or npx). The `runtime_hint` field should be provided when `runtime_arguments` are present.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"package_arguments": {
|
||||
"type": "array",
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"environment_variables": {
|
||||
"type": "array",
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"is_required": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"default": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n"
|
||||
},
|
||||
"is_secret": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"default": false
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input."
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variables": {
|
||||
"type": "object",
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"description": "A positional input is a value inserted verbatim into the command line.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional"
|
||||
},
|
||||
"value_hint": {
|
||||
"type": "string",
|
||||
"description": "An identifier-like hint for the value. This is not part of the command line, but can be used by client configuration and to provide hints to users.",
|
||||
"example": "file_path"
|
||||
},
|
||||
"is_repeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"value_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"NamedArgument": {
|
||||
"description": "A command-line `--flag={value}`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port"
|
||||
},
|
||||
"is_repeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"StdioTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "stdio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "streamable-http"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL template for the streamable-http transport. Variables in {curly_braces} reference argument value_hints, argument names, or environment variable names. After variable substitution, this should produce a valid URI.",
|
||||
"example": "https://api.example.com/mcp"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SseTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "sse"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Server-Sent Events endpoint URL",
|
||||
"example": "https://mcp-fs.example.com/sse"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Server"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json"
|
||||
},
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
}
|
||||
},
|
||||
"remotes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"type": "object",
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"io.modelcontextprotocol.registry/official": {
|
||||
"type": "object",
|
||||
"description": "Official MCP registry metadata (read-only, added by registry)",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
478
internal/validators/schemas/2025-09-16.json
Normal file
478
internal/validators/schemas/2025-09-16.json
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
|
||||
"title": "MCP Server Detail",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"definitions": {
|
||||
"Repository": {
|
||||
"type": "object",
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/<owner>/<repo> --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
|
||||
},
|
||||
"subfolder": {
|
||||
"type": "string",
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Server": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"minLength": 3,
|
||||
"maxLength": 200
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "deprecated", "deleted"],
|
||||
"default": "active",
|
||||
"description": "Server lifecycle status. 'deprecated' indicates the server is no longer recommended for new usage. 'deleted' indicates the server should never be installed and existing installations should be uninstalled - this is rare, and usually indicates malware or a legal takedown."
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"example": "1.0.2",
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*')."
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Package": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"registryType",
|
||||
"identifier",
|
||||
"version",
|
||||
"transport"
|
||||
],
|
||||
"properties": {
|
||||
"registryType": {
|
||||
"type": "string",
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": ["npm", "pypi", "oci", "nuget", "mcpb"]
|
||||
},
|
||||
"registryBaseUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": ["https://registry.npmjs.org", "https://pypi.org", "https://docker.io", "https://api.nuget.org", "https://github.com", "https://gitlab.com"]
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": ["@modelcontextprotocol/server-brave-search", "https://github.com/example/releases/download/v1.0.0/package.mcpb"]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Package version. Must be a specific version. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*').",
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"example": "1.0.2",
|
||||
"minLength": 1
|
||||
},
|
||||
"fileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce"
|
||||
},
|
||||
"runtimeHint": {
|
||||
"type": "string",
|
||||
"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": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/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.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"packageArguments": {
|
||||
"type": "array",
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"environmentVariables": {
|
||||
"type": "array",
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"isRequired": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"default": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n"
|
||||
},
|
||||
"isSecret": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"default": false
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input."
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variables": {
|
||||
"type": "object",
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"description": "A positional input is a value inserted verbatim into the command line.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional"
|
||||
},
|
||||
"valueHint": {
|
||||
"type": "string",
|
||||
"description": "An identifier-like hint for the value. This is not part of the command line, but can be used by client configuration and to provide hints to users.",
|
||||
"example": "file_path"
|
||||
},
|
||||
"isRepeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"valueHint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"NamedArgument": {
|
||||
"description": "A command-line `--flag={value}`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port"
|
||||
},
|
||||
"isRepeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"StdioTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "stdio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "streamable-http"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"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"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SseTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "sse"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Server-Sent Events endpoint URL",
|
||||
"example": "https://mcp-fs.example.com/sse"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Server"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json"
|
||||
},
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
}
|
||||
},
|
||||
"remotes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"type": "object",
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"io.modelcontextprotocol.registry/official": {
|
||||
"type": "object",
|
||||
"description": "Official MCP registry metadata (read-only, added by registry)",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
483
internal/validators/schemas/2025-09-29.json
Normal file
483
internal/validators/schemas/2025-09-29.json
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
||||
"title": "MCP Server Detail",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"definitions": {
|
||||
"Repository": {
|
||||
"type": "object",
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/<owner>/<repo> --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
|
||||
},
|
||||
"subfolder": {
|
||||
"type": "string",
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Server": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"minLength": 3,
|
||||
"maxLength": 200
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"example": "1.0.2",
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*')."
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Package": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"registryType",
|
||||
"identifier",
|
||||
"version",
|
||||
"transport"
|
||||
],
|
||||
"properties": {
|
||||
"registryType": {
|
||||
"type": "string",
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": [
|
||||
"npm",
|
||||
"pypi",
|
||||
"oci",
|
||||
"nuget",
|
||||
"mcpb"
|
||||
]
|
||||
},
|
||||
"registryBaseUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": [
|
||||
"https://registry.npmjs.org",
|
||||
"https://pypi.org",
|
||||
"https://docker.io",
|
||||
"https://api.nuget.org",
|
||||
"https://github.com",
|
||||
"https://gitlab.com"
|
||||
]
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": [
|
||||
"@modelcontextprotocol/server-brave-search",
|
||||
"https://github.com/example/releases/download/v1.0.0/package.mcpb"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Package version. Must be a specific version. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '>=1.2.3', '1.x', '1.*').",
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"example": "1.0.2",
|
||||
"minLength": 1
|
||||
},
|
||||
"fileSha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce"
|
||||
},
|
||||
"runtimeHint": {
|
||||
"type": "string",
|
||||
"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": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/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.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"packageArguments": {
|
||||
"type": "array",
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
}
|
||||
},
|
||||
"environmentVariables": {
|
||||
"type": "array",
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"isRequired": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"default": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n"
|
||||
},
|
||||
"isSecret": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"default": false
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"description": "The default value for the input."
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"variables": {
|
||||
"type": "object",
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"description": "A positional input is a value inserted verbatim into the command line.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional"
|
||||
},
|
||||
"valueHint": {
|
||||
"type": "string",
|
||||
"description": "An identifier-like hint for the value. This is not part of the command line, but can be used by client configuration and to provide hints to users.",
|
||||
"example": "file_path"
|
||||
},
|
||||
"isRepeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"valueHint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"NamedArgument": {
|
||||
"description": "A command-line `--flag={value}`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port"
|
||||
},
|
||||
"isRepeated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"StdioTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "stdio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "streamable-http"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"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"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SseTransport": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"description": "Transport type",
|
||||
"example": "sse"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Server-Sent Events endpoint URL",
|
||||
"example": "https://mcp-fs.example.com/sse"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Server"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json"
|
||||
},
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
}
|
||||
},
|
||||
"remotes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"type": "object",
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
549
internal/validators/schemas/2025-10-11.json
Normal file
549
internal/validators/schemas/2025-10-11.json
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
{
|
||||
"$comment": "This file is auto-generated from docs/reference/api/openapi.yaml. Do not edit manually. Run 'make generate-schema' to update.",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-10-11/server.schema.json",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"Argument": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PositionalArgument"
|
||||
},
|
||||
{
|
||||
"$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."
|
||||
},
|
||||
"Icon": {
|
||||
"description": "An optionally-sized icon that can be displayed in a user interface.",
|
||||
"properties": {
|
||||
"mimeType": {
|
||||
"description": "Optional MIME type override if the source MIME type is missing or generic. Must be one of: image/png, image/jpeg, image/jpg, image/svg+xml, image/webp.",
|
||||
"enum": [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/svg+xml",
|
||||
"image/webp"
|
||||
],
|
||||
"example": "image/png",
|
||||
"type": "string"
|
||||
},
|
||||
"sizes": {
|
||||
"description": "Optional array of strings that specify sizes at which the icon can be used. Each string should be in WxH format (e.g., '48x48', '96x96') or 'any' for scalable formats like SVG. If not provided, the client should assume that the icon can be used at any size.",
|
||||
"examples": [
|
||||
[
|
||||
"48x48",
|
||||
"96x96"
|
||||
],
|
||||
[
|
||||
"any"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"pattern": "^(\\d+x\\d+|any)$",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"src": {
|
||||
"description": "A standard URI pointing to an icon resource. Must be an HTTPS URL. Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the server or a trusted domain. Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.",
|
||||
"example": "https://example.com/icon.png",
|
||||
"format": "uri",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"description": "Optional specifier for the theme this icon is designed for. 'light' indicates the icon is designed to be used with a light background, and 'dark' indicates the icon is designed to be used with a dark background. If not provided, the client should assume the icon can be used with any theme.",
|
||||
"enum": [
|
||||
"light",
|
||||
"dark"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"src"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Input": {
|
||||
"properties": {
|
||||
"choices": {
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"example": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {
|
||||
"description": "The default value for the input. This should be a valid value for the input. If you want to provide input examples or guidance, use the `placeholder` field instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"default": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"isRequired": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSecret": {
|
||||
"default": false,
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"placeholder": {
|
||||
"description": "A placeholder for the input to be displaying during configuration. This is used to provide examples or guidance about the expected form or content of the input.",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "The value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"variables": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NamedArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A command-line `--flag={value}`."
|
||||
},
|
||||
"Package": {
|
||||
"properties": {
|
||||
"environmentVariables": {
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"fileSha256": {
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": [
|
||||
"@modelcontextprotocol/server-brave-search",
|
||||
"https://github.com/example/releases/download/v1.0.0/package.mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"packageArguments": {
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"registryBaseUrl": {
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": [
|
||||
"https://registry.npmjs.org",
|
||||
"https://pypi.org",
|
||||
"https://docker.io",
|
||||
"https://api.nuget.org",
|
||||
"https://github.com",
|
||||
"https://gitlab.com"
|
||||
],
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"registryType": {
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": [
|
||||
"npm",
|
||||
"pypi",
|
||||
"oci",
|
||||
"nuget",
|
||||
"mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"runtimeArguments": {
|
||||
"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.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"runtimeHint": {
|
||||
"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"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"transport": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
],
|
||||
"description": "Transport protocol configuration for the package"
|
||||
},
|
||||
"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.*').",
|
||||
"example": "1.0.2",
|
||||
"minLength": 1,
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryType",
|
||||
"identifier",
|
||||
"transport"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"valueHint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional",
|
||||
"type": "string"
|
||||
},
|
||||
"valueHint": {
|
||||
"description": "An identifier for the positional argument. It is not part of the command line. It may be used by client configuration as a label identifying the argument. It is also used to identify the value in transport URL variable substitution.",
|
||||
"example": "file_path",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A positional input is a value inserted verbatim into the command line."
|
||||
},
|
||||
"Repository": {
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/\u003cowner\u003e/\u003crepo\u003e --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github",
|
||||
"type": "string"
|
||||
},
|
||||
"subfolder": {
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-10-11/server.schema.json",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"_meta": {
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"additionalProperties": true,
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"example": {
|
||||
"buildInfo": {
|
||||
"commit": "abc123def456",
|
||||
"pipelineId": "build-789",
|
||||
"timestamp": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
"tool": "publisher-cli",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"icons": {
|
||||
"description": "Optional set of sized icons that the client can display in a user interface. Clients that support rendering icons MUST support at least the following MIME types: image/png and image/jpeg (safe, universal compatibility). Clients SHOULD also support: image/svg+xml (scalable but requires security precautions) and image/webp (modern, efficient format).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Icon"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"maxLength": 200,
|
||||
"minLength": 3,
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"type": "string"
|
||||
},
|
||||
"packages": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"remotes": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"title": {
|
||||
"description": "Optional human-readable title or display name for the MCP server. MCP subregistries or clients MAY choose to use this for display purposes.",
|
||||
"example": "Weather API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '\u003e=1.2.3', '1.x', '1.*').",
|
||||
"example": "1.0.2",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SseTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"example": "sse",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Server-Sent Events endpoint URL",
|
||||
"example": "https://mcp-fs.example.com/sse",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StdioTransport": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"example": "stdio",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"example": "streamable-http",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"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",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "server.json defining a Model Context Protocol (MCP) server"
|
||||
}
|
||||
549
internal/validators/schemas/2025-10-17.json
Normal file
549
internal/validators/schemas/2025-10-17.json
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
{
|
||||
"$comment": "This file is auto-generated from docs/reference/api/openapi.yaml. Do not edit manually. Run 'make generate-schema' to update.",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"Argument": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PositionalArgument"
|
||||
},
|
||||
{
|
||||
"$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."
|
||||
},
|
||||
"Icon": {
|
||||
"description": "An optionally-sized icon that can be displayed in a user interface.",
|
||||
"properties": {
|
||||
"mimeType": {
|
||||
"description": "Optional MIME type override if the source MIME type is missing or generic. Must be one of: image/png, image/jpeg, image/jpg, image/svg+xml, image/webp.",
|
||||
"enum": [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/svg+xml",
|
||||
"image/webp"
|
||||
],
|
||||
"example": "image/png",
|
||||
"type": "string"
|
||||
},
|
||||
"sizes": {
|
||||
"description": "Optional array of strings that specify sizes at which the icon can be used. Each string should be in WxH format (e.g., '48x48', '96x96') or 'any' for scalable formats like SVG. If not provided, the client should assume that the icon can be used at any size.",
|
||||
"examples": [
|
||||
[
|
||||
"48x48",
|
||||
"96x96"
|
||||
],
|
||||
[
|
||||
"any"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"pattern": "^(\\d+x\\d+|any)$",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"src": {
|
||||
"description": "A standard URI pointing to an icon resource. Must be an HTTPS URL. Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the server or a trusted domain. Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.",
|
||||
"example": "https://example.com/icon.png",
|
||||
"format": "uri",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"description": "Optional specifier for the theme this icon is designed for. 'light' indicates the icon is designed to be used with a light background, and 'dark' indicates the icon is designed to be used with a dark background. If not provided, the client should assume the icon can be used with any theme.",
|
||||
"enum": [
|
||||
"light",
|
||||
"dark"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"src"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Input": {
|
||||
"properties": {
|
||||
"choices": {
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"example": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {
|
||||
"description": "The default value for the input. This should be a valid value for the input. If you want to provide input examples or guidance, use the `placeholder` field instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"default": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"isRequired": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSecret": {
|
||||
"default": false,
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"placeholder": {
|
||||
"description": "A placeholder for the input to be displaying during configuration. This is used to provide examples or guidance about the expected form or content of the input.",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "The value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"variables": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NamedArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A command-line `--flag={value}`."
|
||||
},
|
||||
"Package": {
|
||||
"properties": {
|
||||
"environmentVariables": {
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"fileSha256": {
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": [
|
||||
"@modelcontextprotocol/server-brave-search",
|
||||
"https://github.com/example/releases/download/v1.0.0/package.mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"packageArguments": {
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"registryBaseUrl": {
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": [
|
||||
"https://registry.npmjs.org",
|
||||
"https://pypi.org",
|
||||
"https://docker.io",
|
||||
"https://api.nuget.org",
|
||||
"https://github.com",
|
||||
"https://gitlab.com"
|
||||
],
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"registryType": {
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": [
|
||||
"npm",
|
||||
"pypi",
|
||||
"oci",
|
||||
"nuget",
|
||||
"mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"runtimeArguments": {
|
||||
"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.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"runtimeHint": {
|
||||
"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"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"transport": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
],
|
||||
"description": "Transport protocol configuration for the package"
|
||||
},
|
||||
"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.*').",
|
||||
"example": "1.0.2",
|
||||
"minLength": 1,
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryType",
|
||||
"identifier",
|
||||
"transport"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"valueHint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional",
|
||||
"type": "string"
|
||||
},
|
||||
"valueHint": {
|
||||
"description": "An identifier for the positional argument. It is not part of the command line. It may be used by client configuration as a label identifying the argument. It is also used to identify the value in transport URL variable substitution.",
|
||||
"example": "file_path",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A positional input is a value inserted verbatim into the command line."
|
||||
},
|
||||
"Repository": {
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/\u003cowner\u003e/\u003crepo\u003e --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github",
|
||||
"type": "string"
|
||||
},
|
||||
"subfolder": {
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"_meta": {
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"additionalProperties": true,
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"example": {
|
||||
"buildInfo": {
|
||||
"commit": "abc123def456",
|
||||
"pipelineId": "build-789",
|
||||
"timestamp": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
"tool": "publisher-cli",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"icons": {
|
||||
"description": "Optional set of sized icons that the client can display in a user interface. Clients that support rendering icons MUST support at least the following MIME types: image/png and image/jpeg (safe, universal compatibility). Clients SHOULD also support: image/svg+xml (scalable but requires security precautions) and image/webp (modern, efficient format).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Icon"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"maxLength": 200,
|
||||
"minLength": 3,
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"type": "string"
|
||||
},
|
||||
"packages": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"remotes": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"title": {
|
||||
"description": "Optional human-readable title or display name for the MCP server. MCP subregistries or clients MAY choose to use this for display purposes.",
|
||||
"example": "Weather API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '\u003e=1.2.3', '1.x', '1.*').",
|
||||
"example": "1.0.2",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SseTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"example": "sse",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Server-Sent Events endpoint URL",
|
||||
"example": "https://mcp-fs.example.com/sse",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StdioTransport": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"example": "stdio",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"example": "streamable-http",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"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",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "server.json defining a Model Context Protocol (MCP) server"
|
||||
}
|
||||
574
internal/validators/schemas/2025-12-11.json
Normal file
574
internal/validators/schemas/2025-12-11.json
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
{
|
||||
"$comment": "This file is auto-generated from docs/reference/api/openapi.yaml. Do not edit manually. Run 'make generate-schema' to update.",
|
||||
"$id": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||
"$ref": "#/definitions/ServerDetail",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"Argument": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PositionalArgument"
|
||||
},
|
||||
{
|
||||
"$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."
|
||||
},
|
||||
"Icon": {
|
||||
"description": "An optionally-sized icon that can be displayed in a user interface.",
|
||||
"properties": {
|
||||
"mimeType": {
|
||||
"description": "Optional MIME type override if the source MIME type is missing or generic. Must be one of: image/png, image/jpeg, image/jpg, image/svg+xml, image/webp.",
|
||||
"enum": [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/svg+xml",
|
||||
"image/webp"
|
||||
],
|
||||
"example": "image/png",
|
||||
"type": "string"
|
||||
},
|
||||
"sizes": {
|
||||
"description": "Optional array of strings that specify sizes at which the icon can be used. Each string should be in WxH format (e.g., '48x48', '96x96') or 'any' for scalable formats like SVG. If not provided, the client should assume that the icon can be used at any size.",
|
||||
"examples": [
|
||||
[
|
||||
"48x48",
|
||||
"96x96"
|
||||
],
|
||||
[
|
||||
"any"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"pattern": "^(\\d+x\\d+|any)$",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"src": {
|
||||
"description": "A standard URI pointing to an icon resource. Must be an HTTPS URL. Consumers SHOULD take steps to ensure URLs serving icons are from the same domain as the server or a trusted domain. Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain executable JavaScript.",
|
||||
"example": "https://example.com/icon.png",
|
||||
"format": "uri",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"description": "Optional specifier for the theme this icon is designed for. 'light' indicates the icon is designed to be used with a light background, and 'dark' indicates the icon is designed to be used with a dark background. If not provided, the client should assume the icon can be used with any theme.",
|
||||
"enum": [
|
||||
"light",
|
||||
"dark"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"src"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Input": {
|
||||
"properties": {
|
||||
"choices": {
|
||||
"description": "A list of possible values for the input. If provided, the user must select one of these values.",
|
||||
"example": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {
|
||||
"description": "The default value for the input. This should be a valid value for the input. If you want to provide input examples or guidance, use the `placeholder` field instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "A description of the input, which clients can use to provide context to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"format": {
|
||||
"default": "string",
|
||||
"description": "Specifies the input format. Supported values include `filepath`, which should be interpreted as a file on the user's filesystem.\n\nWhen the input is converted to a string, booleans should be represented by the strings \"true\" and \"false\", and numbers should be represented as decimal values.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"filepath"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"isRequired": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSecret": {
|
||||
"default": false,
|
||||
"description": "Indicates whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"placeholder": {
|
||||
"description": "A placeholder for the input to be displaying during configuration. This is used to provide examples or guidance about the expected form or content of the input.",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "The value for the input. If this is not set, the user may be prompted to provide a value. If a value is set, it should not be configurable by end users.\n\nIdentifiers wrapped in `{curly_braces}` will be replaced with the corresponding properties from the input `variables` map. If an identifier in braces is not found in `variables`, or if `variables` is not provided, the `{curly_braces}` substring should remain unchanged.\n",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"InputWithVariables": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"variables": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
"description": "A map of variable names to their values. Keys in the input `value` that are wrapped in `{curly_braces}` will be replaced with the corresponding variable values.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyValueInput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the header or environment variable.",
|
||||
"example": "SOME_VARIABLE",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"LocalTransport": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StdioTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
],
|
||||
"description": "Transport protocol configuration for local/package context"
|
||||
},
|
||||
"NamedArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "The flag name, including any leading dashes.",
|
||||
"example": "--port",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"named"
|
||||
],
|
||||
"example": "named",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A command-line `--flag={value}`."
|
||||
},
|
||||
"Package": {
|
||||
"properties": {
|
||||
"environmentVariables": {
|
||||
"description": "A mapping of environment variables to be set when running the package.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"fileSha256": {
|
||||
"description": "SHA-256 hash of the package file for integrity verification. Required for MCPB packages and optional for other package types. Authors are responsible for generating correct SHA-256 hashes when creating server.json. If present, MCP clients must validate the downloaded file matches the hash before running packages to ensure file integrity.",
|
||||
"example": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"description": "Package identifier - either a package name (for registries) or URL (for direct downloads)",
|
||||
"examples": [
|
||||
"@modelcontextprotocol/server-brave-search",
|
||||
"https://github.com/example/releases/download/v1.0.0/package.mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"packageArguments": {
|
||||
"description": "A list of arguments to be passed to the package's binary.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"registryBaseUrl": {
|
||||
"description": "Base URL of the package registry",
|
||||
"examples": [
|
||||
"https://registry.npmjs.org",
|
||||
"https://pypi.org",
|
||||
"https://docker.io",
|
||||
"https://api.nuget.org/v3/index.json",
|
||||
"https://github.com",
|
||||
"https://gitlab.com"
|
||||
],
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"registryType": {
|
||||
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'oci', 'nuget', 'mcpb')",
|
||||
"examples": [
|
||||
"npm",
|
||||
"pypi",
|
||||
"oci",
|
||||
"nuget",
|
||||
"mcpb"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"runtimeArguments": {
|
||||
"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.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Argument"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"runtimeHint": {
|
||||
"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"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"transport": {
|
||||
"$ref": "#/definitions/LocalTransport",
|
||||
"description": "Transport protocol configuration for the package"
|
||||
},
|
||||
"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.*').",
|
||||
"example": "1.0.2",
|
||||
"minLength": 1,
|
||||
"not": {
|
||||
"const": "latest"
|
||||
},
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"registryType",
|
||||
"identifier",
|
||||
"transport"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PositionalArgument": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InputWithVariables"
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"valueHint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"value"
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"isRepeated": {
|
||||
"default": false,
|
||||
"description": "Whether the argument can be repeated multiple times in the command line.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"positional"
|
||||
],
|
||||
"example": "positional",
|
||||
"type": "string"
|
||||
},
|
||||
"valueHint": {
|
||||
"description": "An identifier for the positional argument. It is not part of the command line. It may be used by client configuration as a label identifying the argument. It is also used to identify the value in transport URL variable substitution.",
|
||||
"example": "file_path",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A positional input is a value inserted verbatim into the command line."
|
||||
},
|
||||
"RemoteTransport": {
|
||||
"allOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"variables": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Input"
|
||||
},
|
||||
"description": "Configuration variables that can be referenced in URL template {curly_braces}. The key is the variable name, and the value defines the variable properties.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables"
|
||||
},
|
||||
"Repository": {
|
||||
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks - if a repository is deleted and recreated, the ID should change. For GitHub, use: gh api repos/\u003cowner\u003e/\u003crepo\u003e --jq '.id'",
|
||||
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "Repository hosting service identifier. Used by registries to determine validation and API access methods.",
|
||||
"example": "github",
|
||||
"type": "string"
|
||||
},
|
||||
"subfolder": {
|
||||
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure. Must be a clean relative path.",
|
||||
"example": "src/everything",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Repository URL for browsing source code. Should support both web browsing and git clone operations.",
|
||||
"example": "https://github.com/modelcontextprotocol/servers",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerDetail": {
|
||||
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"description": "JSON Schema URI for this server.json format",
|
||||
"example": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"_meta": {
|
||||
"description": "Extension metadata using reverse DNS namespacing for vendor-specific data",
|
||||
"properties": {
|
||||
"io.modelcontextprotocol.registry/publisher-provided": {
|
||||
"additionalProperties": true,
|
||||
"description": "Publisher-provided metadata for downstream registries",
|
||||
"example": {
|
||||
"buildInfo": {
|
||||
"commit": "abc123def456",
|
||||
"pipelineId": "build-789",
|
||||
"timestamp": "2023-12-01T10:30:00Z"
|
||||
},
|
||||
"tool": "publisher-cli",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"description": "Clear human-readable explanation of server functionality. Should focus on capabilities, not implementation details.",
|
||||
"example": "MCP server providing weather data and forecasts via OpenWeatherMap API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"icons": {
|
||||
"description": "Optional set of sized icons that the client can display in a user interface. Clients that support rendering icons MUST support at least the following MIME types: image/png and image/jpeg (safe, universal compatibility). Clients SHOULD also support: image/svg+xml (scalable but requires security precautions) and image/webp (modern, efficient format).",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Icon"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Server name in reverse-DNS format. Must contain exactly one forward slash separating namespace from server name.",
|
||||
"example": "io.github.user/weather",
|
||||
"maxLength": 200,
|
||||
"minLength": 3,
|
||||
"pattern": "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$",
|
||||
"type": "string"
|
||||
},
|
||||
"packages": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Package"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"remotes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteTransport"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"repository": {
|
||||
"$ref": "#/definitions/Repository",
|
||||
"description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection."
|
||||
},
|
||||
"title": {
|
||||
"description": "Optional human-readable title or display name for the MCP server. MCP subregistries or clients MAY choose to use this for display purposes.",
|
||||
"example": "Weather API",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Version string for this server. SHOULD follow semantic versioning (e.g., '1.0.2', '2.1.0-alpha'). Equivalent of Implementation.version in MCP specification. Non-semantic versions are allowed but may not sort predictably. Version ranges are rejected (e.g., '^1.2.3', '~1.2.3', '\u003e=1.2.3', '1.x', '1.*').",
|
||||
"example": "1.0.2",
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"description": "Optional URL to the server's homepage, documentation, or project website. This provides a central link for users to learn more about the server. Particularly useful when the server has custom installation instructions or setup requirements.",
|
||||
"example": "https://modelcontextprotocol.io/examples",
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SseTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"sse"
|
||||
],
|
||||
"example": "sse",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Server-Sent Events endpoint URL template. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
|
||||
"example": "https://mcp-fs.example.com/sse",
|
||||
"pattern": "^https?://[^\\s]+$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StdioTransport": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"stdio"
|
||||
],
|
||||
"example": "stdio",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"StreamableHttpTransport": {
|
||||
"properties": {
|
||||
"headers": {
|
||||
"description": "HTTP headers to include",
|
||||
"items": {
|
||||
"$ref": "#/definitions/KeyValueInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "Transport type",
|
||||
"enum": [
|
||||
"streamable-http"
|
||||
],
|
||||
"example": "streamable-http",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL template for the streamable-http transport. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
|
||||
"example": "https://api.example.com/mcp",
|
||||
"pattern": "^https?://[^\\s]+$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "server.json defining a Model Context Protocol (MCP) server"
|
||||
}
|
||||
23
internal/validators/schemas/README.md
Normal file
23
internal/validators/schemas/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Schema Files
|
||||
|
||||
This directory contains JSON Schema files that are embedded into the Go binary (using the `go:embed` directive) for runtime validation.
|
||||
|
||||
## How Schema Files Get Here
|
||||
|
||||
Schema files are automatically synced from the [modelcontextprotocol/static](https://github.com/modelcontextprotocol/static) repository via the GitHub Actions workflow `.github/workflows/sync-schema.yml`.
|
||||
|
||||
The workflow:
|
||||
1. Checks out the `modelcontextprotocol/static` repository
|
||||
2. Copies all versioned schema files from `static-repo/schemas/*/server.schema.json`
|
||||
3. Saves them here as `{version}.json` (e.g., `2025-10-17.json`)
|
||||
4. Automatically commits and pushes any new or updated schemas
|
||||
|
||||
**Do not manually edit files in this directory** - they are managed by the sync workflow.
|
||||
|
||||
## Usage
|
||||
|
||||
These schema files are embedded into the Go binary using the `go:embed` directive for offline schema validation. The embedded schemas are used by the validation code in `internal/validators/schema.go` to validate `server.json` files against their specified schema version.
|
||||
|
||||
## File Naming
|
||||
|
||||
Files are named `{YYYY-MM-DD}.json` where the date corresponds to the schema version (e.g., `2025-10-17.json`). This matches the version in the schema's `$id` field.
|
||||
447
internal/validators/validation_detailed_test.go
Normal file
447
internal/validators/validation_detailed_test.go
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/validators"
|
||||
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
||||
"github.com/modelcontextprotocol/registry/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const schemaPath = "schema"
|
||||
|
||||
func TestValidateServerJSON_CollectsAllErrors(t *testing.T) {
|
||||
// Create a server JSON with multiple validation errors
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Name: "invalid-name", // Invalid server name format
|
||||
Version: "^1.0.0", // Invalid version range
|
||||
Description: "Test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "not-a-valid-url", // Invalid repository URL
|
||||
Source: "github",
|
||||
},
|
||||
WebsiteURL: "ftp://invalid-scheme.com", // Invalid website URL scheme
|
||||
Packages: []model.Package{
|
||||
{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "package with spaces", // Invalid package name
|
||||
Version: "latest", // Reserved version
|
||||
Transport: model.Transport{
|
||||
Type: model.TransportTypeStdio,
|
||||
URL: "should-not-have-url", // Invalid stdio transport with URL
|
||||
},
|
||||
RuntimeArguments: []model.Argument{
|
||||
{
|
||||
Type: model.ArgumentTypeNamed,
|
||||
Name: "--port <port>", // Invalid argument name
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: model.TransportTypeStdio, // Invalid remote transport type
|
||||
URL: "", // Missing URL for remote
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run detailed validation
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationSchemaVersionAndSemantic)
|
||||
|
||||
// Verify it's invalid
|
||||
assert.False(t, result.Valid)
|
||||
assert.Greater(t, len(result.Issues), 5, "Should have multiple validation issues")
|
||||
|
||||
// Check that we have issues of different types and severities
|
||||
hasError := false
|
||||
hasSemantic := false
|
||||
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Severity == validators.ValidationIssueSeverityError {
|
||||
hasError = true
|
||||
}
|
||||
if issue.Type == validators.ValidationIssueTypeSemantic {
|
||||
hasSemantic = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasError, "Should have error severity issues")
|
||||
assert.True(t, hasSemantic, "Should have semantic type issues")
|
||||
|
||||
// Verify specific issues exist
|
||||
issuePaths := make(map[string]bool)
|
||||
for _, issue := range result.Issues {
|
||||
issuePaths[issue.Path] = true
|
||||
}
|
||||
|
||||
// Check for expected issue paths
|
||||
expectedPaths := []string{
|
||||
"name",
|
||||
"version",
|
||||
"repository.url",
|
||||
"websiteUrl",
|
||||
"packages[0].identifier",
|
||||
"packages[0].version",
|
||||
"packages[0].transport.url",
|
||||
"packages[0].runtimeArguments[0].name",
|
||||
"remotes[0].type",
|
||||
"remotes[0].url",
|
||||
}
|
||||
|
||||
foundPaths := 0
|
||||
for _, expectedPath := range expectedPaths {
|
||||
if issuePaths[expectedPath] {
|
||||
foundPaths++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Greater(t, foundPaths, 5, "Should have issues at multiple JSON paths")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_ValidServer(t *testing.T) {
|
||||
// Create a valid server JSON
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example.test/valid-server",
|
||||
Version: "1.0.0",
|
||||
Description: "A valid test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "https://github.com/example/valid-server",
|
||||
Source: "github",
|
||||
},
|
||||
WebsiteURL: "https://test.example.com",
|
||||
Packages: []model.Package{
|
||||
{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "valid-package",
|
||||
Version: "1.0.0",
|
||||
Transport: model.Transport{
|
||||
Type: model.TransportTypeStdio,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run detailed validation
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationSchemaVersionAndSemantic)
|
||||
|
||||
// Verify it's valid
|
||||
assert.True(t, result.Valid)
|
||||
assert.Empty(t, result.Issues, "Should have no validation issues")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_ContextPaths(t *testing.T) {
|
||||
// Create a server with nested validation errors to test context paths
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Name: "com.example.test/server",
|
||||
Version: "1.0.0",
|
||||
Packages: []model.Package{
|
||||
{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "package-1",
|
||||
Version: "latest", // Error in first package
|
||||
Transport: model.Transport{
|
||||
Type: model.TransportTypeStdio,
|
||||
},
|
||||
},
|
||||
{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "package-2",
|
||||
Version: "2.0.0",
|
||||
Transport: model.Transport{
|
||||
Type: model.TransportTypeStdio,
|
||||
},
|
||||
RuntimeArguments: []model.Argument{
|
||||
{
|
||||
Type: model.ArgumentTypeNamed,
|
||||
Name: "invalid name", // Error in second package's argument
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run detailed validation
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationSchemaVersionAndSemantic)
|
||||
|
||||
// Verify we have issues at the correct paths
|
||||
issuePaths := make(map[string]bool)
|
||||
for _, issue := range result.Issues {
|
||||
issuePaths[issue.Path] = true
|
||||
}
|
||||
|
||||
// Should have issues at specific nested paths
|
||||
assert.True(t, issuePaths["packages[0].version"], "Should have issue at packages[0].version")
|
||||
assert.True(t, issuePaths["packages[1].runtimeArguments[0].name"], "Should have issue at packages[1].runtimeArguments[0].name")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_RefResolution(t *testing.T) {
|
||||
// Create a server JSON with validation errors that will trigger $ref resolution
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example.test/invalid-server",
|
||||
Version: "1.0.0",
|
||||
Description: "Test server with validation errors",
|
||||
Repository: &model.Repository{
|
||||
URL: "", // Empty URL should trigger format validation error in $ref'd Repository
|
||||
Source: "github",
|
||||
},
|
||||
Packages: []model.Package{
|
||||
{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "test-package",
|
||||
Version: "1.0.0",
|
||||
Transport: model.Transport{
|
||||
Type: model.TransportTypeSSE,
|
||||
URL: "https://example.com",
|
||||
},
|
||||
PackageArguments: []model.Argument{
|
||||
{
|
||||
InputWithVariables: model.InputWithVariables{
|
||||
Input: model.Input{
|
||||
Format: "invalid-format", // This should trigger a validation error in the complex path
|
||||
},
|
||||
},
|
||||
Type: "named",
|
||||
Name: "test-arg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run validation with schema validation enabled
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationAll)
|
||||
|
||||
// Check that we have validation errors
|
||||
assert.False(t, result.Valid, "Expected validation errors")
|
||||
assert.Greater(t, len(result.Issues), 0, "Expected at least one validation issue")
|
||||
|
||||
// Check that we have schema validation issues with proper $ref resolution
|
||||
hasSchemaIssues := false
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Type == validators.ValidationIssueTypeSchema {
|
||||
hasSchemaIssues = true
|
||||
// Check that there are no unresolved [$ref] segments
|
||||
assert.NotContains(t, issue.Reference, "[$ref]", "Found unresolved $ref segment in reference: %s", issue.Reference)
|
||||
|
||||
// Check for exact resolved paths we expect
|
||||
if issue.Path == "repository.url" {
|
||||
expectedRef := "#/definitions/Repository/properties/url/format from: [#/definitions/ServerDetail]/properties/repository/[#/definitions/Repository]/properties/url/format"
|
||||
assert.Equal(t, expectedRef, issue.Reference, "Repository URL error should have exact resolved reference")
|
||||
}
|
||||
if issue.Path == "packages[0].packageArguments[0].format" {
|
||||
// The schema uses anyOf for Argument types, so it could match either PositionalArgument or NamedArgument
|
||||
// Just check that it contains the expected definitions
|
||||
assert.Contains(t, issue.Reference, "#/definitions/Input/properties/format/enum", "Should reference the Input format enum")
|
||||
assert.Contains(t, issue.Reference, "[#/definitions/InputWithVariables]", "Should reference InputWithVariables")
|
||||
assert.Contains(t, issue.Reference, "[#/definitions/Input]", "Should reference Input")
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasSchemaIssues, "Expected schema validation issues with $ref resolution")
|
||||
|
||||
// Check that we have issues at expected paths
|
||||
issuePaths := make(map[string]bool)
|
||||
for _, issue := range result.Issues {
|
||||
issuePaths[issue.Path] = true
|
||||
}
|
||||
|
||||
// Should have issues at specific paths that trigger $ref resolution
|
||||
assert.True(t, issuePaths["repository.url"], "Should have issue at repository.url")
|
||||
assert.True(t, issuePaths["packages[0].packageArguments[0].format"], "Should have issue at packages[0].packageArguments[0].format")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_EmptySchema(t *testing.T) {
|
||||
// Test that empty/missing schema produces an error
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
// Schema field intentionally omitted (empty string)
|
||||
Name: "com.example.test/server",
|
||||
Version: "1.0.0",
|
||||
Description: "Test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "https://github.com/example/server",
|
||||
Source: "github",
|
||||
},
|
||||
}
|
||||
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationAll)
|
||||
|
||||
// Should be invalid due to missing schema
|
||||
assert.False(t, result.Valid, "Empty schema should cause validation failure")
|
||||
|
||||
// Should have an error issue for missing schema
|
||||
hasSchemaError := false
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == schemaPath && issue.Severity == validators.ValidationIssueSeverityError {
|
||||
if strings.Contains(issue.Message, "$schema field is required") {
|
||||
hasSchemaError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasSchemaError, "Should have error for missing $schema field")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_NonCurrentSchema_Warning(t *testing.T) {
|
||||
// Test that non-current (but valid) schema produces a warning, not an error
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Schema: "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", // Older but valid schema
|
||||
Name: "com.example.test/server",
|
||||
Version: "1.0.0",
|
||||
Description: "Test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "https://github.com/example/server",
|
||||
Source: "github",
|
||||
},
|
||||
}
|
||||
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationAll)
|
||||
|
||||
// Should be valid (warnings don't make it invalid)
|
||||
assert.True(t, result.Valid, "Non-current schema should produce warning but still be valid")
|
||||
|
||||
// Should have a warning issue for non-current schema
|
||||
hasSchemaWarning := false
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == schemaPath && issue.Severity == validators.ValidationIssueSeverityWarning {
|
||||
if strings.Contains(issue.Message, "not the current version") || strings.Contains(issue.Message, "Consider updating") {
|
||||
hasSchemaWarning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasSchemaWarning, "Should have warning for non-current schema version")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_InvalidSchema_Error(t *testing.T) {
|
||||
// Test that invalid/non-existent schema produces an error
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Schema: "https://static.modelcontextprotocol.io/schemas/2025-01-27/server.schema.json", // Non-existent version
|
||||
Name: "com.example.test/server",
|
||||
Version: "1.0.0",
|
||||
Description: "Test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "https://github.com/example/server",
|
||||
Source: "github",
|
||||
},
|
||||
}
|
||||
|
||||
result := validators.ValidateServerJSON(serverJSON, validators.ValidationAll)
|
||||
|
||||
// Should be invalid due to schema not available
|
||||
assert.False(t, result.Valid, "Invalid schema version should cause validation failure")
|
||||
|
||||
// Should have an error issue for schema not available
|
||||
hasSchemaError := false
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == schemaPath && issue.Severity == validators.ValidationIssueSeverityError {
|
||||
if strings.Contains(issue.Message, "not available") || strings.Contains(issue.Message, "not found") {
|
||||
hasSchemaError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasSchemaError, "Should have error for invalid/non-existent schema version")
|
||||
}
|
||||
|
||||
func TestValidateServerJSON_NonCurrentSchema_Policies(t *testing.T) {
|
||||
// Test all three policies for non-current schema handling
|
||||
serverJSON := &apiv0.ServerJSON{
|
||||
Schema: "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json", // Older but valid schema
|
||||
Name: "com.example.test/server",
|
||||
Version: "1.0.0",
|
||||
Description: "Test server",
|
||||
Repository: &model.Repository{
|
||||
URL: "https://github.com/example/server",
|
||||
Source: "github",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
policy validators.SchemaVersionPolicy
|
||||
expectValid bool
|
||||
expectWarning bool
|
||||
expectError bool
|
||||
expectIssueCount int
|
||||
}{
|
||||
{
|
||||
name: "Allow policy - no warning or error",
|
||||
policy: validators.SchemaVersionPolicyAllow,
|
||||
expectValid: true,
|
||||
expectWarning: false,
|
||||
expectError: false,
|
||||
expectIssueCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Warn policy - warning but still valid",
|
||||
policy: validators.SchemaVersionPolicyWarn,
|
||||
expectValid: true,
|
||||
expectWarning: true,
|
||||
expectError: false,
|
||||
expectIssueCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Error policy - error and invalid",
|
||||
policy: validators.SchemaVersionPolicyError,
|
||||
expectValid: false,
|
||||
expectWarning: false,
|
||||
expectError: true,
|
||||
expectIssueCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := validators.ValidationOptions{
|
||||
ValidateSchema: true,
|
||||
ValidateSemantic: true,
|
||||
NonCurrentSchemaPolicy: tt.policy,
|
||||
}
|
||||
result := validators.ValidateServerJSON(serverJSON, opts)
|
||||
|
||||
assert.Equal(t, tt.expectValid, result.Valid, "Validation result should match expected")
|
||||
|
||||
hasWarning := false
|
||||
hasError := false
|
||||
schemaWarnings := 0
|
||||
schemaErrors := 0
|
||||
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == schemaPath {
|
||||
if issue.Severity == validators.ValidationIssueSeverityWarning {
|
||||
hasWarning = true
|
||||
schemaWarnings++
|
||||
if !strings.Contains(issue.Message, "not the current version") {
|
||||
t.Errorf("Warning message should mention 'not the current version', got: %s", issue.Message)
|
||||
}
|
||||
}
|
||||
if issue.Severity == validators.ValidationIssueSeverityError {
|
||||
if strings.Contains(issue.Message, "not the current version") {
|
||||
hasError = true
|
||||
schemaErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectWarning, hasWarning, "Warning presence should match expected")
|
||||
assert.Equal(t, tt.expectError, hasError, "Error presence should match expected")
|
||||
|
||||
// Count schema-related issues (excluding other validation issues)
|
||||
schemaIssueCount := 0
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path == schemaPath && (strings.Contains(issue.Message, "not the current version") || strings.Contains(issue.Message, "current version")) {
|
||||
schemaIssueCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.expectIssueCount, schemaIssueCount, "Schema issue count should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
161
internal/validators/validation_types.go
Normal file
161
internal/validators/validation_types.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package validators
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Validation issue type with constrained values
|
||||
type ValidationIssueType string
|
||||
|
||||
const (
|
||||
ValidationIssueTypeJSON ValidationIssueType = "json"
|
||||
ValidationIssueTypeSchema ValidationIssueType = "schema"
|
||||
ValidationIssueTypeSemantic ValidationIssueType = "semantic"
|
||||
ValidationIssueTypeLinter ValidationIssueType = "linter"
|
||||
)
|
||||
|
||||
// Validation issue severity with constrained values
|
||||
type ValidationIssueSeverity string
|
||||
|
||||
const (
|
||||
ValidationIssueSeverityError ValidationIssueSeverity = "error"
|
||||
ValidationIssueSeverityWarning ValidationIssueSeverity = "warning"
|
||||
ValidationIssueSeverityInfo ValidationIssueSeverity = "info"
|
||||
)
|
||||
|
||||
// SchemaVersionPolicy determines how non-current schema versions are handled
|
||||
type SchemaVersionPolicy string
|
||||
|
||||
const (
|
||||
// SchemaVersionPolicyAllow allows non-current schemas with no warning or error
|
||||
SchemaVersionPolicyAllow SchemaVersionPolicy = "allow"
|
||||
// SchemaVersionPolicyWarn allows non-current schemas but generates a warning
|
||||
SchemaVersionPolicyWarn SchemaVersionPolicy = "warn"
|
||||
// SchemaVersionPolicyError rejects non-current schemas with an error
|
||||
SchemaVersionPolicyError SchemaVersionPolicy = "error"
|
||||
)
|
||||
|
||||
// ValidationOptions configures which types of validation to perform
|
||||
// ValidateSchema implies ValidateSchemaVersion (the flag is ignored if ValidateSchema is true)
|
||||
type ValidationOptions struct {
|
||||
ValidateSchemaVersion bool // Check schema version (empty, non-current). Ignored if ValidateSchema is true.
|
||||
ValidateSchema bool // Perform full schema validation (implies ValidateSchemaVersion)
|
||||
ValidateSemantic bool // Perform semantic validation
|
||||
NonCurrentSchemaPolicy SchemaVersionPolicy // Policy for non-current schemas (only used when schema validation is performed)
|
||||
}
|
||||
|
||||
// Common validation configurations
|
||||
var (
|
||||
// ValidationSemanticOnly performs only semantic validation (no schema checks)
|
||||
ValidationSemanticOnly = ValidationOptions{
|
||||
ValidateSemantic: true,
|
||||
}
|
||||
|
||||
// ValidationSchemaVersionOnly checks schema version only (empty, non-current)
|
||||
ValidationSchemaVersionOnly = ValidationOptions{
|
||||
ValidateSchemaVersion: true,
|
||||
NonCurrentSchemaPolicy: SchemaVersionPolicyError,
|
||||
}
|
||||
|
||||
// ValidationSchemaVersionAndSemantic checks schema version and performs semantic validation
|
||||
ValidationSchemaVersionAndSemantic = ValidationOptions{
|
||||
ValidateSchemaVersion: true,
|
||||
ValidateSemantic: true,
|
||||
NonCurrentSchemaPolicy: SchemaVersionPolicyWarn,
|
||||
}
|
||||
|
||||
// ValidationAll performs all validation types (schema version, full schema validation, and semantic)
|
||||
ValidationAll = ValidationOptions{
|
||||
ValidateSchema: true, // Implies ValidateSchemaVersion
|
||||
ValidateSemantic: true,
|
||||
NonCurrentSchemaPolicy: SchemaVersionPolicyWarn,
|
||||
}
|
||||
)
|
||||
|
||||
// ValidationIssue represents a single validation problem
|
||||
type ValidationIssue struct {
|
||||
Type ValidationIssueType `json:"type"`
|
||||
Path string `json:"path"` // JSON path like "packages[0].transport.url"
|
||||
Message string `json:"message"` // Error description (extracted from error.Error())
|
||||
Severity ValidationIssueSeverity `json:"severity"`
|
||||
Reference string `json:"reference"` // Reference to validation trigger (schema rule path, named rule, etc.)
|
||||
}
|
||||
|
||||
// ValidationResult contains the results of validation
|
||||
type ValidationResult struct {
|
||||
Valid bool `json:"valid"`
|
||||
Issues []ValidationIssue `json:"issues"`
|
||||
}
|
||||
|
||||
// ValidationContext tracks the current JSON path during validation
|
||||
type ValidationContext struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewValidationIssue creates a validation issue with manual field setting
|
||||
func NewValidationIssue(issueType ValidationIssueType, path, message string, severity ValidationIssueSeverity, reference string) ValidationIssue {
|
||||
return ValidationIssue{
|
||||
Type: issueType,
|
||||
Path: path,
|
||||
Message: message,
|
||||
Severity: severity,
|
||||
Reference: reference,
|
||||
}
|
||||
}
|
||||
|
||||
// NewValidationIssueFromError creates a validation issue from an existing error
|
||||
func NewValidationIssueFromError(issueType ValidationIssueType, path string, err error, reference string) ValidationIssue {
|
||||
return ValidationIssue{
|
||||
Type: issueType,
|
||||
Path: path,
|
||||
Message: err.Error(), // Extract string from error
|
||||
Severity: ValidationIssueSeverityError, // Errors are always severity "error"
|
||||
Reference: reference,
|
||||
}
|
||||
}
|
||||
|
||||
// AddIssue adds a validation issue to the result
|
||||
func (vr *ValidationResult) AddIssue(issue ValidationIssue) {
|
||||
vr.Issues = append(vr.Issues, issue)
|
||||
if issue.Severity == ValidationIssueSeverityError {
|
||||
vr.Valid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Merge combines another validation result into this one
|
||||
func (vr *ValidationResult) Merge(other *ValidationResult) {
|
||||
vr.Issues = append(vr.Issues, other.Issues...)
|
||||
if !other.Valid {
|
||||
vr.Valid = false
|
||||
}
|
||||
}
|
||||
|
||||
// FirstError returns the first error-level issue as an error, or nil if valid
|
||||
// This provides backward compatibility for code that expects an error return type
|
||||
func (vr *ValidationResult) FirstError() error {
|
||||
if vr.Valid {
|
||||
return nil
|
||||
}
|
||||
for _, issue := range vr.Issues {
|
||||
if issue.Severity == ValidationIssueSeverityError {
|
||||
return fmt.Errorf("%s", issue.Message)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Field adds a field name to the context path
|
||||
func (ctx *ValidationContext) Field(name string) *ValidationContext {
|
||||
if ctx.path == "" {
|
||||
return &ValidationContext{path: name}
|
||||
}
|
||||
return &ValidationContext{path: ctx.path + "." + name}
|
||||
}
|
||||
|
||||
// Index adds an array index to the context path
|
||||
func (ctx *ValidationContext) Index(i int) *ValidationContext {
|
||||
return &ValidationContext{path: ctx.path + fmt.Sprintf("[%d]", i)}
|
||||
}
|
||||
|
||||
// String returns the current path as a string
|
||||
func (ctx *ValidationContext) String() string {
|
||||
return ctx.path
|
||||
}
|
||||
173
internal/validators/validation_types_test.go
Normal file
173
internal/validators/validation_types_test.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/validators"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidationIssueTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
issueType validators.ValidationIssueType
|
||||
expected string
|
||||
}{
|
||||
{"JSON type", validators.ValidationIssueTypeJSON, "json"},
|
||||
{"Schema type", validators.ValidationIssueTypeSchema, "schema"},
|
||||
{"Semantic type", validators.ValidationIssueTypeSemantic, "semantic"},
|
||||
{"Linter type", validators.ValidationIssueTypeLinter, "linter"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, string(tt.issueType))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationIssueSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
severity validators.ValidationIssueSeverity
|
||||
expected string
|
||||
}{
|
||||
{"Error severity", validators.ValidationIssueSeverityError, "error"},
|
||||
{"Warning severity", validators.ValidationIssueSeverityWarning, "warning"},
|
||||
{"Info severity", validators.ValidationIssueSeverityInfo, "info"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, string(tt.severity))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidationIssue(t *testing.T) {
|
||||
issue := validators.NewValidationIssue(
|
||||
validators.ValidationIssueTypeSemantic,
|
||||
"repository.url",
|
||||
"invalid repository URL",
|
||||
validators.ValidationIssueSeverityError,
|
||||
"invalid-repository-url",
|
||||
)
|
||||
|
||||
assert.Equal(t, validators.ValidationIssueTypeSemantic, issue.Type)
|
||||
assert.Equal(t, "repository.url", issue.Path)
|
||||
assert.Equal(t, "invalid repository URL", issue.Message)
|
||||
assert.Equal(t, validators.ValidationIssueSeverityError, issue.Severity)
|
||||
assert.Equal(t, "invalid-repository-url", issue.Reference)
|
||||
}
|
||||
|
||||
func TestNewValidationIssueFromError(t *testing.T) {
|
||||
err := errors.New("invalid repository URL: https://bad-url.com")
|
||||
issue := validators.NewValidationIssueFromError(
|
||||
validators.ValidationIssueTypeSemantic,
|
||||
"repository.url",
|
||||
err,
|
||||
"invalid-repository-url",
|
||||
)
|
||||
|
||||
assert.Equal(t, validators.ValidationIssueTypeSemantic, issue.Type)
|
||||
assert.Equal(t, "repository.url", issue.Path)
|
||||
assert.Equal(t, "invalid repository URL: https://bad-url.com", issue.Message)
|
||||
assert.Equal(t, validators.ValidationIssueSeverityError, issue.Severity)
|
||||
assert.Equal(t, "invalid-repository-url", issue.Reference)
|
||||
}
|
||||
|
||||
func TestValidationResultAddIssue(t *testing.T) {
|
||||
result := &validators.ValidationResult{Valid: true, Issues: []validators.ValidationIssue{}}
|
||||
|
||||
// Add a warning issue - should not affect validity
|
||||
warningIssue := validators.NewValidationIssue(
|
||||
validators.ValidationIssueTypeLinter,
|
||||
"description",
|
||||
"consider adding a description",
|
||||
validators.ValidationIssueSeverityWarning,
|
||||
"descriptive-naming",
|
||||
)
|
||||
result.AddIssue(warningIssue)
|
||||
|
||||
assert.True(t, result.Valid)
|
||||
assert.Len(t, result.Issues, 1)
|
||||
|
||||
// Add an error issue - should make invalid
|
||||
errorIssue := validators.NewValidationIssue(
|
||||
validators.ValidationIssueTypeSemantic,
|
||||
"name",
|
||||
"server name is required",
|
||||
validators.ValidationIssueSeverityError,
|
||||
"missing-server-name",
|
||||
)
|
||||
result.AddIssue(errorIssue)
|
||||
|
||||
assert.False(t, result.Valid)
|
||||
assert.Len(t, result.Issues, 2)
|
||||
}
|
||||
|
||||
func TestValidationResultMerge(t *testing.T) {
|
||||
result1 := &validators.ValidationResult{Valid: true, Issues: []validators.ValidationIssue{}}
|
||||
result2 := &validators.ValidationResult{Valid: false, Issues: []validators.ValidationIssue{}}
|
||||
|
||||
// Add issues to both
|
||||
issue1 := validators.NewValidationIssue(
|
||||
validators.ValidationIssueTypeSemantic,
|
||||
"name",
|
||||
"server name is required",
|
||||
validators.ValidationIssueSeverityError,
|
||||
"missing-server-name",
|
||||
)
|
||||
result1.AddIssue(issue1)
|
||||
|
||||
issue2 := validators.NewValidationIssue(
|
||||
validators.ValidationIssueTypeSchema,
|
||||
"version",
|
||||
"version must be a string",
|
||||
validators.ValidationIssueSeverityError,
|
||||
"schema-validation",
|
||||
)
|
||||
result2.AddIssue(issue2)
|
||||
|
||||
// Merge result2 into result1
|
||||
result1.Merge(result2)
|
||||
|
||||
assert.False(t, result1.Valid) // Should be invalid because result2 was invalid
|
||||
assert.Len(t, result1.Issues, 2) // Should have both issues
|
||||
}
|
||||
|
||||
func TestValidationContext(t *testing.T) {
|
||||
// Test empty context
|
||||
ctx := &validators.ValidationContext{}
|
||||
assert.Equal(t, "", ctx.String())
|
||||
|
||||
// Test field addition
|
||||
ctx = ctx.Field("repository")
|
||||
assert.Equal(t, "repository", ctx.String())
|
||||
|
||||
// Test nested field
|
||||
ctx = ctx.Field("url")
|
||||
assert.Equal(t, "repository.url", ctx.String())
|
||||
|
||||
// Test array index
|
||||
ctx = &validators.ValidationContext{}
|
||||
ctx = ctx.Field("packages").Index(0).Field("transport")
|
||||
assert.Equal(t, "packages[0].transport", ctx.String())
|
||||
|
||||
// Test multiple array indices
|
||||
ctx = &validators.ValidationContext{}
|
||||
ctx = ctx.Field("packages").Index(0).Field("environmentVariables").Index(1).Field("name")
|
||||
assert.Equal(t, "packages[0].environmentVariables[1].name", ctx.String())
|
||||
}
|
||||
|
||||
func TestValidationContextImmutability(t *testing.T) {
|
||||
// Test that context operations return new instances
|
||||
ctx1 := &validators.ValidationContext{}
|
||||
ctx2 := ctx1.Field("repository")
|
||||
ctx3 := ctx2.Field("url")
|
||||
|
||||
assert.Equal(t, "", ctx1.String())
|
||||
assert.Equal(t, "repository", ctx2.String())
|
||||
assert.Equal(t, "repository.url", ctx3.String())
|
||||
}
|
||||
|
|
@ -51,206 +51,299 @@ var (
|
|||
dottedVersionLikeRe = regexp.MustCompile(`^\s*(?:v?\d+|x|X|\*)(?:\.(?:\d+|x|X|\*)){1,2}(?:-[0-9A-Za-z.-]+)?\s*$`)
|
||||
)
|
||||
|
||||
func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error {
|
||||
// Validate schema version is provided and supported
|
||||
// Note: Schema field is also marked as required in the ServerJSON struct definition
|
||||
// for API-level validation and documentation
|
||||
if serverJSON.Schema == "" {
|
||||
return fmt.Errorf("$schema field is required")
|
||||
// ValidateServerJSON performs exhaustive validation and returns all issues found
|
||||
// opts specifies which types of validation to perform. ValidateSchema implies ValidateSchemaVersion.
|
||||
// Empty schema is always checked and always produces an error when schema validation is performed.
|
||||
func ValidateServerJSON(serverJSON *apiv0.ServerJSON, opts ValidationOptions) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
ctx := &ValidationContext{}
|
||||
|
||||
// Schema validation (version check and/or full validation)
|
||||
if opts.ValidateSchemaVersion || opts.ValidateSchema {
|
||||
schemaResult := validateServerJSONSchema(serverJSON, opts.ValidateSchema, opts.NonCurrentSchemaPolicy)
|
||||
result.Merge(schemaResult)
|
||||
}
|
||||
if !strings.Contains(serverJSON.Schema, model.CurrentSchemaVersion) {
|
||||
return fmt.Errorf("schema version %s is not supported. Please use schema version %s", serverJSON.Schema, model.CurrentSchemaVersion)
|
||||
|
||||
// Semantic validation (only if requested)
|
||||
if !opts.ValidateSemantic {
|
||||
return result
|
||||
}
|
||||
|
||||
// Validate server name exists and format
|
||||
if _, err := parseServerName(*serverJSON); err != nil {
|
||||
return err
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("name").String(),
|
||||
err,
|
||||
"invalid-server-name",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Validate top-level server version is a specific version (not a range) & not "latest"
|
||||
if err := validateVersion(serverJSON.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
versionResult := validateVersion(ctx.Field("version"), serverJSON.Version)
|
||||
result.Merge(versionResult)
|
||||
|
||||
// Validate repository
|
||||
if err := validateRepository(serverJSON.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
repoResult := validateRepository(ctx.Field("repository"), serverJSON.Repository)
|
||||
result.Merge(repoResult)
|
||||
|
||||
// Validate website URL if provided
|
||||
if err := validateWebsiteURL(serverJSON.WebsiteURL); err != nil {
|
||||
return err
|
||||
}
|
||||
websiteResult := validateWebsiteURL(ctx.Field("websiteUrl"), serverJSON.WebsiteURL)
|
||||
result.Merge(websiteResult)
|
||||
|
||||
// Validate title if provided
|
||||
if err := validateTitle(serverJSON.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
titleResult := validateTitle(ctx.Field("title"), serverJSON.Title)
|
||||
result.Merge(titleResult)
|
||||
|
||||
// Validate icons if provided
|
||||
if err := validateIcons(serverJSON.Icons); err != nil {
|
||||
return err
|
||||
}
|
||||
iconsResult := validateIcons(ctx.Field("icons"), serverJSON.Icons)
|
||||
result.Merge(iconsResult)
|
||||
|
||||
// Validate all packages (basic field validation)
|
||||
// Detailed package validation (including registry checks) is done during publish
|
||||
for _, pkg := range serverJSON.Packages {
|
||||
if err := validatePackageField(&pkg); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, pkg := range serverJSON.Packages {
|
||||
pkgResult := validatePackageField(ctx.Field("packages").Index(i), &pkg)
|
||||
result.Merge(pkgResult)
|
||||
}
|
||||
|
||||
// Validate all remotes
|
||||
for _, remote := range serverJSON.Remotes {
|
||||
if err := validateRemoteTransport(&remote); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, remote := range serverJSON.Remotes {
|
||||
remoteResult := validateRemoteTransport(ctx.Field("remotes").Index(i), &remote)
|
||||
result.Merge(remoteResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateRepository(obj *model.Repository) error {
|
||||
func validateRepository(ctx *ValidationContext, obj *model.Repository) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Skip validation if repository is nil or empty (optional field)
|
||||
if obj == nil || (obj.URL == "" && obj.Source == "") {
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// validate the repository source
|
||||
repoSource := RepositorySource(obj.Source)
|
||||
if !IsValidRepositoryURL(repoSource, obj.URL) {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL),
|
||||
"invalid-repository-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// validate subfolder if present
|
||||
if obj.Subfolder != "" && !IsValidSubfolderPath(obj.Subfolder) {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidSubfolderPath, obj.Subfolder)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("subfolder").String(),
|
||||
fmt.Errorf("%w: %s", ErrInvalidSubfolderPath, obj.Subfolder),
|
||||
"invalid-subfolder-path",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateWebsiteURL(websiteURL string) error {
|
||||
func validateWebsiteURL(ctx *ValidationContext, websiteURL string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Skip validation if website URL is not provided (optional field)
|
||||
if websiteURL == "" {
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// Parse the URL to ensure it's valid
|
||||
parsedURL, err := url.Parse(websiteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid websiteUrl: %w", err)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Errorf("invalid websiteUrl: %w", err),
|
||||
"invalid-website-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Ensure it's an absolute URL with valid scheme
|
||||
if !parsedURL.IsAbs() {
|
||||
return fmt.Errorf("websiteUrl must be absolute (include scheme): %s", websiteURL)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Sprintf("websiteUrl must be absolute (include scheme): %s", websiteURL),
|
||||
ValidationIssueSeverityError,
|
||||
"website-url-must-be-absolute",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Only allow HTTPS scheme for security
|
||||
if parsedURL.Scheme != SchemeHTTPS {
|
||||
return fmt.Errorf("websiteUrl must use https scheme: %s", websiteURL)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Sprintf("websiteUrl must use https scheme: %s", websiteURL),
|
||||
ValidationIssueSeverityError,
|
||||
"website-url-invalid-scheme",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateTitle(title string) error {
|
||||
func validateTitle(ctx *ValidationContext, title string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Skip validation if title is not provided (optional field)
|
||||
if title == "" {
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// Check that title is not only whitespace
|
||||
if strings.TrimSpace(title) == "" {
|
||||
return fmt.Errorf("title cannot be only whitespace")
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Errorf("title cannot be only whitespace"),
|
||||
"title-whitespace-only",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateIcons(icons []model.Icon) error {
|
||||
func validateIcons(ctx *ValidationContext, icons []model.Icon) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Skip validation if no icons are provided (optional field)
|
||||
if len(icons) == 0 {
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// Validate each icon
|
||||
for i, icon := range icons {
|
||||
if err := validateIcon(&icon); err != nil {
|
||||
return fmt.Errorf("invalid icon at index %d: %w", i, err)
|
||||
}
|
||||
iconResult := validateIcon(ctx.Index(i), &icon)
|
||||
result.Merge(iconResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateIcon(icon *model.Icon) error {
|
||||
func validateIcon(ctx *ValidationContext, icon *model.Icon) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Parse the URL to ensure it's valid
|
||||
parsedURL, err := url.Parse(icon.Src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid icon src URL: %w", err)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("src").String(),
|
||||
fmt.Errorf("invalid icon src URL: %w", err),
|
||||
"icon-src-invalid-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Ensure it's an absolute URL
|
||||
if !parsedURL.IsAbs() {
|
||||
return fmt.Errorf("icon src must be an absolute URL (include scheme): %s", icon.Src)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("src").String(),
|
||||
fmt.Errorf("icon src must be an absolute URL (include scheme): %s", icon.Src),
|
||||
"icon-src-not-absolute",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Only allow HTTPS scheme for security (no HTTP or data: URIs)
|
||||
if parsedURL.Scheme != SchemeHTTPS {
|
||||
return fmt.Errorf("icon src must use https scheme (got %s): %s", parsedURL.Scheme, icon.Src)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("src").String(),
|
||||
fmt.Errorf("icon src must use https scheme (got %s): %s", parsedURL.Scheme, icon.Src),
|
||||
"icon-src-invalid-scheme",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validatePackageField(obj *model.Package) error {
|
||||
func validatePackageField(ctx *ValidationContext, obj *model.Package) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Validate identifier has no spaces
|
||||
if !HasNoSpaces(obj.Identifier) {
|
||||
return ErrPackageNameHasSpaces
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("identifier").String(),
|
||||
ErrPackageNameHasSpaces,
|
||||
"package-name-has-spaces",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Validate version string
|
||||
if err := validateVersion(obj.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
versionResult := validateVersion(ctx.Field("version"), obj.Version)
|
||||
result.Merge(versionResult)
|
||||
|
||||
// Validate runtime arguments
|
||||
for _, arg := range obj.RuntimeArguments {
|
||||
if err := validateArgument(&arg); err != nil {
|
||||
return fmt.Errorf("invalid runtime argument: %w", err)
|
||||
}
|
||||
for i, arg := range obj.RuntimeArguments {
|
||||
argResult := validateArgument(ctx.Field("runtimeArguments").Index(i), &arg)
|
||||
result.Merge(argResult)
|
||||
}
|
||||
|
||||
// Validate package arguments
|
||||
for _, arg := range obj.PackageArguments {
|
||||
if err := validateArgument(&arg); err != nil {
|
||||
return fmt.Errorf("invalid package argument: %w", err)
|
||||
}
|
||||
for i, arg := range obj.PackageArguments {
|
||||
argResult := validateArgument(ctx.Field("packageArguments").Index(i), &arg)
|
||||
result.Merge(argResult)
|
||||
}
|
||||
|
||||
// Validate transport with template variable support
|
||||
availableVariables := collectAvailableVariables(obj)
|
||||
if err := validatePackageTransport(&obj.Transport, availableVariables); err != nil {
|
||||
return fmt.Errorf("invalid transport: %w", err)
|
||||
}
|
||||
transportResult := validatePackageTransport(ctx.Field("transport"), &obj.Transport, availableVariables)
|
||||
result.Merge(transportResult)
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// validateVersion validates the version string.
|
||||
// NB: we decided that we would not enforce strict semver for version strings
|
||||
func validateVersion(version string) error {
|
||||
func validateVersion(ctx *ValidationContext, version string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
if version == "latest" {
|
||||
return ErrReservedVersionString
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
ErrReservedVersionString,
|
||||
"reserved-version-string",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Reject semver range-like inputs
|
||||
if looksLikeVersionRange(version) {
|
||||
return fmt.Errorf("%w: %q", ErrVersionLooksLikeRange, version)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Errorf("%w: %q", ErrVersionLooksLikeRange, version),
|
||||
"version-looks-like-range",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// looksLikeVersionRange detects common semver range syntaxes and wildcard patterns.
|
||||
|
|
@ -286,25 +379,34 @@ func looksLikeVersionRange(version string) bool {
|
|||
}
|
||||
|
||||
// validateArgument validates argument details
|
||||
func validateArgument(obj *model.Argument) error {
|
||||
func validateArgument(ctx *ValidationContext, obj *model.Argument) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
if obj.Type == model.ArgumentTypeNamed {
|
||||
// Validate named argument name format
|
||||
if err := validateNamedArgumentName(obj.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
nameResult := validateNamedArgumentName(ctx.Field("name"), obj.Name)
|
||||
result.Merge(nameResult)
|
||||
|
||||
// Validate value and default don't start with the name
|
||||
if err := validateArgumentValueFields(obj.Name, obj.Value, obj.Default); err != nil {
|
||||
return err
|
||||
}
|
||||
valueResult := validateArgumentValueFields(ctx, obj.Name, obj.Value, obj.Default)
|
||||
result.Merge(valueResult)
|
||||
}
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateNamedArgumentName(name string) error {
|
||||
func validateNamedArgumentName(ctx *ValidationContext, name string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Check if name is required for named arguments
|
||||
if name == "" {
|
||||
return ErrNamedArgumentNameRequired
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
ErrNamedArgumentNameRequired,
|
||||
"named-argument-name-required",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
return result
|
||||
}
|
||||
|
||||
// Check for invalid characters that suggest embedded values or descriptions
|
||||
|
|
@ -312,23 +414,43 @@ func validateNamedArgumentName(name string) error {
|
|||
// Invalid: "--directory <absolute_path_to_adfin_mcp_folder>", "--port 8080"
|
||||
if strings.Contains(name, "<") || strings.Contains(name, ">") ||
|
||||
strings.Contains(name, " ") || strings.Contains(name, "$") {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidNamedArgumentName, name)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.String(),
|
||||
fmt.Errorf("%w: %s", ErrInvalidNamedArgumentName, name),
|
||||
"invalid-named-argument-name",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
func validateArgumentValueFields(name, value, defaultValue string) error {
|
||||
func validateArgumentValueFields(ctx *ValidationContext, name, value, defaultValue string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Check if value starts with the argument name (using startsWith, not contains)
|
||||
if value != "" && strings.HasPrefix(value, name) {
|
||||
return fmt.Errorf("%w: value starts with argument name '%s': %s", ErrArgumentValueStartsWithName, name, value)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("value").String(),
|
||||
fmt.Errorf("%w: value starts with argument name '%s': %s", ErrArgumentValueStartsWithName, name, value),
|
||||
"argument-value-starts-with-name",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
if defaultValue != "" && strings.HasPrefix(defaultValue, name) {
|
||||
return fmt.Errorf("%w: default starts with argument name '%s': %s", ErrArgumentDefaultStartsWithName, name, defaultValue)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("default").String(),
|
||||
fmt.Errorf("%w: default starts with argument name '%s': %s", ErrArgumentDefaultStartsWithName, name, defaultValue),
|
||||
"argument-default-starts-with-name",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
// collectAvailableVariables collects all available template variables from a package
|
||||
|
|
@ -378,44 +500,92 @@ func collectRemoteTransportVariables(transport *model.Transport) []string {
|
|||
}
|
||||
|
||||
// validatePackageTransport validates a package's transport with templating support
|
||||
func validatePackageTransport(transport *model.Transport, availableVariables []string) error {
|
||||
func validatePackageTransport(ctx *ValidationContext, transport *model.Transport, availableVariables []string) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Validate transport type is supported
|
||||
switch transport.Type {
|
||||
case model.TransportTypeStdio:
|
||||
// Validate that URL is empty for stdio transport
|
||||
if transport.URL != "" {
|
||||
return fmt.Errorf("url must be empty for %s transport type, got: %s", transport.Type, transport.URL)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
fmt.Sprintf("url must be empty for %s transport type, got: %s", transport.Type, transport.URL),
|
||||
ValidationIssueSeverityError,
|
||||
"stdio-transport-url-not-empty",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
return nil
|
||||
case model.TransportTypeStreamableHTTP, model.TransportTypeSSE:
|
||||
// URL is required for streamable-http and sse
|
||||
if transport.URL == "" {
|
||||
return fmt.Errorf("url is required for %s transport type", transport.Type)
|
||||
}
|
||||
// Validate URL format with template variable support
|
||||
if !IsValidTemplatedURL(transport.URL, availableVariables) {
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
fmt.Sprintf("url is required for %s transport type", transport.Type),
|
||||
ValidationIssueSeverityError,
|
||||
"streamable-transport-url-required",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
} else if !IsValidTemplatedURL(transport.URL, availableVariables) {
|
||||
// Check if it's a template variable issue or basic URL issue
|
||||
templateVars := extractTemplateVariables(transport.URL)
|
||||
var err error
|
||||
if len(templateVars) > 0 {
|
||||
return fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
|
||||
err = fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
|
||||
ErrInvalidPackageTransportURL, transport.URL, availableVariables)
|
||||
} else {
|
||||
err = fmt.Errorf("%w: %s", ErrInvalidPackageTransportURL, transport.URL)
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrInvalidPackageTransportURL, transport.URL)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
err,
|
||||
"invalid-templated-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported transport type: %s", transport.Type)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("type").String(),
|
||||
fmt.Sprintf("unsupported transport type: %s", transport.Type),
|
||||
ValidationIssueSeverityError,
|
||||
"unsupported-transport-type",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// validateRemoteTransport validates a remote transport with optional templating
|
||||
func validateRemoteTransport(obj *model.Transport) error {
|
||||
func validateRemoteTransport(ctx *ValidationContext, obj *model.Transport) *ValidationResult {
|
||||
result := &ValidationResult{Valid: true, Issues: []ValidationIssue{}}
|
||||
|
||||
// Validate transport type is supported - remotes only support streamable-http and sse
|
||||
switch obj.Type {
|
||||
case model.TransportTypeStreamableHTTP, model.TransportTypeSSE:
|
||||
// URL is required for streamable-http and sse
|
||||
if obj.URL == "" {
|
||||
return fmt.Errorf("url is required for %s transport type", obj.Type)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
fmt.Sprintf("url is required for %s transport type", obj.Type),
|
||||
ValidationIssueSeverityError,
|
||||
"remote-transport-url-required",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
} else if !IsValidRemoteURL(obj.URL) {
|
||||
// Validate URL format (no templates allowed for remotes, no localhost)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL),
|
||||
"invalid-remote-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Collect available variables from the transport's Variables field
|
||||
|
|
@ -425,21 +595,34 @@ func validateRemoteTransport(obj *model.Transport) error {
|
|||
if !IsValidTemplatedURL(obj.URL, availableVariables) {
|
||||
// Check if it's a template variable issue or basic URL issue
|
||||
templateVars := extractTemplateVariables(obj.URL)
|
||||
var err error
|
||||
if len(templateVars) > 0 {
|
||||
return fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
|
||||
err = fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
|
||||
ErrInvalidRemoteURL, obj.URL, availableVariables)
|
||||
} else {
|
||||
err = fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
|
||||
issue := NewValidationIssueFromError(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("url").String(),
|
||||
err,
|
||||
"invalid-templated-url",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
// Additional check: reject localhost URLs for remotes (like the old IsValidRemoteURL did)
|
||||
if !IsValidRemoteURL(obj.URL) {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
|
||||
}
|
||||
return nil
|
||||
return result
|
||||
default:
|
||||
return fmt.Errorf("unsupported transport type for remotes: %s (only streamable-http and sse are supported)", obj.Type)
|
||||
issue := NewValidationIssue(
|
||||
ValidationIssueTypeSemantic,
|
||||
ctx.Field("type").String(),
|
||||
fmt.Sprintf("unsupported transport type for remotes: %s (only streamable-http and sse are supported)", obj.Type),
|
||||
ValidationIssueSeverityError,
|
||||
"unsupported-remote-transport-type",
|
||||
)
|
||||
result.AddIssue(issue)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidatePublishRequest validates a complete publish request including extensions
|
||||
|
|
@ -450,22 +633,46 @@ func ValidatePublishRequest(ctx context.Context, req apiv0.ServerJSON, cfg *conf
|
|||
}
|
||||
|
||||
// Validate the server detail (includes all nested validation)
|
||||
if err := ValidateServerJSON(&req); err != nil {
|
||||
result := ValidateServerJSON(&req, ValidationSchemaVersionAndSemantic)
|
||||
if err := result.FirstError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate registry ownership for all packages if validation is enabled
|
||||
if cfg.EnableRegistryValidation {
|
||||
for i, pkg := range req.Packages {
|
||||
if err := ValidatePackage(ctx, pkg, req.Name); err != nil {
|
||||
return fmt.Errorf("registry validation failed for package %d (%s): %w", i, pkg.Identifier, err)
|
||||
}
|
||||
if err := validateRegistryOwnership(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateUpdateRequest(ctx context.Context, req apiv0.ServerJSON, cfg *config.Config, skipRegistryValidation bool) error {
|
||||
// Validate the server detail (includes all nested validation)
|
||||
result := ValidateServerJSON(&req, ValidationSchemaVersionAndSemantic)
|
||||
if err := result.FirstError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.EnableRegistryValidation && !skipRegistryValidation {
|
||||
if err := validateRegistryOwnership(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRegistryOwnership(ctx context.Context, req apiv0.ServerJSON) error {
|
||||
for i, pkg := range req.Packages {
|
||||
if err := ValidatePackage(ctx, pkg, req.Name); err != nil {
|
||||
return fmt.Errorf("registry validation failed for package %d (%s): %w", i, pkg.Identifier, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePublisherExtensions(req apiv0.ServerJSON) error {
|
||||
const maxExtensionSize = 4 * 1024 // 4KB limit
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func TestValidate(t *testing.T) {
|
|||
expectedError: "$schema field is required",
|
||||
},
|
||||
{
|
||||
name: "Schema version rejects old schema (2025-01-27)",
|
||||
name: "Schema version rejects old schema (2025-01-27) - non-existent version",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: "https://static.modelcontextprotocol.io/schemas/2025-01-27/server.schema.json",
|
||||
Name: "com.example/test-server",
|
||||
|
|
@ -44,7 +44,9 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
Version: "1.0.0",
|
||||
},
|
||||
expectedError: "schema version https://static.modelcontextprotocol.io/schemas/2025-01-27/server.schema.json is not supported",
|
||||
// This schema version doesn't exist in embedded schemas, so validation should fail
|
||||
// ValidateServerJSON with ValidationSchemaVersionAndSemantic validates that the schema version exists
|
||||
expectedError: "schema version 2025-01-27 not found in embedded schemas",
|
||||
},
|
||||
{
|
||||
name: "Schema version accepts current schema (2025-10-17)",
|
||||
|
|
@ -750,7 +752,10 @@ func TestValidate(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validators.ValidateServerJSON(&tt.serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&tt.serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
|
||||
if tt.expectedError == "" {
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -905,7 +910,10 @@ func TestValidate_RemoteNamespaceMatch(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validators.ValidateServerJSON(&tt.serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&tt.serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -989,7 +997,10 @@ func TestValidate_ServerNameFormat(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validators.ValidateServerJSON(&tt.serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&tt.serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -1068,7 +1079,10 @@ func TestValidate_MultipleSlashesInServerName(t *testing.T) {
|
|||
Schema: model.CurrentSchemaURL,
|
||||
Name: tt.serverName,
|
||||
}
|
||||
err := validators.ValidateServerJSON(&serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -1122,7 +1136,10 @@ func TestValidateArgument_ValidNamedArguments(t *testing.T) {
|
|||
for _, arg := range validCases {
|
||||
t.Run("Valid_"+arg.Name, func(t *testing.T) {
|
||||
server := createValidServerWithArgument(arg)
|
||||
err := validators.ValidateServerJSON(&server)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
assert.NoError(t, err, "Expected valid argument %+v", arg)
|
||||
})
|
||||
}
|
||||
|
|
@ -1141,7 +1158,10 @@ func TestValidateArgument_ValidPositionalArguments(t *testing.T) {
|
|||
for i, arg := range positionalCases {
|
||||
t.Run(fmt.Sprintf("ValidPositional_%d", i), func(t *testing.T) {
|
||||
server := createValidServerWithArgument(arg)
|
||||
err := validators.ValidateServerJSON(&server)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
assert.NoError(t, err, "Expected valid positional argument %+v", arg)
|
||||
})
|
||||
}
|
||||
|
|
@ -1163,7 +1183,10 @@ func TestValidateArgument_InvalidNamedArgumentNames(t *testing.T) {
|
|||
for _, tc := range invalidNameCases {
|
||||
t.Run("Invalid_"+tc.name, func(t *testing.T) {
|
||||
server := createValidServerWithArgument(tc.arg)
|
||||
err := validators.ValidateServerJSON(&server)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
assert.Error(t, err, "Expected error for invalid named argument name: %+v", tc.arg)
|
||||
})
|
||||
}
|
||||
|
|
@ -1211,7 +1234,10 @@ func TestValidateArgument_InvalidValueFields(t *testing.T) {
|
|||
for _, tc := range invalidValueCases {
|
||||
t.Run("Invalid_"+tc.name, func(t *testing.T) {
|
||||
server := createValidServerWithArgument(tc.arg)
|
||||
err := validators.ValidateServerJSON(&server)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
assert.Error(t, err, "Expected error for argument with value starting with name: %+v", tc.arg)
|
||||
})
|
||||
}
|
||||
|
|
@ -1267,7 +1293,10 @@ func TestValidateArgument_ValidValueFields(t *testing.T) {
|
|||
for _, tc := range validValueCases {
|
||||
t.Run("Valid_"+tc.name, func(t *testing.T) {
|
||||
server := createValidServerWithArgument(tc.arg)
|
||||
err := validators.ValidateServerJSON(&server)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&server, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
assert.NoError(t, err, "Expected valid argument %+v", tc.arg)
|
||||
})
|
||||
}
|
||||
|
|
@ -1734,7 +1763,10 @@ func TestValidate_TransportValidation(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validators.ValidateServerJSON(&tt.serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&tt.serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
|
||||
if tt.expectedError == "" {
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -2163,7 +2195,10 @@ func TestValidateTitle(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validators.ValidateServerJSON(&tt.serverDetail)
|
||||
// ValidateServerJSON returns all validation results; using FirstError() to preserve existing test behavior
|
||||
// In future, consider using result.Issues for comprehensive error reporting
|
||||
result := validators.ValidateServerJSON(&tt.serverDetail, validators.ValidationSchemaVersionAndSemantic)
|
||||
err := result.FirstError()
|
||||
if tt.expectedError == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue