Skip to content

Commit c629a11

Browse files
committed
fix(cli): lazy-load mcp runtime for direct commands
1 parent 0458be8 commit c629a11

2 files changed

Lines changed: 113 additions & 37 deletions

File tree

src/index.ts

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,9 @@ import { promises as fs } from 'fs';
99

1010
import path from 'path';
1111
import { fileURLToPath } from 'url';
12-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14-
import { createServer } from './server/factory.js';
15-
import { startHttpServer } from './server/http.js';
16-
import { loadServerConfig } from './server/config.js';
12+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
1713
import type { ProjectConfig } from './server/config.js';
18-
import {
19-
CallToolRequestSchema,
20-
ListToolsRequestSchema,
21-
ListResourcesRequestSchema,
22-
ReadResourceRequestSchema,
23-
RootsListChangedNotificationSchema,
24-
Resource
25-
} from '@modelcontextprotocol/sdk/types.js';
14+
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
2615
import { CodebaseIndexer } from './core/indexer.js';
2716

2817
import { analyzerRegistry } from './core/analyzer-registry.js';
@@ -69,8 +58,75 @@ analyzerRegistry.register(new NextJsAnalyzer());
6958
analyzerRegistry.register(new ReactAnalyzer());
7059
analyzerRegistry.register(new GenericAnalyzer());
7160

61+
let createServer!: typeof import('./server/factory.js').createServer;
62+
let startHttpServer!: typeof import('./server/http.js').startHttpServer;
63+
let loadServerConfig!: typeof import('./server/config.js').loadServerConfig;
64+
let StdioServerTransport!: typeof import('@modelcontextprotocol/sdk/server/stdio.js').StdioServerTransport;
65+
let CallToolRequestSchema!: typeof import('@modelcontextprotocol/sdk/types.js').CallToolRequestSchema;
66+
let ListToolsRequestSchema!: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema;
67+
let ListResourcesRequestSchema!: typeof import('@modelcontextprotocol/sdk/types.js').ListResourcesRequestSchema;
68+
let ReadResourceRequestSchema!: typeof import('@modelcontextprotocol/sdk/types.js').ReadResourceRequestSchema;
69+
let RootsListChangedNotificationSchema!: typeof import('@modelcontextprotocol/sdk/types.js').RootsListChangedNotificationSchema;
70+
let server!: Server;
71+
let mcpRuntimeReady = false;
72+
let mcpRuntimePromise: Promise<void> | undefined;
73+
7274
// Flags that are NOT project paths — skip them when resolving the bootstrap root.
7375
const KNOWN_FLAGS = new Set(['--http', '--port', '--help']);
76+
const CLI_SUBCOMMANDS = [
77+
'memory',
78+
'search',
79+
'metadata',
80+
'status',
81+
'reindex',
82+
'style-guide',
83+
'patterns',
84+
'refs',
85+
'cycles',
86+
'init',
87+
'map'
88+
];
89+
90+
// Check if this module is the entry point.
91+
const isDirectRun =
92+
process.argv[1]?.replace(/\\/g, '/').endsWith('index.js') ||
93+
process.argv[1]?.replace(/\\/g, '/').endsWith('index.ts');
94+
const directSubcommand = process.argv[2];
95+
const isDirectCliSubcommand =
96+
isDirectRun &&
97+
typeof directSubcommand === 'string' &&
98+
(CLI_SUBCOMMANDS.includes(directSubcommand) || directSubcommand === '--help');
99+
100+
async function ensureMcpRuntimeLoaded(): Promise<void> {
101+
if (mcpRuntimeReady) {
102+
return;
103+
}
104+
105+
mcpRuntimePromise ??= (async () => {
106+
const [factoryModule, httpModule, configModule, stdioModule, sdkTypesModule] =
107+
await Promise.all([
108+
import('./server/factory.js'),
109+
import('./server/http.js'),
110+
import('./server/config.js'),
111+
import('@modelcontextprotocol/sdk/server/stdio.js'),
112+
import('@modelcontextprotocol/sdk/types.js')
113+
]);
114+
115+
createServer = factoryModule.createServer;
116+
startHttpServer = httpModule.startHttpServer;
117+
loadServerConfig = configModule.loadServerConfig;
118+
StdioServerTransport = stdioModule.StdioServerTransport;
119+
CallToolRequestSchema = sdkTypesModule.CallToolRequestSchema;
120+
ListToolsRequestSchema = sdkTypesModule.ListToolsRequestSchema;
121+
ListResourcesRequestSchema = sdkTypesModule.ListResourcesRequestSchema;
122+
ReadResourceRequestSchema = sdkTypesModule.ReadResourceRequestSchema;
123+
RootsListChangedNotificationSchema = sdkTypesModule.RootsListChangedNotificationSchema;
124+
server = createServer({ name: 'codebase-context', version: PKG_VERSION }, registerHandlers);
125+
mcpRuntimeReady = true;
126+
})();
127+
128+
await mcpRuntimePromise;
129+
}
74130

