From fec4f20736449bed15e5ce4e625cf952b6fa9e8d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 13 Aug 2026 22:48:09 -0400 Subject: [PATCH] perf(core): load MCP client lazily (#42468) --- packages/core/src/mcp/index.ts | 11 +++-- .../core/test/mcp-import-boundary.test.ts | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 packages/core/test/mcp-import-boundary.test.ts diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 4685bc31eb..e087c36b29 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -19,8 +19,7 @@ import { KeyedMutex } from "../effect/keyed-mutex.js" import { Location } from "../location.js" import { waitForAbort } from "@opencode-ai/util/process" import { State } from "../state.js" -import { MCPClient } from "./client.js" -import { MCPOAuth } from "./oauth.js" +import type { MCPClient } from "./client.js" export const ServerName = Schema.String.pipe(Schema.brand("MCP.ServerName")) export type ServerName = typeof ServerName.Type @@ -245,7 +244,11 @@ export const layer = (options?: Options) => draft.method.update({ integrationID, method: { id: methodID, type: "oauth", label: name }, - authorize: () => MCPOAuth.authorize({ name, config: remote, methodID }), + authorize: () => + Effect.gen(function* () { + const { MCPOAuth } = yield* Effect.promise(() => import("./oauth.js")) + return yield* MCPOAuth.authorize({ name, config: remote, methodID }) + }), }) }) .pipe(Scope.provide(scope)) @@ -264,6 +267,7 @@ export const layer = (options?: Options) => // opens a browser, so an auth-gated connect ends in UnauthorizedError -> needs_auth rather than a redirect. const connectProvider = Effect.fnUntraced(function* (entry: ServerEntry) { if (entry.config.type !== "remote" || !entry.integrationID) return undefined + const { MCPOAuth } = yield* Effect.promise(() => import("./oauth.js")) const remote = entry.config const oauth = remote.oauth || undefined const base = { @@ -505,6 +509,7 @@ export const layer = (options?: Options) => const scope = yield* Scope.fork(root) entry.scope = scope const authProvider = yield* connectProvider(entry) + const { MCPClient } = yield* Effect.promise(() => import("./client.js")) // List tools as part of connect so a failure here marks the server failed rather than // leaving it connected with a silently empty tool list and no path to recover. const result = yield* MCPClient.connect( diff --git a/packages/core/test/mcp-import-boundary.test.ts b/packages/core/test/mcp-import-boundary.test.ts new file mode 100644 index 0000000000..c48b5bde09 --- /dev/null +++ b/packages/core/test/mcp-import-boundary.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import path from "node:path" + +const root = path.resolve(import.meta.dir, "../../..") + +test("loads the MCP SDK only when connecting or authorizing", async () => { + const temporary = await mkdtemp(path.join(import.meta.dir, ".mcp-import-boundary-")) + const metafile = path.join(temporary, "meta.json") + try { + const result = Bun.spawn( + [ + process.execPath, + "build", + "packages/core/src/mcp/index.ts", + "--target=node", + "--format=esm", + "--packages=bundle", + "--splitting", + `--metafile=${metafile}`, + `--outdir=${path.join(temporary, "out")}`, + ], + { cwd: root, stdout: "pipe", stderr: "pipe" }, + ) + const [exitCode, stdout, stderr] = await Promise.all([ + result.exited, + new Response(result.stdout).text(), + new Response(result.stderr).text(), + ]) + if (exitCode !== 0) throw new Error(stdout + stderr) + + const metadata = await Bun.file(metafile).json() + const imports = metadata.inputs["packages/core/src/mcp/index.ts"].imports + const lazy = imports.filter( + (item: { original?: string }) => item.original === "./client.js" || item.original === "./oauth.js", + ) + expect(new Set(lazy.map((item: { original: string }) => item.original))).toEqual( + new Set(["./client.js", "./oauth.js"]), + ) + expect(lazy.every((item: { kind: string }) => item.kind === "dynamic-import")).toBe(true) + } finally { + await rm(temporary, { recursive: true, force: true }) + } +})