codesearch-mcp 0.0.17-alpha → 0.0.18-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 (83) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +30 -26
  3. package/dist/app.js +34 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +22 -7
  7. package/dist/src/commands/McpCommand.js +4 -3
  8. package/dist/src/commands/WebCommand.d.ts +8 -0
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +43 -0
  11. package/dist/src/config-store.js +92 -27
  12. package/dist/src/config.js +16 -17
  13. package/dist/src/constants.d.ts +2 -1
  14. package/dist/src/constants.js +6 -4
  15. package/dist/src/container.d.ts +10 -6
  16. package/dist/src/container.js +6 -3
  17. package/dist/src/graph.d.ts +60 -0
  18. package/dist/src/graph.js +239 -120
  19. package/dist/src/help.js +15 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +6 -3
  21. package/dist/src/interfaces/IDatabase.d.ts +13 -0
  22. package/dist/src/interfaces/IDatabase.js +2 -0
  23. package/dist/src/interfaces/IEmbedder.d.ts +1 -0
  24. package/dist/src/interfaces/ILogger.d.ts +10 -0
  25. package/dist/src/interfaces/ILogger.js +2 -0
  26. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  27. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  28. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  29. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  30. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  31. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  32. package/dist/src/interfaces/ISearcher.d.ts +36 -0
  33. package/dist/src/interfaces/ISearcher.js +2 -0
  34. package/dist/src/interfaces/index.d.ts +7 -0
  35. package/dist/src/interfaces/index.js +2 -0
  36. package/dist/src/project-selector.d.ts +1 -1
  37. package/dist/src/services/AstParserService.d.ts +4 -6
  38. package/dist/src/services/AstParserService.js +29 -13
  39. package/dist/src/services/DatabaseService.d.ts +2 -1
  40. package/dist/src/services/IndexerService.d.ts +6 -8
  41. package/dist/src/services/IndexerService.js +5 -8
  42. package/dist/src/services/LoggerService.d.ts +2 -1
  43. package/dist/src/services/McpServer.d.ts +16 -7
  44. package/dist/src/services/McpServer.js +54 -367
  45. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  46. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  47. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  48. package/dist/src/services/NxMonorepoResolver.js +308 -0
  49. package/dist/src/services/OllamaEmbedder.d.ts +5 -2
  50. package/dist/src/services/OllamaEmbedder.js +9 -2
  51. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  52. package/dist/src/services/ProjectDiscoveryService.js +52 -0
  53. package/dist/src/services/SearcherService.d.ts +39 -6
  54. package/dist/src/services/SearcherService.js +151 -40
  55. package/dist/src/services/WebServerService.d.ts +72 -0
  56. package/dist/src/services/WebServerService.js +247 -0
  57. package/dist/src/setup.js +7 -7
  58. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  59. package/dist/src/tools/FindSymbolTool.js +45 -0
  60. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  61. package/dist/src/tools/GetCallersTool.js +42 -0
  62. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  63. package/dist/src/tools/GetDependenciesTool.js +38 -0
  64. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  65. package/dist/src/tools/GetDependentsTool.js +57 -0
  66. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  67. package/dist/src/tools/ListFilesTool.js +39 -0
  68. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  69. package/dist/src/tools/ListProjectsTool.js +44 -0
  70. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  71. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  72. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  73. package/dist/src/tools/ReadFileTool.js +42 -0
  74. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  75. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  76. package/dist/src/tools/format-helpers.d.ts +8 -0
  77. package/dist/src/tools/format-helpers.js +57 -0
  78. package/dist/src/tools/index.d.ts +9 -0
  79. package/dist/src/tools/index.js +21 -0
  80. package/dist/src/web/dashboard.d.ts +9 -0
  81. package/dist/src/web/dashboard.js +337 -0
  82. package/docs/web-dashboard.md +187 -0
  83. package/package.json +5 -2
package/dist/src/setup.js CHANGED
@@ -79,7 +79,7 @@ async function waitForDb(host, port, user, pass, db, maxWaitSec = 60, logger, st
79
79
  }
80
80
  const reason = 'PostgreSQL did not become ready in time.';
81
81
  console.error(`\n${reason}`);
