Skip to content

feat: MCP event pipeline + passive status panel - #40

Merged
ranvier2d2 merged 2 commits into
mainfrom
WIP/mcp-event-pipeline-passive-panel
Mar 30, 2026
Merged

feat: MCP event pipeline + passive status panel#40
ranvier2d2 merged 2 commits into
mainfrom
WIP/mcp-event-pipeline-passive-panel

Conversation

@ranvier2d2

@ranvier2d2 ranvier2d2 commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • MCP event pipeline: Wire MCP server startup events from all providers (Codex, Claude, OpenCode) through the runtime ingestion layer into orchestration domain events
  • Passive MCP status panel: New ThreadMcpStatusPanel UI showing real-time MCP server status per thread (Ready/Failed/Starting), gated on provider capability
  • Claude MCP support: Enable Claude MCP panel, translate MCP config through mcpTranslation, extract mcp_servers from system/init
  • Codex envelope unwrap: Handle Codex's payload.msg envelope for mcp_startup_update events
  • Status normalization: normalizeMcpState() maps provider-specific strings (connected→ready, disabled→cancelled) to canonical enum
  • Panel UX: Loading/empty states, provider-aware button visibility (hidden for Cursor)

Test plan

  • Codex: MCP button visible, panel shows 6 servers with real-time status
  • Claude: MCP button visible, panel shows servers from system/init (Ready/Failed)
  • OpenCode: MCP button visible, panel shows servers via HTTP fetch
  • Cursor: MCP button correctly hidden
  • Empty panel shows "No MCP servers detected"
  • bun typecheck / bun lint / bun fmt pass
  • New tests for mcpTranslation, ClaudeAdapter, ProviderRuntimeIngestion, mcp-session-logic

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • Added MCP server support for Claude agent integration and per-session MCP config generation
    • Improved MCP status handling and background refresh states with clearer loading/error visuals
    • Added support for HTTP headers and timeout on remote MCP servers
    • Consistent MCP server name normalization and friendlier display names
  • Documentation

    • Added QA testing guidance for validating the UI with Playwright
  • Tests

    • Added tests covering MCP config resolution and runtime event handling

Wire Claude provider MCP config through mcpTranslation, add MCP config
resolution in ClaudeAdapter, and harden the runtime ingestion pipeline
for MCP status events across all providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds Claude-specific MCP support: name normalization, MCP config translation and file generation, Claude CLI flag propagation, runtime ingestion of configured MCP server states into normalized activities, UI refresh-state improvements, and related tests across server, harness, and web layers.

Changes

