GoClaw MCP test succeeds but save fails for local/private MCP URLs due to SSRF validation mismatch
Summary
When adding a Streamable HTTP MCP server hosted on a local/private network address, GoClaw can successfully test the connection and discover tools, but saving the same MCP server fails with SSRF validation errors.
This appears to be a validation mismatch between:
POST /v1/mcp/servers/test
POST /v1/mcp/servers
The test endpoint can connect and discover tools, while the create/update endpoint rejects the same URL because it resolves to a private IP.
Environment
- GoClaw dashboard:
http://localhost:18790
- GoClaw running in WSL/Linux
- MCP server: Windows-hosted Streamable HTTP server
- MCP health endpoint:
http://127.0.0.1:8765/health
- MCP endpoint attempted from GoClaw:
http://host.docker.internal:8765/mcp
http://10.18.231.2:8765/mcp
Credentials/tokens are intentionally omitted.
Reproduction Steps
-
Start a Streamable HTTP MCP server on the host machine, listening on port 8765.
-
Verify health:
GET http://127.0.0.1:8765/health
=> 200 {"ok":true,"server":"windows-host-mcp"}
-
In GoClaw, test the MCP server with:
{
"transport": "streamable-http",
"url": "http://host.docker.internal:8765/mcp",
"headers": {
"Authorization": "Bearer [REDACTED]"
}
}
-
Observe that the test succeeds:
{
"success": true,
"tool_count": 9
}
-
Try to save/create the MCP server using the same transport, URL, and headers.
Actual Result
Saving fails with SSRF validation errors.
For host.docker.internal:
invalid URL: URL validation failed: ssrf: "host.docker.internal" resolved to blocked IP 10.18.231.2
For direct private IP:
invalid URL: URL validation failed: ssrf: IP 10.18.231.2 is in a blocked range
Expected Result
Either:
- The test and save flows should behave consistently. If the URL is not allowed to be saved, the test endpoint should fail with the same validation error.
or preferably for self-hosted/local development:
- GoClaw should provide a safe, explicit allowlist mechanism for MCP server URLs that are intentionally local/private, such as:
localhost
127.0.0.1
host.docker.internal
- WSL gateway IPs
- Docker bridge IPs
- LAN/private network hosts
Source-Level Root Cause
From the GoClaw source:
Test connection path
internal/http/mcp_tools.go
handleTestConnection calls MCP tool discovery directly via mcpbridge.DiscoverTools(...).
It does not call the same server config URL validator used by create/update.
Result: local/private URLs can pass the test flow and discover tools.
Create/update path
internal/http/mcp.go
handleCreateServer / handleUpdateServer call:
mcp.ValidateServerConfig(srv.Transport, srv.Command, args, srv.URL)
URL validation path
internal/mcp/validation.go
For sse and streamable-http, ValidateServerConfig validates the URL and calls:
security.Validate(rawURL)
SSRF validator
internal/security/ssrf.go
The SSRF validator blocks loopback, link-local, private RFC1918, multicast, and unspecified CIDR ranges, including:
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.0.0/16
fc00::/7
Because host.docker.internal resolves to 10.18.231.2 in this setup, create/update rejects it even though runtime tool discovery succeeds.
Why This Matters
MCP servers are often intentionally local:
- Desktop automation MCP servers
- Browser/filesystem/OS-control MCP servers
- Local coding/dev tools
- Docker/WSL-hosted tooling
- Internal/private network tools
Blocking private addresses by default is a reasonable security posture, but without an allowlist it breaks common self-hosted MCP workflows.
The inconsistent test-vs-save behavior is especially confusing because the user sees “test connection successful” and tool discovery succeeds, then save fails for the same config.
Suggested Fix
Add an explicit MCP URL allowlist / local network policy rather than disabling SSRF globally.
Option A: MCP-specific allowed hosts config
Example env var:
GOCLAW_MCP_ALLOWED_HOSTS=host.docker.internal,localhost,127.0.0.1,10.18.231.2
Or structured config:
{
"mcp": {
"allowed_private_hosts": [
"host.docker.internal",
"127.0.0.1",
"localhost",
"10.18.231.2"
]
}
}
Then in internal/mcp/validation.go, allow these hosts only for MCP sse / streamable-http transports before or around security.Validate(rawURL).
Option B: Explicit private MCP URL toggle
Example:
GOCLAW_MCP_ALLOW_PRIVATE_URLS=true
This is simpler but less safe than a host allowlist.
If implemented, it should be scoped only to MCP server URLs, not all SSRF validation in GoClaw.
Option C: Make test endpoint use the same validation
Even if private/local MCP URLs remain blocked by default, POST /v1/mcp/servers/test should run the same validation as create/update and return the same error.
This avoids false-positive “test successful” results.
Best UX would combine Option A + Option C:
- Default secure SSRF validation.
- Explicit allowlist for self-hosted/local MCP use cases.
- Test and save paths share the same validation behavior.
Temporary Local Workaround
The MCP server can be inserted directly into the mcp_servers table after confirming the test endpoint can discover tools.
Example saved config, with secrets redacted:
name: win-mcp
display_name: Windows MCP
transport: streamable-http
url: http://host.docker.internal:8765/mcp
tool_prefix: win
timeout_sec: 60
enabled: true
headers: {"Authorization":"Bearer [REDACTED]"}
settings: {"require_user_credentials": false}
This is only a local workaround. It bypasses API create validation and should not be considered a product-level fix.
Additional Note
When using a direct IP like:
http://10.18.231.2:8765/mcp
the MCP server itself may reject the request with:
That is separate from the GoClaw save failure.
In my local setup this was resolved by adding the direct IP to the MCP server's allowed hosts/origins.
The main GoClaw issue remains: create/update rejects the URL before runtime connection because the resolved IP is private.
GoClaw MCP test succeeds but save fails for local/private MCP URLs due to SSRF validation mismatch
Summary
When adding a Streamable HTTP MCP server hosted on a local/private network address, GoClaw can successfully test the connection and discover tools, but saving the same MCP server fails with SSRF validation errors.
This appears to be a validation mismatch between:
POST /v1/mcp/servers/testPOST /v1/mcp/serversThe test endpoint can connect and discover tools, while the create/update endpoint rejects the same URL because it resolves to a private IP.
Environment
http://localhost:18790http://127.0.0.1:8765/healthhttp://host.docker.internal:8765/mcphttp://10.18.231.2:8765/mcpCredentials/tokens are intentionally omitted.
Reproduction Steps
Start a Streamable HTTP MCP server on the host machine, listening on port
8765.Verify health:
In GoClaw, test the MCP server with:
{ "transport": "streamable-http", "url": "http://host.docker.internal:8765/mcp", "headers": { "Authorization": "Bearer [REDACTED]" } }Observe that the test succeeds:
{ "success": true, "tool_count": 9 }Try to save/create the MCP server using the same transport, URL, and headers.
Actual Result
Saving fails with SSRF validation errors.
For
host.docker.internal:For direct private IP:
Expected Result
Either:
or preferably for self-hosted/local development:
localhost127.0.0.1host.docker.internalSource-Level Root Cause
From the GoClaw source:
Test connection path
internal/http/mcp_tools.gohandleTestConnectioncalls MCP tool discovery directly viamcpbridge.DiscoverTools(...).It does not call the same server config URL validator used by create/update.
Result: local/private URLs can pass the test flow and discover tools.
Create/update path
internal/http/mcp.gohandleCreateServer/handleUpdateServercall:URL validation path
internal/mcp/validation.goFor
sseandstreamable-http,ValidateServerConfigvalidates the URL and calls:SSRF validator
internal/security/ssrf.goThe SSRF validator blocks loopback, link-local, private RFC1918, multicast, and unspecified CIDR ranges, including:
Because
host.docker.internalresolves to10.18.231.2in this setup, create/update rejects it even though runtime tool discovery succeeds.Why This Matters
MCP servers are often intentionally local:
Blocking private addresses by default is a reasonable security posture, but without an allowlist it breaks common self-hosted MCP workflows.
The inconsistent test-vs-save behavior is especially confusing because the user sees “test connection successful” and tool discovery succeeds, then save fails for the same config.
Suggested Fix
Add an explicit MCP URL allowlist / local network policy rather than disabling SSRF globally.
Option A: MCP-specific allowed hosts config
Example env var:
Or structured config:
{ "mcp": { "allowed_private_hosts": [ "host.docker.internal", "127.0.0.1", "localhost", "10.18.231.2" ] } }Then in
internal/mcp/validation.go, allow these hosts only for MCPsse/streamable-httptransports before or aroundsecurity.Validate(rawURL).Option B: Explicit private MCP URL toggle
Example:
This is simpler but less safe than a host allowlist.
If implemented, it should be scoped only to MCP server URLs, not all SSRF validation in GoClaw.
Option C: Make test endpoint use the same validation
Even if private/local MCP URLs remain blocked by default,
POST /v1/mcp/servers/testshould run the same validation as create/update and return the same error.This avoids false-positive “test successful” results.
Best UX would combine Option A + Option C:
Temporary Local Workaround
The MCP server can be inserted directly into the
mcp_serverstable after confirming the test endpoint can discover tools.Example saved config, with secrets redacted:
This is only a local workaround. It bypasses API create validation and should not be considered a product-level fix.
Additional Note
When using a direct IP like:
the MCP server itself may reject the request with:
That is separate from the GoClaw save failure.
In my local setup this was resolved by adding the direct IP to the MCP server's allowed hosts/origins.
The main GoClaw issue remains: create/update rejects the URL before runtime connection because the resolved IP is private.