Skip to content

Commit 579e73f

Browse files
committed
fix(mcp): await tool registry reconciliation after mutations
MCP.add/remove/connect/disconnect returned before McpTool reconciled the new state into ToolRegistry (debounced async stream). A subsequent session/tool snapshot could still reflect the previous registry. Fix: shared mutable cell (mcp-reconcile.ts) lets McpTool register its reconcile function during layer construction; Mcp mutation methods now await it after publishing. Breaks the circular dependency (McpTool depends on Mcp) without layer wiring changes. Closes anomalyco#39902
1 parent 233fa69 commit 579e73f

3 files changed

Lines changed: 20 additions & 1 deletion

File tree

‎packages/core/src/mcp-reconcile.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Effect } from "effect"
2+
3+
// Shared reconcile cell: McpTool registers its reconcile function here during
4+
// layer construction so the MCP service can await it after mutations without
5+
// introducing a circular layer dependency (McpTool depends on Mcp).
6+
export const cell: { reconcile: Effect.Effect<void, never, never> | null } = { reconcile: null }

‎packages/core/src/mcp/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Bus } from "../bus.js"
1313
import { Environment } from "../environment/index.js"
1414
import { Form } from "../form.js"
1515
import { Integration } from "../integration.js"
16+
import { cell as mcpReconcileCell } from "../mcp-reconcile.js"
1617
import { KeyedMutex } from "../effect/keyed-mutex.js"
1718
import { Location } from "../location.js"
1819
import { waitForAbort } from "@opencode/util/process"
@@ -636,6 +637,9 @@ export const layer = (options?: Options) =>
636637
const name = ServerName.make(server)
637638
overrides.set(name, config)
638639
yield* state.reload()
640+
// Await tool registry reconciliation so a subsequent session/tool
641+
// snapshot reflects the mutation (see #39902).
642+
if (mcpReconcileCell.reconcile) yield* mcpReconcileCell.reconcile
639643
}),
640644
connect: Effect.fn("MCP.connect")(function* (server) {
641645
const name = ServerName.make(server)
@@ -644,6 +648,7 @@ export const layer = (options?: Options) =>
644648
yield* stopServer(name, target.entry)
645649
yield* startServer(name, target.entry)
646650
}).pipe(locks.withLock(name))
651+
if (mcpReconcileCell.reconcile) yield* mcpReconcileCell.reconcile
647652
}),
648653
disconnect: Effect.fn("MCP.disconnect")(function* (server) {
649654
const name = ServerName.make(server)
@@ -653,12 +658,14 @@ export const layer = (options?: Options) =>
653658
target.entry.status = { status: "disabled" }
654659
yield* bus.publish(McpEvent.StatusChanged, { server: name })
655660
}).pipe(locks.withLock(name))
661+
if (mcpReconcileCell.reconcile) yield* mcpReconcileCell.reconcile
656662
}),
657663
remove: Effect.fn("MCP.remove")(function* (server) {
658664
const name = ServerName.make(server)
659665
yield* requireServer(name)
660666
overrides.set(name, false)
661667
yield* state.reload()
668+
if (mcpReconcileCell.reconcile) yield* mcpReconcileCell.reconcile
662669
}),
663670
// Reads report what is connected now; servers still starting contribute once they publish a change.
664671
tools: Effect.fn("MCP.tools")(function* () {

‎packages/core/src/tool/mcp.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * as McpTool from "./mcp.js"
22

3+
import { cell as McpReconcile } from "../mcp-reconcile.js"
4+
35
import { ToolFailure } from "@opencode/ai"
46
import { McpEvent } from "@opencode/schema/mcp-event"
57
import { Context, Effect, Fiber, type JsonSchema, Layer, PubSub, Semaphore, Stream } from "effect"
@@ -19,6 +21,8 @@ export const name = (server: string, tool: string) => `${namespace(server)}_${to
1921
export interface Interface {
2022
/** Wait for the initial MCP tool registration to settle. */
2123
readonly flush: Effect.Effect<void>
24+
/** Reconcile MCP tools into the ToolRegistry after a mutation. */
25+
readonly reconcile: Effect.Effect<void>
2226
}
2327

2428
export class Service extends Context.Service<Service, Interface>()("@opencode/McpTool") {}
@@ -127,6 +131,8 @@ export const layer = Layer.effect(
127131
yield* tools.reload()
128132
}),
129133
)
134+
// Register for the MCP service to await after mutations.
135+
McpReconcile.reconcile = reconcile
130136

131137
// Servers announce tools in bursts and each read loads the whole catalog, so settle and refresh
132138
// once. The bus subscription stays eager; only the already-open sliding subscription is debounced.
@@ -141,7 +147,7 @@ export const layer = Layer.effect(
141147
Stream.runForEach(() => reconcile),
142148
Effect.forkScoped({ startImmediately: true }),
143149
)
144-
return Service.of({ flush: Effect.asVoid(Fiber.await(initial)) })
150+
return Service.of({ flush: Effect.asVoid(Fiber.await(initial)), reconcile })
145151
}),
146152
)
147153

0 commit comments

Comments
 (0)