Skip to content

Commit 17eb56d

Browse files
authored
feat: Add website_url field for MCP servers (#422)
Adds optional website_url field to the server.json schema allowing publishers to provide a 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. Issue: #183 PR: #130 Discussion: #274 ## Motivation and Context This change addresses the need identified in #130, #183 and #274 for a standardized way to direct users to an official landing page similar to `homepage` used in NPM ([example](https://www.npmjs.com/package/@modelcontextprotocol/sdk)). It is also serves as a viable option for MCP Servers that don't follow typical package manager patterns (e.g., embedded in a Desktop app). ## How Has This Been Tested? - Schema validation: All 13 example server.json files validate successfully against the updated schema - Go validator tests: Added additional test cases covering URL format validation and namespace matching - Run all checks: Updated example files to work with anonymous publishing and verified `make check` passes ## Breaking Changes None. The website_url field is optional and backward compatible with existing server.json files. ## Types of changes - [ ] 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) - [x] Documentation update ## Checklist - [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 ## Additional context Security considerations: - Website URL validation enforces that the domain matches the publisher's reverse-DNS namespace - HTTP/HTTPS protocols are required (no file:// or other schemes) - Validation is consistent with existing remote URL validation patterns
1 parent d0d8630 commit 17eb56d

File tree

7 files changed

+181
-4
lines changed

7 files changed

+181
-4
lines changed

docs/reference/api/openapi.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ components:
233233
type: string
234234
example: "1.0.2"
235235
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."
236+
website_url:
237+
type: string
238+
format: uri
239+
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."
240+
example: "https://modelcontextprotocol.io/examples"
236241
created_at:
237242
type: string
238243
format: date-time

docs/reference/server-json/generic-server-json.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ The official registry has some more restrictions on top of this. See the [offici
2323
```json
2424
{
2525
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
26-
"name": "io.modelcontextprotocol/brave-search",
26+
"name": "io.modelcontextprotocol.anonymous/brave-search",
2727
"description": "MCP server for Brave Search API integration",
2828
"status": "active",
29+
"website_url": "https://anonymous.modelcontextprotocol.io/examples",
2930
"repository": {
3031
"url": "https://github.com/modelcontextprotocol/servers",
3132
"source": "github"
@@ -654,6 +655,21 @@ Some CLI tools bundle an MCP server, without a standalone MCP package or a publi
654655
}
655656
```
656657

658+
### Server with Custom Installation Path
659+
660+
For MCP servers that follow a custom installation path or are embedded in applications without standalone packages, use the `website_url` field to direct users to setup documentation:
661+
662+
```json
663+
{
664+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
665+
"name": "io.modelcontextprotocol.anonymous/embedded-mcp",
666+
"description": "MCP server embedded in a Desktop app",
667+
"status": "active",
668+
"website_url": "https://anonymous.modelcontextprotocol.io/embedded-mcp-guide",
669+
"version": "0.1.0"
670+
}
671+
```
672+
657673
### Deprecated Server Example
658674

659675
```json

docs/reference/server-json/server.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
"maxLength": 255,
7474
"example": "1.0.2",
7575
"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."
76+
},
77+
"website_url": {
78+
"type": "string",
79+
"format": "uri",
80+
"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.",
81+
"example": "https://modelcontextprotocol.io/examples"
7682
}
7783
}
7884
},

internal/validators/validators.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error {
2424
return err
2525
}
2626

27+
// Validate website URL if provided
28+
if err := validateWebsiteURL(serverJSON.WebsiteURL); err != nil {
29+
return err
30+
}
31+
2732
// Validate all packages (basic field validation)
2833
// Detailed package validation (including registry checks) is done during publish
2934
for _, pkg := range serverJSON.Packages {
@@ -44,6 +49,11 @@ func ValidateServerJSON(serverJSON *apiv0.ServerJSON) error {
4449
return err
4550
}
4651

52+
// Validate reverse-DNS namespace matching for website URL
53+
if err := validateWebsiteURLNamespaceMatch(*serverJSON); err != nil {
54+
return err
55+
}
56+
4757
return nil
4858
}
4959

@@ -67,6 +77,31 @@ func validateRepository(obj *model.Repository) error {
6777
return nil
6878
}
6979

80+
func validateWebsiteURL(websiteURL string) error {
81+
// Skip validation if website URL is not provided (optional field)
82+
if websiteURL == "" {
83+
return nil
84+
}
85+
86+
// Parse the URL to ensure it's valid
87+
parsedURL, err := url.Parse(websiteURL)
88+
if err != nil {
89+
return fmt.Errorf("invalid website URL: %w", err)
90+
}
91+
92+
// Ensure it's an absolute URL with valid scheme
93+
if !parsedURL.IsAbs() {
94+
return fmt.Errorf("website URL must be absolute (include scheme): %s", websiteURL)
95+
}
96+
97+
// Only allow HTTP/HTTPS schemes for security
98+
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
99+
return fmt.Errorf("website URL must use http or https scheme: %s", websiteURL)
100+
}
101+
102+
return nil
103+
}
104+
70105
func validatePackageField(obj *model.Package) error {
71106
if !HasNoSpaces(obj.Identifier) {
72107
return ErrPackageNameHasSpaces
@@ -319,6 +354,21 @@ func validateRemoteNamespaceMatch(serverJSON apiv0.ServerJSON) error {
319354
return nil
320355
}
321356

357+
// validateWebsiteURLNamespaceMatch validates that website URL matches the reverse-DNS namespace
358+
func validateWebsiteURLNamespaceMatch(serverJSON apiv0.ServerJSON) error {
359+
// Skip validation if website URL is not provided
360+
if serverJSON.WebsiteURL == "" {
361+
return nil
362+
}
363+
364+
namespace := serverJSON.Name
365+
if err := validateRemoteURLMatchesNamespace(serverJSON.WebsiteURL, namespace); err != nil {
366+
return fmt.Errorf("website URL %s does not match namespace %s: %w", serverJSON.WebsiteURL, namespace, err)
367+
}
368+
369+
return nil
370+
}
371+
322372
// validateRemoteURLMatchesNamespace checks if a remote URL's hostname matches the publisher domain from the namespace
323373
func validateRemoteURLMatchesNamespace(remoteURL, namespace string) error {
324374
// Parse the URL to extract the hostname

internal/validators/validators_test.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ func TestValidate(t *testing.T) {
2727
Source: "github",
2828
ID: "owner/repo",
2929
},
30-
Version: "1.0.0",
30+
Version: "1.0.0",
31+
WebsiteURL: "https://example.com/docs",
3132
Packages: []model.Package{
3233
{
3334
Identifier: "test-package",
@@ -170,6 +171,104 @@ func TestValidate(t *testing.T) {
170171
},
171172
expectedError: validators.ErrInvalidSubfolderPath.Error(),
172173
},
174+
{
175+
name: "server with valid website URL",
176+
serverDetail: apiv0.ServerJSON{
177+
Name: "com.example/test-server",
178+
Description: "A test server",
179+
Repository: model.Repository{
180+
URL: "https://github.com/owner/repo",
181+
Source: "github",
182+
},
183+
Version: "1.0.0",
184+
WebsiteURL: "https://example.com/docs",
185+
},
186+
expectedError: "",
187+
},
188+
{
189+
name: "server with invalid website URL - no scheme",
190+
serverDetail: apiv0.ServerJSON{
191+
Name: "com.example/test-server",
192+
Description: "A test server",
193+
Repository: model.Repository{
194+
URL: "https://github.com/owner/repo",
195+
Source: "github",
196+
},
197+
Version: "1.0.0",
198+
WebsiteURL: "example.com/docs",
199+
},
200+
expectedError: "website URL must be absolute (include scheme): example.com/docs",
201+
},
202+
{
203+
name: "server with invalid website URL - invalid scheme",
204+
serverDetail: apiv0.ServerJSON{
205+
Name: "com.example/test-server",
206+
Description: "A test server",
207+
Repository: model.Repository{
208+
URL: "https://github.com/owner/repo",
209+
Source: "github",
210+
},
211+
Version: "1.0.0",
212+
WebsiteURL: "ftp://example.com/docs",
213+
},
214+
expectedError: "website URL must use http or https scheme: ftp://example.com/docs",
215+
},
216+
{
217+
name: "server with malformed website URL",
218+
serverDetail: apiv0.ServerJSON{
219+
Name: "com.example/test-server",
220+
Description: "A test server",
221+
Repository: model.Repository{
222+
URL: "https://github.com/owner/repo",
223+
Source: "github",
224+
},
225+
Version: "1.0.0",
226+
WebsiteURL: "ht tp://example.com/docs",
227+
},
228+
expectedError: "invalid website URL:",
229+
},
230+
{
231+
name: "server with website URL that matches namespace domain",
232+
serverDetail: apiv0.ServerJSON{
233+
Name: "com.example/test-server",
234+
Description: "A test server",
235+
Repository: model.Repository{
236+
URL: "https://github.com/owner/repo",
237+
Source: "github",
238+
},
239+
Version: "1.0.0",
240+
WebsiteURL: "https://example.com/docs",
241+
},
242+
expectedError: "",
243+
},
244+
{
245+
name: "server with website URL subdomain that matches namespace",
246+
serverDetail: apiv0.ServerJSON{
247+
Name: "com.example/test-server",
248+
Description: "A test server",
249+
Repository: model.Repository{
250+
URL: "https://github.com/owner/repo",
251+
Source: "github",
252+
},
253+
Version: "1.0.0",
254+
WebsiteURL: "https://docs.example.com/mcp",
255+
},
256+
expectedError: "",
257+
},
258+
{
259+
name: "server with website URL that does not match namespace",
260+
serverDetail: apiv0.ServerJSON{
261+
Name: "com.example/test-server",
262+
Description: "A test server",
263+
Repository: model.Repository{
264+
URL: "https://github.com/owner/repo",
265+
Source: "github",
266+
},
267+
Version: "1.0.0",
268+
WebsiteURL: "https://different.com/docs",
269+
},
270+
expectedError: "website URL https://different.com/docs does not match namespace com.example/test-server",
271+
},
173272
{
174273
name: "package with spaces in name",
175274
serverDetail: apiv0.ServerJSON{

pkg/api/v0/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ type ServerJSON struct {
3333
Description string `json:"description" minLength:"1" maxLength:"100"`
3434
Status model.Status `json:"status,omitempty" minLength:"1"`
3535
Repository model.Repository `json:"repository,omitempty"`
36-
Version string `json:"version"`
36+
Version string `json:"version"`
37+
WebsiteURL string `json:"website_url,omitempty"`
3738
Packages []model.Package `json:"packages,omitempty"`
3839
Remotes []model.Transport `json:"remotes,omitempty"`
3940
Meta *ServerMeta `json:"_meta,omitempty"`

tools/validate-examples/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const (
2323
// expectedExampleCount is the number of JSON examples we expect to find in generic-server-json.md
2424
// IMPORTANT: Only change this count if you have intentionally added or removed examples. This
2525
// check prevents accidental formatting changes from causing examples to be skipped during validation.
26-
expectedExampleCount = 12
26+
expectedExampleCount = 13
2727
)
2828

2929
func main() {

0 commit comments

Comments
 (0)