From 44e35020b025f269aa81881565b0ca7b213d2ac1 Mon Sep 17 00:00:00 2001 From: adam jones Date: Mon, 6 Oct 2025 11:57:01 +0100 Subject: [PATCH] Add optional title field to server.json schema (#608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Introduces a new optional `title` field for MCP servers to provide a clean, human-readable display name that clients and subregistries can use for display purposes. Fixes #606 ## Changes - Added `title` field to `ServerJSON` type (optional, max 100 chars) - Added validation to reject titles ending with 'MCP Server', 'MCP server', or 'MCP' suffixes - Updated JSON schema and OpenAPI specification - Added comprehensive test coverage for title validation - Updated all documentation examples to demonstrate the new field ## Validation The title field: - Is optional (can be omitted) - Must not be only whitespace if provided - Must not end with MCP-related suffixes (case-insensitive) - Can contain 'MCP' in the middle (e.g., "MCP Weather API" is valid) Examples: - ✅ Valid: "GitHub", "Weather API", "Slack Integration" - ❌ Invalid: "GitHub MCP", "Weather MCP Server", "Slack-MCP" --------- Co-authored-by: Claude --- docs/guides/publishing/publish-server.md | 60 ++++--- docs/reference/api/openapi.yaml | 7 +- .../server-json/generic-server-json.md | 7 + docs/reference/server-json/server.schema.json | 7 + internal/validators/validators.go | 19 +++ internal/validators/validators_test.go | 156 ++++++++++++++++++ pkg/api/v0/types.go | 1 + 7 files changed, 230 insertions(+), 27 deletions(-) diff --git a/docs/guides/publishing/publish-server.md b/docs/guides/publishing/publish-server.md index 196e3e7..4dc6b72 100644 --- a/docs/guides/publishing/publish-server.md +++ b/docs/guides/publishing/publish-server.md @@ -94,13 +94,14 @@ This creates a `server.json` with auto-detected values. You'll see something lik ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.yourname/your-server", - "description": "A description of your MCP server", + "name": "io.github.yourname/weather-data-mcp", + "title": "Weather Data", + "description": "Access real-time weather data and forecasts", "version": "1.0.0", "packages": [ { "registryType": "npm", - "identifier": "your-package-name", + "identifier": "@yourname/weather-data-mcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -153,13 +154,14 @@ Add an `mcpName` field to your `package.json`: ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides npm package functionality", + "name": "io.github.username/slack-integration-mcp", + "title": "Slack Integration", + "description": "Send messages and manage Slack workspaces", "version": "1.0.0", "packages": [ { "registryType": "npm", - "identifier": "your-npm-package", + "identifier": "@username/slack-integration-mcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -191,13 +193,14 @@ Add it to your README.md file (which becomes the package description on PyPI). T ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides PyPI package functionality", + "name": "io.github.username/database-query-mcp", + "title": "Database Query", + "description": "Execute SQL queries and manage database connections", "version": "1.0.0", "packages": [ { "registryType": "pypi", - "identifier": "your-pypi-package", + "identifier": "database-query-mcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -229,13 +232,14 @@ Add a README file to your NuGet package that includes the server name. This can ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides NuGet package functionality", + "name": "io.github.username/azure-devops-mcp", + "title": "Azure DevOps", + "description": "Manage Azure DevOps work items and pipelines", "version": "1.0.0", "packages": [ { "registryType": "nuget", - "identifier": "Your.NuGet.Package", + "identifier": "Username.AzureDevOpsMcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -271,14 +275,15 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides Docker container functionality", + "name": "io.github.username/kubernetes-manager-mcp", + "title": "Kubernetes Manager", + "description": "Deploy and manage Kubernetes resources", "version": "1.0.0", "packages": [ { "registryType": "oci", "registryBaseUrl": "https://docker.io", - "identifier": "yourusername/your-mcp-server", + "identifier": "yourusername/kubernetes-manager-mcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -292,14 +297,15 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name" ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides GitHub container functionality", + "name": "io.github.username/git-operations-mcp", + "title": "Git Operations", + "description": "Advanced Git repository management and operations", "version": "1.0.0", "packages": [ { "registryType": "oci", "registryBaseUrl": "https://ghcr.io", - "identifier": "yourusername/your-mcp-server", + "identifier": "username/git-operations-mcp", "version": "1.0.0", "transport": { "type": "stdio" @@ -334,13 +340,14 @@ openssl dgst -sha256 server.mcpb ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.username/server-name", - "description": "A server that provides MCPB package functionality", + "name": "io.github.username/image-processor-mcp", + "title": "Image Processor", + "description": "Process and transform images with various filters", "version": "1.0.0", "packages": [ { "registryType": "mcpb", - "identifier": "https://github.com/you/your-repo/releases/download/v1.0.0/server.mcpb", + "identifier": "https://github.com/username/image-processor-mcp/releases/download/v1.0.0/image-processor.mcpb", "version": "1.0.0", "fileSha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", "transport": { @@ -379,13 +386,14 @@ Add the `remotes` field to your `server.json` (can coexist with `packages`): ```json { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "com.yourcompany/api-server", - "description": "Cloud-hosted MCP server for API operations", + "name": "com.yourcompany/acme-analytics", + "title": "ACME Analytics", + "description": "Real-time business intelligence and reporting platform", "version": "2.0.0", "remotes": [ { "type": "streamable-http", - "url": "https://mcp.yourcompany.com/http" + "url": "https://mcp.yourcompany.com/mcp" } ] } @@ -400,7 +408,7 @@ You can offer multiple connection methods: "remotes": [ { "type": "streamable-http", - "url": "https://mcp.yourcompany.com/http" + "url": "https://mcp.yourcompany.com/mcp" }, { "type": "sse", @@ -424,7 +432,7 @@ Configure headers that clients should send when connecting: "remotes": [ { "type": "streamable-http", - "url": "https://mcp.yourcompany.com/http", + "url": "https://mcp.yourcompany.com/mcp", "headers": [ { "name": "X-API-Key", diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 683ceea..a30a57d 100644 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -156,6 +156,7 @@ paths: value: name: "io.modelcontextprotocol/filesystem" description: "Node.js server implementing Model Context Protocol (MCP) for filesystem operations." + title: "Filesystem" repository: url: "https://github.com/modelcontextprotocol/servers" source: "github" @@ -177,6 +178,7 @@ paths: value: name: "com.example/demo-server" description: "Example MCP server demonstrating publisher extensions." + title: "Demo Server" repository: url: "https://github.com/example/mcp-demo" source: "github" @@ -267,7 +269,6 @@ components: description: "Optional relative path from repository root to the server location within a monorepo structure" example: "src/everything" - ServerList: type: object required: @@ -502,6 +503,10 @@ components: type: string description: "Human-readable description of the server's functionality" example: "Node.js server implementing Model Context Protocol (MCP) for filesystem operations." + title: + type: string + 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: "Filesystem" repository: $ref: '#/components/schemas/Repository' version: diff --git a/docs/reference/server-json/generic-server-json.md b/docs/reference/server-json/generic-server-json.md index a5deac0..c6a0b4b 100644 --- a/docs/reference/server-json/generic-server-json.md +++ b/docs/reference/server-json/generic-server-json.md @@ -25,6 +25,7 @@ The official registry has some more restrictions on top of this. See the [offici "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/brave-search", "description": "MCP server for Brave Search API integration", + "title": "Brave Search", "websiteUrl": "https://anonymous.modelcontextprotocol.io/examples", "repository": { "url": "https://github.com/modelcontextprotocol/servers", @@ -71,6 +72,7 @@ For MCP servers located within a subdirectory of a larger repository (monorepo s "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol/everything", "description": "MCP server that exercises all the features of the MCP protocol", + "title": "Everything", "repository": { "url": "https://github.com/modelcontextprotocol/servers", "source": "github", @@ -153,6 +155,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.modelcontextprotocol/filesystem", "description": "Node.js server implementing Model Context Protocol (MCP) for filesystem operations.", + "title": "Filesystem", "repository": { "url": "https://github.com/modelcontextprotocol/servers", "source": "github", @@ -288,6 +291,7 @@ This will essentially instruct the MCP client to execute `dnx Knapcode.SampleMcp "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.example/weather-mcp", "description": "Python MCP server for weather data access", + "title": "Weather", "repository": { "url": "https://github.com/example/weather-mcp", "source": "github", @@ -503,6 +507,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol.anonymous/hybrid-mcp", "description": "MCP server available as both local package and remote service", + "title": "Hybrid", "repository": { "url": "https://github.com/example/hybrid-mcp", "source": "github", @@ -588,6 +593,7 @@ The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6. "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.modelcontextprotocol/text-editor", "description": "MCP Bundle server for advanced text editing capabilities", + "title": "Text Editor", "repository": { "url": "https://github.com/modelcontextprotocol/text-editor-mcpb", "source": "github" @@ -632,6 +638,7 @@ Some CLI tools bundle an MCP server, without a standalone MCP package or a publi "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.snyk/cli-mcp", "description": "MCP server provided by the Snyk CLI", + "title": "Snyk", "version": "1.1298.0", "packages": [ { diff --git a/docs/reference/server-json/server.schema.json b/docs/reference/server-json/server.schema.json index 72cf6f1..19e6183 100644 --- a/docs/reference/server-json/server.schema.json +++ b/docs/reference/server-json/server.schema.json @@ -396,6 +396,13 @@ "minLength": 1, "maxLength": 100 }, + "title": { + "type": "string", + "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", + "minLength": 1, + "maxLength": 100 + }, "repository": { "$ref": "#/definitions/Repository", "description": "Optional repository metadata for the MCP server source code. Recommended for transparency and security inspection." diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 8ceb309..213cd5b 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -83,6 +83,11 @@ func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error { return err } + // Validate title if provided + if err := validateTitle(serverJSON.Title); err != nil { + return err + } + // Validate all packages (basic field validation) // Detailed package validation (including registry checks) is done during publish for _, pkg := range serverJSON.Packages { @@ -156,6 +161,20 @@ func validateWebsiteURL(websiteURL string) error { return nil } +func validateTitle(title string) error { + // Skip validation if title is not provided (optional field) + if title == "" { + return nil + } + + // Check that title is not only whitespace + if strings.TrimSpace(title) == "" { + return fmt.Errorf("title cannot be only whitespace") + } + + return nil +} + func validatePackageField(obj *model.Package) error { if !HasNoSpaces(obj.Identifier) { return ErrPackageNameHasSpaces diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index 01276c2..e6c233c 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -1709,3 +1709,159 @@ func createValidServerWithArgument(arg model.Argument) apiv0.ServerJSON { }, } } + +func TestValidateTitle(t *testing.T) { + tests := []struct { + name string + serverDetail apiv0.ServerJSON + expectedError string + }{ + { + name: "Valid title without MCP suffix", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "GitHub", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Valid title with multiple words", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "Weather API", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Empty title is allowed (optional field)", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Allows title with 'MCP Server' suffix", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "GitHub MCP Server", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Allows title with 'MCP' suffix", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "GitHub MCP", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Allows title with 'mcp server' suffix (lowercase)", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "GitHub mcp server", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Allows title with hyphenated 'MCP' suffix", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "GitHub-MCP", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Allows 'MCP' in the middle of title", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: "MCP Weather API", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "", + }, + { + name: "Rejects title with only whitespace", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Title: " ", + Repository: model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + }, + expectedError: "title cannot be only whitespace", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validators.ValidateServerJSON(&tt.serverDetail) + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } + }) + } +} diff --git a/pkg/api/v0/types.go b/pkg/api/v0/types.go index 859ec78..270b1b9 100644 --- a/pkg/api/v0/types.go +++ b/pkg/api/v0/types.go @@ -41,6 +41,7 @@ type ServerJSON struct { Schema string `json:"$schema" required:"true" minLength:"1"` Name string `json:"name" minLength:"1" maxLength:"200"` Description string `json:"description" minLength:"1" maxLength:"100"` + Title string `json:"title,omitempty" minLength:"1" maxLength:"100"` Repository model.Repository `json:"repository,omitempty"` Version string `json:"version"` WebsiteURL string `json:"websiteUrl,omitempty"`