Cohort / File(s) Summary
Docs
AGENTS.md
Added QA Testing guidance for Playwright validation and local-dev assumptions.
Claude harness (Elixir)
apps/harness/lib/harness/providers/claude_session.ex, apps/harness/lib/harness/providers/opencode_session.ex
Appended --mcp-config CLI arg when provided; emit/rehydrate MCP startup statuses; persist mcp config version; tag events with opencode_session_id.
Provider runtime ingestion
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
Normalize MCP server names; map configured MCP lifecycle states; emit mcp.status.updated activities per configured server; added test for varied server states.
Claude adapter & tests
apps/server/src/provider/Layers/ClaudeAdapter.ts, apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Snapshot per-thread MCP config, create generated directory, write .mcp.json, pass config path via query options; added tests asserting file creation and contents.
Harness client adapter
apps/server/src/provider/Layers/HarnessClientAdapter.ts
Added claudeAgent branch to generate/write claude MCP config and return providerOptions { claudeAgent: { configPath } }.
MCP config service & tests
apps/server/src/provider/Layers/McpConfig.ts, apps/server/src/provider/Layers/McpConfig.test.ts
Normalized optional headers and timeout fields; added asPositiveNumber validator and tests for remote server normalization.
MCP translation & tests
apps/server/src/provider/mcpTranslation.ts, apps/server/src/provider/mcpTranslation.test.ts
Added claudeConfigFromResolved generating Claude mcpServers JSON; include headers/timeout in open-code output; extended generatedMcpDir to accept claudeAgent; tests updated/added.
Server contracts
packages/contracts/src/provider.ts
Extended McpRemoteServerConfig schema to optionally accept headers and timeout.
Shared normalization
packages/shared/src/mcp.ts
Added normalizeMcpServerName and updated humanizeMcpServerName to normalize before humanizing; added alias map.
Web UI & view-model
apps/web/src/components/ThreadMcpStatusPanel.tsx, apps/web/src/mcp-session-logic.ts, apps/web/src/mcp-session-logic.test.ts
Use normalized server names in status aggregation; replace isFetching with refreshState union; update rendering and tests to reflect normalization and new states.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Web as Web UI
    participant Adapter as Server Adapter
    participant McpSvc as MCP Config Service
    participant FS as Filesystem
    participant Claude as Claude CLI
    participant Orch as Orchestration

    User->>Web: Start Claude session (threadId)
    Web->>Adapter: startSession(threadId)
    Adapter->>McpSvc: getSnapshot(threadId)
    McpSvc-->>Adapter: resolved MCP config
    Adapter->>FS: mkdir generatedMcpDir(..., "claudeAgent", threadId)
    Adapter->>FS: write .mcp.json (claudeConfigFromResolved)
    FS-->>Adapter: file created
    Adapter->>Claude: createQuery(..., mcpConfig: path)
    Claude-->>Orch: emit session.configured (mcp_servers)
    Orch->>Orch: normalize server names & map states
    Orch-->>Web: emit mcp.status.updated activities
    Web->>Web: deriveMcpSessionViewModel (normalize names)
    Web-->>User: render ThreadMcpStatusPanel (refreshState/servers)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

size:XXL

Poem

