From 22dd18066fc7fd90b661af70b81e4507555917f7 Mon Sep 17 00:00:00 2001 From: JF Date: Fri, 21 Aug 2026 21:36:01 -0400 Subject: [PATCH] perf(startup): lazy-import SSE/HTTP transport stacks in CLI actions stdio mode (every claude mcp add config) no longer parses and instantiates express + the SDK's streamableHttp/sse/express modules it never uses; the sse/http command modules are imported inside their commander actions when the subcommand actually runs (issue #400). The handler re-exports from index.ts are dropped (no consumers; tests import the command modules directly), and an import-graph test locks the laziness in. Co-Authored-By: Claude Fable 5 --- src/cli/stdio-command.ts | 2 +- src/index.ts | 25 ++++++++++++++----------- tests/unit/index-lazy-imports.test.ts | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 tests/unit/index-lazy-imports.test.ts diff --git a/src/cli/stdio-command.ts b/src/cli/stdio-command.ts index fb9b50d5..407c60c1 100644 --- a/src/cli/stdio-command.ts +++ b/src/cli/stdio-command.ts @@ -1,7 +1,7 @@ import type { Logger as WinstonLoggerType } from 'winston'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { DebugMcpServer } from '../server.js'; -import { StdioOptions } from './setup.js'; +import type { StdioOptions } from './setup.js'; import type { ProcessLike } from '../interfaces/process-interfaces.js'; export interface ServerFactoryOptions { diff --git a/src/index.ts b/src/index.ts index 300f448e..b0b0f0dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,8 +51,6 @@ import { setupCheckRustBinaryCommand, } from './cli/setup.js'; import { handleStdioCommand } from './cli/stdio-command.js'; -import { handleSSECommand } from './cli/sse-command.js'; -import { handleHttpCommand } from './cli/http-command.js'; import { handleCheckRustBinaryCommand } from './cli/commands/check-rust-binary.js'; import { getVersion } from './cli/version.js'; import fs from 'fs'; @@ -135,13 +133,18 @@ export async function main(): Promise { handleStdioCommand(options, { logger, serverFactory: createDebugMcpServer }) ); - setupSSECommand(program, (options) => - handleSSECommand(options, { logger, serverFactory: createDebugMcpServer }) - ); + // The SSE/HTTP command modules pull in express and the SDK's HTTP transport + // stacks; import them only when their subcommand actually runs so stdio mode + // (the common case) never pays for them (issue #400). + setupSSECommand(program, async (options) => { + const { handleSSECommand } = await import('./cli/sse-command.js'); + return handleSSECommand(options, { logger, serverFactory: createDebugMcpServer }); + }); - setupHttpCommand(program, (options) => - handleHttpCommand(options, { logger, serverFactory: createDebugMcpServer }) - ); + setupHttpCommand(program, async (options) => { + const { handleHttpCommand } = await import('./cli/http-command.js'); + return handleHttpCommand(options, { logger, serverFactory: createDebugMcpServer }); + }); setupCheckRustBinaryCommand(program, (binaryPath, options) => handleCheckRustBinaryCommand(binaryPath, options) @@ -192,7 +195,9 @@ if (isMainModule) { } } -// Export for testing +// Export for testing. handleSSECommand/handleHttpCommand are deliberately not +// re-exported: their modules are lazy-imported inside the command actions +// (issue #400) and consumers import them from their own modules directly. export { setupErrorHandlers, createCLI, @@ -201,7 +206,5 @@ export { setupHttpCommand, setupCheckRustBinaryCommand, handleStdioCommand, - handleSSECommand, - handleHttpCommand, handleCheckRustBinaryCommand }; diff --git a/tests/unit/index-lazy-imports.test.ts b/tests/unit/index-lazy-imports.test.ts new file mode 100644 index 00000000..dc195af1 --- /dev/null +++ b/tests/unit/index-lazy-imports.test.ts @@ -0,0 +1,25 @@ +/** + * Import-graph test for issue #400: stdio mode (and any mere import of the + * entry point) must not evaluate the HTTP transport stacks. Express and the + * SSE/HTTP command modules are only loaded when their subcommand actually runs. + */ +import { describe, it, expect, vi } from 'vitest'; + +const loaded = vi.hoisted(() => ({ express: false })); + +// The factory only runs if something in the imported module graph actually +// imports express — that evaluation is exactly what this test forbids. +vi.mock('express', () => { + loaded.express = true; + return { default: vi.fn() }; +}); + +describe('index.ts import graph (issue #400)', () => { + it('does not evaluate express when the entry point is imported', async () => { + vi.stubEnv('DEBUG_MCP_SKIP_AUTO_START', '1'); + + await import('../../src/index.js'); + + expect(loaded.express).toBe(false); + }); +});