Add optional title field to server.json schema (#608)
## 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 <noreply@anthropic.com>
This commit is contained in:
parent
44950c23bb
commit
44e35020b0
7 changed files with 230 additions and 27 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
Loading…
Reference in a new issue