Skip to content

GoClaw MCP test succeeds but save fails for local/private MCP URLs due to SSRF validation mismatch #1070

Description

@iamkokono

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

  1. Start a Streamable HTTP MCP server on the host machine, listening on port 8765.

  2. Verify health:

    GET http://127.0.0.1:8765/health
    => 200 {"ok":true,"server":"windows-host-mcp"}
    
  3. In GoClaw, test the MCP server with:

    {
      "transport": "streamable-http",
      "url": "http://host.docker.internal:8765/mcp",
      "headers": {
        "Authorization": "Bearer [REDACTED]"
      }
    }
  4. Observe that the test succeeds:

    {
      "success": true,
      "tool_count": 9
    }
  5. 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:

  1. 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:

  1. 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:

421 Invalid Host header

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2-mediumFunctional bug, UX broken — prioritized backlogagent:github-maintainProcessed by github-maintain automationarea:mcpModel Context Protocol bridge/serverarea:securitySecurity, injection, permissionsbugSomething isn't workingmaintain:triagedTriaged by maintain workflow

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions