codesearch-mcp 0.0.8-alpha → 0.0.10-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/README.md +26 -14
- package/dist/app.js +24 -144
- package/dist/src/commands/BackfillCommand.d.ts +7 -0
- package/dist/src/commands/BackfillCommand.js +27 -0
- package/dist/src/commands/ICommand.d.ts +3 -0
- package/dist/src/commands/ICommand.js +2 -0
- package/dist/src/commands/IndexCommand.d.ts +9 -0
- package/dist/src/commands/IndexCommand.js +75 -0
- package/dist/src/commands/McpCommand.d.ts +7 -0
- package/dist/src/commands/McpCommand.js +21 -0
- package/dist/src/commands/SetupCommand.d.ts +4 -0
- package/dist/src/commands/SetupCommand.js +10 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +41 -2
- package/dist/src/container.d.ts +15 -0
- package/dist/src/container.js +21 -0
- package/dist/src/db/entities/code-chunk.entity.d.ts +1 -1
- package/dist/src/db/entities/code-chunk.entity.js +2 -2
- package/dist/src/db/entities/code-symbol.entity.d.ts +10 -0
- package/dist/src/db/entities/code-symbol.entity.js +59 -0
- package/dist/src/db/index.d.ts +1 -2
- package/dist/src/db/index.js +3 -6
- package/dist/src/file-utils.d.ts +2 -1
- package/dist/src/file-utils.js +10 -5
- package/dist/src/graph.js +5 -4
- package/dist/src/help.js +1 -1
- package/dist/src/indexer.js +1 -0
- package/dist/src/interfaces/IAstParser.d.ts +24 -0
- package/dist/src/interfaces/IAstParser.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +4 -0
- package/dist/src/interfaces/IEmbedder.js +2 -0
- package/dist/src/interrupt.d.ts +1 -3
- package/dist/src/interrupt.js +14 -16
- package/dist/src/mcp.js +101 -9
- package/dist/src/searcher.d.ts +18 -0
- package/dist/src/searcher.js +171 -1
- package/dist/src/services/AstParserService.d.ts +14 -0
- package/dist/src/services/AstParserService.js +256 -0
- package/dist/src/services/DatabaseService.d.ts +14 -0
- package/dist/src/services/DatabaseService.js +133 -0
- package/dist/src/services/IndexerService.d.ts +39 -0
- package/dist/src/services/IndexerService.js +286 -0
- package/dist/src/services/LoggerService.d.ts +12 -0
- package/dist/src/services/LoggerService.js +73 -0
- package/dist/src/services/McpServer.d.ts +13 -0
- package/dist/src/services/McpServer.js +414 -0
- package/dist/src/services/OllamaEmbedder.d.ts +9 -0
- package/dist/src/services/OllamaEmbedder.js +27 -0
- package/dist/src/services/SearcherService.d.ts +91 -0
- package/dist/src/services/SearcherService.js +453 -0
- package/dist/src/setup.js +23 -7
- package/package.json +11 -10
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CodeSymbol = void 0;
|
|
13
|
+
const typeorm_1 = require("typeorm");
|
|
14
|
+
let CodeSymbol = class CodeSymbol {
|
|
15
|
+
id;
|
|
16
|
+
project;
|
|
17
|
+
filePath;
|
|
18
|
+
name;
|
|
19
|
+
kind;
|
|
20
|
+
signature;
|
|
21
|
+
line;
|
|
22
|
+
col;
|
|
23
|
+
};
|
|
24
|
+
exports.CodeSymbol = CodeSymbol;
|
|
25
|
+
__decorate([
|
|
26
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
27
|
+
__metadata("design:type", String)
|
|
28
|
+
], CodeSymbol.prototype, "id", void 0);
|
|
29
|
+
__decorate([
|
|
30
|
+
(0, typeorm_1.Column)(),
|
|
31
|
+
__metadata("design:type", String)
|
|
32
|
+
], CodeSymbol.prototype, "project", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
(0, typeorm_1.Column)(),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], CodeSymbol.prototype, "filePath", void 0);
|
|
37
|
+
__decorate([
|
|
38
|
+
(0, typeorm_1.Column)(),
|
|
39
|
+
__metadata("design:type", String)
|
|
40
|
+
], CodeSymbol.prototype, "name", void 0);
|
|
41
|
+
__decorate([
|
|
42
|
+
(0, typeorm_1.Column)(),
|
|
43
|
+
__metadata("design:type", String)
|
|
44
|
+
], CodeSymbol.prototype, "kind", void 0);
|
|
45
|
+
__decorate([
|
|
46
|
+
(0, typeorm_1.Column)({ type: 'text', nullable: true }),
|
|
47
|
+
__metadata("design:type", Object)
|
|
48
|
+
], CodeSymbol.prototype, "signature", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
51
|
+
__metadata("design:type", Object)
|
|
52
|
+
], CodeSymbol.prototype, "line", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
55
|
+
__metadata("design:type", Object)
|
|
56
|
+
], CodeSymbol.prototype, "col", void 0);
|
|
57
|
+
exports.CodeSymbol = CodeSymbol = __decorate([
|
|
58
|
+
(0, typeorm_1.Entity)()
|
|
59
|
+
], CodeSymbol);
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { CodeChunk } from './entities/code-chunk.entity';
|
|
2
2
|
export { CodeDependency } from './entities/code-dependency.entity';
|
|
3
|
-
export {
|
|
4
|
-
export { initDb, initDbWithSchema } from './init';
|
|
3
|
+
export { CodeSymbol } from './entities/code-symbol.entity';
|
package/dist/src/db/index.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.CodeSymbol = exports.CodeDependency = exports.CodeChunk = void 0;
|
|
4
4
|
var code_chunk_entity_1 = require("./entities/code-chunk.entity");
|
|
5
5
|
Object.defineProperty(exports, "CodeChunk", { enumerable: true, get: function () { return code_chunk_entity_1.CodeChunk; } });
|
|
6
6
|
var code_dependency_entity_1 = require("./entities/code-dependency.entity");
|
|
7
7
|
Object.defineProperty(exports, "CodeDependency", { enumerable: true, get: function () { return code_dependency_entity_1.CodeDependency; } });
|
|
8
|
-
var
|
|
9
|
-
Object.defineProperty(exports, "
|
|
10
|
-
var init_1 = require("./init");
|
|
11
|
-
Object.defineProperty(exports, "initDb", { enumerable: true, get: function () { return init_1.initDb; } });
|
|
12
|
-
Object.defineProperty(exports, "initDbWithSchema", { enumerable: true, get: function () { return init_1.initDbWithSchema; } });
|
|
8
|
+
var code_symbol_entity_1 = require("./entities/code-symbol.entity");
|
|
9
|
+
Object.defineProperty(exports, "CodeSymbol", { enumerable: true, get: function () { return code_symbol_entity_1.CodeSymbol; } });
|
package/dist/src/file-utils.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export interface ProjectMeta {
|
|
|
6
6
|
notes: string | null;
|
|
7
7
|
relations: Relations | null;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
9
|
+
export type RagMetaResult = ProjectMeta | 'missing' | 'invalid';
|
|
10
|
+
export declare function readRagMeta(projectDir: string): RagMetaResult;
|
|
10
11
|
export declare const IGNORE_DIRS: Set<string>;
|
|
11
12
|
export declare function loadIgnore(projectDir: string): ReturnType<typeof ignore>;
|
|
12
13
|
export declare function collectFiles(dir: string, ig: ReturnType<typeof ignore>, rootDir: string): string[];
|
package/dist/src/file-utils.js
CHANGED
|
@@ -43,12 +43,10 @@ exports.collectFiles = collectFiles;
|
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const ignore_1 = __importDefault(require("ignore"));
|
|
46
|
-
// Returns null if file is missing or contains invalid JSON.
|
|
47
|
-
// Returns ProjectMeta (fields may be null) if file exists and parses correctly.
|
|
48
46
|
function readRagMeta(projectDir) {
|
|
49
47
|
const metaPath = path.join(projectDir, '.ragmeta.json');
|
|
50
48
|
if (!fs.existsSync(metaPath))
|
|
51
|
-
return
|
|
49
|
+
return 'missing';
|
|
52
50
|
try {
|
|
53
51
|
const raw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
54
52
|
return {
|
|
@@ -59,7 +57,7 @@ function readRagMeta(projectDir) {
|
|
|
59
57
|
};
|
|
60
58
|
}
|
|
61
59
|
catch {
|
|
62
|
-
return
|
|
60
|
+
return 'invalid';
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
exports.IGNORE_DIRS = new Set([
|
|
@@ -79,7 +77,14 @@ function loadIgnore(projectDir) {
|
|
|
79
77
|
}
|
|
80
78
|
function collectFiles(dir, ig, rootDir) {
|
|
81
79
|
const result = [];
|
|
82
|
-
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(dir);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
for (const file of entries) {
|
|
83
88
|
const fullPath = path.join(dir, file);
|
|
84
89
|
const relPath = path.relative(rootDir, fullPath);
|
|
85
90
|
if (ig.ignores(relPath))
|
package/dist/src/graph.js
CHANGED
|
@@ -243,10 +243,11 @@ function buildHtml(nodes, edges) {
|
|
|
243
243
|
}
|
|
244
244
|
// Single entry point — reads meta once, prints console summary and writes HTML
|
|
245
245
|
function generateProjectGraph(projects, workspacePath) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
const EMPTY = { description: null, stack: null, notes: null, relations: null };
|
|
247
|
+
const nodes = projects.map(p => {
|
|
248
|
+
const meta = (0, file_utils_1.readRagMeta)(p.path);
|
|
249
|
+
return { name: p.name, ...(typeof meta === 'object' ? meta : EMPTY) };
|
|
250
|
+
});
|
|
250
251
|
const edges = buildEdges(nodes);
|
|
251
252
|
printConsole(nodes, edges, workspacePath);
|
|
252
253
|
const htmlPath = path.join(workspacePath, 'project-graph.html');
|
package/dist/src/help.js
CHANGED
|
@@ -19,7 +19,7 @@ function printHelp(command) {
|
|
|
19
19
|
console.log(' -h, --help Show this help message');
|
|
20
20
|
console.log('');
|
|
21
21
|
console.log('Subcommands:');
|
|
22
|
-
console.log(' backfill Populate
|
|
22
|
+
console.log(' backfill Populate symbol index (code_symbol) for already-indexed files');
|
|
23
23
|
break;
|
|
24
24
|
case 'rag-mcp':
|
|
25
25
|
case 'mcp':
|
package/dist/src/indexer.js
CHANGED
|
@@ -236,6 +236,7 @@ async function scanAndIndex(dir, serviceName, force = false, signal, preCollecte
|
|
|
236
236
|
bar.increment();
|
|
237
237
|
}
|
|
238
238
|
bar.stop();
|
|
239
|
+
process.stdout.write('\x1B[J'); // erase any blank lines cli-progress left below the bar
|
|
239
240
|
stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
|
|
240
241
|
return stats;
|
|
241
242
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DepEdge {
|
|
2
|
+
project: string;
|
|
3
|
+
fromFile: string;
|
|
4
|
+
fromSymbol: string | null;
|
|
5
|
+
toFile: string;
|
|
6
|
+
toSymbol: string | null;
|
|
7
|
+
depType: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SymbolEntry {
|
|
10
|
+
project: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
name: string;
|
|
13
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'variable';
|
|
14
|
+
signature: string | null;
|
|
15
|
+
line: number | null;
|
|
16
|
+
col: number | null;
|
|
17
|
+
}
|
|
18
|
+
export interface FileAnalysis {
|
|
19
|
+
edges: DepEdge[];
|
|
20
|
+
symbols: SymbolEntry[];
|
|
21
|
+
}
|
|
22
|
+
export interface IAstParser {
|
|
23
|
+
analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
|
|
24
|
+
}
|
package/dist/src/interrupt.d.ts
CHANGED
package/dist/src/interrupt.js
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Graceful interruption signal for long-running CLI operations.
|
|
3
|
-
// Listens for ESC or Ctrl+C on stdin and aborts the shared controller.
|
|
4
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.signal = void 0;
|
|
6
3
|
exports.setupInterrupt = setupInterrupt;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function setupInterrupt() {
|
|
4
|
+
// Graceful interruption for long-running CLI operations.
|
|
5
|
+
// Caller creates the AbortController; this function wires stdin and returns a cleanup function.
|
|
6
|
+
function setupInterrupt(controller) {
|
|
11
7
|
if (!process.stdin.isTTY)
|
|
12
|
-
return;
|
|
8
|
+
return () => { };
|
|
13
9
|
process.stdin.setRawMode(true);
|
|
14
10
|
process.stdin.resume();
|
|
15
11
|
process.stdin.setEncoding('utf8');
|
|
16
|
-
|
|
12
|
+
const handler = (key) => {
|
|
17
13
|
if (key === '\x1b' || key === '\x03')
|
|
18
14
|
controller.abort();
|
|
19
|
-
}
|
|
15
|
+
};
|
|
16
|
+
process.stdin.on('data', handler);
|
|
20
17
|
console.log('Press ESC or Ctrl+C to interrupt.\n');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
return () => {
|
|
19
|
+
process.stdin.off('data', handler);
|
|
20
|
+
if (process.stdin.isTTY) {
|
|
21
|
+
process.stdin.setRawMode(false);
|
|
22
|
+
process.stdin.pause();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
27
25
|
}
|
package/dist/src/mcp.js
CHANGED
|
@@ -64,8 +64,11 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
64
64
|
|
|
65
65
|
1. Use \`list_projects\` to discover available projects before filtering by \`project\`.
|
|
66
66
|
2. Use \`search_codebase\` to find relevant code by meaning.
|
|
67
|
-
3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and
|
|
67
|
+
3. If a result looks **truncated or incomplete** (e.g. you see only the middle of a function), call \`read_file_range\` with the returned \`file_path\` and \`start_line\` set to the **Line** number shown in the result. Example: if the result shows **Line:** 150, pass \`start_line: 150\`.
|
|
68
68
|
4. For exploratory searches where you expect results to span chunk boundaries, pass \`include_neighbors: true\` to automatically include the chunk before and after each match.
|
|
69
|
+
5. Use \`find_symbol\` when you know the exact name of a function or class and want its definition location immediately — faster than semantic search.
|
|
70
|
+
6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
|
|
71
|
+
7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
|
|
69
72
|
|
|
70
73
|
## When to use read_file_range vs include_neighbors
|
|
71
74
|
|
|
@@ -84,6 +87,9 @@ Lower score = better match (cosine distance). Results above the configured thres
|
|
|
84
87
|
|
|
85
88
|
- **get_dependencies** — show what files a given file imports (direct or transitive). Use when you need to understand what a file depends on.
|
|
86
89
|
- **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers and avoid breaking changes.
|
|
90
|
+
- **find_symbol** — find where a symbol (function, class, type, interface, variable) is defined by exact name. Faster than semantic search when you know the name.
|
|
91
|
+
- **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
|
|
92
|
+
- **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
|
|
87
93
|
|
|
88
94
|
Both tools require \`file_path\` (as returned by \`search_codebase\`) and \`project\` (from \`list_projects\`). Pass \`transitive: true\` for the full dependency chain.
|
|
89
95
|
`.trim();
|
|
@@ -153,8 +159,8 @@ async function startMCP() {
|
|
|
153
159
|
type: 'object',
|
|
154
160
|
properties: {
|
|
155
161
|
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
156
|
-
start_line: { type: 'number', description: 'First line to read (1-based
|
|
157
|
-
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99' },
|
|
162
|
+
start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
|
|
163
|
+
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
|
|
158
164
|
},
|
|
159
165
|
required: ['file_path'],
|
|
160
166
|
},
|
|
@@ -187,6 +193,45 @@ async function startMCP() {
|
|
|
187
193
|
required: ['file_path', 'project'],
|
|
188
194
|
},
|
|
189
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'find_symbol',
|
|
198
|
+
description: 'Find TypeScript/JavaScript symbol definitions (functions, classes, types, interfaces, variables) by exact name. Use when you know the symbol name and want its definition location fast.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
|
|
203
|
+
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
204
|
+
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
205
|
+
},
|
|
206
|
+
required: ['name'],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'list_files',
|
|
211
|
+
description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
|
|
216
|
+
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
217
|
+
depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
|
|
218
|
+
pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
|
|
219
|
+
},
|
|
220
|
+
required: ['path', 'project'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'read_file',
|
|
225
|
+
description: 'Read a file from the workspace by absolute path (as returned by search_codebase). Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
file_path: { type: 'string', description: 'Absolute path to the file (as returned in filePath by search_codebase)' },
|
|
230
|
+
max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
|
|
231
|
+
},
|
|
232
|
+
required: ['file_path'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
190
235
|
],
|
|
191
236
|
}));
|
|
192
237
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
@@ -254,12 +299,11 @@ async function startMCP() {
|
|
|
254
299
|
return { content: [{ type: 'text', text: message }] };
|
|
255
300
|
}
|
|
256
301
|
const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
};
|
|
302
|
+
let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
303
|
+
if (range.hasMore) {
|
|
304
|
+
text += `\n\n[file has more lines — call read_file_range again with start_line: ${range.endLine + 1}]`;
|
|
305
|
+
}
|
|
306
|
+
return { content: [{ type: 'text', text }] };
|
|
263
307
|
}
|
|
264
308
|
if (req.params.name === 'get_dependencies') {
|
|
265
309
|
const filePath = String(req.params.arguments?.file_path);
|
|
@@ -301,6 +345,54 @@ async function startMCP() {
|
|
|
301
345
|
}
|
|
302
346
|
return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
|
|
303
347
|
}
|
|
348
|
+
if (req.params.name === 'find_symbol') {
|
|
349
|
+
const name = String(req.params.arguments?.name ?? '');
|
|
350
|
+
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
351
|
+
const rawKind = req.params.arguments?.kind;
|
|
352
|
+
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
|
|
353
|
+
const kind = validKinds.includes(rawKind) ? rawKind : 'all';
|
|
354
|
+
const { hits, truncated } = (0, searcher_1.findSymbol)(name, project, kind);
|
|
355
|
+
if (hits.length === 0) {
|
|
356
|
+
return { content: [{ type: 'text', text: `No definitions found for symbol: ${name}` }] };
|
|
357
|
+
}
|
|
358
|
+
const lines = [];
|
|
359
|
+
if (truncated)
|
|
360
|
+
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
361
|
+
for (const h of hits) {
|
|
362
|
+
lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
|
|
363
|
+
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
364
|
+
}
|
|
365
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
366
|
+
}
|
|
367
|
+
if (req.params.name === 'list_files') {
|
|
368
|
+
const relPath = String(req.params.arguments?.path ?? '.');
|
|
369
|
+
const project = String(req.params.arguments?.project ?? '');
|
|
370
|
+
const rawDepth = Number(req.params.arguments?.depth);
|
|
371
|
+
const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
|
|
372
|
+
const pattern = req.params.arguments?.pattern ? String(req.params.arguments.pattern) : null;
|
|
373
|
+
const { tree, error } = (0, searcher_1.listFiles)(relPath, project, depth, pattern);
|
|
374
|
+
if (error) {
|
|
375
|
+
return { content: [{ type: 'text', text: `list_files error: ${error}` }] };
|
|
376
|
+
}
|
|
377
|
+
return { content: [{ type: 'text', text: `**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\`` }] };
|
|
378
|
+
}
|
|
379
|
+
if (req.params.name === 'read_file') {
|
|
380
|
+
const filePath = String(req.params.arguments?.file_path ?? '');
|
|
381
|
+
const rawMax = Number(req.params.arguments?.max_lines);
|
|
382
|
+
const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
|
|
383
|
+
const result = await (0, searcher_1.readFileAbsolute)(filePath, maxLines);
|
|
384
|
+
if (!result.ok) {
|
|
385
|
+
const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
|
|
386
|
+
`File not found on disk: ${filePath}`;
|
|
387
|
+
return { content: [{ type: 'text', text: message }] };
|
|
388
|
+
}
|
|
389
|
+
const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
390
|
+
let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
391
|
+
if (result.hasMore) {
|
|
392
|
+
text += `\n\n[truncated: file has more lines — use read_file_range with file_path: "${result.filePath}", start_line: ${result.endLine + 1} to continue]`;
|
|
393
|
+
}
|
|
394
|
+
return { content: [{ type: 'text', text }] };
|
|
395
|
+
}
|
|
304
396
|
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
305
397
|
}
|
|
306
398
|
catch (err) {
|
package/dist/src/searcher.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type FileRangeResult = {
|
|
|
19
19
|
startLine: number;
|
|
20
20
|
endLine: number;
|
|
21
21
|
lines: string[];
|
|
22
|
+
hasMore: boolean;
|
|
22
23
|
} | {
|
|
23
24
|
ok: false;
|
|
24
25
|
reason: 'not_indexed' | 'not_on_disk' | 'invalid_path';
|
|
@@ -53,4 +54,21 @@ export declare function grepSymbol(symbol: string, project: string, excludeFile:
|
|
|
53
54
|
matches: GrepMatch[];
|
|
54
55
|
truncated: boolean;
|
|
55
56
|
};
|
|
57
|
+
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
|
|
58
|
+
export interface SymbolHit {
|
|
59
|
+
filePath: string;
|
|
60
|
+
line: number;
|
|
61
|
+
column: number;
|
|
62
|
+
kind: Exclude<SymbolKind, 'all'>;
|
|
63
|
+
signature: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function findSymbol(name: string, project: string | null, kind: SymbolKind): {
|
|
66
|
+
hits: SymbolHit[];
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
};
|
|
69
|
+
export declare function listFiles(relativePath: string, project: string, depth: number, pattern: string | null): {
|
|
70
|
+
tree: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
};
|
|
73
|
+
export declare function readFileAbsolute(absPath: string, maxLines: number): Promise<FileRangeResult>;
|
|
56
74
|
export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
|
package/dist/src/searcher.js
CHANGED
|
@@ -40,6 +40,9 @@ exports.fetchNeighbors = fetchNeighbors;
|
|
|
40
40
|
exports.getDependencies = getDependencies;
|
|
41
41
|
exports.getDependents = getDependents;
|
|
42
42
|
exports.grepSymbol = grepSymbol;
|
|
43
|
+
exports.findSymbol = findSymbol;
|
|
44
|
+
exports.listFiles = listFiles;
|
|
45
|
+
exports.readFileAbsolute = readFileAbsolute;
|
|
43
46
|
exports.readFileRange = readFileRange;
|
|
44
47
|
const fs = __importStar(require("fs"));
|
|
45
48
|
const path = __importStar(require("path"));
|
|
@@ -240,6 +243,171 @@ function grepSymbol(symbol, project, excludeFile) {
|
|
|
240
243
|
});
|
|
241
244
|
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
|
|
242
245
|
}
|
|
246
|
+
const FIND_SYMBOL_LIMIT = 50;
|
|
247
|
+
const FIND_SYMBOL_RAW_LIMIT = FIND_SYMBOL_LIMIT * 10;
|
|
248
|
+
function buildFindSymbolPattern(name, kind) {
|
|
249
|
+
const patterns = {
|
|
250
|
+
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${name}[[:space:](<]`,
|
|
251
|
+
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${name}[[:space:]{<]`,
|
|
252
|
+
type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
|
|
253
|
+
interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
|
|
254
|
+
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
|
|
255
|
+
};
|
|
256
|
+
if (kind === 'all')
|
|
257
|
+
return Object.values(patterns).join('|');
|
|
258
|
+
return patterns[kind];
|
|
259
|
+
}
|
|
260
|
+
function inferKind(line, name) {
|
|
261
|
+
const before = line.slice(0, line.indexOf(name));
|
|
262
|
+
if (/\bfunction\s+$/.test(before))
|
|
263
|
+
return 'function';
|
|
264
|
+
if (/\bclass\s+$/.test(before))
|
|
265
|
+
return 'class';
|
|
266
|
+
if (/\binterface\s+$/.test(before))
|
|
267
|
+
return 'interface';
|
|
268
|
+
if (/\btype\s+$/.test(before))
|
|
269
|
+
return 'type';
|
|
270
|
+
return 'variable';
|
|
271
|
+
}
|
|
272
|
+
function findSymbol(name, project, kind) {
|
|
273
|
+
if (!/^[\w$]+$/.test(name))
|
|
274
|
+
throw new Error('Invalid symbol name');
|
|
275
|
+
const searchDir = project
|
|
276
|
+
? path.join(config_1.config.workspaceRoot, project)
|
|
277
|
+
: config_1.config.workspaceRoot;
|
|
278
|
+
const pattern = buildFindSymbolPattern(name, kind);
|
|
279
|
+
const result = (0, child_process_1.spawnSync)('grep', [
|
|
280
|
+
'-rn', '-E',
|
|
281
|
+
'-m', String(FIND_SYMBOL_RAW_LIMIT),
|
|
282
|
+
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
283
|
+
...GREP_EXCLUDE_DIRS,
|
|
284
|
+
pattern,
|
|
285
|
+
searchDir,
|
|
286
|
+
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
287
|
+
if (result.error) {
|
|
288
|
+
console.error(`[findSymbol] grep failed: ${result.error.message}`);
|
|
289
|
+
return { hits: [], truncated: false };
|
|
290
|
+
}
|
|
291
|
+
if (!result.stdout)
|
|
292
|
+
return { hits: [], truncated: false };
|
|
293
|
+
const hits = [];
|
|
294
|
+
for (const raw of result.stdout.split('\n')) {
|
|
295
|
+
if (hits.length >= FIND_SYMBOL_LIMIT)
|
|
296
|
+
break;
|
|
297
|
+
// grep -n format: "/abs/path/file.ts:42: export function Foo(...)"
|
|
298
|
+
// File paths can contain colons on some systems, so split carefully:
|
|
299
|
+
// find first colon after a path separator, use lastIndexOf on the line number part
|
|
300
|
+
const m = raw.match(/^(.+?):(\d+):(.*)$/);
|
|
301
|
+
if (!m)
|
|
302
|
+
continue;
|
|
303
|
+
const [, absFile, lineNumStr, matchedLine] = m;
|
|
304
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
305
|
+
if (isNaN(lineNum))
|
|
306
|
+
continue;
|
|
307
|
+
const col = matchedLine.indexOf(name);
|
|
308
|
+
if (col === -1)
|
|
309
|
+
continue;
|
|
310
|
+
hits.push({
|
|
311
|
+
filePath: path.relative(config_1.config.workspaceRoot, absFile),
|
|
312
|
+
line: lineNum,
|
|
313
|
+
column: col + 1,
|
|
314
|
+
kind: inferKind(matchedLine, name),
|
|
315
|
+
signature: matchedLine.trim(),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
|
|
319
|
+
}
|
|
320
|
+
// ── list_files ────────────────────────────────────────────────────────────────
|
|
321
|
+
function walkDir(absDir, currentDepth, maxDepth, pattern, lines) {
|
|
322
|
+
let entries;
|
|
323
|
+
try {
|
|
324
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
entries.sort((a, b) => {
|
|
330
|
+
const ad = a.isDirectory() ? 0 : 1;
|
|
331
|
+
const bd = b.isDirectory() ? 0 : 1;
|
|
332
|
+
if (ad !== bd)
|
|
333
|
+
return ad - bd;
|
|
334
|
+
return a.name.localeCompare(b.name);
|
|
335
|
+
});
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
if (file_utils_1.IGNORE_DIRS.has(entry.name))
|
|
338
|
+
continue;
|
|
339
|
+
const indent = ' '.repeat(currentDepth);
|
|
340
|
+
if (entry.isDirectory()) {
|
|
341
|
+
lines.push(`${indent}${entry.name}/`);
|
|
342
|
+
if (currentDepth < maxDepth) {
|
|
343
|
+
walkDir(path.join(absDir, entry.name), currentDepth + 1, maxDepth, pattern, lines);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
if (pattern && !entry.name.includes(pattern))
|
|
348
|
+
continue;
|
|
349
|
+
lines.push(`${indent}${entry.name}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function listFiles(relativePath, project, depth, pattern) {
|
|
354
|
+
const projectRoot = path.join(config_1.config.workspaceRoot, project);
|
|
355
|
+
const resolvedTarget = path.resolve(projectRoot, relativePath);
|
|
356
|
+
const resolvedProject = path.resolve(projectRoot);
|
|
357
|
+
if (resolvedTarget !== resolvedProject && !resolvedTarget.startsWith(resolvedProject + path.sep)) {
|
|
358
|
+
return { tree: '', error: 'Path escapes project root' };
|
|
359
|
+
}
|
|
360
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
361
|
+
return { tree: '', error: `Path not found: ${relativePath}` };
|
|
362
|
+
}
|
|
363
|
+
if (!fs.statSync(resolvedTarget).isDirectory()) {
|
|
364
|
+
return { tree: '', error: `Not a directory: ${relativePath}` };
|
|
365
|
+
}
|
|
366
|
+
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
367
|
+
const lines = [];
|
|
368
|
+
walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
|
|
369
|
+
if (lines.length === 0) {
|
|
370
|
+
return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
371
|
+
}
|
|
372
|
+
return { tree: lines.join('\n') };
|
|
373
|
+
}
|
|
374
|
+
// ── readFileAbsolute ──────────────────────────────────────────────────────────
|
|
375
|
+
// Variant of readFileRange that accepts an absolute path and skips the DB index check.
|
|
376
|
+
// Used by the read_file MCP tool, which receives absolute paths from search_codebase.
|
|
377
|
+
async function readFileAbsolute(absPath, maxLines) {
|
|
378
|
+
const resolvedRoot = path.resolve(config_1.config.workspaceRoot);
|
|
379
|
+
const resolved = path.resolve(absPath);
|
|
380
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
|
381
|
+
return { ok: false, reason: 'invalid_path' };
|
|
382
|
+
}
|
|
383
|
+
if (!fs.existsSync(resolved))
|
|
384
|
+
return { ok: false, reason: 'not_on_disk' };
|
|
385
|
+
const lines = [];
|
|
386
|
+
const stream = fs.createReadStream(resolved, { encoding: 'utf-8' });
|
|
387
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
388
|
+
let lineNo = 0;
|
|
389
|
+
let hasMore = false;
|
|
390
|
+
try {
|
|
391
|
+
for await (const line of rl) {
|
|
392
|
+
lineNo++;
|
|
393
|
+
if (lineNo <= maxLines)
|
|
394
|
+
lines.push(line);
|
|
395
|
+
if (lineNo >= maxLines) {
|
|
396
|
+
hasMore = true;
|
|
397
|
+
rl.close();
|
|
398
|
+
stream.destroy();
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
rl.close();
|
|
405
|
+
stream.destroy();
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
const relPath = path.relative(resolvedRoot, resolved);
|
|
409
|
+
return { ok: true, filePath: relPath, startLine: 1, endLine: lineNo, lines, hasMore };
|
|
410
|
+
}
|
|
243
411
|
// Read a line range from a file that is present in the index
|
|
244
412
|
async function readFileRange(filePath, startLine, endLine) {
|
|
245
413
|
// Defensive: reject paths that escape the workspace root (e.g. "../../etc/passwd")
|
|
@@ -260,17 +428,19 @@ async function readFileRange(filePath, startLine, endLine) {
|
|
|
260
428
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
261
429
|
let lineNo = 0;
|
|
262
430
|
let lastRead = 0;
|
|
431
|
+
let hasMore = false;
|
|
263
432
|
for await (const line of rl) {
|
|
264
433
|
lineNo++;
|
|
265
434
|
lastRead = lineNo;
|
|
266
435
|
if (lineNo >= startLine && lineNo <= endLine)
|
|
267
436
|
lines.push(line);
|
|
268
437
|
if (lineNo >= endLine) {
|
|
438
|
+
hasMore = true;
|
|
269
439
|
rl.close();
|
|
270
440
|
stream.destroy();
|
|
271
441
|
break;
|
|
272
442
|
}
|
|
273
443
|
}
|
|
274
444
|
// If we broke out early, lastRead is endLine. If file was shorter, lastRead is file length.
|
|
275
|
-
return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines };
|
|
445
|
+
return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
276
446
|
}
|