82
- logger?.error('waitForDb: timeout', { reason, steps });
82
+ logger?.error('waitForDb: timeout', { reason, steps: JSON.stringify(steps) });
83
83
  process.exit(1);
84
84
  }
85
85
  async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
@@ -106,7 +106,7 @@ async function waitForOllama(host, maxWaitSec = 60, logger, steps) {
106
106
  }
107
107
  const reason = 'Ollama did not become ready in time.';
108
108
  console.error(`\n${reason}`);
109
- logger?.error('waitForOllama: timeout', { reason, steps });
109
+ logger?.error('waitForOllama: timeout', { reason, steps: JSON.stringify(steps) });
110
110
  process.exit(1);
111
111
  }
112
112
  async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
@@ -120,7 +120,7 @@ async function checkRemoteDb(host, port, user, pass, db, logger, steps) {
120
120
  catch (err) {
121
121
  const msg = err instanceof Error ? err.message : String(err);
122
122
  console.error(` Connection failed: ${msg}`);
123
- logger?.error('checkRemoteDb: connection failed', { reason: msg, steps });
123
+ logger?.error('checkRemoteDb: connection failed', { reason: msg, steps: JSON.stringify(steps) });
124
124
  return false;
125
125
  }
126
126
  }
@@ -139,7 +139,7 @@ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger,
139
139
  if (upResult.status !== 0) {
140
140
  const reason = 'Failed to start Docker containers. Make sure Docker is running.';
141
141
  console.error(reason);
142
- logger?.error('startDockerContainers: docker startup failed', { reason, steps });
142
+ logger?.error('startDockerContainers: docker startup failed', { reason, steps: JSON.stringify(steps) });
143
143
  process.exit(1);
144
144
  }
145
145
  await waitForDb('localhost', parseInt(dbPort), dbUser, dbPass, 'postgres', 60, logger, steps);
@@ -150,7 +150,7 @@ async function startDockerContainers(ollamaHost, dbPort, dbUser, dbPass, logger,
150
150
  const reason = 'Could not resolve ollama container ID from compose. Pull the model manually:';
151
151
  console.error(reason);
152
152
  console.error(` docker exec <ollama-container> ollama pull ${constants_1.DEFAULT_EMBED_MODEL}`);
153
- logger?.error('startDockerContainers: ollama container not found', { reason, steps });
153
+ logger?.error('startDockerContainers: ollama container not found', { reason, steps: JSON.stringify(steps) });
154
154
  process.exit(1);
155
155
  }
156
156
  console.log(`Pulling ${constants_1.DEFAULT_EMBED_MODEL} model (this may take a while)...`);
@@ -266,11 +266,11 @@ async function runSetup(logger) {
266
266
  }
267
267
  if (initError) {
268
268
  console.error(`Database schema init failed: ${initError.message}`);
269
- logger?.error('runSetup: database schema init failed', { reason: initError.message, steps });
269
+ logger?.error('runSetup: database schema init failed', { reason: initError.message, steps: JSON.stringify(steps) });
270
270
  process.exit(1);
271
271
  }
272
272
  console.log('\nSetup complete!');
273
273
  console.log('\nNext steps:');
274
- console.log(` rag-index ${workspaceRoot}`);
274
+ console.log(' rag-index');
275
275
  console.log(' claude mcp add codesearch-mcp -s user -- rag-mcp');
