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:
Bob Dickinson 2026-01-15 14:49:39 -08:00 • committed by GitHub
commit fc88cf0870
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 6107 additions and 179 deletions

View 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 ""
}

View 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)
})
}
}

View 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
}
}
}
}
}
]
}
}
}

View 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
}
}
}
}
}
]
}
}
}

View 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
}
}
}
}
}
]
}
}
}

View 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"
}

View 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"
}

View 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"
}

View 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.

View 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")
})
}
}

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

View 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())
}

View file

@ -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

View file

@ -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 {