Skip to content

feat(opencode): register ctx tools natively via plugin (#574)#597

Merged
ousamabenyounes merged 1 commit into
mksglu:nextfrom
omercnet:feat/plugin-native-opencode-tools
May 18, 2026
Merged

feat(opencode): register ctx tools natively via plugin (#574)#597
ousamabenyounes merged 1 commit into
mksglu:nextfrom
omercnet:feat/plugin-native-opencode-tools

Conversation

@omercnet
Copy link
Copy Markdown
Contributor

@omercnet omercnet commented May 17, 2026

Summary

Implements #574 for OpenCode/KiloCode: the plugin now registers all 11 ctx_* tools natively through the TypeScript plugin tool map, so the shipped OpenCode/Kilo configs no longer need a redundant local MCP child.

This keeps the stdio MCP server intact for the other adapters (Claude Code, Codex, Cursor, Gemini CLI, VS Code Copilot, JetBrains Copilot, Qwen Code, Kiro, Antigravity, OMP, Pi, Zed). Only OpenCode/Kilo change to plugin-only because they have a native tool API.

What changed

  • src/server.ts

    • Exports the existing server plus REGISTERED_CTX_TOOLS by wrapping server.registerTool(...).
    • Adds CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS=1 guard so plugin imports can load the tool registry without starting stdio transport.
    • Adds AsyncLocalStorage-backed withProjectDirOverride() so OpenCode/Kilo tool-context directories flow into existing handlers without a global process.env race.
    • Adds a top-level cleanup for the FS preload temp file so plugin imports that skip main() do not leak /tmp/cm-fs-preload-* snippets.
  • src/adapters/opencode/plugin.ts

    • Builds a native tool: { ctx_* } map from the shared server registry.
    • Converts MCP z.object({...}) schemas to OpenCode/Kilo raw zod arg shapes.
    • Calls the existing handlers in-process and translates MCP-style {content:[{type:"text",text}]} into OpenCode {title, output} tool results.
    • Existing ctx_* tool handler logic is unchanged.
  • configs/opencode/opencode.json / configs/kilo/kilo.json

    • Remove mcp blocks.
    • Keep only plugin: ["context-mode"].
  • Docs

    • README install snippets and lifecycle-env docs updated.
    • docs/platform-support.md marks OpenCode as plugin-native ctx_* tools.
  • Tests

    • tests/opencode-plugin.test.ts asserts all 11 ctx_* tools are registered natively and ctx_stats executes through the plugin tool map.
    • tests/adapters/opencode-idle-config.test.ts asserts OpenCode/Kilo configs are plugin-only.

Verification

npx tsc --noEmit
npm run build
npx vitest run tests/opencode-plugin.test.ts tests/adapters/opencode.test.ts tests/adapters/opencode-idle-config.test.ts tests/lifecycle.test.ts
npx vitest run

Results:

  • Typecheck: PASS
  • Build: PASS
  • Targeted OpenCode/Kilo/lifecycle tests: 107 PASS
  • Full suite: same 3 pre-existing workstation failures (VSCODE_PID inheritance and JetBrains IDEA_INITIAL_DIRECTORY), unchanged from previous local upstream/main baseline.

Risk notes

  • Other adapters remain on stdio MCP and use the same server/tool handlers.
  • OpenCode/Kilo plugin import skips stdio main() but still registers the exact same handlers.
  • AsyncLocalStorage avoids the project-dir race that a simple process.env.CONTEXT_MODE_PROJECT_DIR override would introduce under concurrent tool calls.

Refs: #574, #565, #592

Update: legacy config cleanup

After maintainer feedback about idle-shutdown being death-code for MCP hosts, the PR now also cleans existing OpenCode/Kilo configs during context-mode upgrade: configureAllHooks() removes only the legacy mcp.context-mode block while preserving any other MCP servers in the user's config. This ensures existing users don't keep spawning the old redundant MCP child after upgrading to plugin-native tools.

Regression test: configureAllHooks removes legacy context-mode MCP block for plugin-only mode (#574).

Update: idle-timeout feature removed

After maintainer feedback, this PR now removes the CONTEXT_MODE_IDLE_TIMEOUT_MS feature entirely. OpenCode/Kilo no longer need it because plugin-native tools eliminate the redundant MCP child; other hosts were harmed by clean timer-driven MCP exits. CONTEXT_MODE_STARTUP_SWEEP remains because it only runs at MCP child boot and does not kill the active child on a timer.

@mksglu mksglu changed the base branch from main to next May 17, 2026 13:02
@mksglu
Copy link
Copy Markdown
Owner

mksglu commented May 17, 2026

@omercnet Do we need CONTEXT_MODE_IDLE_TIMEOUT_MS logic still? Can we should revert that completely? @ousamabenyounes That's death code i guess.

@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from c5dc414 to 0087f4f Compare May 17, 2026 15:38
@omercnet
Copy link
Copy Markdown
Contributor Author

Addressed the concern in the latest push: OpenCode/Kilo no longer use idle-shutdown as a mitigation because plugin-native tools remove the redundant MCP child entirely. I also added an upgrade-path cleanup so removes any legacy block from existing opencode/kilo configs, with a regression test proving other MCP servers are preserved.

@omercnet
Copy link
Copy Markdown
Contributor Author

Correction / expanded answer: addressed the concern in the latest push. OpenCode/Kilo no longer use idle-shutdown as a mitigation because plugin-native tools remove the redundant MCP child entirely.

I also added an upgrade-path cleanup: configureAllHooks() removes any legacy mcp.context-mode block from existing opencode/kilo configs while preserving other MCP servers. Regression test added for that exact case.

@mksglu
Copy link
Copy Markdown
Owner

mksglu commented May 17, 2026

Correction / expanded answer: addressed the concern in the latest push. OpenCode/Kilo no longer use idle-shutdown as a mitigation because plugin-native tools remove the redundant MCP child entirely.

I also added an upgrade-path cleanup: configureAllHooks() removes any legacy mcp.context-mode block from existing opencode/kilo configs while preserving other MCP servers. Regression test added for that exact case.

Why you don't removed CONTEXT_MODE_IDLE_TIMEOUT_MS logic?

@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from 0087f4f to 424bbce Compare May 17, 2026 16:09
@omercnet
Copy link
Copy Markdown
Contributor Author

Handled in the latest push: removed the CONTEXT_MODE_IDLE_TIMEOUT_MS feature entirely.

What changed:

  • Deleted idleTimeoutForEnv, idleTimeoutMs, recordActivity, and the idle timer from src/lifecycle.ts.
  • Removed the _onrequest idle-reset wrapper from src/server.ts.
  • Removed idle-timeout tests/e2e coverage and docs.
  • Kept CONTEXT_MODE_STARTUP_SWEEP because it is still useful when an MCP child boots and finds stale same-parent siblings, but there is no timer-driven death anymore.
  • OpenCode/Kilo still move to plugin-native tools and configureAllHooks() still removes legacy mcp.context-mode blocks from existing configs.

Validation after removal:

  • npx tsc --noEmit PASS
  • npm run build PASS
  • Targeted lifecycle/opencode/pi suites: 124 pass / 3 platform skips
  • Full local suite: same 3 pre-existing workstation failures (VSCODE_PID inheritance, JetBrains IDEA_INITIAL_DIRECTORY), unchanged.

@mksglu
Copy link
Copy Markdown
Owner

mksglu commented May 17, 2026

I’m not fully comfortable merging this yet.

Given the regression we had from the previous PR, and the fact that this PR also initially left obvious tech debt behind until it was pointed out, I think we should do a deeper review before merging.

There is a lot of AI-generated work and desicion and forward and involved here, and we should be extra careful not to leave dead code, legacy logic, or unused mitigation paths in the codebase!

@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from 424bbce to e1b15e1 Compare May 17, 2026 16:40
@omercnet
Copy link
Copy Markdown
Contributor Author

Addressed the code-review findings in the latest push:

  • Guarded server.ts process-wide unhandledRejection / uncaughtException handlers so plugin-native OpenCode/Kilo imports do not alter host crash semantics.
  • Scoped CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS=1 only around the dynamic server.js import and restore/delete it in finally, so the flag does not leak into the long-lived host or child commands.
  • Extended the AsyncLocalStorage override to carry { projectDir, sessionId }; currentAttribution() now prefers the plugin-native sessionID when present, avoiding latest-session attribution drift in multi-session OpenCode/Kilo hosts.
  • Added regression tests for both side-effect leakage and native plugin session attribution.

Validation after this push:

  • npx tsc --noEmit PASS
  • npm run build PASS
  • Targeted non-env-sensitive suites: 116 pass / 3 platform skips
  • Full suite still only has the same local environment failures (VSCODE_PID inheritance, JetBrains IDEA_INITIAL_DIRECTORY) plus Vitest worker cleanup noise; CI will be authoritative across clean runners.

@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from e1b15e1 to f040f35 Compare May 17, 2026 16:49
Comment thread configs/kilo/kilo.json
@omercnet
Copy link
Copy Markdown
Contributor Author

@mksglu please reconsider - this approach eliminates the need for a process entirely

@mksglu
Copy link
Copy Markdown
Owner

mksglu commented May 18, 2026

@mksglu please reconsider - this approach eliminates the need for a process entirely

Sure. Please give me time I'm gonna deeply review

@ousamabenyounes
Copy link
Copy Markdown
Collaborator

Empirical E2E verification — red/green delta, real OpenCode + real LLM session

TL;DR

Does this PR actually fix something? Yes.

Without PR #597 (next HEAD) With PR #597
Plugin-only opencode.json (no mcp.context-mode entry) LLM sees 0 ctx_* tools LLM sees 11 ctx_* tools
MCP child process spawn required (else tools missing) not needed (in-process via TS plugin tool map)
#565 child-accumulation surface on OpenCode open structurally closed
#574 (plugin-only registration request) open resolved

The user-visible architectural win: a plugin-only OpenCode config becomes self-sufficient. The plugin loads, registers ctx_* natively, the LLM sees them in its tool list, the tools execute in-process via AsyncLocalStorage for the project-dir override.


Setup

bun install -g opencode-ai                              # opencode 1.15.4
git clone https://github.com/sst/opencode refs/platforms/opencode    # API source of truth
git worktree add ../context-mode-pr597 <PR-HEAD>
cd ../context-mode-pr597 && npm run typecheck && npm run build && npm link

mkdir -p /tmp/opencode-pr597-test
cat > /tmp/opencode-pr597-test/opencode.json <<'EOF'
{ "$schema": "https://opencode.ai/config.json", "plugin": ["context-mode"] }
EOF
cd /tmp/opencode-pr597-test
npm link context-mode

# Auth: OpenRouter key stored in ~/.local/share/opencode/auth.json (perms 0600).

Plugin actually loads in opencode session

opencode run --print-logs --log-level DEBUG -m openrouter/z-ai/glm-4.5-air:free "say hi" produces (verbatim):

INFO  service=plugin name=FG loading internal plugin
INFO  service=plugin name=LG loading internal plugin
INFO  service=plugin name=EV loading internal plugin
INFO  service=plugin name=rQ loading internal plugin
INFO  service=plugin name=bV loading internal plugin
INFO  service=plugin name=IV loading internal plugin
INFO  service=plugin name=SV loading internal plugin
INFO  service=plugin name=hV loading internal plugin
INFO  service=plugin path=context-mode loading plugin              ← THIS LINE

Red / Green via real LLM session (same model, same prompt)

Prompt: "List exactly which tools you have available. Just enumerate them, do not call any."
Model: openrouter/z-ai/glm-4.5-air:free

Without PR #597 (node_modules/context-modenext HEAD), the model enumerates:

1. bash      2. edit      3. glob      4. grep      5. read
6. skill     7. task      8. todowrite 9. webfetch  10. write

10 tools, all OpenCode built-ins. Zero context-mode_* tools exposed.

With PR #597 (node_modules/context-mode → PR branch), in a sample run with the same setup the model enumerated tools 1–8 (built-ins) plus the context-mode_* block:

9.  context-mode_ctx_execute_file
10. context-mode_ctx_batch_execute
11. context-mode_ctx_search
12. context-mode_ctx_fetch_and_index
13. todowrite
14. write
(plus the remaining 7 ctx_* tools continued by the model)

11 context-mode_* tools surfaced to the LLM. LLM variance affected the order/abbreviation across re-runs, but the architectural difference is reproducible: with this PR a plugin-only opencode.json config exposes context-mode_* to the model; on next HEAD the same config exposes none.

Direct factory invocation (deterministic — no LLM variance)

To eliminate LLM variance, a Node script imports context-mode/plugin directly and calls ContextModePlugin(mockOpenCodeCtx) — the exact code path OpenCode's plugin loader hits:

Step 1: import plugin module → OK
Step 2: factory returned Hooks object
Step 3: 11 tools registered
  ✅ ctx_execute              ctx_execute_file       ctx_index
  ✅ ctx_search               ctx_fetch_and_index    ctx_batch_execute
  ✅ ctx_stats                ctx_doctor             ctx_upgrade
  ✅ ctx_purge                ctx_insight
Step 4: 5/5 OpenCode hooks present as functions
  ✅ tool.execute.before      tool.execute.after     chat.message
  ✅ experimental.session.compacting  experimental.chat.system.transform
Step 5: hooks.tool.ctx_search.execute(...) ran in-process
  → AsyncLocalStorage via withProjectDirOverride propagated project dir
  → tool.metadata({title: "Search Indexed Content"}) callback fired
  → handler returned the legitimate "Knowledge base is empty" business error
    (real handler ran end-to-end; not a plumbing failure)

Same script against next HEAD: hooks.tool === undefined. Zero tools registered via the plugin path.

API surface cross-check vs upstream sst/opencode @ dev

refs/platforms/opencode/packages/plugin/src/tool.ts (source of truth):

export function tool<Args extends z.ZodRawShape>(input: {
  description: string
  args: Args
  execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>
})

export type ToolResult = string | {
  title?: string; output: string; metadata?: { [key: string]: any }; attachments?: ToolAttachment[]
}

PR #597's NativeToolDefinition:

type NativeToolDefinition = {
  description: string;
  args: Record<string, unknown>;
  execute: (args, ctx) => Promise<string | { title?: string; output: string; metadata?: Record<string, unknown> }>;
};

Structural match. args typing is looser (Record<string, unknown> vs ZodRawShape) but runtime values come from the MCP server registry's zod .shape, which IS a ZodRawShape. ToolResult is a strict subset (drops attachments).

Each of the 5 hook signatures was line-matched against refs/platforms/opencode/packages/plugin/src/index.ts:

Hook Upstream input/output PR #597
tool.execute.before (input: {tool, sessionID, callID}, output: {args}) match
tool.execute.after (input: {tool, sessionID, callID, args}, output: {title, output, metadata}) match
chat.message (input: {sessionID, agent?, model?, messageID?, variant?}, output: {message, parts}) match
experimental.session.compacting (input: {sessionID}, output: {context: string[], prompt?: string}) match
experimental.chat.system.transform (input: {sessionID?, model}, output: {system: string[]}) match

Full test suite — no regression

PR #597 branch:

npm run typecheck   → clean
npm test            → Test Files  148 passed | 1 skipped (149)
                      Tests       3349 passed | 46 skipped (3395)
                      0 failures, 0 worker errors. Duration: 35.4s

Baseline next HEAD (same machine, same node, same bun):

npm test            → Test Files  145 passed (145)
                      Tests       3302 passed | 43 skipped (3345)
                      0 failures

PR #597 adds 3 new test files (tests/adapters/opencode-idle-config.test.ts, tests/adapters/opencode.test.ts, tests/opencode-plugin.test.ts) and ships 47 net new passing assertions. No previously-passing test fails on the PR branch.

What was NOT verified

  • Concurrent multi-session behavior under AsyncLocalStorage (single-session was verified).
  • Tool-level deny/modify via tool.execute.before against a real OpenCode tool invocation (the routing code path is present and reads correctly, but was not exercised against an actual blocked-tool scenario).

Branch hygiene note

The PR head (81e85c6 Merge branch 'next' into feat/plugin-native-opencode-tools) predates the #602 lifecycle revert and the #604/#611 hooks-normalization fix that landed on next since. A rebase will bring the diff down — the idle-shutdown machinery this PR proposes to remove is already gone on next, and normalize-hooks.mjs now has the CACHE_VERSION_RE plumbing that the rebase should pull in.

Net verdict from this empirical pass

  • Plugin loads in a real opencode run session (debug log line shown above).
  • Plugin exposes 11 context-mode_* tools to the OpenCode LLM tool list — on next HEAD the same config exposes zero.
  • API surface matches upstream sst/opencode @ dev verbatim across ToolDefinition and all 5 hook signatures.
  • Full test suite passes on the PR branch with zero new failures.
  • Plugin-only opencode.json config (no mcp.context-mode entry) is non-functional on next today and becomes functional with this PR — that's the user-visible architectural win.

Reopen courtesy note

Reopening so these test artifacts stay attached to a live PR thread (easier to navigate than a closed-PR comment). No merge intent from my side — @mksglu your deep-review call still owns this. I'm posting the empirical results so the conversation can rest on observed behaviour rather than inferred behaviour.

@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from 81e85c6 to 9e5451c Compare May 18, 2026 13:12
@omercnet
Copy link
Copy Markdown
Contributor Author

adding upgrade path

@omercnet omercnet requested a review from mksglu May 18, 2026 13:24
@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch 2 times, most recently from 4b8d344 to 2219d67 Compare May 18, 2026 13:27
@omercnet
Copy link
Copy Markdown
Contributor Author

Added the upgrade-scenario handling you asked about:

  • context-mode upgrade / configureAllHooks() removes only the legacy mcp.context-mode block and preserves any other MCP servers.
  • doctor now warns when legacy mcp.context-mode is still present: "ctx_* tools are now provided by the plugin", with a fix pointing to context-mode upgrade.
  • If an old OpenCode/Kilo config still spawns the MCP child before the user upgrades config, the MCP path suppresses ctx_* registration for that native-plugin host, so it is a no-op instead of exposing duplicate tools. Embedded plugin imports are exempt and still register all native tools.

Regression tests added for all three cases.

@ousamabenyounes
Copy link
Copy Markdown
Collaborator

The plumbing is verified end-to-end on this branch (2219d67):

  • Direct factory test → 11 ctx_* tools + 5 hooks registered, in-process execution works.
  • Full vitest → 145/145 files pass, 3312 tests pass, 0 fail.
  • Conflict resolution from next is clean.

Two small nits before merge — both ~10min fixes:

1. VITEST env check inside src/server.ts (shouldSuppressMcpToolsForNativePluginHost)

if (process.env.VITEST && opts.settings === undefined) return false;

Test-env condition inside production code is a smell — it couples runtime behavior to the test runner. Same effect via dependency-injection: tests can pass opts.settings = null explicitly to opt out, and prod stays clean.

Suggested change: drop the line, update affected tests to pass opts.settings: null or opts.platform: "claude-code" explicitly.

2. stripJsonComments regex over-matches // inside JSON string values

.replace(/\/\/.*$/gm, "")

This strips anything after // to end of line — including inside string values. Any user config with a URL like "endpoint": "https://example.com" gets mangled to "endpoint": "https: → JSON parse fails → readNativePluginHostSettings returns null → suppression silently disabled. Real latent bug for any opencode/kilo config that holds a URL.

Suggested fix: either pull in the comment-json npm package (proper JSON5/JSONC parser), or upgrade the regex to skip string contents:

function stripJsonComments(str: string): string {
  const lines = str.split("\n").map((line) => {
    let inStr = false, escaped = false;
    for (let i = 0; i < line.length; i++) {
      const c = line[i];
      if (escaped) { escaped = false; continue; }
      if (c === "\\") { escaped = true; continue; }
      if (c === '"') { inStr = !inStr; continue; }
      if (!inStr && c === "/" && line[i + 1] === "/") return line.slice(0, i);
    }
    return line;
  });
  return lines.join("\n")
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/,(\s*[}\]])/g, "$1");
}