276
276
  }
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class FindSymbolTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FindSymbolTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ const VALID_KINDS = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
6
+ class FindSymbolTool {
7
+ searcher;
8
+ constructor(searcher) {
9
+ this.searcher = searcher;
10
+ }
11
+ definition() {
12
+ return {
13
+ name: 'find_symbol',
14
+ description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables, methods) by exact name. Use when you know the symbol name and want its definition location fast.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
19
+ project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
20
+ kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
21
+ },
22
+ required: ['name'],
23
+ },
24
+ };
25
+ }
26
+ async handle(args) {
27
+ const name = String(args.name ?? '');
28
+ const project = args.project ? String(args.project) : null;
29
+ const rawKind = args.kind;
30
+ const kind = VALID_KINDS.includes(rawKind) ? rawKind : 'all';
31
+ const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
32
+ if (hits.length === 0) {
33
+ return (0, format_helpers_1.textResponse)(`No definitions found for symbol: ${name}`);
34
+ }
35
+ const lines = [];
36
+ if (truncated)
37
+ lines.push(`*(truncated to ${hits.length} results)*\n`);
38
+ for (const h of hits) {
39
+ lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
40
+ lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
41
+ }
42
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
43
+ }
44
+ }
45
+ exports.FindSymbolTool = FindSymbolTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetCallersTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetCallersTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class GetCallersTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'get_callers',
13
+ description: 'Find all call sites of a function or method by exact name. Uses the call graph index — faster and more precise than grep. Use after find_symbol to trace where a symbol is actually called. Returns empty if rag-index backfill --calls has not been run.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ symbol: { type: 'string', description: 'Exact function or method name (case-sensitive)' },
18
+ project: { type: 'string', description: 'Project name (from list_projects)' },
19
+ callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
20
+ },
21
+ required: ['symbol', 'project'],
22
+ },
23
+ };
24
+ }
25
+ async handle(args) {
26
+ const symbol = String(args.symbol ?? '');
27
+ const project = String(args.project ?? '');
28
+ const calleeFile = args.callee_file ? String(args.callee_file) : null;
29
+ const rows = await this.searcher.getCallers(symbol, project, calleeFile);
30
+ if (rows.length === 0) {
31
+ return (0, format_helpers_1.textResponse)(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index backfill --calls\` if the call graph has not been populated yet.`);
32
+ }
33
+ const lines = [`**Call sites of \`${symbol}\`** (${rows.length} found)\n`];
34
+ for (const r of rows) {
35
+ const loc = r.line != null ? ` line ${r.line}` : '';
36
+ const ctx = r.fromSymbol ? `, in \`${r.fromSymbol}\`` : '';
37
+ lines.push(` → \`${r.fromFile}\`${loc}${ctx}`);
38
+ }
39
+ return (0, format_helpers_1.textResponse)(lines.join('\n'));
40
+ }
41
+ }
42
+ exports.GetCallersTool = GetCallersTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetDependenciesTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetDependenciesTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class GetDependenciesTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'get_dependencies',
13
+ description: 'Show what files a given file imports (direct or transitive). Use to understand what a file depends on before modifying it. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
18
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
19
+ symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
20
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
21
+ },
22
+ required: ['file_path', 'project'],
23
+ },
24
+ };
25
+ }
26
+ async handle(args) {
27
+ const filePath = String(args.file_path);
28
+ const project = String(args.project);
29
+ const symbol = args.symbol ? String(args.symbol) : null;
30
+ const transitive = Boolean(args.transitive);
31
+ const rows = await this.searcher.getDependencies(filePath, project, symbol, transitive);
32
+ if (rows.length === 0) {
33
+ return (0, format_helpers_1.textResponse)(`No dependencies found for: ${filePath}`);
34
+ }
35
+ return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependencies)(filePath, rows, transitive));
36
+ }
37
+ }
38
+ exports.GetDependenciesTool = GetDependenciesTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class GetDependentsTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GetDependentsTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ const format_helpers_2 = require("./format-helpers");
6
+ class GetDependentsTool {
7
+ searcher;
8
+ constructor(searcher) {
9
+ this.searcher = searcher;
10
+ }
11
+ definition() {
12
+ return {
13
+ name: 'get_dependents',
14
+ description: 'Show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes. depType values: import | import-default | import-namespace | reexport | reexport-namespace.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
19
+ project: { type: 'string', description: 'Project name (use list_projects to find it)' },
20
+ symbol: { type: 'string', description: 'Exported symbol name. Two behaviors: (1) if graph dependents exist, narrows them to imports of this symbol; (2) if the import graph returns zero results, triggers a whole-word text-search fallback (grep -F -w) across all .ts/.tsx/.js/.jsx files in the project to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
21
+ transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
22
+ },
23
+ required: ['file_path', 'project'],
24
+ },
25
+ };
26
+ }
27
+ async handle(args) {
28
+ const filePath = String(args.file_path);
29
+ const project = String(args.project);
30
+ const symbol = args.symbol ? String(args.symbol) : null;
31
+ const transitive = Boolean(args.transitive);
32
+ const rows = await this.searcher.getDependents(filePath, project, symbol, transitive);
33
+ if (rows.length > 0) {
34
+ return (0, format_helpers_1.textResponse)((0, format_helpers_2.formatDependents)(filePath, rows, transitive));
35
+ }
36
+ const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
37
+ if (!symbol) {
38
+ return (0, format_helpers_1.textResponse)(`${header}\n\nTip: pass \`symbol\` to trigger a text-search fallback for runtime usages (e.g. via singleton or container).`);
39
+ }
40
+ const { matches, truncated } = await this.searcher.grepSymbol(symbol, project, filePath);
41
+ if (matches.length === 0) {
42
+ return (0, format_helpers_1.textResponse)(`${header}\n\n**Fallback \`[text search]\` for \`${symbol}\`:** No results.`);
43
+ }
44
+ const grouped = new Map();
45
+ for (const m of matches) {
46
+ if (!grouped.has(m.filePath))
47
+ grouped.set(m.filePath, []);
48
+ grouped.get(m.filePath).push(m.line);
49
+ }
50
+ const fallbackLines = [`\n**Fallback \`[text search]\` for \`${symbol}\`:**${truncated ? ' *(truncated to 200 results)*' : ''}`];
51
+ for (const [fp, lines] of grouped) {
52
+ fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
53
+ }
54
+ return (0, format_helpers_1.textResponse)(header + fallbackLines.join('\n'));
55
+ }
56
+ }
57
+ exports.GetDependentsTool = GetDependentsTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class ListFilesTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ListFilesTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class ListFilesTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'list_files',
13
+ description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
18
+ project: { type: 'string', description: 'Project name (from list_projects)' },
19
+ depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
20
+ pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
21
+ },
22
+ required: ['path', 'project'],
23
+ },
24
+ };
25
+ }
26
+ async handle(args) {
27
+ const relPath = String(args.path ?? '.');
28
+ const project = String(args.project ?? '');
29
+ const rawDepth = Number(args.depth);
30
+ const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
31
+ const pattern = args.pattern ? String(args.pattern) : null;
32
+ const { tree, error } = this.searcher.listFiles(relPath, project, depth, pattern);
33
+ if (error) {
34
+ return (0, format_helpers_1.textResponse)(`list_files error: ${error}`);
35
+ }
36
+ return (0, format_helpers_1.textResponse)(`**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\``);
37
+ }
38
+ }
39
+ exports.ListFilesTool = ListFilesTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class ListProjectsTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(_args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ListProjectsTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class ListProjectsTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'list_projects',
13
+ description: 'List all indexed projects with their chunk counts and metadata (description, stack, notes, relations from .ragmeta.json).',
14
+ inputSchema: { type: 'object', properties: {} },
15
+ };
16
+ }
17
+ async handle(_args) {
18
+ const rows = await this.searcher.listProjects();
19
+ if (rows.length === 0) {
20
+ return (0, format_helpers_1.textResponse)('No indexed projects found.');
21
+ }
22
+ return (0, format_helpers_1.textResponse)(rows.map(r => {
23
+ const lines = [`${r.serviceName}: ${r.chunks} chunks`];
24
+ if (r.packageName)
25
+ lines.push(` package: ${r.packageName}`);
26
+ if (r.type)
27
+ lines.push(` type: ${r.type}`);
28
+ if (r.description)
29
+ lines.push(` description: ${r.description}`);
30
+ if (r.stack)
31
+ lines.push(` stack: ${r.stack}`);
32
+ if (r.notes)
33
+ lines.push(` notes: ${r.notes}`);
34
+ if (r.relations) {
35
+ for (const [key, val] of Object.entries(r.relations)) {
36
+ const list = Array.isArray(val) ? val.join(', ') : val;
37
+ lines.push(` ${key}: ${list}`);
38
+ }
39
+ }
40
+ return lines.join('\n');
41
+ }).join('\n\n'));
42
+ }
43
+ }
44
+ exports.ListProjectsTool = ListProjectsTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class ReadFileRangeTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReadFileRangeTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class ReadFileRangeTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'read_file_range',
13
+ description: 'Read a specific line range from an indexed source file. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ file_path: { type: 'string', description: 'File path as returned by search_codebase' },
18
+ start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
19
+ end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
20
+ },
21
+ required: ['file_path'],
22
+ },
23
+ };
24
+ }
25
+ async handle(args) {
26
+ const filePath = String(args.file_path);
27
+ const startLine = Math.max(1, Number(args.start_line) || 1);
28
+ const endLine = Number(args.end_line) || startLine + 99;
29
+ const range = await this.searcher.readFileRange(filePath, startLine, endLine);
30
+ if (!range.ok) {
31
+ const message = range.reason === 'invalid_path' ? `Invalid file path (outside workspace): ${filePath}` :
32
+ range.reason === 'not_indexed' ? `File is not indexed: ${filePath}. Run indexing first.` :
33
+ `File was deleted from disk after indexing: ${filePath}. Re-run indexing to update.`;
34
+ return (0, format_helpers_1.textResponse)(message);
35
+ }
36
+ const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
37
+ let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
38
+ if (range.hasMore) {
39
+ text += `\n\n[file has more lines — call read_file_range again with start_line: ${range.endLine + 1}]`;
40
+ }
41
+ return (0, format_helpers_1.textResponse)(text);
42
+ }
43
+ }
44
+ exports.ReadFileRangeTool = ReadFileRangeTool;
@@ -0,0 +1,8 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ export declare class ReadFileTool implements IMcpToolHandler {
4
+ private searcher;
5
+ constructor(searcher: ISearcher);
6
+ definition(): ToolDefinition;
7
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
8
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReadFileTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class ReadFileTool {
6
+ searcher;
7
+ constructor(searcher) {
8
+ this.searcher = searcher;
9
+ }
10
+ definition() {
11
+ return {
12
+ name: 'read_file',
13
+ description: 'Read a file by absolute path. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ file_path: { type: 'string', description: 'Absolute path to the file. Note: search_codebase returns relative paths — construct the absolute path from your workspace root.' },
18
+ max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
19
+ },
20
+ required: ['file_path'],
21
+ },
22
+ };
23
+ }
24
+ async handle(args) {
25
+ const filePath = String(args.file_path ?? '');
26
+ const rawMax = Number(args.max_lines);
27
+ const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
28
+ const result = await this.searcher.readFileAbsolute(filePath, maxLines);
29
+ if (!result.ok) {
30
+ const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
31
+ `File not found on disk: ${filePath}`;
32
+ return (0, format_helpers_1.textResponse)(message);
33
+ }
34
+ const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
35
+ let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
36
+ if (result.hasMore) {
37
+ text += `\n\n[truncated: file has more lines — use read_file_range with file_path: "${result.filePath}", start_line: ${result.endLine + 1} to continue]`;
38
+ }
39
+ return (0, format_helpers_1.textResponse)(text);
40
+ }
41
+ }
42
+ exports.ReadFileTool = ReadFileTool;
@@ -0,0 +1,13 @@
1
+ import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
2
+ import type { ISearcher } from '../interfaces/ISearcher';
3
+ import type { ILogger } from '../interfaces/ILogger';
4
+ export declare class SearchCodebaseTool implements IMcpToolHandler {
5
+ private searcher;
6
+ private defaultLimit;
7
+ private maxLimit;
8
+ private defaultThreshold;
9
+ private logger;
10
+ constructor(searcher: ISearcher, defaultLimit: number, maxLimit: number, defaultThreshold: number, logger: ILogger);
11
+ definition(): ToolDefinition;
12
+ handle(args: Record<string, unknown>): Promise<ToolResponse>;
13
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchCodebaseTool = void 0;
4
+ const format_helpers_1 = require("./format-helpers");
5
+ class SearchCodebaseTool {
6
+ searcher;
7
+ defaultLimit;
8
+ maxLimit;
9
+ defaultThreshold;
10
+ logger;
11
+ constructor(searcher, defaultLimit, maxLimit, defaultThreshold, logger) {
12
+ this.searcher = searcher;
13
+ this.defaultLimit = defaultLimit;
14
+ this.maxLimit = maxLimit;
15
+ this.defaultThreshold = defaultThreshold;
16
+ this.logger = logger;
17
+ }
18
+ definition() {
19
+ return {
20
+ name: 'search_codebase',
21
+ description: 'Search local project codebase by semantic similarity',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ query: { type: 'string' },
26
+ limit: { type: 'number', description: `Max results (default ${this.defaultLimit}, max ${this.maxLimit})` },
27
+ project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
28
+ include_neighbors: { type: 'boolean', description: 'Also include the chunk before and after each result from the same file (Small-to-Big retrieval). Useful when results look truncated.' },
29
+ include_graph_context: { type: 'boolean', description: 'Append direct imports and dependents list to each result. Shows "Imports: X · Y" and "Used by: Z" without extra tool calls.' },
30
+ threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.defaultThreshold}. Use 0.40–0.45 for precise technical queries, 0.55–0.60 for broad exploration.` },
31
+ },
32
+ required: ['query'],
33
+ },
34
+ };
35
+ }
36
+ async handle(args) {
37
+ const t0 = Date.now();
38
+ const raw = String(args.query);
39
+ const limit = Math.min(Math.max(1, Number(args.limit) || this.defaultLimit), this.maxLimit);
40
+ const project = args.project ? String(args.project) : null;
41
+ const includeNeighbors = Boolean(args.include_neighbors);
42
+ const includeGraphContext = Boolean(args.include_graph_context);
43
+ const rawThreshold = args.threshold;
44
+ const parsedThreshold = Number(rawThreshold);
45
+ const threshold = rawThreshold != null && !isNaN(parsedThreshold)
46
+ ? Math.min(1, Math.max(0, parsedThreshold))
47
+ : undefined;
48
+ const { query, translated } = await this.searcher.prepareQuery(raw);
49
+ const results = await this.searcher.searchChunks(query, limit, project, threshold);
50
+ this.logger.debug('SearchCodebaseTool: handle', {
51
+ query: raw.slice(0, 120), project, threshold: threshold ?? this.defaultThreshold,
52
+ limit, translated, count: results.length, ms: Date.now() - t0,
53
+ });
54
+ if (results.length === 0) {
55
+ return (0, format_helpers_1.textResponse)('No relevant results found.');
56
+ }
57
+ const header = translated ? `> Query translated: "${raw}" → "${query}"\n\n` : '';
58
+ let graphCtx = null;
59
+ if (includeGraphContext) {
60
+ graphCtx = await this.searcher.fetchGraphContext(results);
61
+ }
62
+ if (includeNeighbors) {
63
+ const withNeighbors = await this.searcher.fetchNeighbors(results);
64
+ const blocks = withNeighbors.map(({ result, prevChunk, nextChunk }) => {
65
+ const parts = [];
66
+ if (prevChunk)
67
+ parts.push((0, format_helpers_1.formatChunk)(prevChunk, 'prev chunk'));
68
+ parts.push(`${(0, format_helpers_1.formatChunk)(result)}\n**Relevance:** ${result.score}${(0, format_helpers_1.formatGraphContext)(graphCtx?.get(result.filePath))}`);
69
+ if (nextChunk)
70
+ parts.push((0, format_helpers_1.formatChunk)(nextChunk, 'next chunk'));
71
+ return parts.join('\n\n');
72
+ });
73
+ return (0, format_helpers_1.textResponse)(header + blocks.join('\n\n---\n\n'));
74
+ }
75
+ const text = results
76
+ .map(r => `${(0, format_helpers_1.formatChunk)(r)}\n**Relevance:** ${r.score}${(0, format_helpers_1.formatGraphContext)(graphCtx?.get(r.filePath))}`)
77
+ .join('\n\n---\n\n');
78
+ return (0, format_helpers_1.textResponse)(header + text);
79
+ }
80
+ }
81
+ exports.SearchCodebaseTool = SearchCodebaseTool;
@@ -0,0 +1,8 @@
1
+ import type { ChunkRow, DependencyRow, GraphContext } from '../services/SearcherService';
2
+ import type { ToolResponse } from '../interfaces/IMcpToolHandler';
3
+ export declare function textResponse(text: string): ToolResponse;
4
+ export declare function errorResponse(text: string): ToolResponse;
5
+ export declare function formatChunk(r: ChunkRow, label?: string): string;
6
+ export declare function formatGraphContext(ctx: GraphContext | undefined): string;
7
+ export declare function formatDependencies(filePath: string, rows: DependencyRow[], transitive: boolean): string;
8
+ export declare function formatDependents(filePath: string, rows: DependencyRow[], transitive: boolean): string;