fix(openclaw): register MCP sidecar on install so ctx_* tools surface to agent#339
fix(openclaw): register MCP sidecar on install so ctx_* tools surface to agent#339murataslan1 wants to merge 3 commits intomksglu:nextfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes OpenClaw installs where the plugin loads successfully but the agent never sees the ctx_* tools, by ensuring the OpenClaw runtime config registers the context-mode MCP sidecar (mcp.servers.context-mode) that spawns server.bundle.mjs.
Changes:
- Adds a new idempotent helper to mutate
openclaw.json(including registeringmcp.servers.context-mode). - Updates the OpenClaw install script to delegate config mutation to the helper.
- Adds unit tests and docs troubleshooting guidance for missing
ctx_*tools.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| scripts/lib/register-openclaw-config.mjs | New helper that mutates openclaw.json and registers the MCP sidecar. |
| scripts/install-openclaw-plugin.sh | Uses the new helper instead of an inline node -e config mutation. |
| tests/plugins/openclaw.test.ts | Adds unit tests for the new helper’s behavior (MCP entry, idempotency, stale refresh, cleanup). |
| docs/adapters/openclaw.md | Documents the MCP-sidecar requirement and manual recovery via openclaw mcp set. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const serverBundle = `${pluginRoot}/server.bundle.mjs`; | ||
| const existing = servers["context-mode"]; | ||
| const needsWrite = | ||
| !existing || | ||
| existing.command !== "node" || | ||
| !Array.isArray(existing.args) || | ||
| existing.args[0] !== serverBundle; | ||
| if (needsWrite) { | ||
| servers["context-mode"] = { command: "node", args: [serverBundle] }; |
There was a problem hiding this comment.
serverBundle is built via string interpolation (${pluginRoot}/server.bundle.mjs). If pluginRoot is relative or has a trailing slash, this can write a non-absolute or malformed path (e.g., //server.bundle.mjs), despite the docs/tests expecting an absolute path. Consider normalizing with resolve(pluginRoot, "server.bundle.mjs") (or join) and comparing against the normalized value in needsWrite.
| const idx = paths.indexOf(pluginRoot); | ||
| if (idx !== -1) { | ||
| paths.splice(idx, 1); | ||
| if (!paths.length) delete load.paths; |
There was a problem hiding this comment.
Legacy cleanup only removes the first occurrence of pluginRoot from plugins.load.paths via indexOf/splice. If the config accumulated duplicates (which can happen with repeated installs), extra entries remain and may continue to cause duplicate registration. Consider removing all matching entries (e.g., filter) before deciding whether to delete load.paths.
| const idx = paths.indexOf(pluginRoot); | |
| if (idx !== -1) { | |
| paths.splice(idx, 1); | |
| if (!paths.length) delete load.paths; | |
| const filteredPaths = paths.filter((p) => p !== pluginRoot); | |
| if (filteredPaths.length !== paths.length) { | |
| if (filteredPaths.length) load.paths = filteredPaths; | |
| else delete load.paths; |
| // 2. Add to plugins.allow (idempotent) | ||
| const allow = (plugins.allow ??= []); | ||
| if (!allow.includes("context-mode")) allow.unshift("context-mode"); | ||
|
|
||
| // 3. Add to plugins.entries (idempotent) | ||
| const entries = (plugins.entries ??= {}); | ||
| if (!entries["context-mode"]) entries["context-mode"] = { enabled: true }; | ||
|
|
||
| // 4. Register MCP sidecar so OpenClaw spawns server.bundle.mjs and surfaces | ||
| // ctx_* tools to the agent. Without this entry the plugin loads but its | ||
| // tools never reach the agent's tool list (confirmed against OpenClaw | ||
| // 2026.4.22: context-mode 1.0.89 plugin "loaded" but no ctx_* tools | ||
| // visible until mcp.servers.context-mode was set). | ||
| const mcp = (cfg.mcp ??= {}); | ||
| const servers = (mcp.servers ??= {}); | ||
| const serverBundle = `${pluginRoot}/server.bundle.mjs`; | ||
| const existing = servers["context-mode"]; |
There was a problem hiding this comment.
This function assumes plugins.allow is an array and plugins.entries/mcp.servers are plain objects. If a user has a malformed openclaw.json (e.g., plugins.allow as a string), allow.includes(...) / property writes will throw with a confusing error. Consider validating these shapes and throwing a targeted error (or coercing to defaults) before mutating.
| try { | ||
| cfg = JSON.parse(readFileSync(runtimePath, "utf8")); | ||
| } catch (e) { | ||
| throw new Error(`Failed to parse ${runtimePath} — is it valid JSON? (${e.message})`); | ||
| } |
There was a problem hiding this comment.
The JSON parse error path uses e.message, but catch (e) can receive non-Error values in JS. Accessing e.message can itself throw (masking the original parse failure). Safer pattern: derive the message via e instanceof Error ? e.message : String(e) (both here and in the CLI catch below).
| } catch (e) { | ||
| console.error(` ✗ ${e.message}`); | ||
| process.exit(1); |
There was a problem hiding this comment.
Same issue in the CLI error handler: e.message is not safe if a non-Error is thrown. Prefer e instanceof Error ? e.message : String(e) so the script always prints a useful error and exits cleanly.
End-to-end verification — the patched install script surfaces
|
UI QA — three prompts in the Control UI dashboard, plus a follow-up commitBefore asking for review, I ran three prompts in the OpenClaw dashboard chat to stress what happens in practice ( Prompt A — explicit tool name, non-cacheable output
Prompt B — natural language, small target (2KB
Prompt C — natural language, large target (58KB
Root cause — AGENTS.md prefix mismatch$ grep -c 'context-mode__' configs/openclaw/AGENTS.md
0
$ grep -cE 'ctx_[a-z_]+' configs/openclaw/AGENTS.md
14AGENTS.md referred to the tools by their unprefixed names ( Follow-up commit on this PR —
|
| Commit | Purpose |
|---|---|
94b4fca |
register MCP sidecar on install so ctx_* tools surface to agent |
5fc3ac6 |
prefix ctx_* names in AGENTS.md so the model invokes MCP variants |
The second commit is a system-prompt text-only change — tests and typecheck are unchanged (still tests/plugins/openclaw.test.ts + tests/adapters/openclaw.test.ts → 117/117, typecheck clean).
Next-step verification I haven't done yet
Re-running Prompts B and C against the second commit should show web_fetch dropping out and context-mode__ctx_search being called where AGENTS.md now explicitly prescribes the fetch_and_index → search pattern. Happy to post results if that would help review.
Consolidated evidence
Full walkthrough (with all commands, grep results, screenshots described textually): updated gist — §12 UI QA report.
cc @benzntech (the LinkedIn cross-check on the MCP-sidecar reasoning led directly to these findings).
Addressing the review points from Codex's independent CLI review of PR mksglu#339: 1. The helper previously did a full reset of mcp.servers["context-mode"] on every path refresh, which would silently drop user-supplied custom fields (env, cwd, timeout, anything OpenClaw adds in a future release). It now spreads the existing entry and only overwrites the two fields the helper owns (command + args). 2. Added two edge-case tests in tests/plugins/openclaw.test.ts: - "preserves unrelated fields on the existing mcp.servers entry when refreshing the path" — seeds env/cwd/timeout on the old entry, bumps pluginRoot, asserts all three survive alongside the new args[0]. - "throws a useful error when the runtime config is not valid JSON" — exercises the previously-uncovered parse-error path. Test count 117/117 → 119/119 on `tests/plugins/openclaw.test.ts` + `tests/adapters/openclaw.test.ts`. Typecheck still clean. Scope unchanged — still inside OpenClaw adapter/install/tests.
Independent Codex CLI review — results + follow-up commit
|
| Commit | Purpose |
|---|---|
94b4fca |
register MCP sidecar on install so ctx_* tools surface to agent |
5fc3ac6 |
prefix ctx_* names in AGENTS.md so the model invokes MCP variants |
3766cca |
preserve unrelated MCP server fields; add edge-case tests |
- Typecheck: clean
tests/plugins/openclaw.test.ts + tests/adapters/openclaw.test.ts: 119/119- Scope: still OpenClaw-only (adapter docs / install / tests / configs/openclaw)
Full Codex transcript + updated walkthrough lives in the gist: §13 — Independent Codex review follow-up.
|
@murataslan1 seems has a conflict :) |
Post-
|
| Turn | Before 5fc3ac6 (old AGENTS.md) |
After 5fc3ac6 (prefixed AGENTS.md) |
|---|---|---|
| 1 | context-mode__ctx_fetch_and_index |
context-mode__ctx_fetch_and_index |
| 2 | web_fetch ⚠ (redundant) |
— |
| 3 | context-mode__ctx_execute |
context-mode__ctx_execute |
| 3 tool calls | 2 tool calls | |
| Model reply | 4 |
4 |
Headline: web_fetch is gone. The redundant fallback that showed up in both Prompt B (small target) and Prompt C (large target) during the UI QA run no longer fires once AGENTS.md refers to the tools by the names the agent actually sees (context-mode__ctx_*). That was the whole point of 5fc3ac6.
Running-context footprint dropped: R94.7k → R37.1k on the same prompt. Apples-to-oranges across sessions so treat this as directional, not a precise delta — but the direction is right: no web_fetch means less raw web content threading through the conversation context.
Observation on ctx_search: the model still preferred context-mode__ctx_execute for the count instead of context-mode__ctx_search. That's consistent with AGENTS.md's "Think in Code" mandate — for a precise count, deterministic code is a reasonable choice over BM25-scored top-K search, and both are legitimate. Not a regression. If you ever want to push the model toward ctx_search for questions like this, a worked example of the fetch_and_index → search pattern in AGENTS.md would be the lever; that's a follow-up polish rather than something in this PR's scope.
Where this leaves PR #339
All three commits validated end-to-end on OpenClaw 2026.4.22 + context-mode 1.0.89:
| Commit | What it fixes | Verified by |
|---|---|---|
94b4fca |
Tools surface to the agent at all | openclaw plugins list + tool inventory (11 context-mode__ctx_* names) |
5fc3ac6 |
Routing no longer falls back to web_fetch |
This comment (3 tool calls → 2, chain above) |
3766cca |
Install script no longer overwrites user-added MCP server fields | tests/plugins/openclaw.test.ts (119/119) |
Full walkthrough updated in the companion gist: §14 — Post-prefix validation.
Addressing the review points from Codex's independent CLI review of PR mksglu#339: 1. The helper previously did a full reset of mcp.servers["context-mode"] on every path refresh, which would silently drop user-supplied custom fields (env, cwd, timeout, anything OpenClaw adds in a future release). It now spreads the existing entry and only overwrites the two fields the helper owns (command + args). 2. Added two edge-case tests in tests/plugins/openclaw.test.ts: - "preserves unrelated fields on the existing mcp.servers entry when refreshing the path" — seeds env/cwd/timeout on the old entry, bumps pluginRoot, asserts all three survive alongside the new args[0]. - "throws a useful error when the runtime config is not valid JSON" — exercises the previously-uncovered parse-error path. Test count 117/117 → 119/119 on `tests/plugins/openclaw.test.ts` + `tests/adapters/openclaw.test.ts`. Typecheck still clean. Scope unchanged — still inside OpenClaw adapter/install/tests.
3766cca to
cc9f97b
Compare
… to agent context-mode's ctx_* tools live in server.bundle.mjs and are exposed over stdio MCP. Other adapters (Claude Code, Codex, Cursor) spawn the bundle via their platform's mcpServers config. The OpenClaw install script wrote plugins.allow / plugins.entries to openclaw.json but never added mcp.servers.context-mode, so the gateway never spawned the sidecar and the agent never saw the ctx_* tool list — while openclaw plugins list, openclaw doctor, and scripts/ctx-debug.sh all reported healthy state. Changes - scripts/lib/register-openclaw-config.mjs (new): extracts step 5 of the install script into a testable helper. Writes plugins.allow / .entries / cleans legacy plugins.load.paths (existing behavior) and additionally registers mcp.servers.context-mode pointing at <pluginRoot>/server.bundle.mjs. Idempotent: re-running is a no-op; stale server paths are refreshed. - scripts/install-openclaw-plugin.sh: step 5 delegates to the helper. - tests/plugins/openclaw.test.ts: 5 unit tests for the helper (MCP entry presence, idempotency, stale-path refresh, plugins.allow/entries contract, legacy plugins.load.paths cleanup). - docs/adapters/openclaw.md: new troubleshooting entry documenting the MCP sidecar requirement and the manual openclaw mcp set recovery command. Verification - npm run typecheck: clean - vitest tests/plugins/openclaw.test.ts tests/adapters/openclaw.test.ts: 117/117 pass (baseline 112 + 5 new) - Live: on OpenClaw 2026.4.22 + context-mode 1.0.89, after the fix the agent tool inventory includes context-mode__ctx_execute, context-mode__ctx_search, context-mode__ctx_fetch_and_index, and the rest of the ctx_* surface (OpenClaw prefixes MCP-sourced tools with the server name). Debugging walkthrough (initial wrong diagnosis, cross-check with @benzntech, server.ts trace that surfaced the registerTool calls, final resolution): https://gist.github.com/murataslan1/cd7b27577fcb535d56fe318c2339b400 Companion to issue mksglu#45 (follow-up comment: mksglu#45 (comment)). Pre-existing test failures in tests/hooks/integration.test.ts (Security Policy Enforcement) are present on main too (reproduced by git-stashing this PR and re-running), so they are not caused by this change. They appear related to the PreToolUse relaxation commits (415ce57, 2731ca2, ece3abb) and are out of scope here. Scope is limited to OpenClaw adapter files and install path; hooks, src/server.ts, the session layer, and all non-OpenClaw adapter tests are untouched.
…CP variants Complements the previous commit. With the MCP sidecar registered, OpenClaw surfaces the plugin's tools as context-mode__ctx_* (server-name prefix is automatically applied by OpenClaw's MCP aggregator). The routing guidance injected via AGENTS.md still referenced the unprefixed names (ctx_execute, ctx_search, ...), which left the model to bridge the naming gap on its own. UI QA on OpenClaw 2026.4.22 + context-mode 1.0.89 confirmed this was costly in practice: the model would invoke context-mode__ctx_fetch_and_index first (good), but then redundantly fall back to the built-in web_fetch for the raw content, and skip context-mode__ctx_search entirely in favor of a web_fetch + ctx_execute combo. The unprefixed guidance made the built-ins look like the closer match when under pressure. This commit rewrites all 14 ctx_* mentions in configs/openclaw/AGENTS.md to use the context-mode__ctx_* form the model actually sees. Sanity check: $ grep -c 'context-mode__ctx_' configs/openclaw/AGENTS.md # 14 $ grep -cE '\bctx_[a-z]' configs/openclaw/AGENTS.md # 0 Scope is still OpenClaw-only — configs/pi/AGENTS.md and every other adapter's AGENTS.md are untouched, because those platforms don't apply the server-name prefix. Tests unchanged; this is a system-prompt text-only change. typecheck and the full tests/plugins/openclaw.test.ts + tests/adapters/openclaw.test.ts suite still pass (117/117).
Addressing the review points from Codex's independent CLI review of PR mksglu#339: 1. The helper previously did a full reset of mcp.servers["context-mode"] on every path refresh, which would silently drop user-supplied custom fields (env, cwd, timeout, anything OpenClaw adds in a future release). It now spreads the existing entry and only overwrites the two fields the helper owns (command + args). 2. Added two edge-case tests in tests/plugins/openclaw.test.ts: - "preserves unrelated fields on the existing mcp.servers entry when refreshing the path" — seeds env/cwd/timeout on the old entry, bumps pluginRoot, asserts all three survive alongside the new args[0]. - "throws a useful error when the runtime config is not valid JSON" — exercises the previously-uncovered parse-error path. Test count 117/117 → 119/119 on `tests/plugins/openclaw.test.ts` + `tests/adapters/openclaw.test.ts`. Typecheck still clean. Scope unchanged — still inside OpenClaw adapter/install/tests.
cc9f97b to
3eec67d
Compare
|
@mksglu — my bad, the earlier rebase went onto Hashes: @ipedro — thanks for the first read. When you're good, we're ready on our side for whichever release cycle fits. No rush. |
Summary
Before this PR, installing context-mode on OpenClaw
2026.4.22produced a silent registration failure:openclaw plugins listshowed the plugin asloaded,openclaw doctorreported zero errors,scripts/ctx-debug.shmostly passed — but none of thectx_*tools surfaced to the agent. The model even refused an explicit request:Cause: context-mode's
ctx_*tools live insrc/server.ts, exposed over stdio MCP (11×server.registerTool+StdioServerTransport). Other adapters (Claude Code, Codex, Cursor) spawnserver.bundle.mjsvia their platform'smcpServersconfig. The OpenClaw install script writesplugins.allow/plugins.entriestoopenclaw.jsonbut never adds themcp.servers.context-modeentry, so the gateway never spawns the sidecar and the agent never sees the tools.This PR fills in that step.
Changes
scripts/lib/register-openclaw-config.mjs(new) — idempotent helper extracted from the install script. Writesplugins.allow,plugins.entries, cleans legacyplugins.load.paths(existing behavior), and additionally registersmcp.servers.context-modepointing at<pluginRoot>/server.bundle.mjs. Re-running is a no-op; stale server paths are refreshed.scripts/install-openclaw-plugin.sh— step 5 delegates to the new helper (previously inlinenode -e).tests/plugins/openclaw.test.ts— 5 new unit tests against the helper: MCP entry presence, idempotency, stale-path refresh,plugins.allow/entriescontract, legacyplugins.load.pathscleanup.docs/adapters/openclaw.md— new troubleshooting entry documenting the MCP-sidecar requirement and the manualopenclaw mcp setrecovery command.Verification
Live check on OpenClaw
2026.4.22+ context-mode1.0.89after the fix — the agent's tool inventory now includes the fullctx_*surface (OpenClaw prefixes MCP-sourced tool names with the server name):Full debugging writeup
gist — "OpenClaw MCP sidecar gap: debugging walkthrough"
Covers initial (incorrect) diagnosis, cross-check with @benzntech on LinkedIn + his follow-up on #45, the
src/server.tstrace that surfaced the 11server.registerToolcalls, and the final resolution path.Relation to existing work
415ce57revert of Agent gets stuck when WebFetch is blocked but ctx_fetch_and_index MCP tool is unavailable #230,2731ca2curl/wget hook blocks binary downloads with -o flag #166,ece3abbPostToolUse hook matcher "" fires on ALL tools (TaskUpdate, TaskCreate, etc.) causing false "hook error" display #229/"Task" PreToolUse hook matcher incorrectly intercepts TaskCreate/TaskUpdate/TaskList todo tools #241) are not addressed here — they changed hook deny/passthrough behavior but did not cause this particular issue.Full test-suite caveats (environment-dependent)
In my local environment,
npm testsurfaces 5 failures intests/hooks/integration.test.ts → Security Policy Enforcement, andgit stash-ing this PR and re-running reproduces the same 5 failures onmain— so they are not caused by this change on my machine. However, an independent review of the PR via the OpenAI Codex CLI (summary linked in a follow-up comment) could not reproduce those in a clean environment; that environment only hit aRust toolchain not configuredfailure, also present on both branch andmain. So whatever is going on intests/hooks/integration.test.tslocally appears to be environment-specific, not a property of this PR or ofmain. CI on the project's own infra is the authoritative signal.Scope discipline
No changes outside the OpenClaw adapter, the OpenClaw install script, and OpenClaw docs. Other adapter hooks,
src/server.ts, the session layer, and all non-OpenClaw tests are untouched.Open questions for @ipedro / @mksglu (non-blocking)
register()rather than relying onopenclaw.jsonconfig? If yes, what's the preferredapi.*call?configs/openclaw/AGENTS.mdguidance uses unprefixedctx_executerather than thecontext-mode__ctx_executename that the agent actually sees. Prefixless names in routing instructions may mildly confuse the model — worth a pass?openclaw mcp setsurface?