From 2e29481d5c5b92b75b6a29eeca8a8ff8ed166af7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:22:20 -0500 Subject: [PATCH] fix(core): keep MCP quiet while a location shuts down (#60668) --- packages/core/src/mcp/index.ts | 7 ++++++- packages/core/test/mcp.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 9d1b286049d..285af0a9fc9 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -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() const entries = new Map() const attached = new Map() @@ -352,7 +357,7 @@ export const layer = (options?: Options) => const whenLive = (name: ServerName, entry: ServerEntry, connection: McpClient.Connection) => (effect: Effect.Effect) => - fork( + callbacks( Effect.suspend(() => (entry.client === connection ? effect : Effect.void)).pipe( locks.withLock(name), Effect.ignore, diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 89480a7c6e7..3cbeba6cfa3 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -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()