Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
When attempting to use openclaw mcp serve with the gateway auth token, the gateway repeatedly requests scope upgrade approval. Approving each request rotates the device's operator token in a way that appears to replace its scope set rather than augment it, leaving the device unable to perform operations it could before. Each subsequent CLI invocation generates a fresh pending request that supersedes any previously-approved one. The result is a recursive bootstrap problem with no apparent escape path.
Steps to reproduce
1. Healthy CLI baseline: devices list returns auth wall
openclaw devices list
→ "unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)"
(despite gateway.auth.token and gateway.remote.token being verified IDENTICAL via jq)
2. Bypass with explicit gateway auth token: works
openclaw devices list --token "$(jq -r .gateway.auth.token /root/.openclaw/openclaw.json)"
→ returns paired device list cleanly
3. Attempt mcp serve via supergateway (or directly):
supergateway --stdio "openclaw mcp serve --token-file /path/to/auth-token" --port 5005
→ child stderr: "scope upgrade pending approval (requestId: )"
→ child exits code=1
4. Approve the pending request using the bypass token:
openclaw devices approve --token "$(cat /path/to/auth-token)"
→ "Approved ()"
5. Re-run mcp serve:
supergateway --stdio "openclaw mcp serve --token-file /path/to/auth-token" --port 5005
→ child stderr: "scope upgrade pending approval (requestId: )"
(new UUID, not the one we just approved)
6. Try to approve UUID-B:
openclaw devices approve --token "$(cat /path/to/auth-token)"
→ sometimes "unknown requestId" (UUID-B already cleared from pending)
→ sometimes succeeds, but then step 5 produces UUID-C, ad infinitum
Expected behavior
Not a regression: behaviour is identical on 2026.5.2 (8b2a6e5) and 2026.5.6 (c97b9f7). No earlier version was tested on this host, so cannot identify a last-known-good.
Local-fallback path observation: when openclaw devices approve --token is run, the response includes Direct scope access failed; using local fallback. followed by a successful Approved line. This suggests the CLI has an intentional bootstrap path that writes to paired.json directly when WebSocket auth fails. This fallback works for approve but does not address the underlying loop because the next mcp serve invocation immediately generates a new pending request rather than reusing the just-approved one.
Scope set requested by mcp serve: the originally-pending request (Zn5xInl6... token in pending.json from a prior session) was for roles [node, operator] and scopes [operator.approvals, operator.read, operator.talk.secrets, operator.write]. This implies mcp serve requires these four scopes in addition to whatever the device already has — but each approval cycle appears to produce a token carrying only the most-recently-approved subset, never the full union.
Suggested investigation points:
The token-rotation path triggered by devices approve (file: dist/call-CGGbETeo.js near line 240): does it union or replace scopes on the operator token?
Pending-request lifecycle: when is an entry removed from pending.json, and is there an idempotency check before creating a new one?
Whether gateway.auth.token is intended as a master token that bypasses scope checks (it works for --token flag overrides) and whether mcp serve should accept it directly via --token-file to skip device-scope evaluation.
Actual behavior
openclaw mcp serve cannot maintain a connection to the local gateway. Each invocation produces a fresh scope upgrade request that supersedes any previous approval, creating an infinite loop with no terminating state.
Step by step:
openclaw mcp serve (or mcp serve --token-file ) starts, completes the MCP initialize handshake successfully over stdio, advertises capabilities (claude/channel, claude/channel/permission, tools.listChanged), and returns the expected nine MCP tools when queried.
Once the subprocess attempts to connect to the gateway WebSocket at ws://127.0.0.1:18789 for actual tool execution, the gateway responds with unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token) and closes the connection with code 1008.
The error message is misleading: gateway.auth.token and gateway.remote.token are verified IDENTICAL via direct jq comparison. The actual cause is insufficient scope on the device's operator token, not a literal token mismatch.
Simultaneously, a new pending scope upgrade request is added to pending.json requesting operator.write, operator.approvals, operator.read, and operator.talk.secrets. The subprocess exits with code 1.
Approving the pending request via openclaw devices approve --token <gateway.auth.token> succeeds — the response indicates Direct scope access failed; using local fallback. Approved ().
Inspection of paired.json after approval shows the device's top-level scopes and approvedScopes arrays correctly accumulate the new scope, but the rotated operator token's own scopes array contains only the most-recently-approved scope, dropping any prior scopes.
Re-running openclaw mcp serve immediately produces a new pending request with a different UUID, requesting the same scopes, and fails the same way.
Attempting to approve the previous UUID after the next attempt has fired returns unknown requestId — the previous pending entry has been replaced rather than persisted.
This loop is reproducible indefinitely. CLI commands that bypass device-scope evaluation by passing --token <gateway.auth.token> explicitly (e.g. openclaw devices list --token ...) work correctly. openclaw mcp serve does not have an analogous bypass that survives the gateway connection.
The end result: there is no command sequence currently available that leaves openclaw mcp serve in a state where it can connect to the gateway and serve MCP requests, without manual editing of paired.json on a production system.
OpenClaw version
OpenClaw versions tested: 2026.5.2 and 2026.5.6 (identical behaviour on both)
Operating system
OS: Ubuntu 24.04.4 LTS (Noble), kernel 6.8.0-110-generic
Install method
Node: v22.22.2
Model
NOT_ENOUGH_INFO
Provider / routing chain
[CLI / supergateway] → openclaw mcp serve (stdio subprocess) → ws://127.0.0.1:18789 (local loopback gateway) → gateway.auth.mode = "token"
Additional provider/model setup details
Single-node OpenClaw install on a BinaryLane VPS. Gateway runs as a systemd --user service under the root user. No Tailscale exposure. nginx reverse-proxies the dashboard publicly via api.ammacatize.ai, but the failing commands all use the local loopback path.Two paired devices observed in paired.json (both operator role). The CLI device under test is d23692a9...616348, currently with approved scopes [operator.read, operator.pairing] but with its rotated operator token carrying only [operator.pairing].gateway.auth.token and gateway.remote.token were verified IDENTICAL via jq direct comparison (.gateway.auth.token == .gateway.remote.token).No agent overrides relevant to the failing path. Per-agent model routing is irrelevant here because the failure is in the gateway-auth handshake, not in agent execution.OPENCLAW_STATE_DIR and related env vars are at defaults (/root/.openclaw/). No active container/sandbox profile (--container not in use). No dev profile (--dev not in use).
Logs, screenshots, and evidence
$ openclaw devices list
gateway connect failed: GatewayClientRequestError:
unauthorized: gateway token mismatch
(set gateway.remote.token to match gateway.auth.token)
[openclaw] Failed to start CLI: GatewayTransportError:
gateway closed (1008): unauthorized: gateway token mismatch
Gateway target: ws://127.0.0.1:18789
Source: local loopback
Bind: lan
at createGatewayCloseTransportError
(file:///usr/lib/node_modules/openclaw/dist/call-CGGbETeo.js:240:9)
at Object.onClose
(file:///usr/lib/node_modules/openclaw/dist/call-CGGbETeo.js:324:10)
$ jq -r 'if .gateway.auth.token == .gateway.remote.token
then "IDENTICAL" else "DIFFERENT" end' \
/root/.openclaw/openclaw.json
IDENTICAL
$ openclaw devices list --token "$(jq -r .gateway.auth.token /root/.openclaw/openclaw.json)"
Paired (2)
┌─────────────────────┬──────────┬──────────────────────────┬──────────┐
│ Device │ Roles │ Scopes │ Tokens │
├─────────────────────┼──────────┼──────────────────────────┼──────────┤
│ d23692a9...616348 │ operator │ operator.read, pairing │ operator │
│ 84b656a5...01389cf8 │ operator │ operator.read │ operator │
[supergateway] Listening on port 5005
[supergateway] SSE endpoint: http://localhost:5005/sse
[supergateway] Child stderr: gateway connect failed:
GatewayClientRequestError: scope upgrade pending approval
(requestId: 1cad8b20-4170-4aef-a18a-1003ba59cfba)
[supergateway] Child exited: code=1, signal=null
$ openclaw devices approve <UUID> --token "$(cat ./auth-token)"
Direct scope access failed; using local fallback.
Approved <device-id> (<UUID>)
{
"scopes": ["operator.read", "operator.pairing"],
"approvedScopes": ["operator.read", "operator.pairing"],
"tokens": {
"operator": {
"token": "<redacted>",
"role": "operator",
"scopes": ["operator.pairing"],
"createdAtMs": 1776589292105,
"rotatedAtMs": 1778120004946
}
}
}
Impact and severity
Affected users/systems/channels: Single-host installations attempting to expose openclaw mcp serve over a stdio→HTTP/SSE bridge (e.g. supergateway) for consumption by remote MCP clients such as Claude.ai's custom-connector dialog or Claude Desktop's native MCP integration. Telegram, dashboard, and existing agent fleet operations are unaffected — they continue working normally.
Severity: Blocks workflow. The intended use case (executive-oversight integration where a Claude client consumes OpenClaw's MCP tools) cannot be completed. No data loss, no degradation of existing channels.
Frequency: Always reproducible. Every openclaw mcp serve invocation on this host generates a new pending scope upgrade request and fails with Child exited: code=1. Approving the request via the --token bypass succeeds at the JSON-write level but does not unblock the next invocation.
Consequence: Cannot expose OpenClaw's MCP tool surface to external Claude clients via the supported mcp serve path. Forced workaround would be either (a) manual edit of paired.json on a production system (not acceptable), (b) bypass the device-pairing layer entirely with custom integration code (defeats the purpose of using the supported MCP entry point), or (c) wait for a fix.
Additional information
Not a regression: behaviour is identical on 2026.5.2 (8b2a6e5) and 2026.5.6 (c97b9f7). No earlier version was tested on this host, so cannot identify a last-known-good.
Local-fallback path observation: when openclaw devices approve --token is run, the response includes Direct scope access failed; using local fallback. followed by a successful Approved line. This suggests the CLI has an intentional bootstrap path that writes to paired.json directly when WebSocket auth fails. This fallback works for approve but does not address the underlying loop because the next mcp serve invocation immediately generates a new pending request rather than reusing the just-approved one.
Scope set requested by mcp serve: the originally-pending request (Zn5xInl6... token in pending.json from a prior session) was for roles [node, operator] and scopes [operator.approvals, operator.read, operator.talk.secrets, operator.write]. This implies mcp serve requires these four scopes in addition to whatever the device already has — but each approval cycle appears to produce a token carrying only the most-recently-approved subset, never the full union.
Suggested investigation points:
The token-rotation path triggered by devices approve (file: dist/call-CGGbETeo.js near line 240): does it union or replace scopes on the operator token?
Pending-request lifecycle: when is an entry removed from pending.json, and is there an idempotency check before creating a new one?
Whether gateway.auth.token is intended as a master token that bypasses scope checks (it works for --token flag overrides) and whether mcp serve should accept it directly via --token-file to skip device-scope evaluation.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
When attempting to use openclaw mcp serve with the gateway auth token, the gateway repeatedly requests scope upgrade approval. Approving each request rotates the device's operator token in a way that appears to replace its scope set rather than augment it, leaving the device unable to perform operations it could before. Each subsequent CLI invocation generates a fresh pending request that supersedes any previously-approved one. The result is a recursive bootstrap problem with no apparent escape path.
Steps to reproduce
1. Healthy CLI baseline: devices list returns auth wall
openclaw devices list
→ "unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)"
(despite gateway.auth.token and gateway.remote.token being verified IDENTICAL via jq)
2. Bypass with explicit gateway auth token: works
openclaw devices list --token "$(jq -r .gateway.auth.token /root/.openclaw/openclaw.json)"
→ returns paired device list cleanly
3. Attempt mcp serve via supergateway (or directly):
supergateway --stdio "openclaw mcp serve --token-file /path/to/auth-token" --port 5005
→ child stderr: "scope upgrade pending approval (requestId: )"
→ child exits code=1
4. Approve the pending request using the bypass token:
openclaw devices approve --token "$(cat /path/to/auth-token)"
→ "Approved ()"
5. Re-run mcp serve:
supergateway --stdio "openclaw mcp serve --token-file /path/to/auth-token" --port 5005
→ child stderr: "scope upgrade pending approval (requestId: )"
(new UUID, not the one we just approved)
6. Try to approve UUID-B:
openclaw devices approve --token "$(cat /path/to/auth-token)"
→ sometimes "unknown requestId" (UUID-B already cleared from pending)
→ sometimes succeeds, but then step 5 produces UUID-C, ad infinitum
Expected behavior
Not a regression: behaviour is identical on 2026.5.2 (8b2a6e5) and 2026.5.6 (c97b9f7). No earlier version was tested on this host, so cannot identify a last-known-good.
Local-fallback path observation: when openclaw devices approve --token is run, the response includes Direct scope access failed; using local fallback. followed by a successful Approved line. This suggests the CLI has an intentional bootstrap path that writes to paired.json directly when WebSocket auth fails. This fallback works for approve but does not address the underlying loop because the next mcp serve invocation immediately generates a new pending request rather than reusing the just-approved one.
Scope set requested by mcp serve: the originally-pending request (Zn5xInl6... token in pending.json from a prior session) was for roles [node, operator] and scopes [operator.approvals, operator.read, operator.talk.secrets, operator.write]. This implies mcp serve requires these four scopes in addition to whatever the device already has — but each approval cycle appears to produce a token carrying only the most-recently-approved subset, never the full union.
Suggested investigation points:
The token-rotation path triggered by devices approve (file: dist/call-CGGbETeo.js near line 240): does it union or replace scopes on the operator token?
Pending-request lifecycle: when is an entry removed from pending.json, and is there an idempotency check before creating a new one?
Whether gateway.auth.token is intended as a master token that bypasses scope checks (it works for --token flag overrides) and whether mcp serve should accept it directly via --token-file to skip device-scope evaluation.
Actual behavior
openclaw mcp serve cannot maintain a connection to the local gateway. Each invocation produces a fresh scope upgrade request that supersedes any previous approval, creating an infinite loop with no terminating state.
Step by step:
openclaw mcp serve (or mcp serve --token-file ) starts, completes the MCP initialize handshake successfully over stdio, advertises capabilities (claude/channel, claude/channel/permission, tools.listChanged), and returns the expected nine MCP tools when queried.
Once the subprocess attempts to connect to the gateway WebSocket at ws://127.0.0.1:18789 for actual tool execution, the gateway responds with unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token) and closes the connection with code 1008.
The error message is misleading: gateway.auth.token and gateway.remote.token are verified IDENTICAL via direct jq comparison. The actual cause is insufficient scope on the device's operator token, not a literal token mismatch.
Simultaneously, a new pending scope upgrade request is added to pending.json requesting operator.write, operator.approvals, operator.read, and operator.talk.secrets. The subprocess exits with code 1.
Approving the pending request via openclaw devices approve --token <gateway.auth.token> succeeds — the response indicates Direct scope access failed; using local fallback. Approved ().
Inspection of paired.json after approval shows the device's top-level scopes and approvedScopes arrays correctly accumulate the new scope, but the rotated operator token's own scopes array contains only the most-recently-approved scope, dropping any prior scopes.
Re-running openclaw mcp serve immediately produces a new pending request with a different UUID, requesting the same scopes, and fails the same way.
Attempting to approve the previous UUID after the next attempt has fired returns unknown requestId — the previous pending entry has been replaced rather than persisted.
This loop is reproducible indefinitely. CLI commands that bypass device-scope evaluation by passing --token <gateway.auth.token> explicitly (e.g. openclaw devices list --token ...) work correctly. openclaw mcp serve does not have an analogous bypass that survives the gateway connection.
The end result: there is no command sequence currently available that leaves openclaw mcp serve in a state where it can connect to the gateway and serve MCP requests, without manual editing of paired.json on a production system.
OpenClaw version
OpenClaw versions tested: 2026.5.2 and 2026.5.6 (identical behaviour on both)
Operating system
OS: Ubuntu 24.04.4 LTS (Noble), kernel 6.8.0-110-generic
Install method
Node: v22.22.2
Model
NOT_ENOUGH_INFO
Provider / routing chain
[CLI / supergateway] → openclaw mcp serve (stdio subprocess) → ws://127.0.0.1:18789 (local loopback gateway) → gateway.auth.mode = "token"
Additional provider/model setup details
Single-node OpenClaw install on a BinaryLane VPS. Gateway runs as a systemd --user service under the root user. No Tailscale exposure. nginx reverse-proxies the dashboard publicly via api.ammacatize.ai, but the failing commands all use the local loopback path.Two paired devices observed in paired.json (both operator role). The CLI device under test is d23692a9...616348, currently with approved scopes [operator.read, operator.pairing] but with its rotated operator token carrying only [operator.pairing].gateway.auth.token and gateway.remote.token were verified IDENTICAL via jq direct comparison (.gateway.auth.token == .gateway.remote.token).No agent overrides relevant to the failing path. Per-agent model routing is irrelevant here because the failure is in the gateway-auth handshake, not in agent execution.OPENCLAW_STATE_DIR and related env vars are at defaults (/root/.openclaw/). No active container/sandbox profile (--container not in use). No dev profile (--dev not in use).
Logs, screenshots, and evidence
Impact and severity
Affected users/systems/channels: Single-host installations attempting to expose openclaw mcp serve over a stdio→HTTP/SSE bridge (e.g. supergateway) for consumption by remote MCP clients such as Claude.ai's custom-connector dialog or Claude Desktop's native MCP integration. Telegram, dashboard, and existing agent fleet operations are unaffected — they continue working normally.
Severity: Blocks workflow. The intended use case (executive-oversight integration where a Claude client consumes OpenClaw's MCP tools) cannot be completed. No data loss, no degradation of existing channels.
Frequency: Always reproducible. Every openclaw mcp serve invocation on this host generates a new pending scope upgrade request and fails with Child exited: code=1. Approving the request via the --token bypass succeeds at the JSON-write level but does not unblock the next invocation.
Consequence: Cannot expose OpenClaw's MCP tool surface to external Claude clients via the supported mcp serve path. Forced workaround would be either (a) manual edit of paired.json on a production system (not acceptable), (b) bypass the device-pairing layer entirely with custom integration code (defeats the purpose of using the supported MCP entry point), or (c) wait for a fix.
Additional information
Not a regression: behaviour is identical on 2026.5.2 (8b2a6e5) and 2026.5.6 (c97b9f7). No earlier version was tested on this host, so cannot identify a last-known-good.
Local-fallback path observation: when openclaw devices approve --token is run, the response includes Direct scope access failed; using local fallback. followed by a successful Approved line. This suggests the CLI has an intentional bootstrap path that writes to paired.json directly when WebSocket auth fails. This fallback works for approve but does not address the underlying loop because the next mcp serve invocation immediately generates a new pending request rather than reusing the just-approved one.
Scope set requested by mcp serve: the originally-pending request (Zn5xInl6... token in pending.json from a prior session) was for roles [node, operator] and scopes [operator.approvals, operator.read, operator.talk.secrets, operator.write]. This implies mcp serve requires these four scopes in addition to whatever the device already has — but each approval cycle appears to produce a token carrying only the most-recently-approved subset, never the full union.
Suggested investigation points:
The token-rotation path triggered by devices approve (file: dist/call-CGGbETeo.js near line 240): does it union or replace scopes on the operator token?
Pending-request lifecycle: when is an entry removed from pending.json, and is there an idempotency check before creating a new one?
Whether gateway.auth.token is intended as a master token that bypasses scope checks (it works for --token flag overrides) and whether mcp serve should accept it directly via --token-file to skip device-scope evaluation.