You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(mcp): auto-wire MCP gateway into openclaw via device-signed WS RPC (#18)
Replace the broken file-based MCP config injection with a proper
gateway WebSocket RPC flow. The old approach wrote mcp-servers.json
to a shared volume and called "openclaw mcp reload" via docker exec,
but modern openclaw stores MCP server config under mcp.servers in
openclaw.json and has removed the reload CLI command.
New flow:
- At sluice startup, after env injection and secrets reload both
succeed, the startup goroutine invokes an embedded Node.js script
via `docker exec openclaw node -e <script> wire-mcp sluice <url>`.
- The script performs the full openclaw gateway handshake: reads
device identity (ed25519 key + deviceId) from
~/.openclaw/identity/device.json, connects to ws://127.0.0.1:port/,
receives connect.challenge, builds the v3 device auth payload
(pipe-separated deviceId|clientId|clientMode|role|scopes|signedAt|
token|nonce|platform|deviceFamily with ASCII-lowercased metadata),
signs it, and sends a connect request claiming operator.admin scope.
- On hello-ok, it chains config.get (to fetch the current baseHash)
-> config.patch with restartDelayMs:3000 and raw JSON merge-patching
mcp.servers.<name> = {url}. Idempotent: a second run with the same
URL is a noop on the openclaw side.
- The script also handles standalone secrets.reload and direct
config.patch invocations, replacing the previous inline Node one-
liner.
Key pieces:
- internal/container/gateway_rpc.js: the embedded Node script,
loaded via //go:embed and passed to `node -e`. Argv auto-detects
between file-mode and -e-mode invocation.
- internal/container/gateway_rpc.go: GatewayRPCNodeCommand helper
that builds the argv slice.
- ContainerManager.WireMCPGateway(ctx, name, sluiceURL): new method
implemented by Docker, Apple Container, and tart backends.
- cmd/sluice/main.go: Phase 3 of the startup goroutine calls
WireMCPGateway after secrets reload. Exit code 137 is tolerated
(the config.patch response was received; the docker exec was then
killed by the subsequent gateway restart).
Cleanup:
- Remove InjectMCPConfig and WriteMCPConfig helpers entirely.
- Drop --mcp-dir flag, SLUICE_MCP_DIR env var, writeMCPServersJSON
helper, and all associated tests.
- Remove sluice-mcp volume from compose files.
- Remove /home/sluice/mcp directory from Dockerfile.
- compose.yml: pin sluice IP on the internal network to 172.30.0.2
and add extra_hosts ["sluice:172.30.0.2"] on tun2proxy so OpenClaw
(sharing tun2proxy's namespace) can resolve "sluice" via /etc/hosts
without Docker's embedded DNS (unreachable through the TUN).
Also:
- internal/mcp/server_http.go: handle GET on /mcp as an SSE stream
with keepalive comments. openclaw's bundle-mcp probes with GET
and logged 405 warnings; now returns 200 with text/event-stream.
CLAUDE.md rewritten with the new one-command MCP setup flow.
Copy file name to clipboardExpand all lines: CLAUDE.md
+15-3Lines changed: 15 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -86,7 +86,19 @@ When `--destination` is provided, `sluice cred add` also creates an allow rule a
86
86
87
87
Two credential types: `static` (default) for API keys and `oauth` for OAuth access/refresh token pairs. OAuth credentials prompt for tokens via stdin (not CLI flags) to avoid shell history exposure.
88
88
89
-
Runtime flags: `--mcp-dir` (or `SLUICE_MCP_DIR` env var) sets the shared volume path for `mcp-servers.json` auto-injection. Defaults to the MCP volume path in compose files (`/home/sluice/mcp`).
89
+
Runtime flags: `--mcp-base-url` sets the external URL the agent uses to reach sluice's MCP gateway (e.g. `http://sluice:3000`). This is added to `SelfBypass` so sluice does not policy-check its own MCP traffic. Defaults to deriving from `--health-addr`.
90
+
91
+
## MCP Gateway Setup
92
+
93
+
OpenClaw connects to sluice's MCP gateway via Streamable HTTP. This is a one-time setup per deployment:
94
+
95
+
```bash
96
+
docker exec openclaw openclaw mcp set sluice '{"url":"http://sluice:3000/mcp"}'
97
+
```
98
+
99
+
For the hostname `sluice` to resolve inside OpenClaw, the compose file pins sluice's IP on the internal network (172.30.0.2) and adds an `extra_hosts` entry on tun2proxy (which OpenClaw shares). Docker's embedded DNS (127.0.0.11) is not reachable from OpenClaw because its DNS is routed through the TUN device. The `/etc/hosts` entry bypasses DNS entirely.
100
+
101
+
When new MCP upstreams are added to sluice via `sluice mcp add`, restart sluice so the gateway picks them up. OpenClaw does not need to be restarted - its connection to sluice:3000/mcp remains valid and it re-queries the tool list on subsequent agent runs.
`MCPHTTPHandler` serves `POST /mcp` and `DELETE /mcp` on port 3000 (alongside `/healthz`). Session tracking via `Mcp-Session-Id` header. SSE response support.
176
188
177
-
Auto-injection: when container runtime active, writes `mcp-servers.json` to shared MCP volume (`sluice-mcp`), signals agent to reload. SOCKS5 auto-bypasses connections to sluice's own listeners (`SelfBypass`).
189
+
Agent connection: OpenClaw is configured once (via `openclaw mcp set`) to connect to `http://sluice:3000/mcp`. Sluice's `SelfBypass`auto-allows connections to its own MCP listener so the traffic is not policy-checked.
178
190
179
191
### Vault providers
180
192
@@ -196,7 +208,7 @@ Chain provider: `providers = ["1password", "age"]` tries in order, first hit win
196
208
197
209
All backends implement `ContainerManager` interface (`internal/container/types.go`).
198
210
199
-
**Docker**: Three-container compose (sluice + tun2proxy + openclaw). Hot-reload via `docker exec` env var injection into `~/.openclaw/.env` + `docker exec openclaw openclaw secrets reload`. MCP config shared via `sluice-mcp` volume. Fallback: container restart.
211
+
**Docker**: Three-container compose (sluice + tun2proxy + openclaw). Hot-reload via `docker exec` env var injection into `~/.openclaw/.env` + `docker exec openclaw openclaw secrets reload`. MCP wiring is a one-time `openclaw mcp set` (see "MCP Gateway Setup" above). Fallback: container restart.
200
212
201
213
**Apple Container**: Linux micro-VMs via Virtualization.framework. tun2proxy runs on host. `NetworkRouter` manages pf anchor rules to redirect VM bridge traffic. VirtioFS for shared volumes.
certDir:=flag.String("cert-dir", "", "shared volume path for CA certificate (enables MITM trust injection into guest)")
84
84
dnsResolver:=flag.String("dns-resolver", "", "upstream DNS resolver address for DNS interception (default: 8.8.8.8:53)")
85
-
autoInjectMCP:=flag.Bool("auto-inject-mcp", false, "auto-inject MCP config into agent container (default true for docker/apple runtimes)")
86
-
mcpBaseURL:=flag.String("mcp-base-url", "", "base URL for auto-injected MCP config (e.g. http://sluice:3000); derived from --health-addr when empty")
87
-
mcpDir:=flag.String("mcp-dir", "", "shared volume path for MCP config (mcp-servers.json); enables MCP auto-injection into agent container")
88
-
autoInjectMCPSet:=false
85
+
mcpBaseURL:=flag.String("mcp-base-url", "", "external base URL the agent uses to reach sluice's MCP gateway (e.g. http://sluice:3000); added to SelfBypass so sluice does not policy-check its own MCP traffic")
89
86
flag.Parse()
90
87
91
-
// Track whether the flag was explicitly set by the user.
0 commit comments