diff --git a/docs/guides/publishing/publish-server.md b/docs/guides/publishing/publish-server.md index a194f26..b26bb10 100644 --- a/docs/guides/publishing/publish-server.md +++ b/docs/guides/publishing/publish-server.md @@ -100,7 +100,10 @@ This creates a `server.json` with auto-detected values. You'll see something lik { "registryType": "npm", "identifier": "your-package-name", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -148,12 +151,18 @@ Add an `mcpName` field to your `package.json`: ### Example server.json ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "io.github.username/server-name", + "description": "A server that provides npm package functionality", + "version": "1.0.0", "packages": [ { "registryType": "npm", "identifier": "your-npm-package", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -180,12 +189,18 @@ Add it to your README.md file (which becomes the package description on PyPI). T ### Example server.json ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "io.github.username/server-name", + "description": "A server that provides PyPI package functionality", + "version": "1.0.0", "packages": [ { "registryType": "pypi", "identifier": "your-pypi-package", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -212,12 +227,18 @@ Add a README file to your NuGet package that includes the server name. This can ### Example server.json ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "io.github.username/server-name", + "description": "A server that provides NuGet package functionality", + "version": "1.0.0", "packages": [ { "registryType": "nuget", "identifier": "Your.NuGet.Package", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -248,13 +269,19 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ### Example server.json (Docker Hub) ```json { - "name": "io.github.username/server-name", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "name": "io.github.username/server-name", + "description": "A server that provides Docker container functionality", + "version": "1.0.0", "packages": [ { "registryType": "oci", "registryBaseUrl": "https://docker.io", "identifier": "yourusername/your-mcp-server", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -263,13 +290,19 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ### Example server.json (GitHub Container Registry) ```json { - "name": "io.github.username/server-name", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "name": "io.github.username/server-name", + "description": "A server that provides GitHub container functionality", + "version": "1.0.0", "packages": [ { "registryType": "oci", "registryBaseUrl": "https://ghcr.io", "identifier": "yourusername/your-mcp-server", - "version": "1.0.0" + "version": "1.0.0", + "transport": { + "type": "stdio" + } } ] } @@ -299,12 +332,19 @@ openssl dgst -sha256 server.mcpb ### Example server.json ```json { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", "name": "io.github.username/server-name", + "description": "A server that provides MCPB package functionality", + "version": "1.0.0", "packages": [ { "registryType": "mcpb", "identifier": "https://github.com/you/your-repo/releases/download/v1.0.0/server.mcpb", - "fileSha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce" + "version": "1.0.0", + "fileSha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", + "transport": { + "type": "stdio" + } } ] } diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index d11cf40..ab1fd49 100644 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -267,12 +267,6 @@ components: 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" - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time ServerList: @@ -527,9 +521,9 @@ components: required: - serverId - versionId - - published_at - - updated_at - - is_latest + - publishedAt + - updatedAt + - isLatest properties: serverId: type: string @@ -541,17 +535,17 @@ components: format: uuid description: Unique ID for this specific version example: "773f9b2e-1a47-4c8d-b5e6-2f8d9c4a7b3e" - published_at: + publishedAt: type: string format: date-time description: Timestamp when the server was first published to the registry example: "2023-12-01T10:30:00Z" - updated_at: + updatedAt: type: string format: date-time description: Timestamp when the server entry was last updated example: "2023-12-01T11:00:00Z" - is_latest: + isLatest: type: boolean description: Whether this is the latest version of the server example: true diff --git a/tools/validate-examples/main.go b/tools/validate-examples/main.go index 7f2ab51..3cae7d9 100644 --- a/tools/validate-examples/main.go +++ b/tools/validate-examples/main.go @@ -1,7 +1,5 @@ -// validate-examples validates JSON examples in docs/server-json/examples.md -// against both schema.json and registry-schema.json. -// -// For more information, see docs/server-json/README.md +// validate-examples validates JSON examples in documentation files +// against both schema.json and Go validators. package main import ( @@ -19,12 +17,11 @@ import ( jsonschema "github.com/santhosh-tekuri/jsonschema/v5" ) -const ( - // expectedExampleCount is the number of JSON examples we expect to find in generic-server-json.md - // IMPORTANT: Only change this count if you have intentionally added or removed examples. This - // check prevents accidental formatting changes from causing examples to be skipped during validation. - expectedExampleCount = 13 -) +type validationTarget struct { + path string + requireSchema bool + expectedCount *int +} func main() { log.SetFlags(0) // Remove timestamp from logs @@ -35,34 +32,61 @@ func main() { } func runValidation() error { - basePath := filepath.Join("docs", "reference", "server-json") - - examplesPath := filepath.Join(basePath, "generic-server-json.md") - schemaPath := filepath.Join(basePath, "server.schema.json") - - examples, err := extractExamples(examplesPath) - if err != nil { - return fmt.Errorf("failed to extract examples: %w", err) + // Define what we validate and how + expectedServerJSONCount := 13 + targets := []validationTarget{ + { + path: filepath.Join("docs", "reference", "server-json", "generic-server-json.md"), + requireSchema: false, + expectedCount: &expectedServerJSONCount, + }, + { + path: filepath.Join("docs", "guides", "publishing", "publish-server.md"), + requireSchema: true, + expectedCount: nil, // No count validation for guide + }, } - log.Printf("Found %d examples in examples.md\n", len(examples)) - - if len(examples) != expectedExampleCount { - return fmt.Errorf("expected %d examples but found %d - if this is intentional, update expectedExampleCount in %s", - expectedExampleCount, len(examples), "tools/validate-examples/main.go") - } - - log.Println() - + schemaPath := filepath.Join("docs", "reference", "server-json", "server.schema.json") baseSchema, err := compileSchema(schemaPath) if err != nil { return fmt.Errorf("failed to compile server.schema.json: %w", err) } - validatedCount := 0 + for _, target := range targets { + if err := validateFile(target, baseSchema); err != nil { + return err + } + log.Println() + } + log.Println("All validations passed!") + return nil +} + +func validateFile(target validationTarget, baseSchema *jsonschema.Schema) error { + examples, err := extractExamples(target.path, target.requireSchema) + if err != nil { + return fmt.Errorf("failed to extract examples from %s: %w", target.path, err) + } + + log.Printf("Validating %s: found %d examples\n", target.path, len(examples)) + + if target.expectedCount != nil && len(examples) != *target.expectedCount { + return fmt.Errorf("expected %d examples in %s but found %d - if this is intentional, update expectedCount in tools/validate-examples/main.go", + *target.expectedCount, target.path, len(examples)) + } + + if len(examples) == 0 { + log.Println(" No examples to validate") + return nil + } + + log.Println() + + validatedCount := 0 for i, example := range examples { - log.Printf("Example %d:", i+1) + log.Printf(" Example %d (line %d):", i+1, example.line) if validateExample(example, baseSchema) { validatedCount++ @@ -71,19 +95,18 @@ func runValidation() error { log.Println() } - if validatedCount != expectedExampleCount { - return fmt.Errorf("validation failed: expected %d examples to pass all validations but only %d did", - expectedExampleCount, validatedCount) + if validatedCount != len(examples) { + return fmt.Errorf("validation failed for %s: expected %d examples to pass but only %d did", + target.path, len(examples), validatedCount) } - log.Printf("Successfully validated all %d examples!", validatedCount) return nil } func validateExample(ex example, baseSchema *jsonschema.Schema) bool { var data any if err := json.Unmarshal([]byte(ex.content), &data); err != nil { - log.Printf(" ❌ Invalid JSON: %v", err) + log.Printf(" ❌ Invalid JSON: %v", err) return false } @@ -95,7 +118,7 @@ func validateExample(ex example, baseSchema *jsonschema.Schema) bool { // This is a PublishRequest format - validate only expected properties exist for key := range dataMap { if key != "server" && key != "x-publisher" { - log.Printf(" Invalid PublishRequest property: ❌ %s (only 'server' and optional 'x-publisher' are allowed)", key) + log.Printf(" Invalid PublishRequest property: ❌ %s (only 'server' and optional 'x-publisher' are allowed)", key) publishRequestValid = false } } @@ -112,11 +135,11 @@ func validateExample(ex example, baseSchema *jsonschema.Schema) bool { func validateAgainstSchema(data any, schema *jsonschema.Schema, schemaName string) bool { if err := schema.Validate(data); err != nil { - log.Printf(" Validating against %s: ❌", schemaName) - log.Printf(" Error: %v", err) + log.Printf(" Validating against %s: ❌", schemaName) + log.Printf(" Error: %v", err) return false } - log.Printf(" Validating against %s: ✅", schemaName) + log.Printf(" Validating against %s: ✅", schemaName) return true } @@ -124,24 +147,24 @@ func validateWithObjectValidator(serverData any) bool { var serverDetail apiv0.ServerJSON serverDataBytes, err := json.Marshal(serverData) if err != nil { - log.Printf(" Validating with Go Validator: ❌") - log.Printf(" Error marshaling server data: %v", err) + log.Printf(" Validating with Go Validator: ❌") + log.Printf(" Error marshaling server data: %v", err) return false } if err := json.Unmarshal(serverDataBytes, &serverDetail); err != nil { - log.Printf(" Validating with Go Validator: ❌") - log.Printf(" Error unmarshaling to ServerDetail: %v", err) + log.Printf(" Validating with Go Validator: ❌") + log.Printf(" Error unmarshaling to ServerDetail: %v", err) return false } if err := validators.ValidateServerJSON(&serverDetail); err != nil { - log.Printf(" Validating with Go Validator: ❌") - log.Printf(" Error: %v", err) + log.Printf(" Validating with Go Validator: ❌") + log.Printf(" Error: %v", err) return false } - log.Printf(" Validating with Go Validator: ✅") + log.Printf(" Validating with Go Validator: ✅") return true } @@ -150,7 +173,7 @@ type example struct { line int } -func extractExamples(path string) ([]example, error) { +func extractExamples(path string, requireSchema bool) ([]example, error) { data, err := os.ReadFile(path) if err != nil { return nil, err @@ -159,21 +182,25 @@ func extractExamples(path string) ([]example, error) { content := string(data) // Regex to match JSON code blocks in markdown - // Captures everything between ```json and ``` re := regexp.MustCompile("(?s)```json\r?\n(.*?)\r?\n```") matches := re.FindAllStringSubmatchIndex(content, -1) var examples []example for _, match := range matches { if len(match) < 4 { - // should never happen return nil, fmt.Errorf("invalid match - expected at least 4 indices but got %d", len(match)) } start, end := match[2], match[3] - // line numbers start at 1 + jsonContent := content[start:end] + + // Filter by $schema if required + if requireSchema && !strings.Contains(jsonContent, "$schema") { + continue + } + line := 1 + strings.Count(content[:start], "\n") examples = append(examples, example{ - content: content[start:end], + content: jsonContent, line: line, }) } @@ -200,4 +227,4 @@ func compileSchema(path string) (*jsonschema.Schema, error) { } return compiler.Compile(path) -} +} \ No newline at end of file