Skip to content

fix(mcp): harden MCP loopback server and scope CLI tool access to claude-cli#35676

Closed
mylukin wants to merge 13 commits into
openclaw:mainfrom
mylukin:pr/mcp-http-hardening
Closed

fix(mcp): harden MCP loopback server and scope CLI tool access to claude-cli#35676
mylukin wants to merge 13 commits into
openclaw:mainfrom
mylukin:pr/mcp-http-hardening

Conversation

@mylukin

@mylukin mylukin commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Problem

When Claude Code runs as a subprocess via OpenClaw's ACP runtime, it is an "isolated island" — it has access to its own built-in tools (file, bash, grep…) but cannot use any OpenClaw tools (message, cron, browser, nodes, etc.). This makes ACP-spawned agents significantly less capable than the main agent.

Current architecture (broken for ACP):

  User → OpenClaw Gateway → Main Agent (has all OpenClaw tools)
                          ↘ ACP subprocess (Claude Code)
                             └── Only has: read, write, edit, bash, grep
                             └── Missing: message, cron, browser, nodes, canvas...

With this PR:

  User → OpenClaw Gateway → Main Agent (has all OpenClaw tools)
                          ↘ ACP subprocess (Claude Code)
                             ├── Built-in: read, write, edit, bash, grep
                             └── MCP loopback → localhost:PORT+1 → OpenClaw tools
                                 (filtered by tool policy pipeline)

Additional issues fixed

  1. Gateway HTTP tool denylist missing: The gateway HTTP endpoint had no tool filtering, potentially exposing dangerous tools
  2. --mcp-config deduplication: Multiple --mcp-config flags could be passed to Claude CLI, causing config conflicts
  3. accountId not threaded: The CLI runner didn't pass accountId through to tool resolution, breaking account-scoped tools

Fix

  • New mcp-http.ts module (513 lines): Implements a full MCP-over-HTTP server (JSON-RPC 2.0) that:

    • Runs on gateway port + 1 (loopback only)
    • Serves OpenClaw tools to Claude Code via the MCP protocol
    • Filters tools through the same policy pipeline as the main agent
    • Excludes native CLI tools to avoid duplication (read, write, exec, etc.)
    • Applies DEFAULT_GATEWAY_HTTP_TOOL_DENY for security
    • Supports MCP protocol versions 2025-03-26 and 2024-11-05
  • CLI runner hardening:

  • Tests for both MCP server tool filtering and CLI runner integration

Why merge

Without MCP loopback, ACP-spawned agents (Claude Code subprocesses) cannot send messages, schedule crons, control browsers, or use any OpenClaw-specific tools. This is the critical bridge that enables full-featured ACP agent sessions. The tool policy filtering ensures security is maintained — agents only get tools their policy allows.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime agents Agent runtime and tooling size: L labels Mar 5, 2026
@greptile-apps

greptile-apps Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a dedicated MCP loopback HTTP server (src/gateway/mcp-http.ts) that scopes tool access for claude-cli sessions and adds structured logging for auditability. The server design (stateless JSON-RPC over plain HTTP on 127.0.0.1, TTL-based tool cache with config-snapshot invalidation, 1 MB body cap, union schema flattening for Anthropic compatibility) is solid and well-tested.

Two issues need attention before merging:

  • useResume guard removed for model arg (src/agents/cli-runner/helpers.ts line 359): The PR silently drops the !params.useResume guard from the modelArg branch while every other resume-sensitive argument (systemPromptArg, sessionArg) retains it. This is a cross-cutting behavioral change for all CLI backends on resume: if any backend's CLI rejects --model when resuming, all resume sessions will break.
  • OPENCLAW_MCP_ACCOUNT_ID hardcoded to "" (src/agents/cli-runner.ts line 319): The account ID env-var is always set to an empty string, so the x-openclaw-account-id header the CLI forwards to the MCP server is always empty. The server treats an empty string as undefined, meaning account-scoped group tool policies are never applied for CLI MCP calls, even when the session belongs to a named account.

Confidence Score: 2/5

  • Not safe to merge as-is — the unexplained removal of the useResume guard for model args is a potential regression for all resume sessions across all CLI backends.
  • The MCP server and test code are well-written, but the unexplained useResume guard removal in helpers.ts is a behavioral change that affects every CLI backend on resume, not just claude-cli. This is not documented in the PR description and directly contradicts the "No breaking changes" claim. Combined with the hardcoded empty account ID that silently disables account-scoped tool policies, there are two functional correctness issues that should be resolved before merging.
  • Pay close attention to src/agents/cli-runner/helpers.ts (model arg guard removal) and src/agents/cli-runner.ts (hardcoded empty account ID on line 319).

Last reviewed commit: 5d7ad6a

Comment thread src/agents/cli-runner/helpers.ts Outdated
Comment thread src/agents/cli-runner.ts Outdated
Comment thread src/agents/cli-runner.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d7ad6a490

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e8d025bae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts Outdated
@mylukin mylukin force-pushed the pr/mcp-http-hardening branch from a447347 to dba3343 Compare March 5, 2026 16:18
@openclaw-barnacle openclaw-barnacle Bot added the channel: mattermost Channel integration: mattermost label Mar 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dba33438e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts
Comment thread src/gateway/mcp-http.ts Outdated
@mylukin mylukin force-pushed the pr/mcp-http-hardening branch from dba3343 to 9c98253 Compare March 6, 2026 04:57
@openclaw-barnacle openclaw-barnacle Bot removed the channel: mattermost Channel integration: mattermost label Mar 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c98253a0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts
Comment thread src/agents/cli-runner.ts Outdated
@mylukin

mylukin commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

/review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 845ae6d4b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts
Comment on lines +262 to +267
if (mcpConfigPath && backendResolved.id === "claude-cli") {
if (!args.includes("--strict-mcp-config")) {
args.push("--strict-mcp-config");
}
if (!args.includes("--mcp-config")) {
args.push("--mcp-config", mcpConfigPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip MCP flags when loopback server is unavailable

startGatewayServer treats MCP loopback bind failures as non-fatal (src/gateway/server.impl.ts:868-873), but runCliAgent still unconditionally adds --strict-mcp-config/--mcp-config for Claude when it can write the config file. If port+1 is unavailable (for example already in use), Claude runs are still pointed at that endpoint, so OpenClaw MCP tools are silently unavailable or routed to the wrong local service instead of falling back cleanly. Gate the flag injection on confirmed MCP server availability (or disable MCP wiring after startup failure).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a6d467f15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts Outdated

return policyFiltered.filter(
(t) =>
!NATIVE_TOOL_EXCLUDE.has(t.name) && !DEFAULT_GATEWAY_HTTP_TOOL_DENY.includes(t.name as never),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor gateway tool overrides in MCP filtering

This filter hard-codes DEFAULT_GATEWAY_HTTP_TOOL_DENY and never applies gateway.tools.allow / gateway.tools.deny overrides, unlike the existing HTTP invoke path in src/gateway/tools-invoke-http.ts. As a result, operators who configured extra HTTP-denied tools can still expose them through the new MCP loopback endpoint, and operators who explicitly allow default-denied tools cannot re-enable them for MCP, creating policy drift between two gateway HTTP tool surfaces.

Useful? React with 👍 / 👎.

@aisle-research-bot

aisle-research-bot Bot commented Mar 7, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium MCP loopback server ignores session/account scoping headers, resolving tools under main session
2 🟡 Medium MCP loopback token can be leaked to attacker-controlled local process due to predictable port and fail-open startup
3 🔵 Low Symlink/race arbitrary file overwrite via predictable tmp file in ensureMcpConfigFile

1. 🟡 MCP loopback server ignores session/account scoping headers, resolving tools under main session

Property Value
Severity Medium
CWE CWE-285
Location src/gateway/mcp-http.ts:536-546

Description

In startMcpLoopbackServer, the MCP HTTP handler does not use client-provided scoping headers (e.g. x-session-key, x-openclaw-account-id). Instead it always resolves tools using the main session key and forces accountId to undefined.

This creates an access-control / policy-scoping bypass because resolveFilteredTools() uses sessionKey, messageProvider, and accountId to apply:

  • resolveEffectiveToolPolicy({ sessionKey }) (agent/global/profile/provider policy selection)
  • resolveGroupToolPolicy({ sessionKey, messageProvider, accountId }) (group/account-scoped tool policy)
  • resolveSubagentToolPolicy() gated by isSubagentSessionKey(sessionKey)

By hard-coding sessionKey to resolveMainSessionKey(cfg) and dropping accountId, any caller that can present the loopback bearer token can:

  • Obtain the main session’s tool set even if it intended/should be restricted to a subagent or different session
  • Bypass group/account-specific tool restrictions (since accountId is never passed)

Notably, ensureMcpConfigFile() writes an MCP config that includes x-session-key and x-openclaw-account-id headers, and mcp-http.test.ts expects these headers to affect policy resolution, but the server implementation ignores them—meaning tool policy scoping is effectively not enforced for MCP.

Vulnerable code:

const cfg = loadConfig();
const reqSessionKey = resolveMainSessionKey(cfg);
const messageProvider =
  normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined;
const accountId = undefined; // not overrideable via headers
const { tools, toolSchema } = getTools({
  cfg,
  sessionKey: reqSessionKey,
  messageProvider,
  accountId,
});

Recommendation

Use the request scoping headers when resolving tools, and validate them.

Minimum fix (respect headers):

const cfg = loadConfig();

const headerSessionKey = getHeader(req, "x-session-key")?.trim();
const sessionKey = headerSessionKey && headerSessionKey !== "main"
  ? headerSessionKey
  : resolveMainSessionKey(cfg);

const messageProvider =
  normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined;

const accountId = getHeader(req, "x-openclaw-account-id")?.trim() || undefined;

const { tools, toolSchema } = getTools({ cfg, sessionKey, messageProvider, accountId });

Hardening recommendations (to prevent confused-deputy / privilege escalation):

  • Bind authorization to scope: include sessionKey (and optionally accountId) in the token derivation (e.g., HMAC) or store per-session tokens, and reject mismatches.
  • Validate x-session-key format (and optionally confirm x-openclaw-agent-id matches the agent parsed from sessionKey).
  • Update/extend tests to ensure the handler actually passes sessionKey/accountId through to policy resolution and tool creation.

2. 🟡 MCP loopback token can be leaked to attacker-controlled local process due to predictable port and fail-open startup

Property Value
Severity Medium
CWE CWE-522
Location src/gateway/server.impl.ts:906-915

Description

The gateway starts an MCP loopback HTTP server on a predictable port (gatewayPort + 1) but does not fail closed when binding that port fails.

If another local process binds the MCP port first (intentional attack or accidental conflict):

  • startGatewayServer() logs a warning and continues running without the MCP server.
  • The Claude CLI integration (via generated MCP config) will still attempt to connect to http://127.0.0.1:<gatewayPort+1>/mcp and will send the bearer token in the Authorization header.
  • An attacker-controlled local process listening on that port can capture OPENCLAW_MCP_TOKEN (a credential used to access the real MCP endpoint) and later use it to call tools exposed by the gateway MCP server (cross-user local attack on multi-user systems).

Vulnerable behavior (fail-open on bind error):

const mcpPort = port + MCP_PORT_OFFSET;
let mcpServer: { close: () => Promise<void> } | undefined;
try {
  mcpServer = await startMcpLoopbackServer(mcpPort);
  ensureMcpConfigFile(path.join(os.homedir(), ".openclaw"), mcpPort);
  log.info(`MCP loopback server listening on http://127.0.0.1:${mcpPort}/mcp`);
} catch (err) {
  log.warn(`MCP loopback server failed to start: ${String(err)}`);
}

Security impact is amplified by the deterministic port selection (MCP_PORT_OFFSET = 1).

Recommendation

Fail closed and/or make the MCP port non-predictable.

Recommended mitigations:

  1. Treat bind failures as fatal for MCP enablement and ensure clients are not pointed at the MCP endpoint unless it is confirmed listening.

    • If MCP is required for a given mode, abort startup on EADDRINUSE.
    • If MCP is optional, do not generate/advertise MCP config/env for clients when the server is not running.
  2. Use an OS-assigned ephemeral port (or scan for a free port) rather than gatewayPort + 1, then write that chosen port into the MCP config file.

Example (pick a free port and fail closed on unexpected errors):

import net from "node:net";

async function pickFreePort(): Promise<number> {
  return await new Promise((resolve, reject) => {
    const s = net.createServer();
    s.listen(0, "127.0.0.1", () => {
      const addr = s.address();
      s.close(() => {
        if (typeof addr === "object" && addr?.port) resolve(addr.port);
        else reject(new Error("failed to resolve port"));
      });
    });
    s.on("error", reject);
  });
}

const mcpPort = await pickFreePort();
mcpServer = await startMcpLoopbackServer(mcpPort);
ensureMcpConfigFile(openclawDir, mcpPort);
  1. Optionally: detect and explicitly handle EADDRINUSE with a clear error and guidance, instead of silently continuing.

3. 🔵 Symlink/race arbitrary file overwrite via predictable tmp file in ensureMcpConfigFile

Property Value
Severity Low
CWE CWE-59
Location src/gateway/mcp-http.ts:405-410

Description

ensureMcpConfigFile() writes to a predictable path (mcp-<port>.json.tmp) using fs.writeFileSync() and then renames it into place.

If an attacker can write within openclawDir (e.g., the directory is group/other-writable due to permissive umask, or OPENCLAW_HOME points to a shared location), they can pre-create mcp-<port>.json.tmp as a symlink/hardlink to another file. writeFileSync(tmpPath, ...) will follow that link and overwrite the target with attacker-chosen JSON content.

This is a classic local file clobbering primitive and can become privilege escalation if the process runs with elevated privileges or writes into a directory writable by less-privileged users.

Vulnerable code:

const tmpPath = filePath + ".tmp";
fs.writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
fs.renameSync(tmpPath, filePath);

Recommendation

Harden the write to be symlink/race resistant:

  1. Ensure openclawDir is a real directory (not a symlink) with user-only permissions (POSIX 0700).
  2. Write to a unique temp file created with O_EXCL (and ideally O_NOFOLLOW where supported), then fsync and rename.
  3. Refuse to operate if any path component is insecure.

Example (POSIX-focused):

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";

function secureMkdir(dir: string) {
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
  try { fs.chmodSync(dir, 0o700); } catch {}
  const st = fs.lstatSync(dir);
  if (!st.isDirectory() || st.isSymbolicLink()) {
    throw new Error(`Unsafe openclawDir: ${dir}`);
  }
}

function atomicWriteNoFollow(target: string, content: string) {
  const dir = path.dirname(target);
  const tmp = path.join(dir, `${path.basename(target)}.${crypto.randomUUID()}.tmp`);// 'wx' => fail if exists (O_EXCL)
  fs.writeFileSync(tmp, content, { encoding: "utf-8", mode: 0o600, flag: "wx" });
  fs.renameSync(tmp, target);
}

Also consider using fs.openSync with platform-specific O_NOFOLLOW (when available) plus lstatSync/fstatSync verification.


Analyzed PR: #35676 at commit 94272d4

Last updated on: 2026-03-10T00:33:25Z

@mylukin

mylukin commented Mar 7, 2026

Copy link
Copy Markdown
Contributor Author

/review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74ef98ba0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts Outdated
Comment on lines +115 to +117
const gatewayPort = params.config?.gateway?.port ?? 18789;
const mcpPort = gatewayPort + MCP_PORT_OFFSET;
const openclawDir = path.join(os.homedir(), ".openclaw");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve MCP endpoint from runtime gateway port

This computes the MCP URL from params.config?.gateway?.port (defaulting to 18789), but the gateway can run on a different runtime port via openclaw gateway run --port ...; startGatewayServer binds MCP on runtimePort + 1, so this path can write mcp.json with the wrong port and then pass it with --strict-mcp-config, causing Claude CLI MCP startup/tool access to fail even though the loopback server is healthy. Please derive the port from the runtime source (for example OPENCLAW_GATEWAY_PORT) before writing/using MCP config.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes and removed cli CLI command changes labels Mar 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0a343acd0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts Outdated
Comment on lines +494 to +497
const reqSessionKey = getHeader(req, "x-session-key")?.trim() || resolveMainSessionKey(cfg);
const messageProvider =
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined;
const accountId = getHeader(req, "x-openclaw-account-id")?.trim() || undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind MCP auth token to a fixed session context

The loopback server accepts x-session-key and x-openclaw-account-id directly from request headers after only validating a single shared bearer token, so any caller that knows OPENCLAW_MCP_TOKEN can impersonate another session/account and get a different tool policy. Because runCliAgent injects that token into the Claude subprocess environment (src/agents/cli-runner.ts:327-330) and Claude has native exec, a restricted run can issue its own HTTP request to /mcp with forged headers and bypass session-scoped tool restrictions.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb3363e44a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts Outdated
* Returns the absolute path to the config file.
*/
export function ensureMcpConfigFile(openclawDir: string, mcpPort: number): string {
const filePath = path.join(openclawDir, "mcp.json");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use unique MCP config files per gateway/runtime

ensureMcpConfigFile always writes to a single shared ~/.openclaw/mcp.json, and runCliAgent passes that path via --strict-mcp-config --mcp-config for every Claude run. If two gateways are running under the same user (different ports) or Claude runs overlap when backend serialization is disabled, one invocation can rewrite the file while another process is starting, so the earlier process may connect to the wrong MCP endpoint/token and receive the wrong tool surface or fail tool calls.

Useful? React with 👍 / 👎.

mylukin and others added 4 commits March 9, 2026 12:55
…ude-cli

- Add 1MB body size limit to readBody (prevents OOM)
- Wrap httpServer.listen in promise for proper EADDRINUSE handling
- Scope MCP config + env vars to claude-cli only; restore tools-disabled prompt for other backends
- Replace Date.now() with crypto.randomUUID() for tool call IDs
- Remove misleading ?. optional chaining on execute()
- Add toolCache eviction sweep for expired entries
- Extract MCP_PORT_OFFSET constant (replaces duplicated port+1)
- Narrow ensureMcpConfigFile catch to ENOENT only
- Remove unused ServerResponse import
- Fix pre-existing curly lint error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mylukin mylukin force-pushed the pr/mcp-http-hardening branch from fb3363e to 51d9630 Compare March 9, 2026 04:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51d9630b22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts
Comment on lines +257 to +259
? result.content.map((block: { type?: string; text?: string }) => ({
type: (block.type ?? "text") as "text",
text: block.text ?? (typeof block === "string" ? block : JSON.stringify(block)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-text MCP tool result blocks

The tools/call success path coerces every returned content block to type: "text", which silently strips MCP media semantics for tools that legitimately return images (for example nodes camera actions emit { type: "image", data, mimeType } in src/agents/tools/nodes-tool.ts). In Claude MCP sessions this makes those media results unusable as images and can inflate token usage by serializing large binary payloads into text, so tool behavior diverges from the native OpenClaw tool path.

Useful? React with 👍 / 👎.

mylukin and others added 2 commits March 9, 2026 13:23
…, tool policy, home dir, port binding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… compat

Temp directories fail Windows ACL verification in secrets file provider.
Adding allowInsecurePath: true bypasses the check for test-only paths.

Co-Authored-By: Cody <Cody.bot@easymeta.au>
@openclaw-barnacle openclaw-barnacle Bot added the commands Command implementations label Mar 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cbc02dea6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts
Comment on lines +407 to +409
const tmpPath = filePath + ".tmp";
fs.writeFileSync(tmpPath, content, { encoding: "utf-8", mode: 0o600 });
fs.renameSync(tmpPath, filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a unique temp file for MCP config writes

ensureMcpConfigFile always stages through the same mcp-<port>.json.tmp path, so concurrent writers for the same port can race: one call renames the shared temp file first and the other renameSync then fails with ENOENT. In practice this can happen when Claude runs overlap (or multiple local processes share the same config), and runCliAgent then silently falls back without MCP tool access for that run. Generate a per-write temp filename before renaming atomically.

Useful? React with 👍 / 👎.

Comment thread src/gateway/mcp-http.ts
Comment on lines +597 to +600
const cfg = loadConfig();
const defaultSessionKey = resolveMainSessionKey(cfg);
const { tools: initial } = getTools({ cfg, sessionKey: defaultSessionKey });
logDebug(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject MCP startup promise when initial tool load fails

The listen callback eagerly calls loadConfig()/getTools(...) without a local guard, so an exception during initial tool construction escapes the callback instead of rejecting the surrounding startup promise. That bypasses the caller’s non-fatal MCP startup try/catch path and can surface as an uncaught exception (for example when plugin/tool loading throws), taking down the gateway instead of just disabling MCP loopback.

Useful? React with 👍 / 👎.

mylukin added 2 commits March 9, 2026 23:39
…MCP config

- Remove x-session-key/x-openclaw-account-id header trust in mcp-http.ts;
  always use resolveMainSessionKey(cfg) to prevent session impersonation
- Use OPENCLAW_GATEWAY_PORT env var as primary port source in cli-runner.ts,
  falling back to config port when env var is absent or invalid
- Remove TODO(fix9) comment that acknowledged the impersonation vulnerability

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94272d4b14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/mcp-http.ts
Comment on lines +537 to +540
const reqSessionKey = resolveMainSessionKey(cfg);
const messageProvider =
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined;
const accountId = undefined; // not overrideable via headers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect per-run context when resolving MCP tool policy

The MCP request path hard-codes resolveMainSessionKey(cfg) and accountId = undefined, so every Claude MCP call is evaluated as the main session/account regardless of the run context. This breaks subagent and multi-account isolation: subagent policies (resolveSubagentToolPolicy) and account-specific group policies are never applied, so restricted runs can inherit the main tool surface (or lose account-allowed tools). Use the per-run context already passed in MCP headers (or another server-side binding) before calling getTools.

Useful? React with 👍 / 👎.

@mylukin

mylukin commented Mar 23, 2026

Copy link
Copy Markdown
Contributor Author

Closing: upstream not accepting merge at this time.

@steipete

steipete commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Landed maintainer-side via #60714.

Thanks @mylukin!

KimGLee pushed a commit to KimGLee/openclaw that referenced this pull request Apr 4, 2026
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants