codesearch-mcp 0.2.1-alpha → 0.2.3-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.
- package/CHANGELOG.md +40 -0
- package/README.md +15 -8
- package/dist/app.js +45 -7
- package/dist/src/commands/EvalCommand.d.ts +10 -0
- package/dist/src/commands/EvalCommand.js +231 -0
- package/dist/src/commands/IndexCommand.js +79 -53
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +5 -29
- package/dist/src/db/schema.js +2 -8
- package/dist/src/dependency-traversal.d.ts +16 -0
- package/dist/src/dependency-traversal.js +45 -0
- package/dist/src/enriched-prompt.d.ts +3 -2
- package/dist/src/enriched-prompt.js +3 -2
- package/dist/src/eval-results.d.ts +12 -0
- package/dist/src/eval-results.js +114 -0
- package/dist/src/file-utils.d.ts +15 -0
- package/dist/src/file-utils.js +24 -1
- package/dist/src/help.js +4 -1
- package/dist/src/interfaces/IAstParser.d.ts +13 -1
- package/dist/src/interfaces/IDatabase.d.ts +0 -14
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +11 -16
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/project-scope.d.ts +19 -0
- package/dist/src/project-scope.js +49 -0
- package/dist/src/project-selector.d.ts +6 -0
- package/dist/src/project-selector.js +18 -0
- package/dist/src/retrieval-eval.d.ts +16 -0
- package/dist/src/retrieval-eval.js +214 -0
- package/dist/src/services/AstParserService.d.ts +66 -1
- package/dist/src/services/AstParserService.js +432 -28
- package/dist/src/services/DatabaseService.d.ts +0 -12
- package/dist/src/services/DatabaseService.js +2 -52
- package/dist/src/services/IndexerService.d.ts +8 -0
- package/dist/src/services/IndexerService.js +24 -11
- package/dist/src/services/McpServer.d.ts +7 -0
- package/dist/src/services/McpServer.js +42 -38
- package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
- package/dist/src/services/SearcherService.d.ts +189 -25
- package/dist/src/services/SearcherService.js +505 -203
- package/dist/src/symbol-rows.d.ts +14 -9
- package/dist/src/symbol-rows.js +17 -11
- package/dist/src/tools/FindSymbolTool.js +14 -5
- package/dist/src/tools/GetCallersTool.d.ts +3 -1
- package/dist/src/tools/GetCallersTool.js +16 -6
- package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
- package/dist/src/tools/GetDependenciesTool.js +15 -8
- package/dist/src/tools/GetDependentsTool.d.ts +3 -1
- package/dist/src/tools/GetDependentsTool.js +31 -16
- package/dist/src/tools/ListFilesTool.d.ts +3 -1
- package/dist/src/tools/ListFilesTool.js +14 -12
- package/dist/src/tools/ListProjectsTool.js +5 -1
- package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
- package/dist/src/tools/ReadFileRangeTool.js +12 -11
- package/dist/src/tools/ReadFileTool.d.ts +3 -1
- package/dist/src/tools/ReadFileTool.js +12 -10
- package/dist/src/tools/SearchCodebaseTool.js +3 -1
- package/dist/src/tools/format-helpers.d.ts +16 -1
- package/dist/src/tools/format-helpers.js +64 -0
- package/package.json +9 -8
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { SymbolEntry } from './interfaces/IAstParser';
|
|
2
|
-
export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
|
|
2
|
+
export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic' | 'target'>;
|
|
3
3
|
/**
|
|
4
|
-
* Combine top-level symbols and
|
|
5
|
-
* Overload signatures of one declaration — same
|
|
6
|
-
* isStatic)` — collapse to a single representative
|
|
7
|
-
* (the implementation) when present, otherwise the
|
|
8
|
-
* signature in source order). Static and instance
|
|
9
|
-
* never collapsed into each other (`isStatic` is
|
|
10
|
-
*
|
|
4
|
+
* Combine top-level symbols, members and GraphQL-named rows into the rows
|
|
5
|
+
* written to `code_symbol`. Overload signatures of one declaration — same
|
|
6
|
+
* `(container, name, kind, isStatic)` — collapse to a single representative
|
|
7
|
+
* row: the one with a body (the implementation) when present, otherwise the
|
|
8
|
+
* earliest by line (the first signature in source order). Static and instance
|
|
9
|
+
* members sharing a name are never collapsed into each other (`isStatic` is
|
|
10
|
+
* part of the group key). `graphqlNames` bypass this collapse — decorators
|
|
11
|
+
* cannot sit on overload signatures, so there is nothing to collapse, and the
|
|
12
|
+
* collapse's `(container, name, kind, isStatic)` key would wrongly merge two
|
|
13
|
+
* different methods exposing the same GraphQL name from two resolver classes
|
|
14
|
+
* (both with container `Query`). A final dedup by `(container, name, kind,
|
|
15
|
+
* line)` matches the DB's unique index.
|
|
11
16
|
*/
|
|
12
|
-
export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
|
|
17
|
+
export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[], graphqlNames?: SymbolEntry[]): CodeSymbolRow[];
|
package/dist/src/symbol-rows.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildSymbolRows = buildSymbolRows;
|
|
4
4
|
/**
|
|
5
|
-
* Combine top-level symbols and
|
|
6
|
-
* Overload signatures of one declaration — same
|
|
7
|
-
* isStatic)` — collapse to a single representative
|
|
8
|
-
* (the implementation) when present, otherwise the
|
|
9
|
-
* signature in source order). Static and instance
|
|
10
|
-
* never collapsed into each other (`isStatic` is
|
|
11
|
-
*
|
|
5
|
+
* Combine top-level symbols, members and GraphQL-named rows into the rows
|
|
6
|
+
* written to `code_symbol`. Overload signatures of one declaration — same
|
|
7
|
+
* `(container, name, kind, isStatic)` — collapse to a single representative
|
|
8
|
+
* row: the one with a body (the implementation) when present, otherwise the
|
|
9
|
+
* earliest by line (the first signature in source order). Static and instance
|
|
10
|
+
* members sharing a name are never collapsed into each other (`isStatic` is
|
|
11
|
+
* part of the group key). `graphqlNames` bypass this collapse — decorators
|
|
12
|
+
* cannot sit on overload signatures, so there is nothing to collapse, and the
|
|
13
|
+
* collapse's `(container, name, kind, isStatic)` key would wrongly merge two
|
|
14
|
+
* different methods exposing the same GraphQL name from two resolver classes
|
|
15
|
+
* (both with container `Query`). A final dedup by `(container, name, kind,
|
|
16
|
+
* line)` matches the DB's unique index.
|
|
12
17
|
*/
|
|
13
|
-
function buildSymbolRows(symbols, members) {
|
|
18
|
+
function buildSymbolRows(symbols, members, graphqlNames = []) {
|
|
14
19
|
const groups = new Map();
|
|
15
20
|
for (const entry of [...symbols, ...members]) {
|
|
16
21
|
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
|
|
@@ -30,9 +35,10 @@ function buildSymbolRows(symbols, members) {
|
|
|
30
35
|
collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
|
|
31
36
|
}
|
|
32
37
|
const dedup = new Map();
|
|
33
|
-
for (const entry of collapsed) {
|
|
38
|
+
for (const entry of [...collapsed, ...graphqlNames]) {
|
|
34
39
|
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
|
|
35
|
-
dedup.
|
|
40
|
+
if (!dedup.has(key))
|
|
41
|
+
dedup.set(key, entry);
|
|
36
42
|
}
|
|
37
|
-
return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
|
|
43
|
+
return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic, target }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic, target: target ?? null }));
|
|
38
44
|
}
|
|
@@ -5,8 +5,14 @@ const format_helpers_1 = require("./format-helpers");
|
|
|
5
5
|
const VALID_KINDS = [
|
|
6
6
|
'function', 'class', 'type', 'interface', 'variable',
|
|
7
7
|
'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
|
|
8
|
+
'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
|
|
8
9
|
'all',
|
|
9
10
|
];
|
|
11
|
+
const GRAPHQL_KINDS = new Set([
|
|
12
|
+
'graphql_query', 'graphql_mutation', 'graphql_subscription', 'graphql_field', 'graphql_type',
|
|
13
|
+
]);
|
|
14
|
+
const GRAPHQL_STALE_INDEX_HINT = 'Files indexed before GraphQL indexing was added have no GraphQL rows until `rag-index --reindex` — ' +
|
|
15
|
+
'this miss does not prove the name is absent.';
|
|
10
16
|
class FindSymbolTool {
|
|
11
17
|
searcher;
|
|
12
18
|
constructor(searcher) {
|
|
@@ -20,7 +26,10 @@ class FindSymbolTool {
|
|
|
20
26
|
'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
|
|
21
27
|
'(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
|
|
22
28
|
"(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
|
|
23
|
-
'want its definition location fast.'
|
|
29
|
+
'want its definition location fast. Also finds a NestJS GraphQL operation (`@Query`/`@Mutation`/`@Subscription`), ' +
|
|
30
|
+
'resolve field (`@ResolveField`) or type (`@ObjectType`/`@InputType`/`@InterfaceType`) by its GraphQL name — the ' +
|
|
31
|
+
'name the schema exposes, which may differ from the TypeScript declaration name. Pass the matching `graphql_*` ' +
|
|
32
|
+
'kind when the name comes from a schema, a GraphQL document or a frontend.',
|
|
24
33
|
inputSchema: {
|
|
25
34
|
type: 'object',
|
|
26
35
|
properties: {
|
|
@@ -45,21 +54,21 @@ class FindSymbolTool {
|
|
|
45
54
|
return (0, format_helpers_1.errorResponse)(err.message);
|
|
46
55
|
}
|
|
47
56
|
const { hits, truncated, grepFailed } = parsed;
|
|
48
|
-
const notice = await this.searcher.outdatedNotice(project);
|
|
49
57
|
const lines = [];
|
|
50
|
-
if (notice)
|
|
51
|
-
lines.push(notice + '\n');
|
|
52
58
|
if (hits.length === 0) {
|
|
53
59
|
const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
|
|
54
60
|
lines.push(`No definitions found for symbol: ${name}${suffix}`);
|
|
61
|
+
if (GRAPHQL_KINDS.has(kind))
|
|
62
|
+
lines.push(GRAPHQL_STALE_INDEX_HINT);
|
|
55
63
|
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
56
64
|
}
|
|
57
65
|
if (truncated)
|
|
58
66
|
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
59
67
|
for (const h of hits) {
|
|
60
68
|
const staticTag = h.isStatic ? ' static' : '';
|
|
69
|
+
const targetTag = h.target ? ` → \`${h.target}\`` : '';
|
|
61
70
|
const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
|
|
62
|
-
lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
|
|
71
|
+
lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag}${targetTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
|
|
63
72
|
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
64
73
|
}
|
|
65
74
|
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class GetCallersTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -4,8 +4,10 @@ exports.GetCallersTool = void 0;
|
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
5
|
class GetCallersTool {
|
|
6
6
|
searcher;
|
|
7
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
8
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
9
11
|
}
|
|
10
12
|
definition() {
|
|
11
13
|
return {
|
|
@@ -20,7 +22,7 @@ class GetCallersTool {
|
|
|
20
22
|
properties: {
|
|
21
23
|
symbol: { type: 'string', description: 'Bare or qualified (Class.method) function/method name (case-sensitive)' },
|
|
22
24
|
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
23
|
-
callee_file: { type: 'string', description:
|
|
25
|
+
callee_file: { type: 'string', description: `Optional: only calls to the definition in this file, to disambiguate overloaded names. ${(0, format_helpers_1.pathArgDescription)()}` },
|
|
24
26
|
},
|
|
25
27
|
required: ['symbol', 'project'],
|
|
26
28
|
},
|
|
@@ -29,7 +31,18 @@ class GetCallersTool {
|
|
|
29
31
|
async handle(args) {
|
|
30
32
|
const symbol = String(args.symbol ?? '');
|
|
31
33
|
const project = String(args.project ?? '');
|
|
32
|
-
const
|
|
34
|
+
const rawCallee = args.callee_file;
|
|
35
|
+
let calleeFile = null;
|
|
36
|
+
if (rawCallee != null && rawCallee !== '') {
|
|
37
|
+
const input = String(rawCallee);
|
|
38
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
39
|
+
if (!resolved.ok)
|
|
40
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_callers', 'callee_file', input, input, resolved);
|
|
41
|
+
const failure = await this.searcher.checkGraphPath(resolved.relPath);
|
|
42
|
+
if (failure)
|
|
43
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_callers', 'callee_file', input, resolved.relPath, failure);
|
|
44
|
+
calleeFile = resolved.relPath;
|
|
45
|
+
}
|
|
33
46
|
let rows;
|
|
34
47
|
try {
|
|
35
48
|
rows = await this.searcher.getCallers(symbol, project, calleeFile);
|
|
@@ -37,10 +50,7 @@ class GetCallersTool {
|
|
|
37
50
|
catch (err) {
|
|
38
51
|
return (0, format_helpers_1.errorResponse)(err.message);
|
|
39
52
|
}
|
|
40
|
-
const notice = await this.searcher.outdatedNotice(project);
|
|
41
53
|
const lines = [];
|
|
42
|
-
if (notice)
|
|
43
|
-
lines.push(notice + '\n');
|
|
44
54
|
if (rows.length === 0) {
|
|
45
55
|
lines.push(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index --reindex\` if the call graph has not been populated yet.`);
|
|
46
56
|
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class GetDependenciesTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -4,32 +4,39 @@ exports.GetDependenciesTool = void 0;
|
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
5
|
class GetDependenciesTool {
|
|
6
6
|
searcher;
|
|
7
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
8
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
9
11
|
}
|
|
10
12
|
definition() {
|
|
11
13
|
return {
|
|
12
14
|
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.',
|
|
15
|
+
description: 'Show what files a given file imports (direct or transitive), in any project. Use to understand what a file depends on before modifying it. depType values: import | import-default | import-namespace | import-side-effect | reexport | reexport-namespace.',
|
|
14
16
|
inputSchema: {
|
|
15
17
|
type: 'object',
|
|
16
18
|
properties: {
|
|
17
|
-
file_path: { type: 'string', description:
|
|
18
|
-
project: { type: 'string', description: 'Project name (use list_projects to find it)' },
|
|
19
|
+
file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
|
|
19
20
|
symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
|
|
20
21
|
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
|
|
21
22
|
},
|
|
22
|
-
required: ['file_path'
|
|
23
|
+
required: ['file_path'],
|
|
23
24
|
},
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
async handle(args) {
|
|
27
|
-
const
|
|
28
|
-
const project = String(args.project);
|
|
28
|
+
const input = args.file_path == null ? '' : String(args.file_path);
|
|
29
29
|
const symbol = args.symbol ? String(args.symbol) : null;
|
|
30
30
|
const transitive = Boolean(args.transitive);
|
|
31
|
-
const
|
|
31
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
32
|
+
if (!resolved.ok)
|
|
33
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependencies', 'file_path', input, input, resolved);
|
|
34
|
+
const filePath = resolved.relPath;
|
|
35
|
+
const rows = await this.searcher.getDependencies(filePath, symbol, transitive);
|
|
32
36
|
if (rows.length === 0) {
|
|
37
|
+
const failure = await this.searcher.checkGraphPath(filePath);
|
|
38
|
+
if (failure)
|
|
39
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependencies', 'file_path', input, filePath, failure);
|
|
33
40
|
return (0, format_helpers_1.textResponse)(`No dependencies found for: ${filePath}`);
|
|
34
41
|
}
|
|
35
42
|
return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependencies)(filePath, rows, transitive));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class GetDependentsTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -2,52 +2,67 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GetDependentsTool = void 0;
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
|
-
const format_helpers_2 = require("./format-helpers");
|
|
6
5
|
class GetDependentsTool {
|
|
7
6
|
searcher;
|
|
8
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
9
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
10
11
|
}
|
|
11
12
|
definition() {
|
|
12
13
|
return {
|
|
13
14
|
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
|
+
description: 'Show which files import a given file (reverse graph), in any project. Use before refactoring to find all callers and avoid breaking changes. depType values: import | import-default | import-namespace | import-side-effect | reexport | reexport-namespace.',
|
|
15
16
|
inputSchema: {
|
|
16
17
|
type: 'object',
|
|
17
18
|
properties: {
|
|
18
|
-
file_path: { type: 'string', description:
|
|
19
|
-
|
|
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.' },
|
|
19
|
+
file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
|
|
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 of the project owning the file to find runtime usages — DI containers, singletons, dynamic lookups, string references. Capped at 200 matches.' },
|
|
21
21
|
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
|
|
22
22
|
},
|
|
23
|
-
required: ['file_path'
|
|
23
|
+
required: ['file_path'],
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
async handle(args) {
|
|
28
|
-
const
|
|
29
|
-
const project = String(args.project);
|
|
28
|
+
const input = args.file_path == null ? '' : String(args.file_path);
|
|
30
29
|
const symbol = args.symbol ? String(args.symbol) : null;
|
|
31
30
|
const transitive = Boolean(args.transitive);
|
|
32
|
-
const
|
|
31
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
32
|
+
if (!resolved.ok)
|
|
33
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependents', 'file_path', input, input, resolved);
|
|
34
|
+
const filePath = resolved.relPath;
|
|
35
|
+
const rows = await this.searcher.getDependents(filePath, symbol, transitive);
|
|
33
36
|
if (rows.length > 0) {
|
|
34
|
-
return (0, format_helpers_1.textResponse)((0,
|
|
37
|
+
return (0, format_helpers_1.textResponse)((0, format_helpers_1.formatDependents)(filePath, rows, transitive));
|
|
35
38
|
}
|
|
39
|
+
const failure = await this.searcher.checkGraphPath(filePath);
|
|
40
|
+
if (failure)
|
|
41
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'get_dependents', 'file_path', input, filePath, failure);
|
|
36
42
|
const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
|
|
37
43
|
if (!symbol) {
|
|
38
44
|
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
45
|
}
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
return (0, format_helpers_1.textResponse)(
|
|
46
|
+
const rootDir = this.searcher.fallbackRoot(filePath);
|
|
47
|
+
if (rootDir === null)
|
|
48
|
+
return (0, format_helpers_1.textResponse)(header);
|
|
49
|
+
const fallbackHeader = `**Fallback \`[text search]\` for \`${symbol}\`:**`;
|
|
50
|
+
const grep = await this.searcher.grepSymbol(symbol, rootDir, filePath);
|
|
51
|
+
if (!grep.ok) {
|
|
52
|
+
return (0, format_helpers_1.textResponse)(`${header}\n\n${fallbackHeader} failed`);
|
|
53
|
+
}
|
|
54
|
+
if (grep.matches.length === 0) {
|
|
55
|
+
return (0, format_helpers_1.textResponse)(`${header}\n\n${fallbackHeader} No results.`);
|
|
43
56
|
}
|
|
44
57
|
const grouped = new Map();
|
|
45
|
-
for (const m of matches) {
|
|
58
|
+
for (const m of grep.matches) {
|
|
46
59
|
if (!grouped.has(m.filePath))
|
|
47
60
|
grouped.set(m.filePath, []);
|
|
48
61
|
grouped.get(m.filePath).push(m.line);
|
|
49
62
|
}
|
|
50
|
-
const
|
|
63
|
+
const notes = (grep.truncated ? ' *(truncated to 200 results)*' : '') +
|
|
64
|
+
(grep.partial ? ' *(partial — some files could not be searched)*' : '');
|
|
65
|
+
const fallbackLines = [`\n${fallbackHeader}${notes}`];
|
|
51
66
|
for (const [fp, lines] of grouped) {
|
|
52
67
|
fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
|
|
53
68
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class ListFilesTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -4,36 +4,38 @@ exports.ListFilesTool = void 0;
|
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
5
|
class ListFilesTool {
|
|
6
6
|
searcher;
|
|
7
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
8
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
9
11
|
}
|
|
10
12
|
definition() {
|
|
11
13
|
return {
|
|
12
14
|
name: 'list_files',
|
|
13
|
-
description: 'List files and directories
|
|
15
|
+
description: 'List files and directories under a workspace directory as an indented tree. Use to understand project structure before reading files; list_projects prints each project\'s directory.',
|
|
14
16
|
inputSchema: {
|
|
15
17
|
type: 'object',
|
|
16
18
|
properties: {
|
|
17
|
-
path: { type: 'string', description:
|
|
18
|
-
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
19
|
+
path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)('directory') },
|
|
19
20
|
depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
|
|
20
21
|
pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
|
|
21
22
|
},
|
|
22
|
-
required: ['path'
|
|
23
|
+
required: ['path'],
|
|
23
24
|
},
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
async handle(args) {
|
|
27
|
-
const
|
|
28
|
-
const project = String(args.project ?? '');
|
|
28
|
+
const input = args.path == null ? '' : String(args.path);
|
|
29
29
|
const rawDepth = Number(args.depth);
|
|
30
30
|
const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
|
|
31
31
|
const pattern = args.pattern ? String(args.pattern) : null;
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return (0, format_helpers_1.
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
33
|
+
if (!resolved.ok)
|
|
34
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'list_files', 'path', input, input, resolved, 'directory');
|
|
35
|
+
const result = await this.searcher.listFiles(resolved.relPath, depth, pattern);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'list_files', 'path', input, resolved.relPath, result, 'directory');
|
|
38
|
+
return (0, format_helpers_1.textResponse)(`**Directory:** ${resolved.relPath}\n\`\`\`\n${result.tree}\n\`\`\``);
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
exports.ListFilesTool = ListFilesTool;
|
|
@@ -10,7 +10,7 @@ class ListProjectsTool {
|
|
|
10
10
|
definition() {
|
|
11
11
|
return {
|
|
12
12
|
name: 'list_projects',
|
|
13
|
-
description: 'List all indexed projects with their chunk counts and metadata (description, stack, notes, relations from .ragmeta.json).',
|
|
13
|
+
description: 'List all indexed projects with their chunk counts, workspace-relative directory (`path:`) and metadata (description, stack, notes, relations from .ragmeta.json).',
|
|
14
14
|
inputSchema: { type: 'object', properties: {} },
|
|
15
15
|
};
|
|
16
16
|
}
|
|
@@ -21,6 +21,10 @@ class ListProjectsTool {
|
|
|
21
21
|
}
|
|
22
22
|
return (0, format_helpers_1.textResponse)(rows.map(r => {
|
|
23
23
|
const lines = [`${r.serviceName}: ${r.chunks} chunks`];
|
|
24
|
+
if (r.relPath) {
|
|
25
|
+
const state = !r.rootError ? '' : r.rootError === 'ENOENT' ? ' (not on disk)' : ` (${r.rootError})`;
|
|
26
|
+
lines.push(` path: ${r.relPath}${state}`);
|
|
27
|
+
}
|
|
24
28
|
if (r.packageName)
|
|
25
29
|
lines.push(` package: ${r.packageName}`);
|
|
26
30
|
if (r.type)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class ReadFileRangeTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -4,17 +4,19 @@ exports.ReadFileRangeTool = void 0;
|
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
5
|
class ReadFileRangeTool {
|
|
6
6
|
searcher;
|
|
7
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
8
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
9
11
|
}
|
|
10
12
|
definition() {
|
|
11
13
|
return {
|
|
12
14
|
name: 'read_file_range',
|
|
13
|
-
description: 'Read a specific line range from
|
|
15
|
+
description: 'Read a specific line range from a file under the workspace, indexed or not. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
|
|
14
16
|
inputSchema: {
|
|
15
17
|
type: 'object',
|
|
16
18
|
properties: {
|
|
17
|
-
file_path: { type: 'string', description:
|
|
19
|
+
file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
|
|
18
20
|
start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
|
|
19
21
|
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
|
|
20
22
|
},
|
|
@@ -23,16 +25,15 @@ class ReadFileRangeTool {
|
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
async handle(args) {
|
|
26
|
-
const
|
|
28
|
+
const input = args.file_path == null ? '' : String(args.file_path);
|
|
27
29
|
const startLine = Math.max(1, Number(args.start_line) || 1);
|
|
28
30
|
const endLine = Number(args.end_line) || startLine + 99;
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return (0, format_helpers_1.
|
|
35
|
-
}
|
|
31
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
32
|
+
if (!resolved.ok)
|
|
33
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file_range', 'file_path', input, input, resolved);
|
|
34
|
+
const range = await this.searcher.readFileRange(resolved.relPath, startLine, endLine);
|
|
35
|
+
if (!range.ok)
|
|
36
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file_range', 'file_path', input, resolved.relPath, range);
|
|
36
37
|
const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
|
|
37
38
|
let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
38
39
|
if (range.hasMore) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IMcpToolHandler, ToolDefinition, ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
2
2
|
import type { ISearcher } from '../interfaces/ISearcher';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare class ReadFileTool implements IMcpToolHandler {
|
|
4
5
|
private searcher;
|
|
5
|
-
|
|
6
|
+
private logger;
|
|
7
|
+
constructor(searcher: ISearcher, logger: ILogger);
|
|
6
8
|
definition(): ToolDefinition;
|
|
7
9
|
handle(args: Record<string, unknown>): Promise<ToolResponse>;
|
|
8
10
|
}
|
|
@@ -4,17 +4,19 @@ exports.ReadFileTool = void 0;
|
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
5
|
class ReadFileTool {
|
|
6
6
|
searcher;
|
|
7
|
-
|
|
7
|
+
logger;
|
|
8
|
+
constructor(searcher, logger) {
|
|
8
9
|
this.searcher = searcher;
|
|
10
|
+
this.logger = logger;
|
|
9
11
|
}
|
|
10
12
|
definition() {
|
|
11
13
|
return {
|
|
12
14
|
name: 'read_file',
|
|
13
|
-
description: 'Read a file
|
|
15
|
+
description: 'Read a complete file under the workspace, indexed or not. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
|
|
14
16
|
inputSchema: {
|
|
15
17
|
type: 'object',
|
|
16
18
|
properties: {
|
|
17
|
-
file_path: { type: 'string', description:
|
|
19
|
+
file_path: { type: 'string', description: (0, format_helpers_1.pathArgDescription)() },
|
|
18
20
|
max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
|
|
19
21
|
},
|
|
20
22
|
required: ['file_path'],
|
|
@@ -22,15 +24,15 @@ class ReadFileTool {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
async handle(args) {
|
|
25
|
-
const
|
|
27
|
+
const input = args.file_path == null ? '' : String(args.file_path);
|
|
26
28
|
const rawMax = Number(args.max_lines);
|
|
27
29
|
const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const resolved = await this.searcher.resolveToolPath(input);
|
|
31
|
+
if (!resolved.ok)
|
|
32
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file', 'file_path', input, input, resolved);
|
|
33
|
+
const result = await this.searcher.readFile(resolved.relPath, maxLines);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
return (0, format_helpers_1.pathErrorResponse)(this.logger, 'read_file', 'file_path', input, resolved.relPath, result);
|
|
34
36
|
const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
35
37
|
let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
36
38
|
if (result.hasMore) {
|
|
@@ -28,6 +28,7 @@ class SearchCodebaseTool {
|
|
|
28
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
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
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
|
+
rerank: { type: 'boolean', description: 'Override whether this call reranks, regardless of config. false skips reranking (use for "what does X do" / "where is X" lookups described without an exact name — reranking measurably hurts these; if you know the exact symbol name, use find_symbol instead). true forces reranking on if a reranker is configured (no-op otherwise). Omit to use config.reranker.enabled (good default for conceptual/architectural queries).' },
|
|
31
32
|
},
|
|
32
33
|
required: ['query'],
|
|
33
34
|
},
|
|
@@ -45,8 +46,9 @@ class SearchCodebaseTool {
|
|
|
45
46
|
const threshold = rawThreshold != null && !isNaN(parsedThreshold)
|
|
46
47
|
? Math.min(1, Math.max(0, parsedThreshold))
|
|
47
48
|
: undefined;
|
|
49
|
+
const rerank = typeof args.rerank === 'boolean' ? args.rerank : undefined;
|
|
48
50
|
const { query, translated } = await this.searcher.prepareQuery(raw);
|
|
49
|
-
const results = await this.searcher.searchChunks(query, limit, project, threshold);
|
|
51
|
+
const results = await this.searcher.searchChunks(query, limit, project, threshold, rerank);
|
|
50
52
|
this.logger.debug('SearchCodebaseTool: handle', {
|
|
51
53
|
query: raw.slice(0, 120), project, threshold: threshold ?? this.defaultThreshold,
|
|
52
54
|
limit, translated, count: results.length, ms: Date.now() - t0,
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import type { ChunkRow, DependencyRow, GraphContext } from '../services/SearcherService';
|
|
1
|
+
import type { ChunkRow, DependencyRow, GraphContext, FileRangeResult, PathFailure, PathRefusal } from '../services/SearcherService';
|
|
2
2
|
import type { ToolResponse } from '../interfaces/IMcpToolHandler';
|
|
3
|
+
import type { ILogger } from '../interfaces/ILogger';
|
|
3
4
|
export declare function textResponse(text: string): ToolResponse;
|
|
4
5
|
export declare function errorResponse(text: string): ToolResponse;
|
|
5
6
|
export declare function formatChunk(r: ChunkRow, label?: string): string;
|
|
6
7
|
export declare function formatGraphContext(ctx: GraphContext | undefined): string;
|
|
7
8
|
export declare function formatDependencies(filePath: string, rows: DependencyRow[], transitive: boolean): string;
|
|
8
9
|
export declare function formatDependents(filePath: string, rows: DependencyRow[], transitive: boolean): string;
|
|
10
|
+
export type PathKind = 'file' | 'directory';
|
|
11
|
+
export declare const PATH_EXAMPLE: Record<PathKind, string>;
|
|
12
|
+
/** Description of a path argument; `kind` names what the path points at. */
|
|
13
|
+
export declare function pathArgDescription(kind?: PathKind): string;
|
|
14
|
+
export type PathError = PathRefusal | PathFailure | Extract<FileRangeResult, {
|
|
15
|
+
reason: 'past_end' | 'bad_range';
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Text of a path error. `input` is the raw argument (echoed by refusals);
|
|
19
|
+
* `relPath` its canonical form, printed by every other message.
|
|
20
|
+
*/
|
|
21
|
+
export declare function pathErrorText(argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): string;
|
|
22
|
+
/** Error response for a path error, logged at `warn` — a caller or index-freshness problem, not a server fault. */
|
|
23
|
+
export declare function pathErrorResponse(logger: ILogger, tool: string, argName: string, input: string, relPath: string, err: PathError, kind?: PathKind): ToolResponse;
|