Skip to content

Commit 85ec9c6

Browse files
authored
Merge branch 'main' into adamj/improve-stdio-transport-validation
2 parents 4df9270 + fb64e7d commit 85ec9c6

File tree

10 files changed

+232
-5
lines changed

10 files changed

+232
-5
lines changed

docs/server-json/examples.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,47 @@
4646
}
4747
```
4848

49+
## Server in a Monorepo with Subfolder
50+
51+
For MCP servers located within a subdirectory of a larger repository (monorepo structure), use the `subfolder` field to specify the relative path:
52+
53+
```json
54+
{
55+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
56+
"name": "io.modelcontextprotocol/everything",
57+
"description": "MCP server that exercises all the features of the MCP protocol",
58+
"status": "active",
59+
"repository": {
60+
"url": "https://github.com/modelcontextprotocol/servers",
61+
"source": "github",
62+
"subfolder": "src/everything"
63+
},
64+
"version_detail": {
65+
"version": "0.6.2"
66+
},
67+
"packages": [
68+
{
69+
"registry_type": "npm",
70+
"registry_base_url": "https://registry.npmjs.org",
71+
"identifier": "@modelcontextprotocol/everything",
72+
"version": "0.6.2",
73+
"transport": {
74+
"type": "stdio"
75+
}
76+
}
77+
],
78+
"_meta": {
79+
"publisher": {
80+
"tool": "npm-publisher",
81+
"version": "1.0.1",
82+
"build_info": {
83+
"timestamp": "2023-12-01T10:30:00Z"
84+
}
85+
}
86+
}
87+
}
88+
```
89+
4990
## Constant (fixed) arguments needed to start the MCP server
5091

5192
Suppose your MCP server application requires a `mcp start` CLI arguments to start in MCP server mode. Express these as positional arguments like this:
@@ -580,6 +621,43 @@ This example shows an MCPB (MCP Bundle) package that:
580621
- Includes a SHA-256 hash for integrity verification
581622
- Can be downloaded and executed directly by MCP clients that support MCPB
582623

624+
## Embedded MCP inside a CLI tool
625+
626+
Some CLI tools bundle an MCP server, without a standalone MCP package or a public repository. In these cases, reuse the existing `packages` shape by pointing at the host CLI package and supplying the `package_arguments` and `runtime_hint` if needed to start the MCP server.
627+
628+
```json
629+
{
630+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
631+
"name": "io.snyk/cli-mcp",
632+
"description": "MCP server provided by the Snyk CLI",
633+
"status": "active",
634+
"version_detail": {
635+
"version": "1.1298.0"
636+
},
637+
"packages": [
638+
{
639+
"registry_type": "npm",
640+
"registry_base_url": "https://registry.npmjs.org",
641+
"identifier": "snyk",
642+
"version": "1.1298.0",
643+
"transport": {
644+
"type": "stdio"
645+
},
646+
"package_arguments": [
647+
{ "type": "positional", "value": "mcp" },
648+
{
649+
"type": "named",
650+
"name": "-t",
651+
"description": "Transport type for MCP server",
652+
"default": "stdio",
653+
"choices": ["stdio", "sse"]
654+
}
655+
]
656+
}
657+
]
658+
}
659+
```
660+
583661
## Deprecated Server Example
584662

585663
```json

docs/server-json/repository_references.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Consumers of the `server.json` metadata MAY use the `source` property to determi
88

99
The `url` property MAY be used to browse the source code. Some source forges, such as GitHub, support `git clone <url>` on the URL, which also works for web browsing. For the purposes of the Official MCP Registry, the URL MUST be accessible in a web browser.
1010

11+
The optional `subfolder` property MAY be used to specify a relative path from the repository root to the location of the MCP server within a monorepo structure. The value MUST be a clean relative path.
12+
1113
The `id` property is owned and determined by the source forge, such as GitHub. This value SHOULD be stable across repository renames and, if applicable on the source forge, MAY be used to detect repository resurrection attacks. If a repository is renamed, the `id` value SHOULD remain constant. If the repository is deleted and then recreated later, the `id` value SHOULD change.
1214

