Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
56a23b8
First pass at enhanced validation
BobDickinson Oct 5, 2025
0d3950d
Embedded the schema to make it available at runtime.
BobDickinson Oct 6, 2025
0ad650e
Implemented schema "reference" (constraint path and full path with de…
BobDickinson Oct 7, 2025
3d80156
Docs updates
BobDickinson Oct 7, 2025
0dbbc8e
Notes and CLI docs
BobDickinson Oct 7, 2025
1b59820
Modified make process to handle schema embedding more cleanly. Moved…
BobDickinson Oct 8, 2025
2144c55
Update to previous comment (staged)
BobDickinson Oct 8, 2025
19b6621
Merge branch 'main' into enhanced-server-validation
BobDickinson Oct 8, 2025
e4ba595
Added prep-chema to CI (to copy schema file for embed to pass linter …
BobDickinson Oct 8, 2025
28ae714
Fixed linter errors
BobDickinson Oct 8, 2025
4a35d9b
Addressed CI test failures
BobDickinson Oct 8, 2025
6467163
Merge branch 'main' into enhanced-server-validation
BobDickinson Oct 9, 2025
823d0c0
Merge upstream/main into enhanced-server-validation
BobDickinson Dec 9, 2025
8259279
fix: update test file Repository fields to use pointers
BobDickinson Dec 9, 2025
02392d6
Add GitHub Action workflow to sync schema files from modelcontextprot…
BobDickinson Dec 9, 2025
8e00ac6
Add temporary push trigger to register sync-schema workflow
BobDickinson Dec 9, 2025
838f20d
Sync schemas from modelcontextprotocol/static [skip ci]
actions-user Dec 9, 2025
5a16478
Remove temporary push trigger from sync-schema workflow
BobDickinson Dec 9, 2025
728af52
Fix namespace validation tests and documentation examples
BobDickinson Dec 9, 2025
c81b2b8
Removed `ValidateServerJSON` (previously used for backward compatibil…
BobDickinson Dec 11, 2025
d7968ae
Linter fixes
BobDickinson Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/sync-schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Sync Schema

on:
workflow_dispatch: # Manual trigger
# TODO: Add daily schedule later
# schedule:
# - cron: '0 2 * * *' # Run daily at 2 AM UTC

permissions:
contents: write

jobs:
sync-schema:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Checkout static repo
uses: actions/checkout@v4
with:
repository: modelcontextprotocol/static
path: static-repo

- name: Sync schemas from static repo
run: |
echo "🔍 Syncing schemas from modelcontextprotocol/static..."
mkdir -p internal/validators/schemas

# Copy all versioned schema files
for dir in static-repo/schemas/*/; do
if [ -f "$dir/server.schema.json" ]; then
version=$(basename "$dir")
# Skip draft directory if it exists
if [ "$version" != "draft" ]; then
output_file="internal/validators/schemas/${version}.json"
if [ ! -f "$output_file" ] || ! cmp -s "$dir/server.schema.json" "$output_file"; then
echo "⬇ Adding/updating ${version}/server.schema.json -> ${version}.json"
cp "$dir/server.schema.json" "$output_file"
else
echo "✓ ${version} is already up to date"
fi
fi
fi
done

echo "✅ Schema sync complete"

- name: Check for changes
id: changes
run: |
# Check for both modified and untracked files
if [ -n "$(git status --porcelain internal/validators/schemas/)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
git status --porcelain internal/validators/schemas/
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "No changes to schemas"
fi

- name: Commit and push changes
if: steps.changes.outputs.changed == 'true'
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git add internal/validators/schemas/
git commit -m "Sync schemas from modelcontextprotocol/static [skip ci]"
git push
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ validate-schemas
coverage.out
coverage.html
deploy/infra/infra
./registry
registry
6 changes: 3 additions & 3 deletions cmd/publisher/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func InitCommand() error {

// Create the server structure
server := createServerJSON(
name, description, version, repoURL, repoSource, subfolder,
model.CurrentSchemaURL, name, description, version, repoURL, repoSource, subfolder,
packageType, packageIdentifier, version, envVars,
)

Expand Down Expand Up @@ -327,7 +327,7 @@ func detectPackageIdentifier(serverName string, packageType string) string {
}

func createServerJSON(
name, description, version, repoURL, repoSource, subfolder,
currentSchema, name, description, version, repoURL, repoSource, subfolder,
packageType, packageIdentifier, packageVersion string,
envVars []model.KeyValueInput,
) apiv0.ServerJSON {
Expand Down Expand Up @@ -406,7 +406,7 @@ func createServerJSON(

// Create server structure
return apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Schema: currentSchema,
Name: name,
Description: description,
Repository: repo,
Expand Down
18 changes: 8 additions & 10 deletions cmd/publisher/commands/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"path/filepath"
"strings"

"github.com/modelcontextprotocol/registry/internal/validators"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
"github.com/modelcontextprotocol/registry/pkg/model"
)

func PublishCommand(args []string) error {
Expand All @@ -38,15 +38,13 @@ func PublishCommand(args []string) error {
return fmt.Errorf("invalid server.json: %w", err)
}

// Check for deprecated schema and recommend migration
// Allow empty schema (will use default) but reject old schemas
if serverJSON.Schema != "" && !strings.Contains(serverJSON.Schema, model.CurrentSchemaVersion) {
return fmt.Errorf(`deprecated schema detected: %s.

Migrate to the current schema format for new servers.

📋 Migration checklist: https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/CHANGELOG.md#migration-checklist-for-publishers
📖 Full changelog with examples: https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/CHANGELOG.md`, serverJSON.Schema)
// Validate schema version (non-empty schema, valid schema, and current schema)
// This performs schema version checks without full schema validation
// Note: When we enable full validation, use validators.ValidationAll instead
result := runValidationAndPrintIssues(&serverJSON, validators.ValidationSchemaVersionOnly)
if !result.Valid {
// Return error after printing (all errors already printed by validateServerJSON)
return fmt.Errorf("validation failed")
}

// Load saved token
Expand Down
151 changes: 151 additions & 0 deletions cmd/publisher/commands/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package commands

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/modelcontextprotocol/registry/internal/validators"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
"github.com/modelcontextprotocol/registry/pkg/model"
)

// printSchemaValidationErrors prints nicely formatted error messages for schema validation issues
// (empty schema or non-current schema) with migration guidance to stdout.
// Returns true if any schema errors/warnings were printed.
func printSchemaValidationErrors(result *validators.ValidationResult, serverJSON *apiv0.ServerJSON) bool {
currentSchemaURL := model.CurrentSchemaURL
migrationURL := "https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/CHANGELOG.md"
checklistURL := migrationURL + "#migration-checklist-for-publishers"

printed := false

for _, issue := range result.Issues {
switch issue.Reference {
case "schema-field-required":
// Empty/missing schema
_, _ = fmt.Fprintf(os.Stdout, "$schema field is required.\n")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, "Expected current schema: %s\n", currentSchemaURL)
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "Run 'mcp-publisher init' to create a new server.json with the correct schema, or update your existing server.json file.")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, "📋 Migration checklist: %s\n", checklistURL)
_, _ = fmt.Fprintf(os.Stdout, "📖 Full changelog with examples: %s\n", migrationURL)
_, _ = fmt.Fprintln(os.Stdout)
printed = true
return printed // Only one schema error at a time

case "schema-version-deprecated":
// Non-current schema
if issue.Severity == validators.ValidationIssueSeverityWarning {
// Warning format (for validate command)
_, _ = fmt.Fprintf(os.Stdout, "⚠️ Deprecated schema detected: %s\n", serverJSON.Schema)
} else {
// Error format (for publish command)
_, _ = fmt.Fprintf(os.Stdout, "deprecated schema detected: %s.\n", serverJSON.Schema)
}
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, "Expected current schema: %s\n", currentSchemaURL)
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "Migrate to the current schema format for new servers.")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, "📋 Migration checklist: %s\n", checklistURL)
_, _ = fmt.Fprintf(os.Stdout, "📖 Full changelog with examples: %s\n", migrationURL)
_, _ = fmt.Fprintln(os.Stdout)
printed = true
return printed // Only one schema error at a time
}
}

return printed
}

// runValidationAndPrintIssues validates the server JSON, prints schema validation errors, and prints all issues.
// Validation failures are always printed (for both validate and publish commands).
func runValidationAndPrintIssues(serverJSON *apiv0.ServerJSON, opts validators.ValidationOptions) *validators.ValidationResult {
result := validators.ValidateServerJSON(serverJSON, opts)

// Print schema validation errors/warnings with friendly messages
printSchemaValidationErrors(result, serverJSON)

if result.Valid {
return result
}

// Print all issues
_, _ = fmt.Fprintf(os.Stdout, "❌ Validation failed with %d issue(s):\n", len(result.Issues))
_, _ = fmt.Fprintln(os.Stdout)

// Track which schema issues we've already printed to avoid duplicates
issueNum := 1

for _, issue := range result.Issues {
// Skip schema issues that were already printed (they're printed by printSchemaValidationErrors above)
if issue.Reference == "schema-field-required" || issue.Reference == "schema-version-deprecated" {
continue
}

// Print other issues normally
_, _ = fmt.Fprintf(os.Stdout, "%d. [%s] %s (%s)\n", issueNum, issue.Severity, issue.Path, issue.Type)
_, _ = fmt.Fprintf(os.Stdout, " %s\n", issue.Message)
if issue.Reference != "" {
_, _ = fmt.Fprintf(os.Stdout, " Reference: %s\n", issue.Reference)
}
_, _ = fmt.Fprintln(os.Stdout)
issueNum++
}

return result
}

func ValidateCommand(args []string) error {
// Parse arguments
serverFile := "server.json"

for _, arg := range args {
if arg == "--help" || arg == "-h" {
_, _ = fmt.Fprintln(os.Stdout, "Usage: mcp-publisher validate [file]")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "Validate a server.json file without publishing.")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "Arguments:")
_, _ = fmt.Fprintln(os.Stdout, " file Path to server.json file (default: ./server.json)")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "The validate command performs exhaustive validation, reporting all issues at once.")
_, _ = fmt.Fprintln(os.Stdout, "It validates JSON syntax, schema compliance, and semantic rules.")
return nil
}
if !strings.HasPrefix(arg, "-") {
serverFile = arg
}
}

// Read server file
serverData, err := os.ReadFile(serverFile)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s not found, please check the file path", serverFile)
}
return fmt.Errorf("failed to read %s: %w", serverFile, err)
}

// Validate JSON
var serverJSON apiv0.ServerJSON
if err := json.Unmarshal(serverData, &serverJSON); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}

// Run detailed validation (this is the whole point of the validate command)
// Include schema validation for comprehensive validation
// Warn about non-current schemas (don't error, just inform)
result := runValidationAndPrintIssues(&serverJSON, validators.ValidationAll)

if result.Valid {
_, _ = fmt.Fprintln(os.Stdout, "✅ server.json is valid")
return nil
}

return fmt.Errorf("validation failed")
}
3 changes: 3 additions & 0 deletions cmd/publisher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func main() {
err = commands.LogoutCommand()
case "publish":
err = commands.PublishCommand(os.Args[2:])
case "validate":
err = commands.ValidateCommand(os.Args[2:])
case "--version", "-v", "version":
log.Printf("mcp-publisher %s (commit: %s, built: %s)", Version, GitCommit, BuildTime)
return
Expand Down Expand Up @@ -65,6 +67,7 @@ func printUsage() {
_, _ = fmt.Fprintln(os.Stdout, " login Authenticate with the registry")
_, _ = fmt.Fprintln(os.Stdout, " logout Clear saved authentication")
_, _ = fmt.Fprintln(os.Stdout, " publish Publish server.json to the registry")
_, _ = fmt.Fprintln(os.Stdout, " validate Validate server.json without publishing")
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintln(os.Stdout, "Use 'mcp-publisher <command> --help' for more information about a command.")
}
Loading
Loading