mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
125 lines
5.5 KiB
TypeScript
125 lines
5.5 KiB
TypeScript
/**
|
|
* MCP Command
|
|
*
|
|
* Starts the MCP server in standalone mode.
|
|
* Loads all indexed repos from the global registry.
|
|
* No longer depends on cwd — works from any directory.
|
|
*
|
|
* IMPORTANT: this module's static-import closure is intentionally tiny
|
|
* (one chain: `mcp/stdio-context.js` → `mcp/stdio-capture.js`, which is a
|
|
* leaf with zero non-`node:` imports). All heavy backend modules
|
|
* (`startMCPServer`, `LocalBackend`, `warnMissingOptionalGrammars`) load
|
|
* via `await import(...)` AFTER `installGlobalStdoutSentinel()` runs.
|
|
*
|
|
* This closes the ESM-evaluation-order window where native init banners
|
|
* from `@ladybugdb/core` (or any future heavy import) could reach raw
|
|
* stdout before the sentinel exists. Codex's adversarial review on
|
|
* PR #1383 found that even with the sentinel-install call as the first
|
|
* statement of `mcpCommand`, ESM evaluates static imports of THIS module
|
|
* before the function body runs — so any native side effects during
|
|
* those imports happen before the sentinel can intercept them.
|
|
*
|
|
* If you find yourself adding a static `import` to this file, ask
|
|
* whether the imported module (or anything it transitively imports)
|
|
* touches `process.stdout` or loads a native binding at module init. If
|
|
* either is true, switch it to a dynamic `await import(...)` inside
|
|
* `mcpCommand` after the sentinel install. The regression test at
|
|
* `gitnexus/test/integration/mcp/import-closure.test.ts` enforces this.
|
|
*/
|
|
|
|
import { installGlobalStdoutSentinel } from '../mcp/stdio-context.js';
|
|
|
|
export const mcpCommand = async (options?: {
|
|
http?: boolean;
|
|
port?: string;
|
|
host?: string;
|
|
authToken?: string;
|
|
}) => {
|
|
// Install the global stdout sentinel as the very first thing — before
|
|
// ANY other module loads. The static-import closure above is leaf-only
|
|
// (stdio-context → stdio-capture, zero non-`node:` deps), so this is
|
|
// also the first chance any code in this process has to write to stdout.
|
|
installGlobalStdoutSentinel();
|
|
|
|
// uncaughtException/unhandledRejection handlers are owned by
|
|
// startMCPServer (gitnexus/src/mcp/server.ts) so the server's shutdown
|
|
// path runs cleanly with full stack traces. Registering duplicates here
|
|
// would only produce noisy double-logging on the same exception.
|
|
|
|
// Dynamically import heavy backend modules AND the pino logger AFTER
|
|
// the sentinel installs. The logger is dynamic-imported (rather than
|
|
// static) to preserve the leaf-only static-import closure documented at
|
|
// the top of this file — `core/logger.js` itself doesn't write to
|
|
// stdout at module init, but transitive deps (pino, pino-pretty, the
|
|
// worker-thread transport) could in theory, and the import-closure
|
|
// regression test enforces the leaf invariant.
|
|
const [{ startMCPServer }, { LocalBackend }, { logger }, { createMcpRepositoryPolicy }] =
|
|
await Promise.all([
|
|
import('../mcp/server.js'),
|
|
import('../mcp/local/local-backend.js'),
|
|
import('../core/logger.js'),
|
|
import('../mcp/repository-policy.js'),
|
|
]);
|
|
|
|
// Missing-optional-grammar warnings are intentionally NOT emitted here.
|
|
// `gitnexus analyze` already warns at index time, filtered by the repo's
|
|
// actual extensions, and a repo can only be served by MCP after analyze
|
|
// has run. Repeating an unconditional warning at every MCP startup is
|
|
// pure noise for users whose indexed repos don't use Dart/Proto.
|
|
|
|
// Initialize multi-repo backend from registry.
|
|
// The server starts even with 0 repos — tools call refreshRepos() lazily,
|
|
// so repos indexed after the server starts are discovered automatically.
|
|
const backend = new LocalBackend();
|
|
await backend.init();
|
|
|
|
const repositoryPolicy = await createMcpRepositoryPolicy(backend);
|
|
const repos = await repositoryPolicy.scopeBackend(backend).listRepos();
|
|
if (repos.length === 0) {
|
|
// Operator-actionable but the server still starts and serves; warn-level,
|
|
// not error. Tools will discover newly-analyzed repos via lazy refresh.
|
|
logger.warn(
|
|
'GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.',
|
|
);
|
|
} else {
|
|
logger.info(
|
|
{ repoCount: repos.length, repos: repos.map((r) => r.name) },
|
|
'GitNexus: MCP server starting',
|
|
);
|
|
}
|
|
|
|
// Start HTTP server or fall back to stdio (default).
|
|
if (options?.http) {
|
|
// Dynamically import the HTTP transport module AFTER the sentinel installs.
|
|
// http-transport.ts pulls in express/cors/MCP SDK HTTP transport; these must
|
|
// not load before installGlobalStdoutSentinel() runs (see module doc above).
|
|
const port = Number(options.port ?? 3000);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
logger.error(
|
|
{ port: options.port },
|
|
`Invalid --port value: "${options.port ?? ''}". Must be an integer between 1 and 65535.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
// Dynamic import keeps express/cors out of mcp.ts's static graph (stdio sentinel).
|
|
const { startMcpHttpServer, resolveAuthToken } = await import('../mcp/http-transport.js');
|
|
try {
|
|
await startMcpHttpServer(backend, {
|
|
port,
|
|
host: options.host ?? '127.0.0.1',
|
|
authToken: resolveAuthToken(options.authToken, process.env),
|
|
repositoryPolicy,
|
|
});
|
|
} catch (err) {
|
|
logger.error(
|
|
{ err: err instanceof Error ? err.message : err },
|
|
'Failed to start the MCP HTTP server',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Start MCP server (serves all repos, discovers new ones lazily)
|
|
await startMCPServer(backend, repositoryPolicy);
|
|
};
|