codesearch-mcp 0.2.2-alpha → 0.3.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +60 -33
  3. package/dist/app.js +46 -22
  4. package/dist/src/chunking/AstChunker.d.ts +12 -0
  5. package/dist/src/chunking/AstChunker.js +30 -0
  6. package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
  7. package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
  8. package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
  9. package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
  10. package/dist/src/chunking/FragmentMerger.d.ts +11 -0
  11. package/dist/src/chunking/FragmentMerger.js +61 -0
  12. package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
  13. package/dist/src/chunking/ImportOnlyFilter.js +14 -0
  14. package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
  15. package/dist/src/chunking/LangchainTextSplitter.js +133 -0
  16. package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
  17. package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
  18. package/dist/src/chunking/RunPacker.d.ts +12 -0
  19. package/dist/src/chunking/RunPacker.js +84 -0
  20. package/dist/src/chunking/create-chunker.d.ts +3 -0
  21. package/dist/src/chunking/create-chunker.js +15 -0
  22. package/dist/src/chunking/interfaces.d.ts +27 -0
  23. package/dist/src/chunking/interfaces.js +2 -0
  24. package/dist/src/commands/ConfigCommand.js +1 -9
  25. package/dist/src/commands/EvalCommand.d.ts +11 -0
  26. package/dist/src/commands/EvalCommand.js +264 -0
  27. package/dist/src/commands/IndexCommand.js +70 -66
  28. package/dist/src/commands/McpCommand.js +2 -8
  29. package/dist/src/commands/WebCommand.js +2 -5
  30. package/dist/src/config-error.d.ts +0 -4
  31. package/dist/src/config-error.js +0 -4
  32. package/dist/src/config-store.d.ts +1 -54
  33. package/dist/src/config-store.js +8 -69
  34. package/dist/src/config.d.ts +8 -17
  35. package/dist/src/config.js +35 -34
  36. package/dist/src/constants.d.ts +2 -28
  37. package/dist/src/constants.js +6 -28
  38. package/dist/src/container.d.ts +6 -0
  39. package/dist/src/container.js +12 -3
  40. package/dist/src/db/index.d.ts +1 -1
  41. package/dist/src/db/index.js +1 -2
  42. package/dist/src/db/schema.d.ts +22 -29
  43. package/dist/src/db/schema.js +3 -14
  44. package/dist/src/declarations/DeclarationTree.d.ts +19 -0
  45. package/dist/src/declarations/DeclarationTree.js +66 -0
  46. package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
  47. package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
  48. package/dist/src/declarations/SourceLines.d.ts +37 -0
  49. package/dist/src/declarations/SourceLines.js +415 -0
  50. package/dist/src/dependency-traversal.d.ts +10 -0
  51. package/dist/src/dependency-traversal.js +39 -0
  52. package/dist/src/enriched-prompt.d.ts +0 -5
  53. package/dist/src/enriched-prompt.js +0 -5
  54. package/dist/src/error-record.d.ts +12 -0
  55. package/dist/src/error-record.js +248 -0
  56. package/dist/src/eval-corpus.d.ts +16 -0
  57. package/dist/src/eval-corpus.js +169 -0
  58. package/dist/src/eval-results.d.ts +15 -0
  59. package/dist/src/eval-results.js +397 -0
  60. package/dist/src/file-utils.d.ts +10 -0
  61. package/dist/src/file-utils.js +19 -1
  62. package/dist/src/graph.d.ts +0 -25
  63. package/dist/src/graph.js +0 -28
  64. package/dist/src/help.js +4 -1
  65. package/dist/src/hnsw-settings.d.ts +3 -0
  66. package/dist/src/hnsw-settings.js +24 -0
  67. package/dist/src/interfaces/IAstParser.d.ts +5 -30
  68. package/dist/src/interfaces/IChunker.d.ts +3 -12
  69. package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
  70. package/dist/src/interfaces/ICorpusInspector.js +2 -0
  71. package/dist/src/interfaces/IDatabase.d.ts +2 -36
  72. package/dist/src/interfaces/IEmbedder.d.ts +0 -9
  73. package/dist/src/interfaces/ILogger.d.ts +1 -11
  74. package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
  75. package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
  76. package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
  77. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
  78. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  79. package/dist/src/interfaces/ISearcher.d.ts +26 -20
  80. package/dist/src/interfaces/index.d.ts +1 -0
  81. package/dist/src/interrupt.js +0 -2
  82. package/dist/src/owner-path.d.ts +9 -10
  83. package/dist/src/owner-path.js +11 -23
  84. package/dist/src/project-scope.d.ts +7 -0
  85. package/dist/src/project-scope.js +37 -0
  86. package/dist/src/project-selector.d.ts +1 -0
  87. package/dist/src/project-selector.js +13 -6
  88. package/dist/src/retrieval-eval.d.ts +24 -0
  89. package/dist/src/retrieval-eval.js +500 -0
  90. package/dist/src/search/result-shaping.d.ts +4 -0
  91. package/dist/src/search/result-shaping.js +19 -0
  92. package/dist/src/services/AstParserService.d.ts +28 -23
  93. package/dist/src/services/AstParserService.js +391 -98
  94. package/dist/src/services/DatabaseService.d.ts +2 -19
  95. package/dist/src/services/DatabaseService.js +44 -81
  96. package/dist/src/services/EmbedderFactory.d.ts +0 -4
  97. package/dist/src/services/EmbedderFactory.js +0 -5
  98. package/dist/src/services/IndexerService.d.ts +2 -10
  99. package/dist/src/services/IndexerService.js +29 -45
  100. package/dist/src/services/InfinityEmbedder.d.ts +0 -21
  101. package/dist/src/services/InfinityEmbedder.js +0 -15
  102. package/dist/src/services/LoggerService.d.ts +7 -5
  103. package/dist/src/services/LoggerService.js +37 -41
  104. package/dist/src/services/McpServer.d.ts +25 -10
  105. package/dist/src/services/McpServer.js +101 -64
  106. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
  107. package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
  108. package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
  109. package/dist/src/services/NxMonorepoResolver.js +5 -56
  110. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
  111. package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
  112. package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
  113. package/dist/src/services/ProjectDiscoveryService.js +0 -4
  114. package/dist/src/services/SearcherService.d.ts +127 -70
  115. package/dist/src/services/SearcherService.js +526 -305
  116. package/dist/src/services/VoyageEmbedder.d.ts +0 -6
  117. package/dist/src/services/VoyageEmbedder.js +0 -6
  118. package/dist/src/services/WebServerService.d.ts +1 -53
  119. package/dist/src/services/WebServerService.js +11 -57
  120. package/dist/src/services/http-retry.d.ts +0 -7
  121. package/dist/src/services/http-retry.js +1 -10
  122. package/dist/src/services/infinity-provisioning.d.ts +0 -74
  123. package/dist/src/services/infinity-provisioning.js +5 -88
  124. package/dist/src/services/legacy-ollama-index.d.ts +0 -16
  125. package/dist/src/services/legacy-ollama-index.js +0 -23
  126. package/dist/src/services/reranker.d.ts +0 -9
  127. package/dist/src/services/reranker.js +46 -12
  128. package/dist/src/setup.d.ts +11 -23
  129. package/dist/src/setup.js +98 -71
  130. package/dist/src/symbol-name.d.ts +0 -12
  131. package/dist/src/symbol-name.js +0 -13
  132. package/dist/src/symbol-rows.d.ts +2 -11
  133. package/dist/src/symbol-rows.js +5 -13
  134. package/dist/src/tool-call-log.d.ts +2 -0
  135. package/dist/src/tool-call-log.js +24 -0
  136. package/dist/src/tools/FindSymbolTool.js +14 -5
  137. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  138. package/dist/src/tools/GetCallersTool.js +16 -6
  139. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  140. package/dist/src/tools/GetDependenciesTool.js +15 -8
  141. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  142. package/dist/src/tools/GetDependentsTool.js +31 -16
  143. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  144. package/dist/src/tools/ListFilesTool.js +14 -12
  145. package/dist/src/tools/ListProjectsTool.js +5 -1
  146. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  147. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  148. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  149. package/dist/src/tools/ReadFileTool.js +12 -10
  150. package/dist/src/tools/SearchCodebaseTool.js +7 -2
  151. package/dist/src/tools/format-helpers.d.ts +10 -1
  152. package/dist/src/tools/format-helpers.js +145 -8
  153. package/dist/src/web/dashboard.d.ts +0 -8
  154. package/dist/src/web/dashboard.js +7 -10
  155. package/package.json +10 -9
  156. package/dist/src/services/AstChunker.d.ts +0 -42
  157. package/dist/src/services/AstChunker.js +0 -158
  158. package/docs/web-dashboard.md +0 -206
