codesearch-mcp 0.0.11-alpha → 0.0.13-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/dist/app.js +1 -1
- package/dist/src/interfaces/IAstParser.d.ts +1 -1
- package/dist/src/services/AstParserService.js +2 -1
- package/dist/src/services/IndexerService.js +2 -2
- package/dist/src/services/LoggerService.js +15 -1
- package/dist/src/services/McpServer.js +14 -7
- package/dist/src/services/SearcherService.d.ts +2 -2
- package/dist/src/services/SearcherService.js +54 -9
- package/docker/docker-compose.yml +4 -1
- package/package.json +5 -2
package/dist/app.js
CHANGED
|
@@ -47,7 +47,7 @@ async function main() {
|
|
|
47
47
|
const isIndex = argv1 === 'rag-index' || process.argv[2] === 'index';
|
|
48
48
|
const isMcp = argv1 === 'rag-mcp' || process.argv[2] === 'mcp';
|
|
49
49
|
const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup';
|
|
50
|
-
const isBackfill = argv1 === 'rag-index' && process.argv[2] === 'backfill';
|
|
50
|
+
const isBackfill = (argv1 === 'rag-index' || process.argv[2] === 'backfill') && process.argv[2] === 'backfill';
|
|
51
51
|
const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : (process.argv[2] ?? '');
|
|
52
52
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
53
53
|
(0, help_1.printHelp)(argv1 !== 'app.js' ? argv1 : mode);
|
|
@@ -10,7 +10,7 @@ export interface SymbolEntry {
|
|
|
10
10
|
project: string;
|
|
11
11
|
filePath: string;
|
|
12
12
|
name: string;
|
|
13
|
-
kind: 'function' | 'class' | 'interface' | 'type' | 'variable';
|
|
13
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method';
|
|
14
14
|
signature: string | null;
|
|
15
15
|
line: number | null;
|
|
16
16
|
col: number | null;
|
|
@@ -116,7 +116,8 @@ class AstParserService {
|
|
|
116
116
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
117
117
|
if (!resolved) {
|
|
118
118
|
const specifier = decl.getModuleSpecifierValue();
|
|
119
|
-
|
|
119
|
+
const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
|
|
120
|
+
if (specifier.startsWith('.') && isCodeImport) {
|
|
120
121
|
this.logger.debug('AstParser: unresolved import', { specifier, file: filePath });
|
|
121
122
|
}
|
|
122
123
|
continue;
|
|
@@ -80,7 +80,7 @@ class IndexerService {
|
|
|
80
80
|
}
|
|
81
81
|
try {
|
|
82
82
|
const { symbols: rawSymbols } = await this.ast.analyzeFile(absPath, serviceName, projectRoot);
|
|
83
|
-
const symbols = rawSymbols.map(s => ({ ...s, filePath }));
|
|
83
|
+
const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
|
|
84
84
|
await this.db.transaction(async (manager) => {
|
|
85
85
|
await manager.delete(db_1.CodeSymbol, { project: serviceName, filePath });
|
|
86
86
|
if (symbols.length > 0) {
|
|
@@ -223,7 +223,7 @@ class IndexerService {
|
|
|
223
223
|
toFile: path.relative(this.config.workspaceRoot, e.toFile),
|
|
224
224
|
}));
|
|
225
225
|
// normalize symbols: replace absolute filePath with workspace-relative
|
|
226
|
-
const symbols = rawSymbols.map(s => ({ ...s, filePath: relFilePath }));
|
|
226
|
+
const symbols = [...new Map(rawSymbols.map(s => ({ ...s, filePath: relFilePath })).map(s => [`${s.name}:${s.kind}`, s])).values()];
|
|
227
227
|
const depsMs = Date.now() - t0;
|
|
228
228
|
const code = fs.readFileSync(filePath, 'utf-8');
|
|
229
229
|
const chunks = await this.getSplitter(filePath).createDocuments([code]);
|
|
@@ -58,7 +58,21 @@ class LoggerService {
|
|
|
58
58
|
if (filePath)
|
|
59
59
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
60
|
const transport = filePath
|
|
61
|
-
? pino_1.default.transport({
|
|
61
|
+
? pino_1.default.transport({
|
|
62
|
+
targets: [{
|
|
63
|
+
target: 'pino-roll',
|
|
64
|
+
options: {
|
|
65
|
+
file: filePath,
|
|
66
|
+
frequency: 'daily',
|
|
67
|
+
dateFormat: 'yyyy-MM-dd',
|
|
68
|
+
mkdir: true,
|
|
69
|
+
symlink: true,
|
|
70
|
+
limit: { count: 30 },
|
|
71
|
+
size: '50m',
|
|
72
|
+
},
|
|
73
|
+
level,
|
|
74
|
+
}],
|
|
75
|
+
})
|
|
62
76
|
: undefined;
|
|
63
77
|
const logger = transport
|
|
64
78
|
? (0, pino_1.default)({ level }, transport)
|
|
@@ -58,7 +58,7 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
58
58
|
- **read_file_range** — read a specific line range from any indexed file. Use this to expand context around a search result.
|
|
59
59
|
- **get_dependencies** — show what files a given file imports (direct or transitive). Use to understand what a file depends on.
|
|
60
60
|
- **get_dependents** — show which files import a given file (reverse graph). Use before refactoring to find all callers.
|
|
61
|
-
- **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.
|
|
61
|
+
- **find_symbol** — find where a symbol (function, class, type, interface, variable, method) is defined by exact name. Faster than semantic search when you know the name.
|
|
62
62
|
- **list_files** — browse the directory tree of a project path. Use to understand project structure before reading files.
|
|
63
63
|
- **read_file** — read a complete file by absolute path with line numbers. Truncates at max_lines with a hint to continue.
|
|
64
64
|
|
|
@@ -68,9 +68,10 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
68
68
|
2. Use \`search_codebase\` to find relevant code by meaning.
|
|
69
69
|
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\`.
|
|
70
70
|
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.
|
|
71
|
-
5. Use \`find_symbol\` when you know the exact name of a function or
|
|
71
|
+
5. Use \`find_symbol\` when you know the exact name of a function, class, or method and want its definition location immediately — faster than semantic search. Use \`kind: "method"\` to find public class methods like \`canActivate\`, \`execute\`, \`handle\`.
|
|
72
72
|
6. Use \`list_files\` to explore project structure (src/, components/, etc.) before diving into files.
|
|
73
73
|
7. Use \`read_file\` to read a complete file when you have its absolute path from \`search_codebase\` results.
|
|
74
|
+
8. If \`search_codebase\` returns results that seem off-topic, pass a lower \`threshold\` (e.g. \`0.35\`) to filter out weak semantic matches.
|
|
74
75
|
|
|
75
76
|
## When to use read_file_range vs include_neighbors
|
|
76
77
|
|
|
@@ -83,7 +84,7 @@ You have access to a local semantic code search server. Use these tools to navig
|
|
|
83
84
|
|
|
84
85
|
## Relevance scores
|
|
85
86
|
|
|
86
|
-
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically.
|
|
87
|
+
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Pass \`threshold\` to \`search_codebase\` to override the default per-query (e.g. \`0.35\` for strict matching, \`0.55\` for broader results).
|
|
87
88
|
|
|
88
89
|
## Dependency graph
|
|
89
90
|
|
|
@@ -112,6 +113,7 @@ class McpServer {
|
|
|
112
113
|
limit: { type: 'number', description: `Max results (default ${this.config.search.defaultLimit}, max ${this.config.search.maxLimit})` },
|
|
113
114
|
project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
|
|
114
115
|
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.' },
|
|
116
|
+
threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.config.search.similarityThreshold}. Use a lower value (e.g. 0.35) to filter out weak matches.` },
|
|
115
117
|
},
|
|
116
118
|
required: ['query'],
|
|
117
119
|
},
|
|
@@ -164,13 +166,13 @@ class McpServer {
|
|
|
164
166
|
},
|
|
165
167
|
{
|
|
166
168
|
name: 'find_symbol',
|
|
167
|
-
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.',
|
|
169
|
+
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.',
|
|
168
170
|
inputSchema: {
|
|
169
171
|
type: 'object',
|
|
170
172
|
properties: {
|
|
171
173
|
name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
|
|
172
174
|
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
173
|
-
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
175
|
+
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
174
176
|
},
|
|
175
177
|
required: ['name'],
|
|
176
178
|
},
|
|
@@ -232,8 +234,13 @@ class McpServer {
|
|
|
232
234
|
const limit = Math.min(Math.max(1, Number(req.params.arguments?.limit) || this.config.search.defaultLimit), this.config.search.maxLimit);
|
|
233
235
|
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
234
236
|
const includeNeighbors = Boolean(req.params.arguments?.include_neighbors);
|
|
237
|
+
const rawThreshold = req.params.arguments?.threshold;
|
|
238
|
+
const parsedThreshold = Number(rawThreshold);
|
|
239
|
+
const threshold = rawThreshold != null && !isNaN(parsedThreshold)
|
|
240
|
+
? Math.min(1, Math.max(0, parsedThreshold))
|
|
241
|
+
: undefined;
|
|
235
242
|
const { query, translated } = await this.searcher.prepareQuery(raw);
|
|
236
|
-
const results = await this.searcher.searchChunks(query, limit, project);
|
|
243
|
+
const results = await this.searcher.searchChunks(query, limit, project, threshold);
|
|
237
244
|
if (results.length === 0) {
|
|
238
245
|
return { content: [{ type: 'text', text: 'No relevant results found.' }] };
|
|
239
246
|
}
|
|
@@ -318,7 +325,7 @@ class McpServer {
|
|
|
318
325
|
const name = String(req.params.arguments?.name ?? '');
|
|
319
326
|
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
320
327
|
const rawKind = req.params.arguments?.kind;
|
|
321
|
-
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'all'];
|
|
328
|
+
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
|
|
322
329
|
const kind = validKinds.includes(rawKind) ? rawKind : 'all';
|
|
323
330
|
const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
|
|
324
331
|
if (hits.length === 0) {
|
|
@@ -45,7 +45,7 @@ export interface GrepMatch {
|
|
|
45
45
|
filePath: string;
|
|
46
46
|
line: number;
|
|
47
47
|
}
|
|
48
|
-
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'all';
|
|
48
|
+
export type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable' | 'method' | 'all';
|
|
49
49
|
export interface SymbolHit {
|
|
50
50
|
filePath: string;
|
|
51
51
|
line: number;
|
|
@@ -64,7 +64,7 @@ export declare class SearcherService {
|
|
|
64
64
|
translated: boolean;
|
|
65
65
|
}>;
|
|
66
66
|
listProjects(): Promise<ProjectRow[]>;
|
|
67
|
-
searchChunks(query: string, limit: number, project: string | null): Promise<SearchResult[]>;
|
|
67
|
+
searchChunks(query: string, limit: number, project: string | null, threshold?: number): Promise<SearchResult[]>;
|
|
68
68
|
fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
|
|
69
69
|
getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
70
70
|
getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
@@ -59,8 +59,11 @@ class SearcherService {
|
|
|
59
59
|
}
|
|
60
60
|
async prepareQuery(raw) {
|
|
61
61
|
if (this.config.search.autoTranslate && this.isNonEnglish(raw) && this.embedder.translate) {
|
|
62
|
-
|
|
62
|
+
const query = await this.embedder.translate(raw, this.config.search.translateModel);
|
|
63
|
+
this.logger.debug('SearcherService: prepareQuery', { translated: true, raw: raw.slice(0, 120), query: query.slice(0, 120) });
|
|
64
|
+
return { query, translated: true };
|
|
63
65
|
}
|
|
66
|
+
this.logger.debug('SearcherService: prepareQuery', { translated: false, query: raw.slice(0, 120) });
|
|
64
67
|
return { query: raw, translated: false };
|
|
65
68
|
}
|
|
66
69
|
async listProjects() {
|
|
@@ -73,25 +76,36 @@ class SearcherService {
|
|
|
73
76
|
return { ...r, ...(typeof meta === 'object' ? meta : EMPTY_META) };
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
|
-
async searchChunks(query, limit, project) {
|
|
79
|
+
async searchChunks(query, limit, project, threshold) {
|
|
80
|
+
const effectiveThreshold = threshold ?? this.config.search.similarityThreshold;
|
|
81
|
+
this.logger.debug('SearcherService: searchChunks', {
|
|
82
|
+
query: query.slice(0, 120), limit, project, threshold: effectiveThreshold,
|
|
83
|
+
});
|
|
77
84
|
const embedding = await this.embedder.embed(query);
|
|
78
|
-
const params = [`[${embedding}]`,
|
|
85
|
+
const params = [`[${embedding}]`, effectiveThreshold, limit];
|
|
79
86
|
let projectFilter = '';
|
|
80
87
|
if (project) {
|
|
81
88
|
params.push(project);
|
|
82
89
|
projectFilter = `AND "serviceName" = $${params.length}`;
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
const results = await this.db.query(`SELECT "serviceName", "filePath", "startLine", content,
|
|
85
92
|
ROUND((embedding <=> $1)::numeric, 4)::float8 AS score
|
|
86
93
|
FROM code_chunk
|
|
87
94
|
WHERE embedding <=> $1 < $2
|
|
88
95
|
${projectFilter}
|
|
89
96
|
ORDER BY embedding <=> $1
|
|
90
97
|
LIMIT $3`, params);
|
|
98
|
+
this.logger.debug('SearcherService: searchChunks results', {
|
|
99
|
+
count: results.length,
|
|
100
|
+
scores: results.map(r => r.score),
|
|
101
|
+
files: results.map(r => r.filePath),
|
|
102
|
+
});
|
|
103
|
+
return results;
|
|
91
104
|
}
|
|
92
105
|
async fetchNeighbors(results) {
|
|
93
106
|
if (results.length === 0)
|
|
94
107
|
return [];
|
|
108
|
+
this.logger.debug('SearcherService: fetchNeighbors', { count: results.length });
|
|
95
109
|
const values = results.map((_, i) => `(${i}, $${i * 2 + 1}, $${i * 2 + 2}::int)`).join(',');
|
|
96
110
|
const params = results.flatMap(r => [r.filePath, r.startLine]);
|
|
97
111
|
const rows = await this.db.query(`SELECT r.idx,
|
|
@@ -109,13 +123,19 @@ class SearcherService {
|
|
|
109
123
|
ORDER BY "startLine" ASC LIMIT 1
|
|
110
124
|
) next ON true
|
|
111
125
|
ORDER BY r.idx`, params);
|
|
112
|
-
|
|
126
|
+
const neighbors = rows.map((row, i) => ({
|
|
113
127
|
result: results[i],
|
|
114
128
|
prevChunk: row.prev_fp ? { serviceName: row.prev_sn, filePath: row.prev_fp, startLine: row.prev_sl, content: row.prev_c } : null,
|
|
115
129
|
nextChunk: row.next_fp ? { serviceName: row.next_sn, filePath: row.next_fp, startLine: row.next_sl, content: row.next_c } : null,
|
|
116
130
|
}));
|
|
131
|
+
this.logger.debug('SearcherService: fetchNeighbors results', {
|
|
132
|
+
withPrev: neighbors.filter(n => n.prevChunk).length,
|
|
133
|
+
withNext: neighbors.filter(n => n.nextChunk).length,
|
|
134
|
+
});
|
|
135
|
+
return neighbors;
|
|
117
136
|
}
|
|
118
137
|
async getDependencies(filePath, project, symbol = null, transitive = false) {
|
|
138
|
+
this.logger.debug('SearcherService: getDependencies', { filePath, project, symbol, transitive });
|
|
119
139
|
const params = [filePath, project];
|
|
120
140
|
let symbolFilter = '';
|
|
121
141
|
if (symbol) {
|
|
@@ -127,8 +147,10 @@ class SearcherService {
|
|
|
127
147
|
WHERE "fromFile" = $1 AND project = $2
|
|
128
148
|
${symbolFilter}
|
|
129
149
|
ORDER BY "toFile"`, params);
|
|
130
|
-
if (!transitive)
|
|
150
|
+
if (!transitive) {
|
|
151
|
+
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length });
|
|
131
152
|
return direct;
|
|
153
|
+
}
|
|
132
154
|
const indirect = await this.db.query(`WITH RECURSIVE deps AS (
|
|
133
155
|
SELECT "toFile", 1 AS depth, "toFile" AS via
|
|
134
156
|
FROM code_dependency
|
|
@@ -151,9 +173,11 @@ class SearcherService {
|
|
|
151
173
|
WHERE depth > 1
|
|
152
174
|
GROUP BY "toFile"
|
|
153
175
|
ORDER BY MIN(depth), "toFile"`, [filePath, project]);
|
|
176
|
+
this.logger.debug('SearcherService: getDependencies result', { direct: direct.length, indirect: indirect.length });
|
|
154
177
|
return [...direct, ...indirect];
|
|
155
178
|
}
|
|
156
179
|
async getDependents(filePath, project, symbol = null, transitive = false) {
|
|
180
|
+
this.logger.debug('SearcherService: getDependents', { filePath, project, symbol, transitive });
|
|
157
181
|
const params = [filePath, project];
|
|
158
182
|
let symbolFilter = '';
|
|
159
183
|
if (symbol) {
|
|
@@ -165,8 +189,10 @@ class SearcherService {
|
|
|
165
189
|
WHERE "toFile" = $1 AND project = $2
|
|
166
190
|
${symbolFilter}
|
|
167
191
|
ORDER BY "fromFile"`, params);
|
|
168
|
-
if (!transitive)
|
|
192
|
+
if (!transitive) {
|
|
193
|
+
this.logger.debug('SearcherService: getDependents result', { direct: direct.length });
|
|
169
194
|
return direct;
|
|
195
|
+
}
|
|
170
196
|
const indirect = await this.db.query(`WITH RECURSIVE rdeps AS (
|
|
171
197
|
SELECT "fromFile", 1 AS depth, "fromFile" AS via
|
|
172
198
|
FROM code_dependency
|
|
@@ -189,9 +215,11 @@ class SearcherService {
|
|
|
189
215
|
WHERE depth > 1
|
|
190
216
|
GROUP BY "fromFile"
|
|
191
217
|
ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
|
|
218
|
+
this.logger.debug('SearcherService: getDependents result', { direct: direct.length, indirect: indirect.length });
|
|
192
219
|
return [...direct, ...indirect];
|
|
193
220
|
}
|
|
194
221
|
grepSymbol(symbol, project, excludeFile) {
|
|
222
|
+
this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
|
|
195
223
|
const projectDir = path.join(this.config.workspaceRoot, project);
|
|
196
224
|
const result = (0, child_process_1.spawnSync)('grep', [
|
|
197
225
|
'-rn', '-F', '-w',
|
|
@@ -216,16 +244,21 @@ class SearcherService {
|
|
|
216
244
|
return [];
|
|
217
245
|
return [{ filePath: path.relative(this.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
218
246
|
});
|
|
219
|
-
|
|
247
|
+
const truncated = all.length > GREP_MAX_RESULTS;
|
|
248
|
+
this.logger.debug('SearcherService: grepSymbol result', { matches: Math.min(all.length, GREP_MAX_RESULTS), truncated });
|
|
249
|
+
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
|
|
220
250
|
}
|
|
221
251
|
// Sync wrapper: validates name (throws synchronously), then returns Promise.
|
|
222
|
-
//
|
|
252
|
+
// Methods are never indexed — always resolved via grep. Other kinds use index with grep fallback.
|
|
223
253
|
findSymbol(name, project, kind) {
|
|
224
254
|
if (!/^[\w$]+$/.test(name))
|
|
225
255
|
throw new Error('Invalid symbol name');
|
|
256
|
+
if (kind === 'method')
|
|
257
|
+
return Promise.resolve(this.grepFindSymbol(name, project, kind));
|
|
226
258
|
return this.findSymbolFromIndex(name, project, kind);
|
|
227
259
|
}
|
|
228
260
|
async findSymbolFromIndex(name, project, kind) {
|
|
261
|
+
this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'index' });
|
|
229
262
|
try {
|
|
230
263
|
const params = [name];
|
|
231
264
|
let projectFilter = '';
|
|
@@ -252,6 +285,7 @@ class SearcherService {
|
|
|
252
285
|
kind: row.kind,
|
|
253
286
|
signature: row.signature ?? row.name,
|
|
254
287
|
}));
|
|
288
|
+
this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated });
|
|
255
289
|
return { hits, truncated };
|
|
256
290
|
}
|
|
257
291
|
catch {
|
|
@@ -260,6 +294,7 @@ class SearcherService {
|
|
|
260
294
|
}
|
|
261
295
|
}
|
|
262
296
|
grepFindSymbol(name, project, kind) {
|
|
297
|
+
this.logger.debug('SearcherService: findSymbol', { name, project, kind, source: 'grep' });
|
|
263
298
|
const searchDir = project
|
|
264
299
|
? path.join(this.config.workspaceRoot, project)
|
|
265
300
|
: this.config.workspaceRoot;
|
|
@@ -300,9 +335,11 @@ class SearcherService {
|
|
|
300
335
|
signature: matchedLine.trim(),
|
|
301
336
|
});
|
|
302
337
|
}
|
|
338
|
+
this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: hits.length, truncated: hits.length >= FIND_SYMBOL_LIMIT });
|
|
303
339
|
return { hits, truncated: hits.length >= FIND_SYMBOL_LIMIT };
|
|
304
340
|
}
|
|
305
341
|
listFiles(relativePath, project, depth, pattern) {
|
|
342
|
+
this.logger.debug('SearcherService: listFiles', { relativePath, project, depth, pattern });
|
|
306
343
|
const projectRoot = path.join(this.config.workspaceRoot, project);
|
|
307
344
|
const resolvedTarget = path.resolve(projectRoot, relativePath);
|
|
308
345
|
const resolvedProject = path.resolve(projectRoot);
|
|
@@ -318,12 +355,14 @@ class SearcherService {
|
|
|
318
355
|
const clampedDepth = Math.min(5, Math.max(1, depth));
|
|
319
356
|
const lines = [];
|
|
320
357
|
this.walkDir(resolvedTarget, 0, clampedDepth - 1, pattern, lines);
|
|
358
|
+
this.logger.debug('SearcherService: listFiles result', { entries: lines.length });
|
|
321
359
|
if (lines.length === 0) {
|
|
322
360
|
return { tree: pattern ? `No files matching "${pattern}" found.` : '(empty directory)' };
|
|
323
361
|
}
|
|
324
362
|
return { tree: lines.join('\n') };
|
|
325
363
|
}
|
|
326
364
|
async readFileAbsolute(absPath, maxLines) {
|
|
365
|
+
this.logger.debug('SearcherService: readFileAbsolute', { absPath, maxLines });
|
|
327
366
|
const resolvedRoot = path.resolve(this.config.workspaceRoot);
|
|
328
367
|
const resolved = path.resolve(absPath);
|
|
329
368
|
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
|
@@ -356,9 +395,11 @@ class SearcherService {
|
|
|
356
395
|
throw err;
|
|
357
396
|
}
|
|
358
397
|
const relPath = path.relative(resolvedRoot, resolved);
|
|
398
|
+
this.logger.debug('SearcherService: readFileAbsolute result', { linesRead: lines.length, hasMore });
|
|
359
399
|
return { ok: true, filePath: relPath, startLine: 1, endLine: lines.length, lines, hasMore };
|
|
360
400
|
}
|
|
361
401
|
async readFileRange(filePath, startLine, endLine) {
|
|
402
|
+
this.logger.debug('SearcherService: readFileRange', { filePath, startLine, endLine });
|
|
362
403
|
const resolvedRoot = path.resolve(this.config.workspaceRoot);
|
|
363
404
|
const absPath = path.resolve(resolvedRoot, filePath);
|
|
364
405
|
if (absPath !== resolvedRoot && !absPath.startsWith(resolvedRoot + path.sep)) {
|
|
@@ -388,6 +429,7 @@ class SearcherService {
|
|
|
388
429
|
break;
|
|
389
430
|
}
|
|
390
431
|
}
|
|
432
|
+
this.logger.debug('SearcherService: readFileRange result', { linesRead: lines.length, hasMore });
|
|
391
433
|
return { ok: true, filePath, startLine, endLine: Math.min(endLine, lastRead), lines, hasMore };
|
|
392
434
|
}
|
|
393
435
|
isNonEnglish(text) {
|
|
@@ -432,6 +474,7 @@ class SearcherService {
|
|
|
432
474
|
type: `(export[[:space:]]+)?type[[:space:]]+${name}[[:space:]=<]`,
|
|
433
475
|
interface: `(export[[:space:]]+)?interface[[:space:]]+${name}[[:space:]{<]`,
|
|
434
476
|
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${name}[[:space:]=:]`,
|
|
477
|
+
method: `(public[[:space:]]+|async[[:space:]]+|static[[:space:]]+|override[[:space:]]+)+(async[[:space:]]+)?${name}[[:space:]]*[(<]`,
|
|
435
478
|
};
|
|
436
479
|
if (kind === 'all')
|
|
437
480
|
return Object.values(patterns).join('|');
|
|
@@ -447,6 +490,8 @@ class SearcherService {
|
|
|
447
490
|
return 'interface';
|
|
448
491
|
if (/\btype\s+$/.test(before))
|
|
449
492
|
return 'type';
|
|
493
|
+
if (/\b(public|async|static|override)\s+$/.test(before))
|
|
494
|
+
return 'method';
|
|
450
495
|
return 'variable';
|
|
451
496
|
}
|
|
452
497
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
name: codesearch-mcp
|
|
2
|
+
|
|
1
3
|
services:
|
|
2
4
|
db:
|
|
3
5
|
image: pgvector/pgvector:pg17
|
|
@@ -8,7 +10,7 @@ services:
|
|
|
8
10
|
ports:
|
|
9
11
|
- "5433:5432"
|
|
10
12
|
volumes:
|
|
11
|
-
-
|
|
13
|
+
- pgdata:/var/lib/postgresql/data
|
|
12
14
|
|
|
13
15
|
ollama:
|
|
14
16
|
image: ollama/ollama
|
|
@@ -18,4 +20,5 @@ services:
|
|
|
18
20
|
- ollama_data:/root/.ollama
|
|
19
21
|
|
|
20
22
|
volumes:
|
|
23
|
+
pgdata:
|
|
21
24
|
ollama_data:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13-alpha",
|
|
4
4
|
"description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/app.js",
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
"start:mcp": "ts-node app.ts mcp",
|
|
22
22
|
"index:workspace": "ts-node app.ts index",
|
|
23
23
|
"index:reindex": "ts-node app.ts index --reindex",
|
|
24
|
+
"index:backfill": "ts-node app.ts backfill",
|
|
24
25
|
"typecheck": "tsc --noEmit",
|
|
25
26
|
"test": "vitest run",
|
|
26
|
-
"test:watch": "vitest"
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"verify": "tsc --noEmit && vitest run"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
31
|
"@inquirer/prompts": "^8.4.1",
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"ollama": "^0.5.0",
|
|
36
38
|
"pg": "^8.13.0",
|
|
37
39
|
"pino": "^10.3.1",
|
|
40
|
+
"pino-roll": "^4.0.0",
|
|
38
41
|
"reflect-metadata": "^0.2.2",
|
|
39
42
|
"ts-morph": "^28.0.0",
|
|
40
43
|
"typeorm": "^0.3.20",
|