codesearch-mcp 0.0.17-alpha.1 → 0.1.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +95 -6
- package/dist/app.js +70 -10
- package/dist/src/commands/ConfigCommand.d.ts +1 -0
- package/dist/src/commands/ConfigCommand.js +84 -4
- package/dist/src/commands/IndexCommand.js +35 -7
- package/dist/src/commands/McpCommand.js +9 -4
- package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
- package/dist/src/commands/WebCommand.js +56 -0
- package/dist/src/config-store.d.ts +85 -4
- package/dist/src/config-store.js +157 -32
- package/dist/src/config.d.ts +32 -8
- package/dist/src/config.js +93 -30
- package/dist/src/constants.d.ts +35 -0
- package/dist/src/constants.js +57 -4
- package/dist/src/container.d.ts +10 -6
- package/dist/src/container.js +10 -5
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +111 -0
- package/dist/src/db/schema.js +15 -1
- package/dist/src/graph.d.ts +60 -0
- package/dist/src/graph.js +239 -120
- package/dist/src/help.js +20 -5
- package/dist/src/interfaces/IAstParser.d.ts +20 -3
- package/dist/src/interfaces/IChunker.d.ts +24 -0
- package/dist/src/interfaces/IChunker.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +27 -0
- package/dist/src/interfaces/IDatabase.js +2 -0
- package/dist/src/interfaces/IEmbedder.d.ts +15 -2
- package/dist/src/interfaces/ILogger.d.ts +10 -0
- package/dist/src/interfaces/ILogger.js +2 -0
- package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
- package/dist/src/interfaces/IMcpToolHandler.js +2 -0
- package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
- package/dist/src/interfaces/IMonorepoResolver.js +2 -0
- package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
- package/dist/src/interfaces/IProjectDiscovery.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +37 -0
- package/dist/src/interfaces/ISearcher.js +2 -0
- package/dist/src/interfaces/index.d.ts +8 -0
- package/dist/src/interfaces/index.js +2 -0
- package/dist/src/owner-path.d.ts +10 -0
- package/dist/src/owner-path.js +27 -0
- package/dist/src/project-selector.d.ts +1 -1
- package/dist/src/services/AstChunker.d.ts +31 -0
- package/dist/src/services/AstChunker.js +114 -0
- package/dist/src/services/AstParserService.d.ts +10 -6
- package/dist/src/services/AstParserService.js +60 -15
- package/dist/src/services/DatabaseService.d.ts +14 -1
- package/dist/src/services/DatabaseService.js +70 -25
- package/dist/src/services/EmbedderFactory.d.ts +9 -0
- package/dist/src/services/EmbedderFactory.js +29 -0
- package/dist/src/services/IndexerService.d.ts +22 -12
- package/dist/src/services/IndexerService.js +106 -46
- package/dist/src/services/InfinityEmbedder.d.ts +79 -0
- package/dist/src/services/InfinityEmbedder.js +199 -0
- package/dist/src/services/LoggerService.d.ts +2 -1
- package/dist/src/services/McpServer.d.ts +16 -7
- package/dist/src/services/McpServer.js +54 -367
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
- package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
- package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
- package/dist/src/services/NxMonorepoResolver.js +308 -0
- package/dist/src/services/OllamaEmbedder.d.ts +19 -5
- package/dist/src/services/OllamaEmbedder.js +91 -36
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
- package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
- package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
- package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
- package/dist/src/services/SearcherService.d.ts +46 -7
- package/dist/src/services/SearcherService.js +183 -53
- package/dist/src/services/VoyageEmbedder.d.ts +22 -0
- package/dist/src/services/VoyageEmbedder.js +72 -0
- package/dist/src/services/WebServerService.d.ts +77 -0
- package/dist/src/services/WebServerService.js +267 -0
- package/dist/src/services/http-retry.d.ts +19 -0
- package/dist/src/services/http-retry.js +54 -0
- package/dist/src/services/reranker.d.ts +15 -0
- package/dist/src/services/reranker.js +44 -0
- package/dist/src/setup.d.ts +6 -0
- package/dist/src/setup.js +249 -31
- package/dist/src/tools/FindSymbolTool.d.ts +8 -0
- package/dist/src/tools/FindSymbolTool.js +45 -0
- package/dist/src/tools/GetCallersTool.d.ts +8 -0
- package/dist/src/tools/GetCallersTool.js +42 -0
- package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
- package/dist/src/tools/GetDependenciesTool.js +38 -0
- package/dist/src/tools/GetDependentsTool.d.ts +8 -0
- package/dist/src/tools/GetDependentsTool.js +57 -0
- package/dist/src/tools/ListFilesTool.d.ts +8 -0
- package/dist/src/tools/ListFilesTool.js +39 -0
- package/dist/src/tools/ListProjectsTool.d.ts +8 -0
- package/dist/src/tools/ListProjectsTool.js +44 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
- package/dist/src/tools/ReadFileRangeTool.js +44 -0
- package/dist/src/tools/ReadFileTool.d.ts +8 -0
- package/dist/src/tools/ReadFileTool.js +42 -0
- package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
- package/dist/src/tools/SearchCodebaseTool.js +81 -0
- package/dist/src/tools/format-helpers.d.ts +8 -0
- package/dist/src/tools/format-helpers.js +58 -0
- package/dist/src/tools/index.d.ts +9 -0
- package/dist/src/tools/index.js +21 -0
- package/dist/src/web/dashboard.d.ts +9 -0
- package/dist/src/web/dashboard.js +342 -0
- package/docker/docker-compose.infinity-gpu.yml +19 -0
- package/docker/docker-compose.yml +25 -0
- package/docs/web-dashboard.md +187 -0
- package/package.json +25 -3
- package/dist/src/ast.d.ts +0 -2
- package/dist/src/ast.js +0 -110
- package/dist/src/commands/SetupCommand.js +0 -14
- package/dist/src/db/data-source.d.ts +0 -2
- package/dist/src/db/data-source.js +0 -13
- package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
- package/dist/src/db/entities/code-chunk.entity.js +0 -54
- package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
- package/dist/src/db/entities/code-dependency.entity.js +0 -54
- package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
- package/dist/src/db/entities/code-symbol.entity.js +0 -59
- package/dist/src/db/init.d.ts +0 -2
- package/dist/src/db/init.js +0 -62
- package/dist/src/env-loader.d.ts +0 -1
- package/dist/src/indexer.d.ts +0 -17
- package/dist/src/indexer.js +0 -242
- package/dist/src/mcp.d.ts +0 -1
- package/dist/src/mcp.js +0 -403
- package/dist/src/ollama.d.ts +0 -2
- package/dist/src/ollama.js +0 -6
- package/dist/src/searcher.d.ts +0 -74
- package/dist/src/searcher.js +0 -446
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -9
|
@@ -39,15 +39,15 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
40
40
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
41
41
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
42
|
-
const
|
|
43
|
-
|
|
42
|
+
const tools_1 = require("../tools");
|
|
43
|
+
function readPackageVersion() {
|
|
44
44
|
try {
|
|
45
45
|
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version ?? '0.0.0';
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
48
|
return '0.0.0';
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|
|
51
51
|
function buildInstructions(t) {
|
|
52
52
|
return `
|
|
53
53
|
You have access to a local code intelligence server. Use these tools to search codebases by meaning, navigate import graphs, look up symbol definitions, and read source files.
|
|
@@ -124,402 +124,89 @@ If \`get_dependents\` returns 0 results, add \`symbol\` (the exported name you c
|
|
|
124
124
|
Lower score = better match (cosine distance). Results above the configured threshold are filtered out automatically. Treat scores above 0.50 as weak hints — verify with \`read_file_range\` or \`Grep\`. Pass \`threshold\` to \`search_codebase\` to override per-query: \`0.40–0.45\` for precise technical queries (known term, exact concept); \`${t}\` default (broad but reliable). Avoid \`0.35\` — too strict for DSL files (GraphQL, Prisma) and infrastructure code.
|
|
125
125
|
`.trim();
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* McpServer — thin orchestration layer that delegates tool handling
|
|
129
|
+
* to individual IMcpToolHandler implementations (Open/Closed Principle).
|
|
130
|
+
*
|
|
131
|
+
* To add a new tool: implement IMcpToolHandler and register it in createTools().
|
|
132
|
+
*/
|
|
127
133
|
class McpServer {
|
|
128
134
|
searcher;
|
|
129
135
|
config;
|
|
130
136
|
logger;
|
|
131
|
-
|
|
137
|
+
version;
|
|
138
|
+
tools;
|
|
139
|
+
constructor(searcher, config, logger) {
|
|
132
140
|
this.searcher = searcher;
|
|
133
141
|
this.config = config;
|
|
134
142
|
this.logger = logger;
|
|
143
|
+
this.version = readPackageVersion();
|
|
144
|
+
this.tools = this.createTools();
|
|
135
145
|
}
|
|
136
146
|
async start(startupError) {
|
|
137
147
|
if (startupError) {
|
|
138
148
|
this.logger.error('McpServer: startup failed, server will report error on every tool call', { reason: startupError.message });
|
|
139
149
|
}
|
|
140
|
-
const server = new index_js_1.Server({ name: 'local-rag', version:
|
|
150
|
+
const server = new index_js_1.Server({ name: 'local-rag', version: this.version }, { capabilities: { tools: {} }, instructions: buildInstructions(this.config.search.similarityThreshold) });
|
|
141
151
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
142
|
-
tools: [
|
|
143
|
-
{
|
|
144
|
-
name: 'search_codebase',
|
|
145
|
-
description: 'Search local project codebase by semantic similarity',
|
|
146
|
-
inputSchema: {
|
|
147
|
-
type: 'object',
|
|
148
|
-
properties: {
|
|
149
|
-
query: { type: 'string' },
|
|
150
|
-
limit: { type: 'number', description: `Max results (default ${this.config.search.defaultLimit}, max ${this.config.search.maxLimit})` },
|
|
151
|
-
project: { type: 'string', description: 'Filter by project name (optional). Use list_projects to see available names.' },
|
|
152
|
-
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.' },
|
|
153
|
-
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.' },
|
|
154
|
-
threshold: { type: 'number', description: `Max cosine distance (0–1). Lower = stricter. Default: ${this.config.search.similarityThreshold}. Use 0.40–0.45 for precise technical queries, 0.55–0.60 for broad exploration.` },
|
|
155
|
-
},
|
|
156
|
-
required: ['query'],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'list_projects',
|
|
161
|
-
description: 'List all indexed projects with their chunk counts and metadata (description, stack, notes, relations from .ragmeta.json).',
|
|
162
|
-
inputSchema: { type: 'object', properties: {} },
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
name: 'read_file_range',
|
|
166
|
-
description: 'Read a specific line range from an indexed source file. Use this when a search result looks incomplete — read the surrounding lines to get full context.',
|
|
167
|
-
inputSchema: {
|
|
168
|
-
type: 'object',
|
|
169
|
-
properties: {
|
|
170
|
-
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
171
|
-
start_line: { type: 'number', description: 'First line to read (1-based). Use the **Line** number from search_codebase results. Default: 1.' },
|
|
172
|
-
end_line: { type: 'number', description: 'Last line to read (1-based, inclusive). Default: start_line + 99.' },
|
|
173
|
-
},
|
|
174
|
-
required: ['file_path'],
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
name: 'get_dependencies',
|
|
179
|
-
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.',
|
|
180
|
-
inputSchema: {
|
|
181
|
-
type: 'object',
|
|
182
|
-
properties: {
|
|
183
|
-
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
184
|
-
project: { type: 'string', description: 'Project name (use list_projects to find it)' },
|
|
185
|
-
symbol: { type: 'string', description: 'Filter to a specific imported symbol name (optional)' },
|
|
186
|
-
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependencies. Default: false.' },
|
|
187
|
-
},
|
|
188
|
-
required: ['file_path', 'project'],
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: 'get_dependents',
|
|
193
|
-
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.',
|
|
194
|
-
inputSchema: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
198
|
-
project: { type: 'string', description: 'Project name (use list_projects to find it)' },
|
|
199
|
-
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.' },
|
|
200
|
-
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
|
|
201
|
-
},
|
|
202
|
-
required: ['file_path', 'project'],
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
name: 'find_symbol',
|
|
207
|
-
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.',
|
|
208
|
-
inputSchema: {
|
|
209
|
-
type: 'object',
|
|
210
|
-
properties: {
|
|
211
|
-
name: { type: 'string', description: 'Exact symbol name (case-sensitive, whole-word match)' },
|
|
212
|
-
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
213
|
-
kind: { type: 'string', enum: ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'], description: 'Symbol kind filter. Default: all.' },
|
|
214
|
-
},
|
|
215
|
-
required: ['name'],
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: 'get_callers',
|
|
220
|
-
description: 'Find all call sites of a function or method by exact name. Uses the call graph index — faster and more precise than grep. Use after find_symbol to trace where a symbol is actually called. Returns empty if rag-index backfill --calls has not been run.',
|
|
221
|
-
inputSchema: {
|
|
222
|
-
type: 'object',
|
|
223
|
-
properties: {
|
|
224
|
-
symbol: { type: 'string', description: 'Exact function or method name (case-sensitive)' },
|
|
225
|
-
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
226
|
-
callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
|
|
227
|
-
},
|
|
228
|
-
required: ['symbol', 'project'],
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: 'list_files',
|
|
233
|
-
description: 'List files and directories in a project path as an indented tree. Use to understand project structure before reading files.',
|
|
234
|
-
inputSchema: {
|
|
235
|
-
type: 'object',
|
|
236
|
-
properties: {
|
|
237
|
-
path: { type: 'string', description: 'Directory path relative to the project root (e.g. "src/components" or "." for root)' },
|
|
238
|
-
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
239
|
-
depth: { type: 'number', description: 'Tree depth: 1 = immediate children only, up to 5. Default: 2.' },
|
|
240
|
-
pattern: { type: 'string', description: 'Optional filename filter (substring or extension like ".ts"). Only matching files are shown.' },
|
|
241
|
-
},
|
|
242
|
-
required: ['path', 'project'],
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: 'read_file',
|
|
247
|
-
description: 'Read a file by absolute path. Returns contents with line numbers. Truncates at max_lines with a hint to continue with read_file_range.',
|
|
248
|
-
inputSchema: {
|
|
249
|
-
type: 'object',
|
|
250
|
-
properties: {
|
|
251
|
-
file_path: { type: 'string', description: 'Absolute path to the file. Note: search_codebase returns relative paths — construct the absolute path from your workspace root.' },
|
|
252
|
-
max_lines: { type: 'number', description: 'Maximum lines to return (default 300, max 1000)' },
|
|
253
|
-
},
|
|
254
|
-
required: ['file_path'],
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
],
|
|
152
|
+
tools: [...this.tools.values()].map(t => t.definition()),
|
|
258
153
|
}));
|
|
259
154
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
260
155
|
if (startupError) {
|
|
261
156
|
return {
|
|
157
|
+
isError: true,
|
|
262
158
|
content: [{
|
|
263
159
|
type: 'text',
|
|
264
160
|
text: `MCP server is unavailable: ${startupError.message}\n\nCheck the log file and fix the issue, then restart the MCP server.`,
|
|
265
161
|
}],
|
|
266
162
|
};
|
|
267
163
|
}
|
|
164
|
+
const handler = this.tools.get(req.params.name);
|
|
165
|
+
if (!handler) {
|
|
166
|
+
return {
|
|
167
|
+
isError: true,
|
|
168
|
+
content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
268
171
|
try {
|
|
269
172
|
const t0 = Date.now();
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (rows.length === 0) {
|
|
274
|
-
return { content: [{ type: 'text', text: 'No indexed projects found.' }] };
|
|
275
|
-
}
|
|
276
|
-
return { content: [{ type: 'text', text: rows.map(r => {
|
|
277
|
-
const lines = [`${r.serviceName}: ${r.chunks} chunks`];
|
|
278
|
-
if (r.description)
|
|
279
|
-
lines.push(` description: ${r.description}`);
|
|
280
|
-
if (r.stack)
|
|
281
|
-
lines.push(` stack: ${r.stack}`);
|
|
282
|
-
if (r.notes)
|
|
283
|
-
lines.push(` notes: ${r.notes}`);
|
|
284
|
-
if (r.relations) {
|
|
285
|
-
for (const [key, val] of Object.entries(r.relations)) {
|
|
286
|
-
const list = Array.isArray(val) ? val.join(', ') : val;
|
|
287
|
-
lines.push(` ${key}: ${list}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return lines.join('\n');
|
|
291
|
-
}).join('\n\n') }] };
|
|
292
|
-
}
|
|
293
|
-
if (req.params.name === 'search_codebase') {
|
|
294
|
-
const raw = String(req.params.arguments?.query);
|
|
295
|
-
const limit = Math.min(Math.max(1, Number(req.params.arguments?.limit) || this.config.search.defaultLimit), this.config.search.maxLimit);
|
|
296
|
-
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
297
|
-
const includeNeighbors = Boolean(req.params.arguments?.include_neighbors);
|
|
298
|
-
const includeGraphContext = Boolean(req.params.arguments?.include_graph_context);
|
|
299
|
-
const rawThreshold = req.params.arguments?.threshold;
|
|
300
|
-
const parsedThreshold = Number(rawThreshold);
|
|
301
|
-
const threshold = rawThreshold != null && !isNaN(parsedThreshold)
|
|
302
|
-
? Math.min(1, Math.max(0, parsedThreshold))
|
|
303
|
-
: undefined;
|
|
304
|
-
const { query, translated } = await this.searcher.prepareQuery(raw);
|
|
305
|
-
const results = await this.searcher.searchChunks(query, limit, project, threshold);
|
|
306
|
-
this.logger.debug('McpServer: search_codebase', {
|
|
307
|
-
query: raw.slice(0, 120),
|
|
308
|
-
project,
|
|
309
|
-
threshold: threshold ?? this.config.search.similarityThreshold,
|
|
310
|
-
limit,
|
|
311
|
-
translated,
|
|
312
|
-
count: results.length,
|
|
313
|
-
ms: Date.now() - t0,
|
|
314
|
-
});
|
|
315
|
-
if (results.length === 0) {
|
|
316
|
-
return { content: [{ type: 'text', text: 'No relevant results found.' }] };
|
|
317
|
-
}
|
|
318
|
-
const header = translated ? `> Query translated: "${raw}" → "${query}"\n\n` : '';
|
|
319
|
-
let graphCtx = null;
|
|
320
|
-
if (includeGraphContext) {
|
|
321
|
-
graphCtx = await this.searcher.fetchGraphContext(results);
|
|
322
|
-
}
|
|
323
|
-
if (includeNeighbors) {
|
|
324
|
-
const withNeighbors = await this.searcher.fetchNeighbors(results);
|
|
325
|
-
const blocks = withNeighbors.map(({ result, prevChunk, nextChunk }) => {
|
|
326
|
-
const parts = [];
|
|
327
|
-
if (prevChunk)
|
|
328
|
-
parts.push(this.formatChunk(prevChunk, 'prev chunk'));
|
|
329
|
-
parts.push(`${this.formatChunk(result)}\n**Relevance:** ${result.score}${this.formatGraphContext(graphCtx?.get(result.filePath))}`);
|
|
330
|
-
if (nextChunk)
|
|
331
|
-
parts.push(this.formatChunk(nextChunk, 'next chunk'));
|
|
332
|
-
return parts.join('\n\n');
|
|
333
|
-
});
|
|
334
|
-
return { content: [{ type: 'text', text: header + blocks.join('\n\n---\n\n') }] };
|
|
335
|
-
}
|
|
336
|
-
const text = results
|
|
337
|
-
.map(r => `${this.formatChunk(r)}\n**Relevance:** ${r.score}${this.formatGraphContext(graphCtx?.get(r.filePath))}`)
|
|
338
|
-
.join('\n\n---\n\n');
|
|
339
|
-
return { content: [{ type: 'text', text: header + text }] };
|
|
340
|
-
}
|
|
341
|
-
if (req.params.name === 'read_file_range') {
|
|
342
|
-
const filePath = String(req.params.arguments?.file_path);
|
|
343
|
-
const startLine = Math.max(1, Number(req.params.arguments?.start_line) || 1);
|
|
344
|
-
const endLine = Number(req.params.arguments?.end_line) || startLine + 99;
|
|
345
|
-
const range = await this.searcher.readFileRange(filePath, startLine, endLine);
|
|
346
|
-
this.logger.debug('McpServer: read_file_range', { file: filePath, startLine, endLine, ok: range.ok, ms: Date.now() - t0 });
|
|
347
|
-
if (!range.ok) {
|
|
348
|
-
const message = range.reason === 'invalid_path' ? `Invalid file path (outside workspace): ${filePath}` :
|
|
349
|
-
range.reason === 'not_indexed' ? `File is not indexed: ${filePath}. Run indexing first.` :
|
|
350
|
-
`File was deleted from disk after indexing: ${filePath}. Re-run indexing to update.`;
|
|
351
|
-
return { content: [{ type: 'text', text: message }] };
|
|
352
|
-
}
|
|
353
|
-
const numbered = range.lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
|
|
354
|
-
let text = `**File:** ${range.filePath}\n**Lines:** ${range.startLine}–${range.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
355
|
-
if (range.hasMore) {
|
|
356
|
-
text += `\n\n[file has more lines — call read_file_range again with start_line: ${range.endLine + 1}]`;
|
|
357
|
-
}
|
|
358
|
-
return { content: [{ type: 'text', text }] };
|
|
359
|
-
}
|
|
360
|
-
if (req.params.name === 'get_dependencies') {
|
|
361
|
-
const filePath = String(req.params.arguments?.file_path);
|
|
362
|
-
const project = String(req.params.arguments?.project);
|
|
363
|
-
const symbol = req.params.arguments?.symbol ? String(req.params.arguments.symbol) : null;
|
|
364
|
-
const transitive = Boolean(req.params.arguments?.transitive);
|
|
365
|
-
const rows = await this.searcher.getDependencies(filePath, project, symbol, transitive);
|
|
366
|
-
this.logger.debug('McpServer: get_dependencies', { file: filePath, project, transitive, count: rows.length, ms: Date.now() - t0 });
|
|
367
|
-
if (rows.length === 0) {
|
|
368
|
-
return { content: [{ type: 'text', text: `No dependencies found for: ${filePath}` }] };
|
|
369
|
-
}
|
|
370
|
-
return { content: [{ type: 'text', text: this.formatDependencies(filePath, rows, transitive) }] };
|
|
371
|
-
}
|
|
372
|
-
if (req.params.name === 'get_dependents') {
|
|
373
|
-
const filePath = String(req.params.arguments?.file_path);
|
|
374
|
-
const project = String(req.params.arguments?.project);
|
|
375
|
-
const symbol = req.params.arguments?.symbol ? String(req.params.arguments.symbol) : null;
|
|
376
|
-
const transitive = Boolean(req.params.arguments?.transitive);
|
|
377
|
-
const rows = await this.searcher.getDependents(filePath, project, symbol, transitive);
|
|
378
|
-
this.logger.debug('McpServer: get_dependents', { file: filePath, project, symbol, transitive, count: rows.length, ms: Date.now() - t0 });
|
|
379
|
-
if (rows.length > 0) {
|
|
380
|
-
return { content: [{ type: 'text', text: this.formatDependents(filePath, rows, transitive) }] };
|
|
381
|
-
}
|
|
382
|
-
const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
|
|
383
|
-
if (!symbol) {
|
|
384
|
-
return { content: [{ type: 'text', text: `${header}\n\nTip: pass \`symbol\` to trigger a text-search fallback for runtime usages (e.g. via singleton or container).` }] };
|
|
385
|
-
}
|
|
386
|
-
const { matches, truncated } = this.searcher.grepSymbol(symbol, project, filePath);
|
|
387
|
-
if (matches.length === 0) {
|
|
388
|
-
return { content: [{ type: 'text', text: `${header}\n\n**Fallback \`[text search]\` for \`${symbol}\`:** No results.` }] };
|
|
389
|
-
}
|
|
390
|
-
const grouped = new Map();
|
|
391
|
-
for (const m of matches) {
|
|
392
|
-
if (!grouped.has(m.filePath))
|
|
393
|
-
grouped.set(m.filePath, []);
|
|
394
|
-
grouped.get(m.filePath).push(m.line);
|
|
395
|
-
}
|
|
396
|
-
const fallbackLines = [`\n**Fallback \`[text search]\` for \`${symbol}\`:**${truncated ? ' *(truncated to 200 results)*' : ''}`];
|
|
397
|
-
for (const [fp, lines] of grouped) {
|
|
398
|
-
fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
|
|
399
|
-
}
|
|
400
|
-
return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
|
|
401
|
-
}
|
|
402
|
-
if (req.params.name === 'find_symbol') {
|
|
403
|
-
const name = String(req.params.arguments?.name ?? '');
|
|
404
|
-
const project = req.params.arguments?.project ? String(req.params.arguments.project) : null;
|
|
405
|
-
const rawKind = req.params.arguments?.kind;
|
|
406
|
-
const validKinds = ['function', 'class', 'type', 'interface', 'variable', 'method', 'all'];
|
|
407
|
-
const kind = validKinds.includes(rawKind) ? rawKind : 'all';
|
|
408
|
-
const { hits, truncated } = await this.searcher.findSymbol(name, project, kind);
|
|
409
|
-
this.logger.debug('McpServer: find_symbol', { name, kind, project, count: hits.length, ms: Date.now() - t0 });
|
|
410
|
-
if (hits.length === 0) {
|
|
411
|
-
return { content: [{ type: 'text', text: `No definitions found for symbol: ${name}` }] };
|
|
412
|
-
}
|
|
413
|
-
const lines = [];
|
|
414
|
-
if (truncated)
|
|
415
|
-
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
416
|
-
for (const h of hits) {
|
|
417
|
-
lines.push(`**${h.kind}** \`${h.filePath}\` line ${h.line}, col ${h.column}`);
|
|
418
|
-
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
419
|
-
}
|
|
420
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
421
|
-
}
|
|
422
|
-
if (req.params.name === 'list_files') {
|
|
423
|
-
const relPath = String(req.params.arguments?.path ?? '.');
|
|
424
|
-
const project = String(req.params.arguments?.project ?? '');
|
|
425
|
-
const rawDepth = Number(req.params.arguments?.depth);
|
|
426
|
-
const depth = isNaN(rawDepth) || rawDepth < 1 ? 2 : Math.min(5, rawDepth);
|
|
427
|
-
const pattern = req.params.arguments?.pattern ? String(req.params.arguments.pattern) : null;
|
|
428
|
-
const { tree, error } = this.searcher.listFiles(relPath, project, depth, pattern);
|
|
429
|
-
this.logger.debug('McpServer: list_files', { path: relPath, project, depth, ms: Date.now() - t0 });
|
|
430
|
-
if (error) {
|
|
431
|
-
return { content: [{ type: 'text', text: `list_files error: ${error}` }] };
|
|
432
|
-
}
|
|
433
|
-
return { content: [{ type: 'text', text: `**${project}/${relPath}**\n\`\`\`\n${tree}\n\`\`\`` }] };
|
|
434
|
-
}
|
|
435
|
-
if (req.params.name === 'read_file') {
|
|
436
|
-
const filePath = String(req.params.arguments?.file_path ?? '');
|
|
437
|
-
const rawMax = Number(req.params.arguments?.max_lines);
|
|
438
|
-
const maxLines = isNaN(rawMax) || rawMax < 1 ? 300 : Math.min(1000, rawMax);
|
|
439
|
-
const result = await this.searcher.readFileAbsolute(filePath, maxLines);
|
|
440
|
-
this.logger.debug('McpServer: read_file', { file: filePath, maxLines, ok: result.ok, ms: Date.now() - t0 });
|
|
441
|
-
if (!result.ok) {
|
|
442
|
-
const message = result.reason === 'invalid_path' ? `Invalid path (outside workspace): ${filePath}` :
|
|
443
|
-
`File not found on disk: ${filePath}`;
|
|
444
|
-
return { content: [{ type: 'text', text: message }] };
|
|
445
|
-
}
|
|
446
|
-
const numbered = result.lines.map((line, i) => `${i + 1}: ${line}`).join('\n');
|
|
447
|
-
let text = `**File:** ${result.filePath}\n**Lines:** 1–${result.endLine}\n\`\`\`\n${numbered}\n\`\`\``;
|
|
448
|
-
if (result.hasMore) {
|
|
449
|
-
text += `\n\n[truncated: file has more lines — use read_file_range with file_path: "${result.filePath}", start_line: ${result.endLine + 1} to continue]`;
|
|
450
|
-
}
|
|
451
|
-
return { content: [{ type: 'text', text }] };
|
|
452
|
-
}
|
|
453
|
-
if (req.params.name === 'get_callers') {
|
|
454
|
-
const symbol = String(req.params.arguments?.symbol ?? '');
|
|
455
|
-
const project = String(req.params.arguments?.project ?? '');
|
|
456
|
-
const calleeFile = req.params.arguments?.callee_file ? String(req.params.arguments.callee_file) : null;
|
|
457
|
-
const rows = await this.searcher.getCallers(symbol, project, calleeFile);
|
|
458
|
-
this.logger.debug('McpServer: get_callers', { symbol, project, count: rows.length, ms: Date.now() - t0 });
|
|
459
|
-
if (rows.length === 0) {
|
|
460
|
-
return { content: [{ type: 'text', text: `No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index backfill --calls\` if the call graph has not been populated yet.` }] };
|
|
461
|
-
}
|
|
462
|
-
const lines = [`**Call sites of \`${symbol}\`** (${rows.length} found)\n`];
|
|
463
|
-
for (const r of rows) {
|
|
464
|
-
const loc = r.line != null ? ` line ${r.line}` : '';
|
|
465
|
-
const ctx = r.fromSymbol ? `, in \`${r.fromSymbol}\`` : '';
|
|
466
|
-
lines.push(` → \`${r.fromFile}\`${loc}${ctx}`);
|
|
467
|
-
}
|
|
468
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
469
|
-
}
|
|
470
|
-
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
173
|
+
const result = await handler.handle((req.params.arguments ?? {}));
|
|
174
|
+
this.logger.debug('McpServer: tool handled', { tool: req.params.name, ms: Date.now() - t0 });
|
|
175
|
+
return result;
|
|
471
176
|
}
|
|
472
177
|
catch (err) {
|
|
473
|
-
|
|
474
|
-
|
|
178
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
179
|
+
this.logger.error('McpServer: tool handler error', { tool: req.params.name, reason });
|
|
180
|
+
return {
|
|
181
|
+
isError: true,
|
|
182
|
+
content: [{ type: 'text', text: `Tool error: ${reason}` }],
|
|
183
|
+
};
|
|
475
184
|
}
|
|
476
185
|
});
|
|
477
186
|
await server.connect(new stdio_js_1.StdioServerTransport());
|
|
478
|
-
this.logger.info('McpServer: started', { version:
|
|
479
|
-
}
|
|
480
|
-
formatGraphContext(ctx) {
|
|
481
|
-
const parts = [];
|
|
482
|
-
if (ctx?.imports.length)
|
|
483
|
-
parts.push(`**Imports:** ${ctx.imports.join(' · ')}`);
|
|
484
|
-
if (ctx?.usedBy.length)
|
|
485
|
-
parts.push(`**Used by:** ${ctx.usedBy.join(' · ')}`);
|
|
486
|
-
return parts.length > 0 ? '\n' + parts.join('\n') : '';
|
|
187
|
+
this.logger.info('McpServer: started', { version: this.version });
|
|
487
188
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
formatDependents(filePath, rows, transitive) {
|
|
509
|
-
const direct = rows.filter(r => !transitive || (r.depth ?? 1) === 1);
|
|
510
|
-
const indirect = transitive ? rows.filter(r => (r.depth ?? 1) > 1) : [];
|
|
511
|
-
const lines = [`**Dependents of** \`${filePath}\`\n`, '**Direct:**'];
|
|
512
|
-
for (const r of direct) {
|
|
513
|
-
const sym = r.toSymbol ? ` (imports \`${r.toSymbol}\`)` : '';
|
|
514
|
-
lines.push(` ← \`${r.fromFile}\`${sym}`);
|
|
515
|
-
}
|
|
516
|
-
if (indirect.length > 0) {
|
|
517
|
-
lines.push('\n**Transitive:**');
|
|
518
|
-
for (const r of indirect) {
|
|
519
|
-
lines.push(` ← \`${r.fromFile}\` *(via ${r.via}, depth ${r.depth})*`);
|
|
520
|
-
}
|
|
189
|
+
/**
|
|
190
|
+
* Factory method: creates and registers all tool handlers.
|
|
191
|
+
* New tools are added here without modifying the dispatch logic above.
|
|
192
|
+
*/
|
|
193
|
+
createTools() {
|
|
194
|
+
const handlers = [
|
|
195
|
+
new tools_1.ListProjectsTool(this.searcher),
|
|
196
|
+
new tools_1.SearchCodebaseTool(this.searcher, this.config.search.defaultLimit, this.config.search.maxLimit, this.config.search.similarityThreshold, this.logger),
|
|
197
|
+
new tools_1.ReadFileRangeTool(this.searcher),
|
|
198
|
+
new tools_1.GetDependenciesTool(this.searcher),
|
|
199
|
+
new tools_1.GetDependentsTool(this.searcher),
|
|
200
|
+
new tools_1.FindSymbolTool(this.searcher),
|
|
201
|
+
new tools_1.GetCallersTool(this.searcher),
|
|
202
|
+
new tools_1.ListFilesTool(this.searcher),
|
|
203
|
+
new tools_1.ReadFileTool(this.searcher),
|
|
204
|
+
];
|
|
205
|
+
const map = new Map();
|
|
206
|
+
for (const h of handlers) {
|
|
207
|
+
map.set(h.definition().name, h);
|
|
521
208
|
}
|
|
522
|
-
return
|
|
209
|
+
return map;
|
|
523
210
|
}
|
|
524
211
|
}
|
|
525
212
|
exports.McpServer = McpServer;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ILogger } from '../interfaces';
|
|
2
|
+
import type { IMonorepoResolver } from '../interfaces/IMonorepoResolver';
|
|
3
|
+
import type { DiscoveredProject, IProjectDiscovery } from '../interfaces';
|
|
4
|
+
export interface WorkspaceInfo {
|
|
5
|
+
isMonorepo: boolean;
|
|
6
|
+
rootTsConfigPath: string | null;
|
|
7
|
+
projects: DiscoveredProject[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Expand discovery units into the leaf projects to index: a nested monorepo
|
|
11
|
+
* unit (with `children`) becomes its sub-projects; an ordinary project stays
|
|
12
|
+
* itself. Shared by IndexCommand (indexing loop) and SearcherService (name→path
|
|
13
|
+
* resolution) so both agree on the leaf set.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toLeafProjects(projects: DiscoveredProject[]): DiscoveredProject[];
|
|
16
|
+
/**
|
|
17
|
+
* Project discovery that is monorepo-aware.
|
|
18
|
+
*
|
|
19
|
+
* When a monorepo is detected (via the resolver), it uses the resolver
|
|
20
|
+
* to discover projects. Otherwise, it falls back to the original
|
|
21
|
+
* flat-workspace discovery (directories with package.json). Cross-project
|
|
22
|
+
* import resolution relies on the tsconfig `paths` attached via
|
|
23
|
+
* `rootTsConfigPath` — no separate scope map is built.
|
|
24
|
+
*
|
|
25
|
+
* Single Responsibility: only discovers projects and workspace metadata.
|
|
26
|
+
* Does not index or search.
|
|
27
|
+
*
|
|
28
|
+
* Implements {@link IProjectDiscovery} so it can be wired as the default
|
|
29
|
+
* discovery in the container. `discover()` returns one entry per top-level
|
|
30
|
+
* project: when the root itself is a monorepo, each entry carries the
|
|
31
|
+
* workspace-wide `rootTsConfigPath`; under a flat root, an ordinary project
|
|
32
|
+
* gets `rootTsConfigPath: null`, while a top-level directory that is itself a
|
|
33
|
+
* nested monorepo is returned as a single unit with expanded `children` (see
|
|
34
|
+
* `toLeafProjects`). `analyzeWorkspace()` exposes the full workspace metadata
|
|
35
|
+
* (monorepo flag, root tsconfig, project list).
|
|
36
|
+
*/
|
|
37
|
+
export declare class MonorepoAwareDiscoveryService implements IProjectDiscovery {
|
|
38
|
+
private resolvers;
|
|
39
|
+
private logger;
|
|
40
|
+
/** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
|
|
41
|
+
private flatDiscovery;
|
|
42
|
+
constructor(resolvers: IMonorepoResolver[], logger: ILogger,
|
|
43
|
+
/** Discovery used when no monorepo is detected. Defaults to flat package.json scan. */
|
|
44
|
+
flatDiscovery?: IProjectDiscovery);
|
|
45
|
+
/**
|
|
46
|
+
* IProjectDiscovery contract: enumerate projects in the workspace.
|
|
47
|
+
* Attaches the workspace-wide root tsconfig to every project so the
|
|
48
|
+
* indexer can resolve cross-project path aliases.
|
|
49
|
+
*/
|
|
50
|
+
discover(rootDir: string): DiscoveredProject[];
|
|
51
|
+
/**
|
|
52
|
+
* If `project`'s directory is itself a monorepo (resolver detects it),
|
|
53
|
+
* return it with expanded `children` named `<repo>/<sub-project>`, each
|
|
54
|
+
* carrying the repo's workspace-wide tsconfig. Otherwise return it unchanged.
|
|
55
|
+
*/
|
|
56
|
+
private expandIfNestedMonorepo;
|
|
57
|
+
/**
|
|
58
|
+
* Analyze the workspace root and return full workspace information:
|
|
59
|
+
* monorepo detection flag, root tsconfig path, and project list.
|
|
60
|
+
*/
|
|
61
|
+
analyzeWorkspace(rootDir: string): WorkspaceInfo;
|
|
62
|
+
}
|