75131
// Resolve optional bootstrap root with validation handled later in main().
76132
function resolveRootPath(): string | undefined {
@@ -997,11 +1053,6 @@ export function registerHandlers(target: Server): void {
9971053
});
9981054
}
9991055

1000-
const server: Server = createServer(
1001-
{ name: 'codebase-context', version: PKG_VERSION },
1002-
registerHandlers
1003-
);
1004-
10051056
function buildResources(): Resource[] {
10061057
const resources: Resource[] = [
10071058
{
@@ -1234,6 +1285,8 @@ async function validateClientRootEntries(
12341285
}
12351286

12361287
async function refreshKnownRootsFromClient(): Promise<void> {
1288+
await ensureMcpRuntimeLoaded();
1289+
12371290
try {
12381291
const { roots } = await server.listRoots();
12391292
const fileRoots = await validateClientRootEntries(
@@ -1534,6 +1587,8 @@ async function applyServerConfig(
15341587
}
15351588

15361589
async function main() {
1590+
await ensureMcpRuntimeLoaded();
1591+
15371592
const serverConfig = await loadServerConfig();
15381593
await applyServerConfig(serverConfig);
15391594

@@ -1691,6 +1746,8 @@ export { performIndexing };
16911746
* sharing the same module-level project state.
16921747
*/
16931748
async function startHttp(explicitPort?: number): Promise<void> {
1749+
await ensureMcpRuntimeLoaded();
1750+
16941751
const serverConfig = await loadServerConfig();
16951752
await applyServerConfig(serverConfig);
16961753

@@ -1764,25 +1821,9 @@ async function startHttp(explicitPort?: number): Promise<void> {
17641821
}
17651822
}
17661823

1767-
// Only auto-start when run directly as CLI (not when imported as module)
1768-
// Check if this module is the entry point
1769-
const isDirectRun =
1770-
process.argv[1]?.replace(/\\/g, '/').endsWith('index.js') ||
1771-
process.argv[1]?.replace(/\\/g, '/').endsWith('index.ts');
1772-
1773-
const CLI_SUBCOMMANDS = [
1774-
'memory',
1775-
'search',
1776-
'metadata',
1777-
'status',
1778-
'reindex',
1779-
'style-guide',
1780-
'patterns',
1781-
'refs',
1782-
'cycles',
1783-
'init',
1784-
'map'
1785-
];
1824+
if (!isDirectCliSubcommand) {
1825+
await ensureMcpRuntimeLoaded();
1826+
}
17861827

17871828
if (isDirectRun) {
17881829
const subcommand = process.argv[2];
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { resolve } from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
5+
const root = resolve(import.meta.dirname, '..');
6+
const entrypoint = resolve(root, 'src', 'index.ts');
7+
8+
type MapJson = {
9+
project?: string;
10+
architecture?: object;
11+
activePatterns?: unknown[];
12+
};
13+
14+
describe('CLI entrypoint runtime', () => {
15+
it('dispatches map without loading MCP server runtime on the CLI path', () => {
16+
const result = spawnSync(process.execPath, ['--import', 'tsx', entrypoint, 'map', '--json'], {
17+
cwd: root,
18+
env: {
19+
...process.env,
20+
CODEBASE_ROOT: root
21+
},
22+
encoding: 'utf8',
23+
timeout: 120_000
24+
});
25+
26+
expect(result.status).toBe(0);
27+
expect(result.stderr).not.toContain('ERR_MODULE_NOT_FOUND');
28+
expect(result.stderr).not.toContain('@modelcontextprotocol/sdk/server/stdio.js');
29+
30+
const parsed = JSON.parse(result.stdout) as MapJson;
31+
expect(typeof parsed.project).toBe('string');
32+
expect(parsed.architecture).toBeTruthy();
33+
expect(Array.isArray(parsed.activePatterns)).toBe(true);
34+
}, 120_000);
35+
});

0 commit comments

Comments
 (0)