🐇 I bounded through configs, tidy and spry,
Wrote .mcp.json beneath the sky,
Names made tidy, statuses told,
CLI flags set, and tests enrolled —
A rabbit cheers: the MCP hops by!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: adding an MCP event pipeline and introducing a passive status panel.
Description check ✅ Passed The description comprehensively covers all template sections with clear explanations of what changed, why, and includes an extensive test plan with checkbox validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch WIP/mcp-event-pipeline-passive-panel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 30, 2026

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +93 to +112
export function claudeConfigFromResolved(config: ResolvedMcpConfig): string {
const payload = {
mcpServers: Object.fromEntries(
config.servers.map((server) => [
server.name,
server.transport === "stdio"
? {
command: server.command,
args: server.args ?? [],
...(server.env ? { env: server.env } : {}),
}
: {
type: server.transport,
url: server.url,
},
]),
),
};
return `${JSON.stringify(payload, null, 2)}\n`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 claudeConfigFromResolved includes disabled MCP servers in Claude config

claudeConfigFromResolved maps all servers from the resolved config into the .mcp.json output, including servers with enabled: false. Unlike codexTomlFromResolved (which emits enabled = false) and openCodeConfigFromResolved (which emits enabled: false), the Claude format has no enabled field, so disabled servers silently become active when passed to the Claude CLI.

Example: disabled SSE server included without any disable indicator

The sseServer fixture has enabled: false, but claudeConfigFromResolved produces:

"event-stream": { "type": "sse", "url": "https://events.example.com/stream" }

The Claude CLI will treat this as an active server and attempt to connect to it.

Suggested change
export function claudeConfigFromResolved(config: ResolvedMcpConfig): string {
const payload = {
mcpServers: Object.fromEntries(
config.servers.map((server) => [
server.name,
server.transport === "stdio"
? {
command: server.command,
args: server.args ?? [],
...(server.env ? { env: server.env } : {}),
}
: {
type: server.transport,
url: server.url,
},
]),
),
};
return `${JSON.stringify(payload, null, 2)}\n`;
}
export function claudeConfigFromResolved(config: ResolvedMcpConfig): string {
const payload = {
mcpServers: Object.fromEntries(
config.servers
.filter((server) => server.enabled)
.map((server) => [
server.name,
server.transport === "stdio"
? {
command: server.command,
args: server.args ?? [],
...(server.env ? { env: server.env } : {}),
}
: {
type: server.transport,
url: server.url,
},
]),
),
};
return `${JSON.stringify(payload, null, 2)}\n`;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +171 to 174
{showEmptyState ? (
<p className="py-4 text-center text-sm text-muted-foreground">
No MCP servers detected for this session.
</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 "No MCP servers detected" empty state shown while background refresh is still in progress

The refactored ThreadMcpStatusPanel renders the background refresh banner (showBackgroundRefresh) and the empty-state message (showEmptyState) as independent, non-exclusive conditions. When the fetch is still in-flight and no event-derived servers exist yet, both "Refreshing live MCP status" and "No MCP servers detected for this session" are shown simultaneously. The old code (apps/web/src/components/ThreadMcpStatusPanel.tsx:143-148 in the left diff) used a mutually exclusive ternary that showed "Loading MCP status..." exclusively during fetch.

Suggested change
{showEmptyState ? (
<p className="py-4 text-center text-sm text-muted-foreground">
No MCP servers detected for this session.
</p>
{showEmptyState && !showBackgroundRefresh ? (
<p className="py-4 text-center text-sm text-muted-foreground">
No MCP servers detected for this session.
</p>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (1)
apps/web/src/mcp-session-logic.ts (1)

218-218: Consider normalizing server name in runtime.warning handler.

At line 218, detail.server from parseMcpStartupWarningDetail is used directly without normalization. If the warning's server name differs from the normalized name (e.g., "claude.ai OpenAI Documentation" vs "openaiDeveloperDocs"), the warning state will be stored under a different key than the status state, causing a mismatch.

Consider normalizing for consistency:

♻️ Proposed fix
       hasAnyMcpActivity = true;
-      const server = ensureServerState(byServer, detail.server);
+      const server = ensureServerState(byServer, normalizeMcpServerName(detail.server));
       server.warningMessage = message;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/mcp-session-logic.ts` at line 218, The runtime.warning handler
is passing detail.server from parseMcpStartupWarningDetail directly into
ensureServerState, which can create a different key than the normalized server
name used elsewhere; update the handler to normalize detail.server (using the
same normalization routine used for status keys — e.g., the existing
normalizeServerName/normalizeServerKey function or the same code path used when
setting status) before calling ensureServerState so warnings and statuses share
the same server key (apply normalization to detail.server immediately after
parseMcpStartupWarningDetail and use that normalized value in ensureServerState
and any subsequent lookups).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`:
- Around line 271-273: The new case in ProviderRuntimeIngestion that directly
calls sessionConfiguredMcpActivities for "session.configured" must be routed
through the centralized providerManager coordination point; replace the direct
call in the switch case (the "session.configured" branch in
ProviderRuntimeIngestion) with a call into the providerManager API (or a small
delegating function in providerManager) that accepts the same event and
maybeSequence and is responsible for invoking sessionConfiguredMcpActivities and
any thread-event logging. Update providerManager.ts to expose a handler (e.g.,
handleSessionConfigured or a generic dispatchThreadEvent) that performs the
sessionConfiguredMcpActivities call and logs the thread event, then call that
handler from ProviderRuntimeIngestion instead of calling
sessionConfiguredMcpActivities directly.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts`:
- Around line 173-175: Remove the custom ClaudeQueryOptionsWithMcp type and stop
using mcpConfig; instead construct and pass an mcpServers value (Record<string,
McpServerConfig>) directly into the query options you send to createQuery.
Update the code that builds the options for createQuery (the place currently
using ClaudeQueryOptionsWithMcp / mcpConfig and writing the .mcp.json) to
load/transform the existing MCP config into the correct mcpServers shape (keys =
server names, values = McpServerConfig) and include that mcpServers property on
the options object passed to createQuery; delete the ClaudeQueryOptionsWithMcp
type and any mcpConfig usage so the SDK receives the supported mcpServers field.

In `@apps/server/src/provider/mcpTranslation.test.ts`:
- Around line 198-210: The test shows a disabled MCP server ("event-stream" with
enabled: false) being serialized into claudeConfigFromResolved output; update
claudeConfigFromResolved to ignore servers where enabled is false (e.g., filter
the resolved servers map/array before building mcpServers) so only enabled
entries are included, and adjust the test expectation to assert that only the
enabled "remote-api" is present; reference the claudeConfigFromResolved function
and the makeConfig/httpServer/sseServer fixtures when locating the change.
- Around line 39-43: The Claude remote-MCP translation test currently strips out
the remote object's headers (and incorrectly expects timeout to be preserved) —
update the Claude translation assertion that currently expects just { type, url
} to include the headers field (e.g., { type, url, headers: { Authorization:
"...", "x-ref-api-key": "..." } }) and ensure timeout is not expected (timeout
is global-only), so the test asserts headers are preserved and timeout is
absent; change the expected value in the Claude translation assertion
accordingly.

In `@apps/server/src/provider/mcpTranslation.ts`:
- Around line 81-84: The current object spread emits server.timeout when typeof
server.timeout === "number", which can allow NaN/Infinity/zero/negative values;
update the guard to only emit timeout if Number.isFinite(server.timeout) &&
server.timeout > 0 (i.e., replace the typeof check with a finite positive-number
check for the server.timeout property used in the spread), so the constructed
config only contains valid positive finite timeout values.

In `@apps/web/src/components/ThreadMcpStatusPanel.tsx`:
- Around line 102-110: The fetchedServers state is not cleared when threadId
changes, causing stale server entries to persist; inside the useEffect that
depends on threadId (the one using ensureNativeApi().mcp.status(threadId)),
clear fetchedServers immediately when the effect runs (call setFetchedServers to
an empty array or initial value) before starting the async request and keep the
existing cancellation logic (cancelled flag and setting refreshState). This
ensures convertFetchedStatus(data) only populates results for the current
threadId and prevents showing stale entries on thread switches or refresh
failures.

---

Nitpick comments:
In `@apps/web/src/mcp-session-logic.ts`:
- Line 218: The runtime.warning handler is passing detail.server from
parseMcpStartupWarningDetail directly into ensureServerState, which can create a
different key than the normalized server name used elsewhere; update the handler
to normalize detail.server (using the same normalization routine used for status
keys — e.g., the existing normalizeServerName/normalizeServerKey function or the
same code path used when setting status) before calling ensureServerState so
warnings and statuses share the same server key (apply normalization to
detail.server immediately after parseMcpStartupWarningDetail and use that
normalized value in ensureServerState and any subsequent lookups).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 279db2f5-0035-48c9-ae71-3b0c74f03e88

📥 Commits

Reviewing files that changed from the base of the PR and between ba144f7 and 441b918.

📒 Files selected for processing (16)
  • AGENTS.md
  • apps/harness/lib/harness/providers/claude_session.ex
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.test.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.ts
  • apps/server/src/provider/Layers/HarnessClientAdapter.ts
  • apps/server/src/provider/Layers/McpConfig.test.ts
  • apps/server/src/provider/Layers/McpConfig.ts
  • apps/server/src/provider/mcpTranslation.test.ts
  • apps/server/src/provider/mcpTranslation.ts
  • apps/web/src/components/ThreadMcpStatusPanel.tsx
  • apps/web/src/mcp-session-logic.test.ts
  • apps/web/src/mcp-session-logic.ts
  • packages/contracts/src/provider.ts
  • packages/shared/src/mcp.ts

Comment on lines +271 to +273
case "session.configured": {
return sessionConfiguredMcpActivities(event, maybeSequence);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Coordinate new session.configured thread-activity logging via providerManager.ts.

This adds another thread event logging path in ProviderRuntimeIngestion, which diverges from the required coordination point. Please broker this coordination through apps/server/src/providerManager.ts (or delegate from there) to keep event logging centralized.

As per coding guidelines: "Provider dispatch and thread event logging must be coordinated in apps/server/src/providerManager.ts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` around
lines 271 - 273, The new case in ProviderRuntimeIngestion that directly calls
sessionConfiguredMcpActivities for "session.configured" must be routed through
the centralized providerManager coordination point; replace the direct call in
the switch case (the "session.configured" branch in ProviderRuntimeIngestion)
with a call into the providerManager API (or a small delegating function in
providerManager) that accepts the same event and maybeSequence and is
responsible for invoking sessionConfiguredMcpActivities and any thread-event
logging. Update providerManager.ts to expose a handler (e.g.,
handleSessionConfigured or a generic dispatchThreadEvent) that performs the
sessionConfiguredMcpActivities call and logs the thread event, then call that
handler from ProviderRuntimeIngestion instead of calling
sessionConfiguredMcpActivities directly.

Comment on lines +173 to +175
type ClaudeQueryOptionsWithMcp = ClaudeQueryOptions & {
readonly mcpConfig?: string;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For the exact version of @anthropic-ai/claude-agent-sdkused by this repository, doesquery({ options })support anmcpConfig option? If yes, what type does it expect and how is it applied?

💡 Result:

No, the query({ options }) method in @anthropic-ai/claude-agent-sdk does not support an mcpConfig option. It supports mcpServers within the options object for configuring MCP servers. The mcpServers option expects a Record<string, McpServerConfig>, where keys are server names (e.g., "github") and values are configurations like: typescript mcpServers: { "github": { type: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] } } This configuration is passed to the underlying Claude Code CLI subprocess spawned by query, which uses it to connect MCP servers, enabling tools from those servers (with allowedTools for permissions). mcpConfig appears in CLI contexts (e.g., --mcp-config flag or .mcp.json files auto-loaded by the SDK), but not as a direct query option. No evidence of mcpConfig in SDK docs, types, or code examples across versions up to 0.2.87 (latest as of 2026-03-30). The query cannot identify the "exact version used by this repository" without the specific repo, but the API is consistent across documented versions.

Citations:


Remove mcpConfig from query options; use mcpServers instead.

Lines 173-175 extend ClaudeQueryOptions with an unsupported field. The pinned @anthropic-ai/claude-agent-sdk does NOT accept mcpConfig as a query option—it only supports mcpServers (a Record<string, McpServerConfig> of server names to configurations). The generated .mcp.json file and type extension at lines 173-175 will have no effect on the SDK's behavior, and tests mocking createQuery will not catch this since they don't execute the real SDK. Replace the mcpConfig approach with direct mcpServers configuration in the query options passed at lines 2828-2845.

Also applies to: 2828-2845

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts` around lines 173 - 175,
Remove the custom ClaudeQueryOptionsWithMcp type and stop using mcpConfig;
instead construct and pass an mcpServers value (Record<string, McpServerConfig>)
directly into the query options you send to createQuery. Update the code that
builds the options for createQuery (the place currently using
ClaudeQueryOptionsWithMcp / mcpConfig and writing the .mcp.json) to
load/transform the existing MCP config into the correct mcpServers shape (keys =
server names, values = McpServerConfig) and include that mcpServers property on
the options object passed to createQuery; delete the ClaudeQueryOptionsWithMcp
type and any mcpConfig usage so the SDK receives the supported mcpServers field.

Comment on lines +39 to +43
headers: {
Authorization: "Bearer secret-token",
"x-ref-api-key": "ref-123",
},
timeout: 2500,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the current Claude Code / @anthropic-ai/claude-agent-sdkMCP config schema, what fields are supported for remote HTTP/SSE entries in.mcp.json? Specifically, are headersandtimeout allowed?

💡 Result:

In the current Claude Code / @anthropic-ai/claude-agent-sdk MCP config schema for .mcp.json, remote HTTP/SSE entries support the following fields: - type: "http" or "sse" (required) - url: string (required) - headers?: Record<string, string> (optional) The 'timeout' field is not supported directly in the per-server configuration for remote HTTP/SSE entries. Timeouts are handled via environment variables like MCP_TIMEOUT, which applies globally or to stdio servers, but not explicitly to remote connections in the schema.

Citations:


Preserve headers in Claude's remote MCP translation.

Lines 39-43 add headers and timeout to the remote fixture. Claude's MCP schema supports headers as an optional field for remote servers, but not timeout (which is only configurable globally via environment variables). The Claude translation at lines 203-206 currently drops both fields to { type, url }, but should preserve headers to maintain authentication for remote servers across both OpenCode and Claude implementations.

Code reference
  headers: {
    Authorization: "Bearer secret-token",
    "x-ref-api-key": "ref-123",
  },
  timeout: 2500,

Lines 203-206 should include headers in the assertion.

Also applies to: 198-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/mcpTranslation.test.ts` around lines 39 - 43, The
Claude remote-MCP translation test currently strips out the remote object's
headers (and incorrectly expects timeout to be preserved) — update the Claude
translation assertion that currently expects just { type, url } to include the
headers field (e.g., { type, url, headers: { Authorization: "...",
"x-ref-api-key": "..." } }) and ensure timeout is not expected (timeout is
global-only), so the test asserts headers are preserved and timeout is absent;
change the expected value in the Claude translation assertion accordingly.

Comment on lines +198 to +210
it.effect("claudeConfigFromResolved maps remote server with explicit transport type", () =>
Effect.sync(() => {
const result = claudeConfigFromResolved(makeConfig([httpServer, sseServer]));
const parsed = JSON.parse(result) as { mcpServers: Record<string, Record<string, unknown>> };

assert.deepEqual(parsed.mcpServers["remote-api"], {
type: "http",
url: "https://api.example.com/mcp",
});
assert.deepEqual(parsed.mcpServers["event-stream"], {
type: "sse",
url: "https://events.example.com/stream",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t serialize disabled MCP servers into Claude config.

This expectation still requires event-stream even though the fixture marks it enabled: false. The translation should filter disabled entries before building mcpServers; otherwise a server the user turned off gets handed back to Claude as active configuration.

Expected assertion once disabled entries are filtered out
     assert.deepEqual(parsed.mcpServers["remote-api"], {
       type: "http",
       url: "https://api.example.com/mcp",
     });
-    assert.deepEqual(parsed.mcpServers["event-stream"], {
-      type: "sse",
-      url: "https://events.example.com/stream",
-    });
+    assert.isUndefined(parsed.mcpServers["event-stream"]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/mcpTranslation.test.ts` around lines 198 - 210, The
test shows a disabled MCP server ("event-stream" with enabled: false) being
serialized into claudeConfigFromResolved output; update claudeConfigFromResolved
to ignore servers where enabled is false (e.g., filter the resolved servers
map/array before building mcpServers) so only enabled entries are included, and
adjust the test expectation to assert that only the enabled "remote-api" is
present; reference the claudeConfigFromResolved function and the
makeConfig/httpServer/sseServer fixtures when locating the change.

Comment on lines +81 to +84
...("headers" in server && server.headers ? { headers: server.headers } : {}),
...("timeout" in server && typeof server.timeout === "number"
? { timeout: server.timeout }
: {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten timeout emission guard to finite positive numbers only.

Current check allows any number value. Emitting NaN/Infinity/non-positive values can produce invalid downstream config payloads.

Suggested fix
-              ...("timeout" in server && typeof server.timeout === "number"
-                ? { timeout: server.timeout }
-                : {}),
+              ...("timeout" in server &&
+              typeof server.timeout === "number" &&
+              Number.isFinite(server.timeout) &&
+              server.timeout > 0
+                ? { timeout: server.timeout }
+                : {}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
...("headers" in server && server.headers ? { headers: server.headers } : {}),
...("timeout" in server && typeof server.timeout === "number"
? { timeout: server.timeout }
: {}),
...("headers" in server && server.headers ? { headers: server.headers } : {}),
...("timeout" in server &&
typeof server.timeout === "number" &&
Number.isFinite(server.timeout) &&
server.timeout > 0
? { timeout: server.timeout }
: {}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/provider/mcpTranslation.ts` around lines 81 - 84, The current
object spread emits server.timeout when typeof server.timeout === "number",
which can allow NaN/Infinity/zero/negative values; update the guard to only emit
timeout if Number.isFinite(server.timeout) && server.timeout > 0 (i.e., replace
the typeof check with a finite positive-number check for the server.timeout
property used in the spread), so the constructed config only contains valid
positive finite timeout values.

Comment on lines 102 to +110
useEffect(() => {
let cancelled = false;
setIsFetching(true);
setRefreshState("refreshing");
ensureNativeApi()
.mcp.status(threadId)
.then((data) => {
if (!cancelled) setFetchedServers(convertFetchedStatus(data));
if (cancelled) return;
setFetchedServers(convertFetchedStatus(data));
setRefreshState("ready");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear fetched MCP server cache when threadId changes.

fetchedServers persists across thread switches, so stale entries can briefly appear (or remain on refresh failure) for the wrong thread.

Suggested fix
   useEffect(() => {
     let cancelled = false;
+    setFetchedServers([]);
     setRefreshState("refreshing");
     ensureNativeApi()
       .mcp.status(threadId)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
let cancelled = false;
setIsFetching(true);
setRefreshState("refreshing");
ensureNativeApi()
.mcp.status(threadId)
.then((data) => {
if (!cancelled) setFetchedServers(convertFetchedStatus(data));
if (cancelled) return;
setFetchedServers(convertFetchedStatus(data));
setRefreshState("ready");
useEffect(() => {
let cancelled = false;
setFetchedServers([]);
setRefreshState("refreshing");
ensureNativeApi()
.mcp.status(threadId)
.then((data) => {
if (cancelled) return;
setFetchedServers(convertFetchedStatus(data));
setRefreshState("ready");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ThreadMcpStatusPanel.tsx` around lines 102 - 110, The
fetchedServers state is not cleared when threadId changes, causing stale server
entries to persist; inside the useEffect that depends on threadId (the one using
ensureNativeApi().mcp.status(threadId)), clear fetchedServers immediately when
the effect runs (call setFetchedServers to an empty array or initial value)
before starting the async request and keep the existing cancellation logic
(cancelled flag and setting refreshState). This ensures
convertFetchedStatus(data) only populates results for the current threadId and
prevents showing stale entries on thread switches or refresh failures.

… ID attribution

Research-driven improvements to OpenCode harness resilience:

1. SSE reconnect MCP rehydration: After SSE reconnect (and initial setup),
   fetch GET /mcp and re-emit mcpServer/startupStatus/updated events so the
   MCP panel recovers from forward-only SSE gaps. Previously the panel went
   stale after any SSE connection drop.

2. MCP config version in resumeCursor: Include the config fingerprint in
   persist_binding so on resume the harness can detect config drift and
   decide if MCP servers need reconfiguration.

3. Session ID attribution: Tag all emitted events with opencode_session_id
   via Map.put_new in emit_event. Prepares for shared-runtime (multiple
   sessions per opencode serve process) where events need explicit scoping.

Co-Authored-By: Bastian Venegas Arevalo <r2d2@ranvier-technologies.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/harness/lib/harness/providers/opencode_session.ex`:
- Around line 1645-1664: The persisted mcpConfigVersion is being written via
maybe_put_mcp_config_version into cursor_json and upserted with
Harness.Storage.upsert_binding, but the resume/reuse logic still only reads
sessionId; update the resume path that fetches the binding to also read and
compare the stored "mcpConfigVersion" against the current
state.mcp_config["version"] (or treat missing/unequal as a mismatch) and only
reuse the stored sessionId when the versions match; otherwise fall back to
creating a fresh OpenCode session so the MCP config change triggers
reconfiguration.
- Around line 228-231: Instead of calling rehydrate_mcp_status(state) directly
inside your setup/reconnect code paths (which races with older SSE messages),
change those direct calls to send/queue a :rehydrate_mcp_status message to the
process and implement handle_info(:rehydrate_mcp_status, state) to call
rehydrate_mcp_status(state) and return {:noreply, state}; update each place
mentioned (the setup/reconnect locations where rehydrate_mcp_status/1 is
currently invoked, including the spots around the current calls and any
reconnect handlers) so wait_for_ready/2 no longer depends on the immediate HTTP
call and the MCP snapshot is processed only after the mailbox’s live backlog is
drained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd87d811-36bb-46f0-84b0-f51951364c38

📥 Commits

Reviewing files that changed from the base of the PR and between 441b918 and eed3553.

📒 Files selected for processing (1)
  • apps/harness/lib/harness/providers/opencode_session.ex

Comment on lines +228 to +231
# Rehydrate MCP server status from REST API so the panel
# populates immediately — SSE is forward-only and won't replay
# startup events that fired before we connected.
rehydrate_mcp_status(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Queue MCP rehydration after the live backlog, not inside setup/reconnect.

Calling rehydrate_mcp_status/1 inline from Line 231 and Line 329 emits the /mcp snapshot before older SSE updates already sitting in the mailbox are drained. The MCP view is last-write-wins per server, so those older queued events can overwrite the fresher rehydrated state and leave the panel stale after connect/reconnect. It also makes wait_for_ready/2 depend on an extra best-effort HTTP call.

💡 Suggested change
-            rehydrate_mcp_status(state)
+            send(self(), :rehydrate_mcp_status)
-    rehydrate_mcp_status(state)
+    send(self(), :rehydrate_mcp_status)
`@impl` true
def handle_info(:rehydrate_mcp_status, state) do
  rehydrate_mcp_status(state)
  {:noreply, state}
end

Also applies to: 327-329, 1666-1711

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/harness/lib/harness/providers/opencode_session.ex` around lines 228 -
231, Instead of calling rehydrate_mcp_status(state) directly inside your
setup/reconnect code paths (which races with older SSE messages), change those
direct calls to send/queue a :rehydrate_mcp_status message to the process and
implement handle_info(:rehydrate_mcp_status, state) to call
rehydrate_mcp_status(state) and return {:noreply, state}; update each place
mentioned (the setup/reconnect locations where rehydrate_mcp_status/1 is
currently invoked, including the spots around the current calls and any
reconnect handlers) so wait_for_ready/2 no longer depends on the immediate HTTP
call and the MCP snapshot is processed only after the mailbox’s live backlog is
drained.

Comment on lines +1645 to +1664
# Only persist durable identifiers — port is ephemeral and stale after restart.
# Include mcpConfigVersion so on resume we can detect config drift.
cursor_json =
Jason.encode!(%{
"threadId" => state.thread_id,
"sessionId" => state.opencode_session_id
})
Jason.encode!(
%{
"threadId" => state.thread_id,
"sessionId" => state.opencode_session_id
}
|> maybe_put_mcp_config_version(state)
)

Harness.Storage.upsert_binding(state.thread_id, state.provider, cursor_json)
end

defp maybe_put_mcp_config_version(cursor, state) do
case state.mcp_config do
%{"version" => v} when is_binary(v) -> Map.put(cursor, "mcpConfigVersion", v)
_ -> cursor
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persisting mcpConfigVersion here does not change resume behavior yet.

These lines write the fingerprint, but the resume path still extracts only sessionId and reuses the session whenever it exists. A changed MCP config will therefore reopen the stale OpenCode session and skip reconfiguration. Please treat the persisted version as part of the reuse key, and fall back to a fresh session when it is missing or different.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/harness/lib/harness/providers/opencode_session.ex` around lines 1645 -
1664, The persisted mcpConfigVersion is being written via
maybe_put_mcp_config_version into cursor_json and upserted with
Harness.Storage.upsert_binding, but the resume/reuse logic still only reads
sessionId; update the resume path that fetches the binding to also read and
compare the stored "mcpConfigVersion" against the current
state.mcp_config["version"] (or treat missing/unequal as a mismatch) and only
reuse the stored sessionId when the versions match; otherwise fall back to
creating a fresh OpenCode session so the MCP config change triggers
reconfiguration.

@ranvier2d2
ranvier2d2 merged commit 41a4dde into main Mar 30, 2026
7 checks passed
@ranvier2d2
ranvier2d2 deleted the WIP/mcp-event-pipeline-passive-panel branch March 30, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant