fix(core): keep MCP quiet while a location shuts down (#60668)

This commit is contained in:
Aiden Cline
2026-10-30 14:22:20 -05:00
committed by GitHub
Unverified
parent bf5d12c588
commit 2e29481d5c
2 changed files with 39 additions and 1 deletions
+6 -1
View File
@@ -184,6 +184,11 @@ export const layer = (options?: Options) =>
// Server scopes close in parallel on shutdown, so one slow server's stop grace does not delay
// the rest past a service restart's kill deadline and leave them orphaned.
const servers = yield* Scope.fork(root, "parallel")
// Connection callbacks run here. Created after `servers`, so the layer scope closes this set first
// (finalizers run in reverse): in-flight callbacks are interrupted and later ones never start, so
// connections closing with the Location publish nothing. Clients re-fetch on MCP events, which
// would boot the Location straight back up.
const callbacks = yield* FiberSet.makeRuntime<never, void, never>()
const entries = new Map<ServerName, ServerEntry>()
const attached = new Map<ServerName, AttachedEntry[]>()
@@ -352,7 +357,7 @@ export const layer = (options?: Options) =>
const whenLive =
(name: ServerName, entry: ServerEntry, connection: McpClient.Connection) =>
<E>(effect: Effect.Effect<void, E>) =>
fork(
callbacks(
Effect.suspend(() => (entry.client === connection ? effect : Effect.void)).pipe(
locks.withLock(name),
Effect.ignore,
+33
View File
@@ -1883,6 +1883,39 @@ test("isolates nested configured MCP mutations and reconciles them", async () =>
)
})
test("publishes MCP changes while live but not while the location shuts down", async () => {
const published: string[] = []
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const server = yield* resourceServer({ listChanged: true })
const scope = yield* Scope.make()
const context = yield* Layer.buildWithScope(
resourceMcpLayer(server.url, undefined, undefined, { published }),
scope,
)
yield* Effect.gen(function* () {
const service = yield* Mcp.Service
expect(yield* settled(service)).toEqual({ status: "connected" })
published.length = 0
yield* Effect.promise(server.sendResourceListChanged)
yield* Effect.sync(() => published).pipe(
Effect.filterOrFail(
(types) => types.includes(McpEvent.ResourcesChanged.type),
() => new Error("resource change was not published"),
),
Effect.retry({ times: 200, schedule: Schedule.spaced("10 millis") }),
)
}).pipe(Effect.provide(context))
published.length = 0
// Location shutdown closes plugin registrations under State.shutdown, so config does not reconcile.
yield* State.shutdown(Scope.close(scope, Exit.void))
expect(published.filter((type) => type.startsWith("mcp."))).toEqual([])
}),
),
)
})
testEffect(Layer.empty).live("batches MCP transforms without connecting intermediate configurations", () =>
Effect.gen(function* () {
const server = yield* resourceServer()