Skip to content

Commit e2b9847

Browse files
Mossakapelikhan
andauthored
feat: add MCP network permissions (#106)
* Add proxy configuration support for MCP tools with network restrictions Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance MCP configuration schema with additional properties for container, args, env, headers, and permissions Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Refactor proxy configuration handling: consolidate proxy file generation and introduce inline proxy configuration support Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Update workflow triggers for test-proxy Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Removed the step to start proxy services Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Update permissions in test-proxy workflow to allow issue reporting Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Fix proxy service naming in Docker Compose configuration Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance MCP configuration handling: pass tool name to getMCPConfig and transformContainerToDockerCommand, update Docker command arguments in proxy scenarios Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Update Docker command in transformContainerToDockerCommand for proxy scenarios Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Remove unnecessary includae statements from test-proxy workflow documentation Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance proxy configuration: update Docker Compose command for proxy-enabled containers, and support custom proxy arguments in MCP config. Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Add DEBUG environment variable to MCP configuration for enhanced logging Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Debugging: Update Claude Code Action to use forked version Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance proxy setup: remove unused proxy domain, pre-pull Docker images, and start Squid proxy service for MCP tools Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * regenerate the yaml Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Update Claude Code Action to use forked version Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance proxy configuration: enforce egress control for tools, update Docker Compose generation, and improve permission checks in tool configurations. Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * regenerate the yaml Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance proxy configuration: add iptables rules to accept established connections and egress from Squid IP Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Enhance proxy configuration: update iptables rules for established connections and egress control, improve YAML generation for MCP tools Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * fmt Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * fixed some linting issues Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> * Refactor GitHub Actions workflow to enhance output sanitization and streamline job structure --------- Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 9531735 commit e2b9847

9 files changed

Lines changed: 1229 additions & 25 deletions

File tree

.github/workflows/test-proxy.lock.yml

Lines changed: 616 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/test-proxy.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
on:
3+
pull_request:
4+
branches: [ "main" ]
5+
workflow_dispatch:
6+
7+
permissions:
8+
issues: write # needed to write the output report to an issue
9+
10+
tools:
11+
fetch:
12+
mcp:
13+
type: stdio
14+
container: mcp/fetch
15+
permissions:
16+
network:
17+
allowed:
18+
- "example.com"
19+
allowed:
20+
- "fetch"
21+
22+
github:
23+
allowed:
24+
- "create_issue"
25+
- "create_comment"
26+
- "get_issue"
27+
28+
engine: claude
29+
runs-on: ubuntu-latest
30+
---
31+
32+
# Test Network Permissions
33+
34+
## Task Description
35+
36+
Test the MCP network permissions feature to validate that domain restrictions are properly enforced.
37+
38+
- Use the fetch tool to successfully retrieve content from `https://example.com/` (the only allowed domain)
39+
- Attempt to access blocked domains and verify they fail with network errors:
40+
- `https://httpbin.org/json`
41+
- `https://api.github.com/user`
42+
- `https://www.google.com/`
43+
- `http://malicious-example.com/`
44+
- Verify that all blocked requests fail at the network level (proxy enforcement)
45+
- Confirm that only example.com is accessible through the Squid proxy
46+
47+
Create a GitHub issue with the test results, documenting:
48+
- Which domains were successfully accessed vs blocked
49+
- Error messages received for blocked domains
50+
- Confirmation that network isolation is working correctly
51+
- Any security observations or recommendations
52+
53+
The test should demonstrate that MCP containers are properly isolated and can only access explicitly allowed domains through the network proxy.

pkg/parser/schemas/mcp_config_schema.json

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,133 @@
1414
"command": {
1515
"type": "string",
1616
"description": "Command for stdio MCP connections"
17+
},
18+
"container": {
19+
"type": "string",
20+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$",
21+
"description": "Container image for stdio MCP connections (alternative to command)"
22+
},
23+
"args": {
24+
"type": "array",
25+
"items": {
26+
"type": "string"
27+
},
28+
"description": "Arguments for command or container execution"
29+
},
30+
"env": {
31+
"type": "object",
32+
"patternProperties": {
33+
"^[A-Z_][A-Z0-9_]*$": {
34+
"type": "string"
35+
}
36+
},
37+
"additionalProperties": false,
38+
"description": "Environment variables for MCP server"
39+
},
40+
"headers": {
41+
"type": "object",
42+
"patternProperties": {
43+
"^[A-Za-z0-9-]+$": {
44+
"type": "string"
45+
}
46+
},
47+
"additionalProperties": false,
48+
"description": "HTTP headers for HTTP MCP connections"
49+
},
50+
"permissions": {
51+
"type": "object",
52+
"properties": {
53+
"network": {
54+
"type": "object",
55+
"properties": {
56+
"allowed": {
57+
"type": "array",
58+
"items": {
59+
"type": "string",
60+
"pattern": "^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*$",
61+
"description": "Allowed domain name"
62+
},
63+
"minItems": 1,
64+
"uniqueItems": true,
65+
"description": "List of allowed domain names for network access"
66+
}
67+
},
68+
"required": ["allowed"],
69+
"additionalProperties": false,
70+
"description": "Network access permissions"
71+
}
72+
},
73+
"additionalProperties": false,
74+
"description": "Permissions configuration for container-based MCP servers"
1775
}
1876
},
1977
"required": ["type"],
20-
"additionalProperties": true,
21-
"if": {
22-
"properties": {
23-
"type": {
24-
"const": "http"
78+
"additionalProperties": false,
79+
"allOf": [
80+
{
81+
"if": {
82+
"properties": {
83+
"type": {
84+
"const": "http"
85+
}
86+
}
87+
},
88+
"then": {
89+
"required": ["url"],
90+
"not": {
91+
"anyOf": [
92+
{"required": ["command"]},
93+
{"required": ["container"]},
94+
{"required": ["permissions"]}
95+
]
96+
}
2597
}
26-
}
27-
},
28-
"then": {
29-
"required": ["url"]
30-
},
31-
"else": {
32-
"if": {
33-
"properties": {
34-
"type": {
35-
"const": "stdio"
98+
},
99+
{
100+
"if": {
101+
"properties": {
102+
"type": {
103+
"const": "stdio"
104+
}
105+
}
106+
},
107+
"then": {
108+
"anyOf": [
109+
{"required": ["command"]},
110+
{"required": ["container"]}
111+
],
112+
"not": {
113+
"allOf": [
114+
{"required": ["command"]},
115+
{"required": ["container"]}
116+
]
36117
}
37118
}
38119
},
39-
"then": {
40-
"required": ["command"]
120+
{
121+
"if": {
122+
"required": ["container"]
123+
},
124+
"then": {
125+
"properties": {
126+
"type": {
127+
"const": "stdio"
128+
}
129+
}
130+
}
131+
},
132+
{
133+
"if": {
134+
"required": ["permissions"]
135+
},
136+
"then": {
137+
"required": ["container"],
138+
"properties": {
139+
"type": {
140+
"const": "stdio"
141+
}
142+
}
143+
}
41144
}
42-
}
145+
]
43146
}

pkg/workflow/compiler.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,8 @@ func (c *Compiler) generateSafetyChecks(yaml *strings.Builder, data *WorkflowDat
19121912
func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, engine AgenticEngine) {
19131913
// Collect tools that need MCP server configuration
19141914
var mcpTools []string
1915+
var proxyTools []string
1916+
19151917
for toolName, toolValue := range tools {
19161918
// Standard MCP tools
19171919
if toolName == "github" {
@@ -1920,12 +1922,72 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any,
19201922
// Check if it's explicitly marked as MCP type in the new format
19211923
if hasMcp, _ := hasMCPConfig(mcpConfig); hasMcp {
19221924
mcpTools = append(mcpTools, toolName)
1925+
1926+
// Check if this tool needs proxy
1927+
if needsProxySetup, _ := needsProxy(mcpConfig); needsProxySetup {
1928+
proxyTools = append(proxyTools, toolName)
1929+
}
19231930
}
19241931
}
19251932
}
19261933

1927-
// Sort MCP tools to ensure stable code generation
1934+
// Sort tools to ensure stable code generation
19281935
sort.Strings(mcpTools)
1936+
sort.Strings(proxyTools)
1937+
1938+
// Generate proxy configuration files inline for proxy-enabled tools
1939+
// These files will be used automatically by docker compose when MCP tools run
1940+
if len(proxyTools) > 0 {
1941+
yaml.WriteString(" - name: Setup Proxy Configuration for MCP Network Restrictions\n")
1942+
yaml.WriteString(" run: |\n")
1943+
yaml.WriteString(" echo \"Generating proxy configuration files for MCP tools with network restrictions...\"\n")
1944+
yaml.WriteString(" \n")
1945+
1946+
// Generate proxy configurations inline for each proxy-enabled tool
1947+
for _, toolName := range proxyTools {
1948+
if toolConfig, ok := tools[toolName].(map[string]any); ok {
1949+
c.generateInlineProxyConfig(yaml, toolName, toolConfig)
1950+
}
1951+
}
1952+
1953+
yaml.WriteString(" echo \"Proxy configuration files generated.\"\n")
1954+
1955+
// Pre-pull images and start squid proxy ahead of time to avoid timeouts
1956+
yaml.WriteString(" - name: Pre-pull images and start Squid proxy\n")
1957+
yaml.WriteString(" run: |\n")
1958+
yaml.WriteString(" set -e\n")
1959+
yaml.WriteString(" echo 'Pre-pulling Docker images for proxy-enabled MCP tools...'\n")
1960+
yaml.WriteString(" docker pull ubuntu/squid:latest\n")
1961+
1962+
// Pull each tool's container image if specified, and bring up squid service
1963+
for _, toolName := range proxyTools {
1964+
if toolConfig, ok := tools[toolName].(map[string]any); ok {
1965+
if mcpConf, err := getMCPConfig(toolConfig, toolName); err == nil {
1966+
if containerVal, hasContainer := mcpConf["container"]; hasContainer {
1967+
if containerStr, ok := containerVal.(string); ok && containerStr != "" {
1968+
yaml.WriteString(fmt.Sprintf(" echo 'Pulling %s for tool %s'\n", containerStr, toolName))
1969+
yaml.WriteString(fmt.Sprintf(" docker pull %s\n", containerStr))
1970+
}
1971+
}
1972+
}
1973+
yaml.WriteString(fmt.Sprintf(" echo 'Starting squid-proxy service for %s'\n", toolName))
1974+
yaml.WriteString(fmt.Sprintf(" docker compose -f docker-compose-%s.yml up -d squid-proxy\n", toolName))
1975+
1976+
// Enforce that egress from this tool's network can only reach the Squid proxy
1977+
subnetCIDR, squidIP, _ := computeProxyNetworkParams(toolName)
1978+
yaml.WriteString(fmt.Sprintf(" echo 'Enforcing egress to proxy for %s (subnet %s, squid %s)'\n", toolName, subnetCIDR, squidIP))
1979+
yaml.WriteString(" if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi\n")
1980+
// Accept established/related connections first (position 1)
1981+
yaml.WriteString(" $SUDO iptables -C DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT\n")
1982+
// Accept all egress from Squid IP (position 2)
1983+
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 2 -s %s -j ACCEPT\n", squidIP, squidIP))
1984+
// Allow traffic to squid:3128 from the subnet (position 3)
1985+
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -d %s -p tcp --dport 3128 -j ACCEPT 2>/dev/null || $SUDO iptables -I DOCKER-USER 3 -s %s -d %s -p tcp --dport 3128 -j ACCEPT\n", subnetCIDR, squidIP, subnetCIDR, squidIP))
1986+
// Then reject all other egress from that subnet (append to end)
1987+
yaml.WriteString(fmt.Sprintf(" $SUDO iptables -C DOCKER-USER -s %s -j REJECT 2>/dev/null || $SUDO iptables -A DOCKER-USER -s %s -j REJECT\n", subnetCIDR, subnetCIDR))
1988+
}
1989+
}
1990+
}
19291991

19301992
// If no MCP tools, no configuration needed
19311993
if len(mcpTools) == 0 {

pkg/workflow/compiler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3210,7 +3210,7 @@ func TestTransformImageToDockerCommand(t *testing.T) {
32103210
mcpConfig[k] = v
32113211
}
32123212

3213-
err := transformContainerToDockerCommand(mcpConfig)
3213+
err := transformContainerToDockerCommand(mcpConfig, "test")
32143214

32153215
if tt.wantErr {
32163216
if err == nil {

0 commit comments

Comments
 (0)