@@ -41,21 +41,20 @@ const pino_1 = __importDefault(require("pino"));
41
41
  const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const fs = __importStar(require("fs"));
44
+ const async_hooks_1 = require("async_hooks");
45
+ const error_record_1 = require("../error-record");
44
46
  const DEFAULT_LOG_DIR = path.join(os.homedir(), '.config', 'codesearch-mcp', 'logs');
45
47
  class LoggerService {
46
48
  logger;
47
- // Dedicated `info`-level channel for search telemetry, independent of the
48
- // main logger's configured level. Null when file logging is disabled so
49
- // metrics never reach stdout (owned by the MCP JSON transport).
50
- metricsLogger;
51
- constructor(logger, metricsLogger = null) {
49
+ context;
50
+ constructor(logger, context) {
52
51
  this.logger = logger;
53
- this.metricsLogger = metricsLogger;
52
+ this.context = context;
54
53
  }
55
54
  static silent() {
56
- return new LoggerService((0, pino_1.default)({ level: 'silent' }));
55
+ return LoggerService.build('debug', undefined);
57
56
  }
58
- static fromConfig(cfg, name) {
57
+ static fromConfig(cfg, name, bindings) {
59
58
  const level = cfg.level;
60
59
  const dirVal = cfg.dir;
61
60
  const enabled = dirVal !== 'false' && dirVal !== '0';
@@ -66,45 +65,42 @@ class LoggerService {
66
65
  ? path.join(logDir, name, `${name}.log`)
67
66
  : path.join(logDir, 'rag.log');
68
67
  }
69
- return LoggerService.buildFromPath(level, filePath);
68
+ return LoggerService.build(level, filePath ? LoggerService.fileTransport(level, filePath) : undefined, bindings);
70
69
  }
71
- static buildFromPath(level, filePath) {
72
- if (filePath)
73
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
74
- // The transport target must accept `info` so the metrics channel can emit
75
- // even when the main level is `warn`/`error`; the main logger still gates
76
- // normal calls at `level`, so nothing else leaks below it.
77
- const targetLevel = pino_1.default.levels.values[level] < pino_1.default.levels.values.info ? level : 'info';
78
- const transport = filePath
79
- ? pino_1.default.transport({
80
- targets: [{
81
- target: 'pino-roll',
82
- options: {
83
- file: filePath,
84
- frequency: 'daily',
85
- dateFormat: 'yyyy-MM-dd',
86
- mkdir: true,
87
- symlink: true,
88
- limit: { count: 30 },
89
- size: '50m',
90
- },
91
- level: targetLevel,
92
- }],
93
- })
94
- : undefined;
95
- const logger = transport
96
- ? (0, pino_1.default)({ level }, transport)
70
+ static toStream(level, stream, bindings) {
71
+ return LoggerService.build(level, stream, bindings);
72
+ }
73
+ static fileTransport(level, filePath) {
74
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
75
+ return pino_1.default.transport({
76
+ targets: [{
77
+ target: 'pino-roll',
78
+ options: {
79
+ file: filePath,
80
+ frequency: 'daily',
81
+ dateFormat: 'yyyy-MM-dd',
82
+ mkdir: true,
83
+ symlink: true,
84
+ limit: { count: 30 },
85
+ size: '50m',
86
+ },
87
+ level,
88
+ }],
89
+ });
90
+ }
91
+ static build(level, destination, bindings) {
92
+ const context = new async_hooks_1.AsyncLocalStorage();
93
+ const base = destination
94
+ ? (0, pino_1.default)({ level, serializers: error_record_1.LOG_SERIALIZERS, mixin: () => ({ ...context.getStore() }) }, destination)
97
95
  : (0, pino_1.default)({ level: 'silent' });
98
- // Child logger pinned at `info` regardless of the parent level; only used by
99
- // metric(). Null when there is no file transport (default stdout must stay
100
- // clean for the MCP transport).
101
- const metricsLogger = transport ? logger.child({}, { level: 'info' }) : null;
102
- return new LoggerService(logger, metricsLogger);
96
+ return new LoggerService(bindings ? base.child(bindings) : base, context);
103
97
  }
104
98
  debug(msg, data) { this.logger.debug(data ?? {}, msg); }
105
99
  info(msg, data) { this.logger.info(data ?? {}, msg); }
106
100
  warn(msg, data) { this.logger.warn(data ?? {}, msg); }
107
101
  error(msg, data) { this.logger.error(data ?? {}, msg); }
108
- metric(msg, data) { this.metricsLogger?.info(data ?? {}, msg); }
102
+ withContext(context, fn) {
103
+ return this.context.run(context, fn);
104
+ }
109
105
  }
110
106
  exports.LoggerService = LoggerService;
@@ -1,12 +1,30 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1
2
  import type { Config } from '../config';
2
3
  import type { ISearcher } from '../interfaces/ISearcher';
3
4
  import type { ILogger } from '../interfaces/ILogger';
4
- /**
5
- * McpServer — thin orchestration layer that delegates tool handling
6
- * to individual IMcpToolHandler implementations (Open/Closed Principle).
7
- *
8
- * To add a new tool: implement IMcpToolHandler and register it in createTools().
9
- */
5
+ import type { IMcpToolHandler, ToolResponse } from '../interfaces/IMcpToolHandler';
6
+ export declare function dispatchTool(tools: Map<string, IMcpToolHandler>, name: string, args: Record<string, unknown>, logger: ILogger): Promise<ToolResponse>;
7
+ export interface ToolCallRequest {
8
+ name: string;
9
+ arguments?: Record<string, unknown>;
10
+ _meta?: Record<string, unknown>;
11
+ }
12
+ export interface ToolCaller {
13
+ callId: number;
14
+ session: string | null;
15
+ client: {
16
+ name: string;
17
+ version: string;
18
+ } | null;
19
+ startupError?: Error;
20
+ }
21
+ export declare function handleToolCall(tools: Map<string, IMcpToolHandler>, req: ToolCallRequest, signal: AbortSignal, caller: ToolCaller, logger: ILogger): Promise<ToolResponse>;
22
+ export declare function createToolCallHandler(tools: Map<string, IMcpToolHandler>, server: Pick<Server, 'getClientVersion'>, identity: Pick<ToolCaller, 'session' | 'startupError'>, logger: ILogger): (req: {
23
+ params: ToolCallRequest;
24
+ }, extra: {
25
+ signal: AbortSignal;
26
+ }) => Promise<ToolResponse>;
27
+ export declare function buildInstructions(t: number): string;
10
28
  export declare class McpServer {
11
29
  private searcher;
12
30
  private config;
@@ -15,9 +33,6 @@ export declare class McpServer {
15
33
  private readonly tools;
16
34
  constructor(searcher: ISearcher, config: Config, logger: ILogger);
17
35
  start(startupError?: Error): Promise<void>;
18
- /**
19
- * Factory method: creates and registers all tool handlers.
20
- * New tools are added here without modifying the dispatch logic above.
21
- */
36
+ createServer(startupError?: Error): Server;
22
37
  private createTools;
23
38
  }
@@ -34,12 +34,18 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.McpServer = void 0;
37
+ exports.dispatchTool = dispatchTool;
38
+ exports.handleToolCall = handleToolCall;
39
+ exports.createToolCallHandler = createToolCallHandler;
40
+ exports.buildInstructions = buildInstructions;
37
41
  const fs = __importStar(require("fs"));
38
42
  const path = __importStar(require("path"));
39
43
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
40
44
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
41
45
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
42
46
  const tools_1 = require("../tools");
47
+ const tool_call_log_1 = require("../tool-call-log");
48
+ const error_record_1 = require("../error-record");
43
49
  function readPackageVersion() {
44
50
  try {
45
51
  return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version ?? '0.0.0';
@@ -48,6 +54,72 @@ function readPackageVersion() {
48
54
  return '0.0.0';
49
55
  }
50
56
  }
57
+ async function dispatchTool(tools, name, args, logger) {
58
+ const handler = tools.get(name);
59
+ if (!handler) {
60
+ return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
61
+ }
62
+ try {
63
+ return await handler.handle(args);
64
+ }
65
+ catch (err) {
66
+ logger.error('McpServer: tool handler error', { tool: name, err });
67
+ const reason = err instanceof Error ? err.message : String(err);
68
+ return { isError: true, content: [{ type: 'text', text: `Tool error: ${reason}` }] };
69
+ }
70
+ }
71
+ function unavailableResponse(startupError) {
72
+ return {
73
+ isError: true,
74
+ content: [{
75
+ type: 'text',
76
+ text: `MCP server is unavailable: ${(0, error_record_1.errorMessage)(startupError)}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
77
+ }],
78
+ };
79
+ }
80
+ function metaForLog(meta) {
81
+ if (!meta)
82
+ return null;
83
+ const { progressToken: _, ...rest } = meta;
84
+ return Object.keys(rest).length ? (0, tool_call_log_1.truncateForLog)(rest) : null;
85
+ }
86
+ async function handleToolCall(tools, req, signal, caller, logger) {
87
+ const t0 = Date.now();
88
+ const toolUseId = req._meta?.['claudecode/toolUseId'];
89
+ const context = typeof toolUseId === 'string' ? { callId: caller.callId, toolUseId } : { callId: caller.callId };
90
+ return logger.withContext(context, async () => {
91
+ const args = req.arguments ?? {};
92
+ const response = caller.startupError
93
+ ? unavailableResponse(caller.startupError)
94
+ : await dispatchTool(tools, req.name, args, logger);
95
+ const ms = Date.now() - t0;
96
+ if (!response.isError)
97
+ logger.debug('McpServer: tool handled', { tool: req.name, ms });
98
+ logger.info('tool call', {
99
+ tool: req.name,
100
+ args: (0, tool_call_log_1.truncateForLog)(args),
101
+ isError: response.isError ?? false,
102
+ responseChars: response.content.reduce((n, c) => n + (typeof c.text === 'string' ? c.text.length : 0), 0),
103
+ ms,
104
+ client: caller.client,
105
+ session: caller.session,
106
+ meta: metaForLog(req._meta),
107
+ ...(signal.aborted ? { cancelled: true } : {}),
108
+ });
109
+ return response;
110
+ });
111
+ }
112
+ function createToolCallHandler(tools, server, identity, logger) {
113
+ let nextCallId = 1;
114
+ return (req, extra) => {
115
+ const client = server.getClientVersion();
116
+ return handleToolCall(tools, req.params, extra.signal, {
117
+ ...identity,
118
+ callId: nextCallId++,
119
+ client: client ? { name: client.name, version: client.version } : null,
120
+ }, logger);
121
+ };
122
+ }
51
123
  function buildInstructions(t) {
52
124
  return `
53
125
  You have access to a local code intelligence server. Use these tools to search codebases by meaning, navigate import graphs, look up symbol definitions, and read source files.
@@ -58,7 +130,7 @@ Pick one workflow based on what you need:
58
130
 
59
131
  | Type | Signals | Workflow |
60
132
  |---|---|---|
61
- | **Lookup** | "where is X defined", "find Y" | find_symbol (if name known) → search_codebase (if not found) |
133
+ | **Lookup** | "where is X defined", "find Y" | find_symbol first when the exact name is known — exact and faster. Call search_codebase too, in parallel, when you also want surrounding context or the name might not match verbatim; fall back to it alone if find_symbol comes back empty. |
62
134
  | **Caller trace** | "who calls X", "find usages of Y", "what invokes Z" | find_symbol → get_callers |
63
135
  | **Conceptual** | "how does X work", "what is Y" | search_codebase → read_file_range |
64
136
  | **Flow audit** | "how does X connect to Y", "audit this flow", "trace the path" | search_codebase → **get_dependencies + get_dependents** → Grep |
@@ -68,26 +140,28 @@ Pick one workflow based on what you need:
68
140
 
69
141
  ## Tools
70
142
 
71
- - **list_projects** — call this first to see what projects are indexed and their exact names.
72
- - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk.
73
- - **read_file_range** — read a specific line range from any indexed file. Expects a **relative path** as returned by search_codebase.
143
+ Every file or directory argument (\`file_path\`, \`callee_file\`, \`path\`) is workspace-relative, exactly as printed by search_codebase (e.g. \`my-app/src/index.ts\`; \`.\` is the workspace root) — copy paths between tools unchanged.
144
+
145
+ - **list_projects** — call this first to see what projects are indexed, their exact names and their directories (\`path:\`).
146
+ - **search_codebase** — semantic search across indexed source files. Returns file path, line number, and a code chunk. **Pass \`project\` whenever the target project is known or guessable from the question** — this workspace indexes many projects at once, and an unscoped query competes against all of them, which \`rerank\` cannot fix (reranking only reorders whatever the vector search already retrieved). Pass \`rerank: false\` for "what does X do" / "where is X" lookups described without an exact name — measurement shows reranking hurts these (-0.13 MRR@10, hit@5 68% → 50%). If you already know the exact symbol name, use \`find_symbol\` instead, not \`search_codebase\` — it is faster and exact, and the rerank setting does not matter there. For conceptual/architectural queries reranking is roughly neutral (-0.05 MRR@10, not significant), so omitting \`rerank\` is fine.
147
+ - **read_file_range** — read a specific line range from any file under the workspace, indexed or not.
74
148
  - **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
75
149
  - **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
76
150
  - **find_symbol** — find where a symbol is defined by exact name: top-level declarations, class/object-literal members (methods, get/set accessors, constructors, function properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name (\`assertUsersInOrg\`), a qualified name (\`RepairsService.assertUsersInOrg\`, \`NS.Parser.parse\`) to disambiguate same-named members, a quoted name for string-literal members (\`Router.'kebab-name'\`), or a computed name (\`Stack.[Symbol.iterator]\`). Faster than semantic search when you know the name.
77
151
  - **get_callers** — find all call sites of a function or method by exact name. Accepts a bare or qualified (\`Class.method\`) symbol, using the same suffix matching as \`find_symbol\`. Uses the call graph index — faster and more precise than grep. A call through an interface-typed receiver is recorded under the interface member (e.g. \`IEmbedder.embedBatch\`), not the implementing class. Use after find_symbol to trace where a symbol is actually called.
78
- - **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
79
- - **read_file** — read a complete file by **absolute path** with line numbers. Truncates at max_lines with a hint to continue.
152
+ - **list_files** — browse the directory tree of a directory. Use to understand project structure before reading files.
153
+ - **read_file** — read a complete file with line numbers. Truncates at max_lines with a hint to continue.
80
154
 
81
155
  ## Recommended workflow
82
156
 
83
157
  ### Lookup / Conceptual
84
158
  1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
85
- 2. Use \`search_codebase\` to find relevant code by meaning. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
159
+ 2. Use \`search_codebase\` to find relevant code by meaning. **Always pass \`project\` when the question names or implies one** — an unscoped search ranks candidates across every indexed project together, so an unrelated project's weak match can outrank the right project's real one. If your query uses an ambiguous term (e.g. "subscription" could mean pub/sub or billing), pass a lower \`threshold\` (e.g. \`0.40–0.45\`) to filter out weak matches.
86
160
  3. If a result looks **truncated or incomplete**, call \`read_file_range\` with the returned \`file_path\` and \`start_line\`.
87
- 4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`.
161
+ 4. For exploratory searches where results span chunk boundaries, pass \`include_neighbors: true\`. Results hold at most 2 chunks per file by default; pass \`per_file\` (e.g. \`5\`) when several answers are expected in one file, such as several methods of one class.
88
162
  5. To immediately see a file's imports and callers without extra tool calls, pass \`include_graph_context: true\` to \`search_codebase\`. Use this when doing a quick structural survey; switch to \`get_dependencies\`/\`get_dependents\` for full transitive graphs.
89
- 6. Use \`find_symbol\` when you know the exact name — faster than semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep.
90
- 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file by absolute path.
163
+ 6. Use \`find_symbol\` when you know the exact name — faster and exact, unlike semantic search. Use a qualified name (\`Class.method\`) to disambiguate same-named methods of different classes; a bare name searches every container. When you also want surrounding context (callers' intent, nearby helpers, how it's described elsewhere), call \`search_codebase\` in parallel rather than after — the two tools answer different questions and neither blocks the other. Hits marked \`(grep — not in index; may be stale or imprecise)\` come from a fallback text search, not the index — treat them as less reliable. After locating a symbol, use \`get_callers\` to find all call sites — faster and more precise than grep. For a NestJS GraphQL operation, resolve field or type, pass the matching \`graphql_query\`/\`graphql_mutation\`/\`graphql_subscription\`/\`graphql_field\`/\`graphql_type\` kind to look it up by the name the schema exposes, not the TypeScript declaration name.
164
+ 7. Use \`list_files\` to explore project structure before diving into files. Use \`read_file\` to read a complete file.
91
165
 
92
166
  ### Flow audit (mandatory steps — do not skip 3 and 4)
93
167
  1. Use \`list_projects\` to confirm project names.
@@ -97,8 +171,6 @@ Pick one workflow based on what you need:
97
171
  5. For each peripheral file found in steps 3–4: use \`read_file_range\` or \`find_symbol\` to inspect it.
98
172
  6. Only after steps 3–4 draw architectural conclusions — \`search_codebase\` alone misses peripheral files because they are semantically thin (configs, types, loaders contain no domain vocabulary).
99
173
 
100
- > **Note:** If \`get_dependencies\` / \`get_dependents\` return empty, the file likely uses path aliases (e.g. \`@core/\`, \`@/\`, \`@db/\`). Fall back to \`Grep\` on the symbol name across the project.
101
-
102
174
  ### Impact analysis
103
175
  1. \`get_dependents(target_file)\` — direct callers.
104
176
  2. \`get_dependents(target_file, transitive: true)\` — full reverse graph.
@@ -115,7 +187,7 @@ Pick one workflow based on what you need:
115
187
 
116
188
  ## Dependency graph
117
189
 
118
- \`get_dependencies\` and \`get_dependents\` require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain. depType values: \`import\` | \`import-default\` | \`import-namespace\` | \`reexport\` | \`reexport-namespace\`. \`fromSymbol\` — the top-level class or function in the importing file that uses this import (null if ambiguous or not tracked).
190
+ \`get_dependencies\` and \`get_dependents\` take \`file_path\` alone and list edges in every project. A path that names no file is an error, never an empty result. Pass \`transitive: true\` for the full dependency chain. The direct section prints one line per file, with the names imported from/by it (\`default\` / \`* as namespace\` / \`side-effect\` standing in for a nameless import, re-exports grouped after plain imports under one \`re-export \` prefix, \`export *\` for a namespace re-export); \`get_dependents\` also lists the enclosing declarations (\`· in \`PlansPage\`\`) that use the import — only those resolved unambiguously (a binding used in 2+ declarations yields none), so the list can be partial or absent.
119
191
 
120
192
  If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you care about) — this triggers a grep fallback that finds runtime usages in DI containers, string references, and dynamic lookups that the import graph misses.
121
193
 
@@ -124,12 +196,6 @@ If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you c
124
196
  Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Treat scores above 0.50 as weak hints — verify with \`read_file_range\` or \`Grep\`. Pass \`threshold\` to \`search_codebase\` to override per-query: \`0.40–0.45\` for precise technical queries (known term, exact concept); \`${t}\` default (broad but reliable). Avoid \`0.35\` — too strict for DSL files (GraphQL, Prisma) and infrastructure code.
125
197
  `.trim();
126
198
  }
127
- /**
128
- * McpServer — thin orchestration layer that delegates tool handling
129
- * to individual IMcpToolHandler implementations (Open/Closed Principle).
130
- *
131
- * To add a new tool: implement IMcpToolHandler and register it in createTools().
132
- */
133
199
  class McpServer {
134
200
  searcher;
135
201
  config;
@@ -145,62 +211,33 @@ class McpServer {
145
211
  }
146
212
  async start(startupError) {
147
213
  if (startupError) {
148
- this.logger.error('McpServer: startup failed, server will report error on every tool call', { reason: startupError.message });
214
+ this.logger.error('McpServer: startup failed, server will report error on every tool call', { err: startupError });
149
215
  }
216
+ await this.createServer(startupError).connect(new stdio_js_1.StdioServerTransport());
217
+ this.logger.info('McpServer: started', { version: this.version });
218
+ }
219
+ createServer(startupError) {
150
220
  const server = new index_js_1.Server({ name: 'local-rag', version: this.version }, { capabilities: { tools: {} }, instructions: buildInstructions(this.config.search.similarityThreshold) });
151
221
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
152
222
  tools: [...this.tools.values()].map(t => t.definition()),
153
223
  }));
154
- server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
155
- if (startupError) {
156
- return {
157
- isError: true,
158
- content: [{
159
- type: 'text',
160
- text: `MCP server is unavailable: ${startupError.message}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
161
- }],
162
- };
163
- }
164
- const handler = this.tools.get(req.params.name);
165
- if (!handler) {
166
- return {
167
- isError: true,
168
- content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
169
- };
170
- }
171
- try {
172
- const t0 = Date.now();
173
- const result = await handler.handle((req.params.arguments ?? {}));
174
- this.logger.debug('McpServer: tool handled', { tool: req.params.name, ms: Date.now() - t0 });
175
- return result;
176
- }
177
- catch (err) {
178
- const reason = err instanceof Error ? err.message : String(err);
179
- this.logger.error('McpServer: tool handler error', { tool: req.params.name, reason });
180
- return {
181
- isError: true,
182
- content: [{ type: 'text', text: `Tool error: ${reason}` }],
183
- };
184
- }
185
- });
186
- await server.connect(new stdio_js_1.StdioServerTransport());
187
- this.logger.info('McpServer: started', { version: this.version });
224
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, createToolCallHandler(this.tools, server, {
225
+ session: (0, tool_call_log_1.resolveSession)(process.env),
226
+ startupError,
227
+ }, this.logger));
228
+ return server;
188
229
  }
189
- /**
190
- * Factory method: creates and registers all tool handlers.
191
- * New tools are added here without modifying the dispatch logic above.
192
- */
193
230
  createTools() {
194
231
  const handlers = [
195
232
  new tools_1.ListProjectsTool(this.searcher),
196
233
  new tools_1.SearchCodebaseTool(this.searcher, this.config.search.defaultLimit, this.config.search.maxLimit, this.config.search.similarityThreshold, this.logger),
197
- new tools_1.ReadFileRangeTool(this.searcher),
198
- new tools_1.GetDependenciesTool(this.searcher),
199
- new tools_1.GetDependentsTool(this.searcher),
234
+ new tools_1.ReadFileRangeTool(this.searcher, this.logger),
235
+ new tools_1.GetDependenciesTool(this.searcher, this.logger),
236
+ new tools_1.GetDependentsTool(this.searcher, this.logger),
200
237
  new tools_1.FindSymbolTool(this.searcher),
201
- new tools_1.GetCallersTool(this.searcher),
202
- new tools_1.ListFilesTool(this.searcher),
203
- new tools_1.ReadFileTool(this.searcher),
238
+ new tools_1.GetCallersTool(this.searcher, this.logger),
239
+ new tools_1.ListFilesTool(this.searcher, this.logger),
240
+ new tools_1.ReadFileTool(this.searcher, this.logger),
204
241
  ];
205
242
  const map = new Map();
206
243
  for (const h of handlers) {
@@ -6,57 +6,13 @@ export interface WorkspaceInfo {
6
6
  rootTsConfigPath: string | null;
7
7
  projects: DiscoveredProject[];
8
8
  }
9
- /**
10
- * Expand discovery units into the leaf projects to index: a nested monorepo
11
- * unit (with `children`) becomes its sub-projects; an ordinary project stays
12
- * itself. Shared by IndexCommand (indexing loop) and SearcherService (name→path
13
- * resolution) so both agree on the leaf set.
14
- */
15
9
  export declare function toLeafProjects(projects: DiscoveredProject[]): DiscoveredProject[];
16
- /**
17
- * Project discovery that is monorepo-aware.
18
- *
19
- * When a monorepo is detected (via the resolver), it uses the resolver
20
- * to discover projects. Otherwise, it falls back to the original
21
- * flat-workspace discovery (directories with package.json). Cross-project
22
- * import resolution relies on the tsconfig `paths` attached via
23
- * `rootTsConfigPath` — no separate scope map is built.
24
- *
25
- * Single Responsibility: only discovers projects and workspace metadata.
26
- * Does not index or search.
27
- *
28
- * Implements {@link IProjectDiscovery} so it can be wired as the default
29
- * discovery in the container. `discover()` returns one entry per top-level
30
- * project: when the root itself is a monorepo, each entry carries the
31
- * workspace-wide `rootTsConfigPath`; under a flat root, an ordinary project
32
- * gets `rootTsConfigPath: null`, while a top-level directory that is itself a
33
- * nested monorepo is returned as a single unit with expanded `children` (see
34
- * `toLeafProjects`). `analyzeWorkspace()` exposes the full workspace metadata
35
- * (monorepo flag, root tsconfig, project list).
36
- */
37
10
  export declare class MonorepoAwareDiscoveryService implements IProjectDiscovery {
38
11
  private resolvers;
39
12
  private logger;
40
- /** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
41
13
  private flatDiscovery;
42
- constructor(resolvers: IMonorepoResolver[], logger: ILogger,
43
- /** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
44
- flatDiscovery?: IProjectDiscovery);
45
- /**
46
- * IProjectDiscovery contract: enumerate projects in the workspace.
47
- * Attaches the workspace-wide root tsconfig to every project so the
48
- * indexer can resolve cross-project path aliases.
49
- */
14
+ constructor(resolvers: IMonorepoResolver[], logger: ILogger, flatDiscovery?: IProjectDiscovery);
50
15
  discover(rootDir: string): DiscoveredProject[];
51
- /**
52
- * If `project`'s directory is itself a monorepo (resolver detects it),
53
- * return it with expanded `children` named `<repo>/<sub-project>`, each
54
- * carrying the repo's workspace-wide tsconfig. Otherwise return it unchanged.
55
- */
56
16
  private expandIfNestedMonorepo;
57
- /**
58
- * Analyze the workspace root and return full workspace information:
59
- * monorepo detection flag, root tsconfig path, and project list.
60
- */
61
17
  analyzeWorkspace(rootDir: string): WorkspaceInfo;
62
18
  }
@@ -3,71 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MonorepoAwareDiscoveryService = void 0;
4
4
  exports.toLeafProjects = toLeafProjects;
5
5
  const ProjectDiscoveryService_1 = require("./ProjectDiscoveryService");
6
- /**
7
- * Expand discovery units into the leaf projects to index: a nested monorepo
8
- * unit (with `children`) becomes its sub-projects; an ordinary project stays
9
- * itself. Shared by IndexCommand (indexing loop) and SearcherService (name→path
10
- * resolution) so both agree on the leaf set.
11
- */
12
6
  function toLeafProjects(projects) {
13
7
  return projects.flatMap(p => (p.children && p.children.length > 0 ? p.children : [p]));
14
8
  }
15
- /**
16
- * Project discovery that is monorepo-aware.
17
- *
18
- * When a monorepo is detected (via the resolver), it uses the resolver
19
- * to discover projects. Otherwise, it falls back to the original
20
- * flat-workspace discovery (directories with package.json). Cross-project
21
- * import resolution relies on the tsconfig `paths` attached via
22
- * `rootTsConfigPath` — no separate scope map is built.
23
- *
24
- * Single Responsibility: only discovers projects and workspace metadata.
25
- * Does not index or search.
26
- *
27
- * Implements {@link IProjectDiscovery} so it can be wired as the default
28
- * discovery in the container. `discover()` returns one entry per top-level
29
- * project: when the root itself is a monorepo, each entry carries the
30
- * workspace-wide `rootTsConfigPath`; under a flat root, an ordinary project
31
- * gets `rootTsConfigPath: null`, while a top-level directory that is itself a
32
- * nested monorepo is returned as a single unit with expanded `children` (see
33
- * `toLeafProjects`). `analyzeWorkspace()` exposes the full workspace metadata
34
- * (monorepo flag, root tsconfig, project list).
35
- */
36
9
  class MonorepoAwareDiscoveryService {
37
10
  resolvers;
38
11
  logger;
39
12
  flatDiscovery;
40
- constructor(resolvers, logger,
41
- /** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
42
- flatDiscovery = new ProjectDiscoveryService_1.ProjectDiscoveryService()) {
13
+ constructor(resolvers, logger, flatDiscovery = new ProjectDiscoveryService_1.ProjectDiscoveryService()) {
43
14
  this.resolvers = resolvers;
44
15
  this.logger = logger;
45
16
  this.flatDiscovery = flatDiscovery;
46
17
  }
47
- /**
48
- * IProjectDiscovery contract: enumerate projects in the workspace.
49
- * Attaches the workspace-wide root tsconfig to every project so the
50
- * indexer can resolve cross-project path aliases.
51
- */
52
18
  discover(rootDir) {
53
19
  const workspace = this.analyzeWorkspace(rootDir);
54
- // Case A: workspaceRoot itself is a monorepo — attach the workspace-wide
55
- // tsconfig to every project (original behavior).
56
20
  if (workspace.isMonorepo) {
57
21
  return workspace.projects.map(p => ({ ...p, rootTsConfigPath: workspace.rootTsConfigPath }));
58
22
  }
59
- // Case B: flat workspaceRoot — a top-level project directory may itself be a
60
- // nested monorepo (e.g. an Nx repo). Expand such a directory into its
61
- // sub-projects as `children`, keeping the repo a single selectable unit.
62
23
  return workspace.projects.map(p => this.expandIfNestedMonorepo(p));
63
24
  }
64
- /**
65
- * If `project`'s directory is itself a monorepo (resolver detects it),
66
- * return it with expanded `children` named `<repo>/<sub-project>`, each
67
- * carrying the repo's workspace-wide tsconfig. Otherwise return it unchanged.
68
- */
69
25
  expandIfNestedMonorepo(project) {
70
- // Ordinary flat project — preserve the original contract (null root tsconfig).
71
26
  const flat = { ...project, rootTsConfigPath: null };
72
27
  const resolver = this.resolvers.find(r => r.isMonorepo(project.path));
73
28
  if (!resolver)
@@ -89,10 +44,6 @@ class MonorepoAwareDiscoveryService {
89
44
  });
90
45
  return { ...project, children };
91
46
  }
92
- /**
93
- * Analyze the workspace root and return full workspace information:
94
- * monorepo detection flag, root tsconfig path, and project list.
95
- */
96
47
  analyzeWorkspace(rootDir) {
97
48
  for (const resolver of this.resolvers) {
98
49
  if (resolver.isMonorepo(rootDir)) {
@@ -112,7 +63,6 @@ class MonorepoAwareDiscoveryService {
112
63
  return { isMonorepo: true, rootTsConfigPath, projects };
113
64
  }
114
65
  }
115
- // Fallback: flat workspace (original behavior) — delegated to the default discovery.
116
66
  this.logger.debug('MonorepoAwareDiscovery: no monorepo detected, using flat discovery', { rootDir });
117
67
  const projects = this.flatDiscovery.discover(rootDir);
118
68
  return {
@@ -1,40 +1,11 @@
1
1
  import type { IMonorepoResolver, MonorepoProject } from '../interfaces/IMonorepoResolver';
2
2
  import type { ILogger } from '../interfaces';
3
- /**
4
- * Nx monorepo resolver.
5
- *
6
- * Detection: presence of `nx.json` at the workspace root.
7
- *
8
- * Project discovery strategies (results deduplicated by absolute path):
9
- * 1. Read `workspace.json` or `angular.json` → projects map
10
- * 2. Scan standard Nx directories (`apps/`, `libs/`, `packages/`) for
11
- * `project.json`, then `package.json` (only when strategy 1 found nothing)
12
- * 3. `tsconfig.base.json` `paths` — root-level libs referenced only by path
13
- * alias (e.g. `contracts/`, `interfaces/`) that the directory scan misses
14
- * 4. Root-level fallback: treat the workspace root itself as a project
15
- * (standalone Nx setup) when nothing else matched
16
- *
17
- * tsconfig resolution:
18
- * - Returns `tsconfig.base.json` at workspace root (Nx convention)
19
- * - Falls back to `tsconfig.json` at workspace root
20
- *
21
- * Cross-project import resolution is handled by the AST parser via the
22
- * workspace-wide tsconfig `paths` (tsconfig.base.json), so no scope map is built here.
23
- */
24
3
  export declare class NxMonorepoResolver implements IMonorepoResolver {
25
4
  private logger;
26
5
  constructor(logger: ILogger);
27
6
  isMonorepo(rootDir: string): boolean;
28
7
  discoverProjects(rootDir: string): MonorepoProject[];
29
- /**
30
- * Discover projects from `tsconfig.base.json` `compilerOptions.paths`. Each
31
- * alias (e.g. `@scope/interfaces` → `interfaces/src/index.ts`) resolves to a
32
- * lib root (the path before `/src/`), which for real Nx libs contains a
33
- * project.json. Aliases pointing at non-projects (no project.json/package.json,
34
- * e.g. `@env` → `environments/environment.ts`) are skipped.
35
- */
36
8
  private discoverFromTsconfigPaths;
37
- /** Parse JSON that may contain comments / trailing commas (tsconfig style). */
38
9
  private parseJsonc;
39
10
  getRootTsConfigPath(rootDir: string): string | null;
40
11
  private readWorkspaceJson;