Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c388c0
fix(mcp): fix cancelPending key mismatch, add lifecycle tests
kitlangton Mar 25, 2026
c551c60
refactor(mcp): effectify MCP service with InstanceState
kitlangton Mar 25, 2026
5b9906a
docs: mark MCP as effectified in migration checklist
kitlangton Mar 25, 2026
74b57f8
fix: add definite assignment assertion for mock client state
kitlangton Mar 25, 2026
7d88939
refactor(mcp): push Effect deeper into MCP service
kitlangton Mar 25, 2026
204059e
refactor(mcp): deepen Effect usage, reduce Effect.promise calls
kitlangton Mar 25, 2026
6c905a4
refactor(mcp): effectify McpAuth with AppFileSystem
kitlangton Mar 25, 2026
6c45a55
refactor(mcp): wire McpAuth.Service as layer dependency, drop Effect.…
kitlangton Mar 25, 2026
50ac5b6
fix(mcp): auto-reconnect MCP servers after transient listTools failure
kitlangton Mar 25, 2026
5980aeb
refactor(mcp): move OAuth impl into Effect layer, eliminate async impls
kitlangton Mar 25, 2026
3188679
cleanup(mcp): simplify from review feedback
kitlangton Mar 25, 2026
07c1cb0
cleanup(mcp): final review fixes
kitlangton Mar 25, 2026
023ca69
fix(mcp): use Effect.ignore for reconnection fork (Effect.catch can't…
kitlangton Mar 25, 2026
f8b5049
fix(mcp): close dead client before reconnection to prevent orphaned p…
kitlangton Mar 25, 2026
eac9139
refactor(mcp): let createAndStore own the full close→create→store lif…
kitlangton Mar 25, 2026
a80d7ec
cleanup(mcp): use Effect.fnUntraced for helpers, run prettier
kitlangton Mar 25, 2026
db1fe72
feat(mcp): cache tool definitions, refresh via ToolListChangedNotific…
kitlangton Mar 25, 2026
db13970
refactor(mcp): extract updateField/clearField helpers in McpAuth
kitlangton Mar 25, 2026
7bfe822
Merge branch 'dev' into kit/mcp-tests-and-fixes
kitlangton Mar 25, 2026
0d72654
Merge branch 'dev' into kit/mcp-tests-and-fixes
kitlangton Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(mcp): auto-reconnect MCP servers after transient listTools failure
When tools() encounters a listTools() failure, fork a background
reconnection fiber via createAndStore() instead of permanently
removing the server. The current turn gets no tools from that server,
but the background fiber re-establishes the connection so tools are
available on the next turn.

Previously, a single transient error (network blip, server restart)
would permanently delete the MCP server from the session with no
recovery path.
  • Loading branch information
kitlangton committed Mar 25, 2026
commit 50ac5b62f83f9cf6be6e01652c55e4fe260f827f
16 changes: 14 additions & 2 deletions packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { BusEvent } from "../bus/bus-event"
import { Bus } from "@/bus"
import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open"
import { Effect, Layer, ServiceMap, Stream } from "effect"
import { Effect, Layer, Scope, ServiceMap, Stream } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRunPromise } from "@/effect/run-service"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
Expand Down Expand Up @@ -464,6 +464,7 @@ export namespace MCP {
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const scope = yield* Scope.Scope
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const auth = yield* McpAuth.Service

Expand Down Expand Up @@ -658,7 +659,18 @@ export namespace MCP {
return e
},
}).pipe(Effect.option)
if (toolsResult._tag === "None") return

if (toolsResult._tag === "None") {
// Fork background reconnection — tools come back next turn
const mcpConfig = config[clientName]
if (mcpConfig && isMcpConfigured(mcpConfig)) {
yield* createAndStore(clientName, mcpConfig).pipe(
Effect.catch(() => Effect.void),
Effect.forkIn(scope),
)
}
return
}

const mcpConfig = config[clientName]
const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined
Expand Down
13 changes: 8 additions & 5 deletions packages/opencode/test/mcp/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function withInstance(
// Bug #1: tools() silently deletes clients on transient listTools() failure
// ========================================================================

test.todo(
test(
"tools() removes client from state when listTools fails",
withInstance(
{
Expand Down Expand Up @@ -216,9 +216,10 @@ test.todo(
serverState.listToolsShouldFail = true
await MCP.tools()

// After the transient error clears, tools should recover
// automatically — the server shouldn't be permanently removed.
// After the transient error clears, the background reconnection
// fiber should restore the server. Give it a tick to complete.
serverState.listToolsShouldFail = false
await new Promise((r) => setTimeout(r, 100))

const toolsRecovered = await MCP.tools()
expect(Object.keys(toolsRecovered).length).toBeGreaterThan(0)
Expand All @@ -230,7 +231,7 @@ test.todo(
// Bug #2: status() shows stale data after tools() deletes a client
// ========================================================================

test.todo(
test(
"status shows 'failed' after tools() encounters a transient error",
withInstance(
{
Expand All @@ -256,7 +257,9 @@ test.todo(
await MCP.tools()
serverState.listToolsShouldFail = false

// After the error clears, status should recover automatically
// Give background reconnection fiber time to complete
await new Promise((r) => setTimeout(r, 100))

await MCP.tools()
const statusAfter = await MCP.status()
expect(statusAfter["status-server"]?.status).toBe("connected")
Expand Down
Loading