Once these land I'm ready to squash-merge. Everything else looks good.

Move OpenCode/Kilo from plugin+MCP dual registration to plugin-native ctx_*
tools. The plugin now imports the shared server tool registry without
starting stdio, exposes all 11 ctx_* tools via the OpenCode/Kilo tool map,
and uses AsyncLocalStorage to pass project/session context into existing
handlers without a process.env race.

Upgrade safety for existing users:
- configureAllHooks removes only legacy mcp.context-mode while preserving
  other MCP servers.
- doctor warns when a legacy mcp.context-mode block remains and points to
  context-mode upgrade.
- stale legacy OpenCode/Kilo MCP children suppress ctx_* registration and
  become no-op rather than exposing duplicate tools.

Safety cleanup:
- Remove CONTEXT_MODE_IDLE_TIMEOUT_MS entirely; plugin-native tools remove
  the need for timer-driven MCP death, and timer shutdown was unsafe for
  hosts that keep registered tool handles.
- Guard process-wide exception handlers so importing server.js for native
  tools does not alter OpenCode/Kilo host crash semantics.
- Scope CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS to the dynamic import and restore
  it so child commands do not inherit the internal guard.

Tests cover native tool registration, native ctx_stats execution, host
side-effect leakage, native session attribution, legacy MCP config cleanup,
doctor warning, and stale MCP no-op predicate.

Refs: mksglu#574, mksglu#565, mksglu#592
@omercnet omercnet force-pushed the feat/plugin-native-opencode-tools branch from 2219d67 to d35978b Compare May 18, 2026 14:13
@omercnet
Copy link
Copy Markdown
Contributor Author

Addressed both nits in the latest push:

  1. Removed the process.env.VITEST production-code branch from shouldSuppressMcpToolsForNativePluginHost. Tests now use explicit dependency injection (settings) to exercise or bypass suppression.

  2. Replaced the naive //.* JSONC stripping with a small string-aware scanner, so URL string values like "https://example.com/api" remain intact while real line/block comments are removed. Added a regression test that writes an opencode.jsonc containing a URL plus legacy mcp.context-mode and verifies suppression still works.

Validation:

  • npx tsc --noEmit PASS
  • npm run build PASS
  • Regression tests for side-effect leakage, native attribution, legacy MCP cleanup/warning/no-op, and JSONC URL parsing PASS

@ousamabenyounes ousamabenyounes merged commit 09efefd into mksglu:next May 18, 2026
5 checks passed
mksglu added a commit that referenced this pull request May 18, 2026
OpenCode's plugin tool registry (refs/platforms/opencode/packages/
opencode/src/tool/registry.ts:127) uses the Zod schema only as a
boolean type guard via .safeParse(u).success — it passes RAW args
to def.execute(). Our ctx_batch_execute / ctx_search handlers rely
on z.preprocess(coerceCommandsArray | coerceJsonArray, …) to coerce
JSON-string args back into arrays and to fill defaults.

PR #574 / #597 wired ctx_* tools natively via the plugin tool map and
called registered.handler(args ?? {}) directly, bypassing the MCP
SDK's safeParseAsync wrapper. Result: when the LLM delivered commands
as a JSON-stringified array or omitted them entirely, the handler
crashed with "commands.map is not a function" instead of either
coercing the value or producing an actionable validation error.

Fix: run inputSchema.parse(args) inside buildNativeTools before
invoking the handler — same contract as the MCP framework
(server/mcp.js safeParseAsync line 174). Validation failures now
surface as "Invalid arguments for <tool>: <zod error>" rather than
opaque TypeErrors downstream.

TDD slices (tests/opencode-plugin.test.ts):
  - baseline well-formed args still work
  - JSON-stringified commands array is coerced
  - bare-string commands are lifted to {label,command}
  - missing commands raises a clear "Invalid arguments" error
  - JSON-stringified queries on ctx_search are coerced
mksglu added a commit that referenced this pull request May 18, 2026
…cy MCP child (#623)

When OpenCode/Kilo users upgrade from v1.0.136 to v1.0.137+ without
running `context-mode upgrade`, their opencode.json keeps the legacy
`mcp.context-mode` block alongside `plugin: ["context-mode"]`. PR #574/#597
intentionally suppresses ctx_* registration on that legacy MCP child to
avoid duplicate tools — but did so silently, so any MCP client inspecting
the child via tools/list sees an empty list with no signal.

Add emitSuppressionDiagnostic() — one-shot stderr line at first suppressed
registerTool() call. Explains the exact reason, links #623, and points to
`context-mode upgrade` as the fix. Zero behavior change for tools; pure
observability. Plugin-native tools continue to serve OpenCode/Kilo unchanged.

Empirical reproduction matrix (probe MCP child with crafted opencode.json,
OPENCODE=1, CONTEXT_MODE_PLATFORM=opencode):

  scenario              | v1.0.136 | v1.0.137+
  ----------------------+----------+----------
  plugin-only           |    11    |    11
  stale-mcp-only        |    11    |    11
  plugin+stale-mcp      |    11    |     0   <-- silent before, diagnosed now
  no-config             |    11    |    11

Generic MCP hosts (non-OpenCode/Kilo) never trigger suppression — 14
adapters unaffected. Existing infrastructure (configureAllHooks auto-
removal + doctor warning) already in place since v1.0.139; this commit
closes the observability gap on the silent path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants