Skip to content

Commit ee04c5d

Browse files
Claudelpcox
andcommitted
Add guard-policies support to MCP gateway config
- Added GuardPolicies field to ServerConfig (TOML and JSON formats) - Updated JSON schema URL to fetch latest spec from main branch - Added guard-policies to known fields in StdinServerConfig unmarshal - Preserved guard-policies in config conversion for both stdio and HTTP servers - Added comprehensive tests for guard-policies in both TOML and JSON formats - Updated config examples with GitHub guard policy examples - Updated README with detailed guard-policies documentation and examples - Fixed test assertion to handle new error message format Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 1861af2 commit ee04c5d

6 files changed

Lines changed: 168 additions & 21 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ For the complete JSON configuration specification with all validation rules, see
130130
"env": {
131131
"GITHUB_PERSONAL_ACCESS_TOKEN": "",
132132
"EXPANDED_VAR": "${MY_HOME}/config"
133+
},
134+
"guard-policies": {
135+
"github": {
136+
"owner": "github",
137+
"repos": ["gh-aw-mcpg", "gh-aw"]
138+
}
133139
}
134140
}
135141
},
@@ -182,6 +188,21 @@ For the complete JSON configuration specification with all validation rules, see
182188

183189
- **`url`** (required for http): HTTP endpoint URL for `type: "http"` servers
184190

191+
- **`guard-policies`** (optional): Guard policies for access control at the MCP gateway level
192+
- Structure is server-specific and depends on the MCP server implementation
193+
- For **GitHub MCP server**, controls repository access with the following structure:
194+
```toml
195+
[servers.github.guard_policies]
196+
[servers.github.guard_policies.github]
197+
owner = "github" # GitHub organization or user name
198+
repos = ["gh-aw-mcpg", "gh-aw"] # List of allowed repositories
199+
```
200+
- **Meaning**: Restricts the GitHub MCP server to only access repositories `github/gh-aw-mcpg` and `github/gh-aw`
201+
- Tools like `get_file_contents`, `search_code`, etc. will only work on these repositories
202+
- Attempts to access other repositories will be denied by the guard policy
203+
- For **other MCP servers** (Jira, WorkIQ, etc.), different policy schemas apply
204+
- JSON format uses `"guard-policies"` (with hyphen), TOML uses `guard_policies` (with underscore)
205+
185206
**Validation Rules:**
186207

187208
- **JSON stdin format**:

config.example.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ GITHUB_PERSONAL_ACCESS_TOKEN = "" # Pass through from host
6969
# tools = ["*"] # Allow all tools (default)
7070
# tools = ["read_file", "list_files"] # Allow specific tools only
7171

72+
# Optional: Guard policies for access control at the MCP gateway level
73+
# The structure is server-specific. For GitHub MCP server, this controls repository access.
74+
# Example: Restrict access to specific GitHub organization and repositories
75+
# [servers.github.guard_policies]
76+
# [servers.github.guard_policies.github]
77+
# owner = "github" # GitHub organization or user
78+
# repos = ["gh-aw-mcpg", "gh-aw"] # List of allowed repositories
79+
7280
# Example 2: Memory MCP Server (stdio via Docker)
7381
[servers.memory]
7482
command = "docker"

internal/config/config_core.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ type ServerConfig struct {
119119

120120
// Registry is the URI to the installation location in an MCP registry (informational)
121121
Registry string `toml:"registry" json:"registry,omitempty"`
122+
123+
// GuardPolicies holds guard policies for access control at the MCP gateway level.
124+
// The structure is server-specific. For GitHub MCP server, see the GitHub guard policy schema.
125+
GuardPolicies map[string]interface{} `toml:"guard_policies" json:"guard-policies,omitempty"`
122126
}
123127

124128
// applyGatewayDefaults applies default values to a GatewayConfig if they are not set.

internal/config/config_stdin.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type StdinServerConfig struct {
6868
// Registry is the URI to the installation location in an MCP registry (informational)
6969
Registry string `json:"registry,omitempty"`
7070

71+
// GuardPolicies holds guard policies for access control at the MCP gateway level.
72+
// The structure is server-specific. For GitHub MCP server, see the GitHub guard policy schema.
73+
GuardPolicies map[string]interface{} `json:"guard-policies,omitempty"`
74+
7175
// AdditionalProperties stores any extra fields for custom server types
7276
// This allows custom schemas to define their own fields beyond the standard ones
7377
AdditionalProperties map[string]interface{} `json:"-"`
@@ -107,6 +111,7 @@ func (s *StdinServerConfig) UnmarshalJSON(data []byte) error {
107111
"headers": true,
108112
"tools": true,
109113
"registry": true,
114+
"guard-policies": true,
110115
}
111116

112117
// Store additional properties (fields not in the struct)
@@ -270,11 +275,12 @@ func convertStdinServerConfig(name string, server *StdinServerConfig, customSche
270275
logConfig.Printf("Configured HTTP MCP server: name=%s, url=%s", name, server.URL)
271276
log.Printf("[CONFIG] Configured HTTP MCP server: %s -> %s", name, server.URL)
272277
return &ServerConfig{
273-
Type: "http",
274-
URL: server.URL,
275-
Headers: server.Headers,
276-
Tools: server.Tools,
277-
Registry: server.Registry,
278+
Type: "http",
279+
URL: server.URL,
280+
Headers: server.Headers,
281+
Tools: server.Tools,
282+
Registry: server.Registry,
283+
GuardPolicies: server.GuardPolicies,
278284
}, nil
279285
}
280286

@@ -332,12 +338,13 @@ func buildStdioServerConfig(name string, server *StdinServerConfig) *ServerConfi
332338
logConfig.Printf("Configured stdio MCP server: name=%s, container=%s", name, server.Container)
333339

334340
return &ServerConfig{
335-
Type: "stdio",
336-
Command: "docker",
337-
Args: args,
338-
Env: make(map[string]string),
339-
Tools: server.Tools,
340-
Registry: server.Registry,
341+
Type: "stdio",
342+
Command: "docker",
343+
Args: args,
344+
Env: make(map[string]string),
345+
Tools: server.Tools,
346+
Registry: server.Registry,
347+
GuardPolicies: server.GuardPolicies,
341348
}
342349
}
343350

internal/config/config_test.go

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,11 @@ func TestLoadFromStdin_UnsupportedType(t *testing.T) {
166166
// Should fail validation for unsupported type
167167
require.Error(t, err)
168168

169-
// Error should mention configuration error
170-
assert.Contains(t, err.Error(), "Configuration error", "Expected configuration error")
169+
// Error should mention configuration error or validation error
170+
errorMsg := err.Error()
171+
assert.True(t,
172+
strings.Contains(errorMsg, "Configuration error") || strings.Contains(errorMsg, "Configuration validation error"),
173+
"Expected configuration error or validation error, got: %s", errorMsg)
171174

172175
// Config should be nil on validation error
173176
assert.Nil(t, cfg, "Config should be nil when validation fails")
@@ -1531,3 +1534,108 @@ registry = "https://api.mcp.github.com/v0/servers/github/github-mcp-server"
15311534

15321535
assert.Equal(t, "https://api.mcp.github.com/v0/servers/github/github-mcp-server", server.Registry, "Registry field not preserved in TOML config")
15331536
}
1537+
1538+
// TestLoadFromStdin_WithGuardPolicies tests that guard-policies field is correctly parsed from JSON stdin
1539+
func TestLoadFromStdin_WithGuardPolicies(t *testing.T) {
1540+
jsonConfig := `{
1541+
"mcpServers": {
1542+
"github": {
1543+
"type": "stdio",
1544+
"container": "ghcr.io/github/github-mcp-server:latest",
1545+
"guard-policies": {
1546+
"github": {
1547+
"owner": "github",
1548+
"repos": ["gh-aw-mcpg", "gh-aw"]
1549+
}
1550+
}
1551+
},
1552+
"http-server": {
1553+
"type": "http",
1554+
"url": "https://example.com/mcp",
1555+
"guard-policies": {
1556+
"network": {
1557+
"allow": ["api.example.com"]
1558+
}
1559+
}
1560+
}
1561+
},
1562+
"gateway": {
1563+
"port": 8080,
1564+
"domain": "localhost",
1565+
"apiKey": "test-key"
1566+
}
1567+
}`
1568+
1569+
// Mock stdin
1570+
r, w, _ := os.Pipe()
1571+
oldStdin := os.Stdin
1572+
os.Stdin = r
1573+
go func() {
1574+
w.Write([]byte(jsonConfig))
1575+
w.Close()
1576+
}()
1577+
1578+
cfg, err := LoadFromStdin()
1579+
os.Stdin = oldStdin
1580+
1581+
require.NoError(t, err, "LoadFromStdin() failed")
1582+
require.NotNil(t, cfg, "Config should not be nil")
1583+
1584+
// Test stdio server with guard policies
1585+
githubServer, ok := cfg.Servers["github"]
1586+
require.True(t, ok, "Server 'github' not found")
1587+
require.NotNil(t, githubServer.GuardPolicies, "GuardPolicies should not be nil")
1588+
1589+
githubPolicy, ok := githubServer.GuardPolicies["github"]
1590+
require.True(t, ok, "GitHub policy not found in guard-policies")
1591+
githubPolicyMap, ok := githubPolicy.(map[string]interface{})
1592+
require.True(t, ok, "GitHub policy should be a map")
1593+
assert.Equal(t, "github", githubPolicyMap["owner"], "Owner field mismatch")
1594+
1595+
// Test HTTP server with guard policies
1596+
httpServer, ok := cfg.Servers["http-server"]
1597+
require.True(t, ok, "Server 'http-server' not found")
1598+
require.NotNil(t, httpServer.GuardPolicies, "GuardPolicies should not be nil for HTTP server")
1599+
1600+
networkPolicy, ok := httpServer.GuardPolicies["network"]
1601+
require.True(t, ok, "Network policy not found in guard-policies")
1602+
require.NotNil(t, networkPolicy, "Network policy should not be nil")
1603+
}
1604+
1605+
// TestLoadFromFile_WithGuardPolicies tests that guard_policies field is correctly parsed from TOML
1606+
func TestLoadFromFile_WithGuardPolicies(t *testing.T) {
1607+
tmpDir := t.TempDir()
1608+
tmpFile := filepath.Join(tmpDir, "config.toml")
1609+
1610+
tomlContent := `
1611+
[gateway]
1612+
port = 8080
1613+
api_key = "test-key"
1614+
1615+
[servers.github]
1616+
command = "docker"
1617+
args = ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server:latest"]
1618+
1619+
[servers.github.guard_policies]
1620+
[servers.github.guard_policies.github]
1621+
owner = "github"
1622+
repos = ["gh-aw-mcpg", "gh-aw"]
1623+
`
1624+
1625+
err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
1626+
require.NoError(t, err, "Failed to write temp TOML file")
1627+
1628+
cfg, err := LoadFromFile(tmpFile)
1629+
require.NoError(t, err, "LoadFromFile() failed")
1630+
require.NotNil(t, cfg, "Config should not be nil")
1631+
1632+
server, ok := cfg.Servers["github"]
1633+
require.True(t, ok, "Server 'github' not found")
1634+
require.NotNil(t, server.GuardPolicies, "GuardPolicies should not be nil")
1635+
1636+
githubPolicy, ok := server.GuardPolicies["github"]
1637+
require.True(t, ok, "GitHub policy not found in guard_policies")
1638+
githubPolicyMap, ok := githubPolicy.(map[string]interface{})
1639+
require.True(t, ok, "GitHub policy should be a map")
1640+
assert.Equal(t, "github", githubPolicyMap["owner"], "Owner field mismatch in TOML config")
1641+
}

internal/config/validation_schema.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,19 @@ var (
3030
// This URL points to the source of truth for the MCP Gateway configuration schema.
3131
//
3232
// Schema Version Pinning:
33-
// The schema is pinned to a specific gh-aw version (v0.41.1) for build reproducibility.
34-
// This ensures predictable builds and prevents unexpected breaking changes from upstream
35-
// schema updates.
33+
// The schema is fetched from the main branch to get the latest version with guard-policies support.
34+
// This ensures the gateway supports the latest configuration features including guard policies.
3635
//
37-
// To update the schema version:
36+
// To update to a specific pinned version:
3837
// 1. Check the latest gh-aw release: https://github.com/github/gh-aw/releases
39-
// 2. Update the version tag in the URL below
38+
// 2. Update the URL below to use a version tag instead of main
4039
// 3. Run tests to ensure compatibility: make test
41-
// 4. Update this comment with the new version number
40+
// 4. Update this comment with the version number
4241
//
43-
// Current schema version: v0.41.1 (February 2026)
42+
// Current schema version: main (latest with guard-policies support)
4443
//
4544
// Alternative: Embed the schema using go:embed directive for zero network dependency.
46-
schemaURL = "https://raw.githubusercontent.com/github/gh-aw/v0.41.1/docs/public/schemas/mcp-gateway-config.schema.json"
45+
schemaURL = "https://raw.githubusercontent.com/github/gh-aw/main/pkg/workflow/schemas/mcp-gateway-config.schema.json"
4746

4847
// Schema caching to avoid recompiling the JSON schema on every validation
4948
// This improves performance by compiling the schema once and reusing it

0 commit comments

Comments
 (0)