Skip to content

Commit e6988b8

Browse files
authored
Merge pull request #257 from githubnext/copilot/add-mount-support-mcp-gateway
2 parents 64b34b4 + 77e12ff commit e6988b8

4 files changed

Lines changed: 109 additions & 3 deletions

File tree

internal/config/config_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,26 @@ func TestLoadFromStdin_InvalidMountFormat(t *testing.T) {
802802
mounts: `["/host::ro"]`,
803803
errorMsg: "validation error",
804804
},
805+
{
806+
name: "relative source path",
807+
mounts: `["relative/path:/container:ro"]`,
808+
errorMsg: "mount source must be an absolute path",
809+
},
810+
{
811+
name: "relative destination path",
812+
mounts: `["/host:relative/path:ro"]`,
813+
errorMsg: "mount destination must be an absolute path",
814+
},
815+
{
816+
name: "dot relative source",
817+
mounts: `["./config:/app/config:ro"]`,
818+
errorMsg: "mount source must be an absolute path",
819+
},
820+
{
821+
name: "dot relative destination",
822+
mounts: `["/host/config:./config:ro"]`,
823+
errorMsg: "mount destination must be an absolute path",
824+
},
805825
}
806826

807827
for _, tt := range tests {

internal/config/rules/rules.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ func TimeoutPositive(timeout int, fieldName, jsonPath string) *ValidationError {
106106

107107
// MountFormat validates a mount specification in the format "source:dest:mode"
108108
// Returns nil if valid, *ValidationError if invalid
109+
// Per MCP Gateway specification v1.7.0 section 4.1.5:
110+
// - Host path MUST be an absolute path
111+
// - Container path MUST be an absolute path
112+
// - Mode MUST be either "ro" (read-only) or "rw" (read-write)
109113
func MountFormat(mount, jsonPath string, index int) *ValidationError {
110114
parts := strings.Split(mount, ":")
111115
if len(parts) != 3 {
@@ -125,7 +129,17 @@ func MountFormat(mount, jsonPath string, index int) *ValidationError {
125129
Field: "mounts",
126130
Message: fmt.Sprintf("mount source cannot be empty in '%s'", mount),
127131
JSONPath: fmt.Sprintf("%s.mounts[%d]", jsonPath, index),
128-
Suggestion: "Provide a valid source path",
132+
Suggestion: "Provide a valid absolute source path (e.g., '/host/path')",
133+
}
134+
}
135+
136+
// Validate source is an absolute path (MCP spec requirement)
137+
if !strings.HasPrefix(source, "/") {
138+
return &ValidationError{
139+
Field: "mounts",
140+
Message: fmt.Sprintf("mount source must be an absolute path, got '%s'", source),
141+
JSONPath: fmt.Sprintf("%s.mounts[%d]", jsonPath, index),
142+
Suggestion: "Use an absolute path starting with '/' (e.g., '/var/data' instead of 'data')",
129143
}
130144
}
131145

@@ -135,7 +149,17 @@ func MountFormat(mount, jsonPath string, index int) *ValidationError {
135149
Field: "mounts",
136150
Message: fmt.Sprintf("mount destination cannot be empty in '%s'", mount),
137151
JSONPath: fmt.Sprintf("%s.mounts[%d]", jsonPath, index),
138-
Suggestion: "Provide a valid destination path",
152+
Suggestion: "Provide a valid absolute destination path (e.g., '/app/data')",
153+
}
154+
}
155+
156+
// Validate dest is an absolute path (MCP spec requirement)
157+
if !strings.HasPrefix(dest, "/") {
158+
return &ValidationError{
159+
Field: "mounts",
160+
Message: fmt.Sprintf("mount destination must be an absolute path, got '%s'", dest),
161+
JSONPath: fmt.Sprintf("%s.mounts[%d]", jsonPath, index),
162+
Suggestion: "Use an absolute path starting with '/' (e.g., '/app/data' instead of 'app/data')",
139163
}
140164
}
141165

internal/config/rules/rules_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,68 @@ func TestMountFormat(t *testing.T) {
236236
shouldErr: true,
237237
errMsg: "invalid mount mode",
238238
},
239+
{
240+
name: "invalid source - relative path",
241+
mount: "relative/path:/container/path:ro",
242+
jsonPath: "mcpServers.github",
243+
index: 0,
244+
shouldErr: true,
245+
errMsg: "mount source must be an absolute path",
246+
},
247+
{
248+
name: "invalid dest - relative path",
249+
mount: "/host/path:relative/path:ro",
250+
jsonPath: "mcpServers.github",
251+
index: 0,
252+
shouldErr: true,
253+
errMsg: "mount destination must be an absolute path",
254+
},
255+
{
256+
name: "invalid source - dot relative",
257+
mount: "./config:/app/config:ro",
258+
jsonPath: "mcpServers.github",
259+
index: 0,
260+
shouldErr: true,
261+
errMsg: "mount source must be an absolute path",
262+
},
263+
{
264+
name: "invalid dest - dot relative",
265+
mount: "/host/config:./config:ro",
266+
jsonPath: "mcpServers.github",
267+
index: 0,
268+
shouldErr: true,
269+
errMsg: "mount destination must be an absolute path",
270+
},
271+
{
272+
name: "invalid source - parent relative",
273+
mount: "../config:/app/config:ro",
274+
jsonPath: "mcpServers.github",
275+
index: 0,
276+
shouldErr: true,
277+
errMsg: "mount source must be an absolute path",
278+
},
279+
{
280+
name: "invalid dest - parent relative",
281+
mount: "/host/config:../config:ro",
282+
jsonPath: "mcpServers.github",
283+
index: 0,
284+
shouldErr: true,
285+
errMsg: "mount destination must be an absolute path",
286+
},
287+
{
288+
name: "valid mount - root paths",
289+
mount: "/:/root:ro",
290+
jsonPath: "mcpServers.github",
291+
index: 0,
292+
shouldErr: false,
293+
},
294+
{
295+
name: "valid mount - deep nested paths",
296+
mount: "/var/lib/docker/volumes/data:/app/data/volumes:rw",
297+
jsonPath: "mcpServers.github",
298+
index: 0,
299+
shouldErr: false,
300+
},
239301
}
240302

241303
for _, tt := range tests {

internal/difc/difc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func TestFormatViolationError(t *testing.T) {
268268
SecrecyToAdd: []Tag{"private"},
269269
Reason: "Empty agent cannot access private resource",
270270
}
271-
agentSecrecy := NewSecrecyLabel() // Empty
271+
agentSecrecy := NewSecrecyLabel() // Empty
272272
agentIntegrity := NewIntegrityLabel() // Empty
273273

274274
resource := NewLabeledResource("private-resource")

0 commit comments

Comments
 (0)