1315
Determining the `id` is specific to the source forge. For GitHub, the following [GitHub CLI](https://cli.github.com/) command MAY be used (works for both public and private repositories):

docs/server-json/server.schema.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"id": {
2525
"type": "string",
2626
"example": "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
27+
},
28+
"subfolder": {
29+
"type": "string",
30+
"description": "Optional relative path from repository root to the server location within a monorepo or nested package structure",
31+
"example": "src/everything"
2732
}
2833
}
2934
},
@@ -448,7 +453,7 @@
448453
"additionalProperties": true
449454
},
450455
"io.modelcontextprotocol.registry": {
451-
"type": "object",
456+
"type": "object",
452457
"description": "Official MCP registry metadata (read-only, added by registry)",
453458
"additionalProperties": true
454459
}

docs/server-registry-api/openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ components:
195195
id:
196196
type: string
197197
example: "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
198+
subfolder:
199+
type: string
200+
description: "Optional relative path from repository root to the server location within a monorepo structure"
201+
example: "src/everything"
198202

199203
Server:
200204
type: object

internal/validators/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "errors"
66
var (
77
// Repository validation errors
88
ErrInvalidRepositoryURL = errors.New("invalid repository URL")
9+
ErrInvalidSubfolderPath = errors.New("invalid subfolder path")
910

1011
// Package validation errors
1112
ErrPackageNameHasSpaces = errors.New("package name cannot contain spaces")

internal/validators/utils.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,41 @@ func IsValidURL(rawURL string) bool {
9090
return true
9191
}
9292

93+
// IsValidSubfolderPath checks if a subfolder path is valid
94+
func IsValidSubfolderPath(path string) bool {
95+
// Empty path is valid (subfolder is optional)
96+
if path == "" {
97+
return true
98+
}
99+
100+
// Must not start with / (must be relative)
101+
if strings.HasPrefix(path, "/") {
102+
return false
103+
}
104+
105+
// Must not end with / (clean path format)
106+
if strings.HasSuffix(path, "/") {
107+
return false
108+
}
109+
110+
// Check for valid path characters (alphanumeric, dash, underscore, dot, forward slash)
111+
validPathRegex := regexp.MustCompile(`^[a-zA-Z0-9\-_./]+$`)
112+
if !validPathRegex.MatchString(path) {
113+
return false
114+
}
115+
116+
// Check that path segments are valid
117+
segments := strings.Split(path, "/")
118+
for _, segment := range segments {
119+
// Disallow empty segments ("//"), current dir ("."), and parent dir ("..")
120+
if segment == "" || segment == "." || segment == ".." {
121+
return false
122+
}
123+
}
124+
125+
return true
126+
}
127+
93128
// IsValidRemoteURL checks if a URL is valid for remotes (stricter than packages - no localhost allowed)
94129
func IsValidRemoteURL(rawURL string) bool {
95130
// First check basic URL structure

internal/validators/validators.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func validateRepository(obj *model.Repository) error {
5959
return fmt.Errorf("%w: %s", ErrInvalidRepositoryURL, obj.URL)
6060
}
6161

62+
// validate subfolder if present
63+
if obj.Subfolder != "" && !IsValidSubfolderPath(obj.Subfolder) {
64+
return fmt.Errorf("%w: %s", ErrInvalidSubfolderPath, obj.Subfolder)
65+
}
66+
6267
return nil
6368
}
6469

internal/validators/validators_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,102 @@ func TestValidate(t *testing.T) {
9494
},
9595
expectedError: validators.ErrInvalidRepositoryURL.Error(),
9696
},
97+
{
98+
name: "server with valid repository subfolder",
99+
serverDetail: apiv0.ServerJSON{
100+
Name: "com.example/test-server",
101+
Description: "A test server",
102+
Repository: model.Repository{
103+
URL: "https://github.com/owner/repo",
104+
Source: "github",
105+
Subfolder: "servers/my-server",
106+
},
107+
VersionDetail: model.VersionDetail{
108+
Version: "1.0.0",
109+
},
110+
},
111+
expectedError: "",
112+
},
113+
{
114+
name: "server with repository subfolder containing path traversal",
115+
serverDetail: apiv0.ServerJSON{
116+
Name: "com.example/test-server",
117+
Description: "A test server",
118+
Repository: model.Repository{
119+
URL: "https://github.com/owner/repo",
120+
Source: "github",
121+
Subfolder: "../parent/folder",
122+
},
123+
VersionDetail: model.VersionDetail{
124+
Version: "1.0.0",
125+
},
126+
},
127+
expectedError: validators.ErrInvalidSubfolderPath.Error(),
128+
},
129+
{
130+
name: "server with repository subfolder starting with slash",
131+
serverDetail: apiv0.ServerJSON{
132+
Name: "com.example/test-server",
133+
Description: "A test server",
134+
Repository: model.Repository{
135+
URL: "https://github.com/owner/repo",
136+
Source: "github",
137+
Subfolder: "/absolute/path",
138+
},
139+
VersionDetail: model.VersionDetail{
140+
Version: "1.0.0",
141+
},
142+
},
143+
expectedError: validators.ErrInvalidSubfolderPath.Error(),
144+
},
145+
{
146+
name: "server with repository subfolder ending with slash",
147+
serverDetail: apiv0.ServerJSON{
148+
Name: "com.example/test-server",
149+
Description: "A test server",
150+
Repository: model.Repository{
151+
URL: "https://github.com/owner/repo",
152+
Source: "github",
153+
Subfolder: "servers/my-server/",
154+
},
155+
VersionDetail: model.VersionDetail{
156+
Version: "1.0.0",
157+
},
158+
},
159+
expectedError: validators.ErrInvalidSubfolderPath.Error(),
160+
},
161+
{
162+
name: "server with repository subfolder containing invalid characters",
163+
serverDetail: apiv0.ServerJSON{
164+
Name: "com.example/test-server",
165+
Description: "A test server",
166+
Repository: model.Repository{
167+
URL: "https://github.com/owner/repo",
168+
Source: "github",
169+
Subfolder: "servers/my server",
170+
},
171+
VersionDetail: model.VersionDetail{
172+
Version: "1.0.0",
173+
},
174+
},
175+
expectedError: validators.ErrInvalidSubfolderPath.Error(),
176+
},
177+
{
178+
name: "server with repository subfolder containing empty segments",
179+
serverDetail: apiv0.ServerJSON{
180+
Name: "com.example/test-server",
181+
Description: "A test server",
182+
Repository: model.Repository{
183+
URL: "https://github.com/owner/repo",
184+
Source: "github",
185+
Subfolder: "servers//my-server",
186+
},
187+
VersionDetail: model.VersionDetail{
188+
Version: "1.0.0",
189+
},
190+
},
191+
expectedError: validators.ErrInvalidSubfolderPath.Error(),
192+
},
97193
{
98194
name: "package with spaces in name",
99195
serverDetail: apiv0.ServerJSON{

pkg/model/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ type Package struct {
3535

3636
// Repository represents a source code repository as defined in the spec
3737
type Repository struct {
38-
URL string `json:"url"`
39-
Source string `json:"source"`
40-
ID string `json:"id,omitempty"`
38+
URL string `json:"url"`
39+
Source string `json:"source"`
40+
ID string `json:"id,omitempty"`
41+
Subfolder string `json:"subfolder,omitempty"`
4142
}
4243

4344
// Format represents the input format type

tools/validate-examples/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
// IMPORTANT: Only change this count if you have intentionally added or removed examples
2525
// from the examples.md file. This check prevents accidental formatting changes from
2626
// causing examples to be skipped during validation.
27-
expectedExampleCount = 10
27+
expectedExampleCount = 12
2828
)
2929

3030
func main() {

0 commit comments

Comments
 (0)