@crowley/rag-mcp 1.0.2 → 1.0.4

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.
@@ -26,6 +26,19 @@ export declare function formatMemoryResults(results: Array<{
26
26
  memory: Record<string, unknown>;
27
27
  score: number;
28
28
  }>, emptyMessage?: string): string;
29
+ /** Format navigate-mode search results as compact navigation pointers */
30
+ export declare function formatNavigationResults(results: Array<{
31
+ file: string;
32
+ lines?: [number, number];
33
+ symbols?: string[];
34
+ imports?: string[];
35
+ connections?: string[];
36
+ layer?: string;
37
+ service?: string;
38
+ preview?: string;
39
+ score: number;
40
+ graphExpanded?: boolean;
41
+ }>): string;
29
42
  /** Format a simple list of files with scores */
30
43
  export declare function formatFileList(files: Array<{
31
44
  file: string;
@@ -60,6 +60,26 @@ export function formatMemoryResults(results, emptyMessage = "No memories found."
60
60
  });
61
61
  return result;
62
62
  }
63
+ /** Format navigate-mode search results as compact navigation pointers */
64
+ export function formatNavigationResults(results) {
65
+ if (!results?.length)
66
+ return "No results found.";
67
+ return results.map(r => {
68
+ const loc = r.lines ? `:${r.lines[0]}-${r.lines[1]}` : '';
69
+ let out = `**${r.file}${loc}** (${pct(r.score)})`;
70
+ if (r.layer)
71
+ out += ` [${r.layer}]`;
72
+ if (r.graphExpanded)
73
+ out += ' _(graph)_';
74
+ if (r.preview)
75
+ out += `\n\`${truncate(r.preview, 100)}\``;
76
+ if (r.symbols?.length)
77
+ out += `\nSymbols: ${r.symbols.join(', ')}`;
78
+ if (r.connections?.length)
79
+ out += `\nConnections: ${r.connections.map(c => '`' + c + '`').join(', ')}`;
80
+ return out;
81
+ }).join('\n\n');
82
+ }
63
83
  /** Format a simple list of files with scores */
64
84
  export function formatFileList(files, emptyMessage = "No files found.") {
65
85
  if (!files || files.length === 0)
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Indexing tools module - codebase indexing, status, zero-downtime reindex,
3
3
  * and alias management.
4
+ *
5
+ * index_codebase and reindex_zero_downtime read files locally and upload
6
+ * them to the RAG API in batches via POST /api/index/upload. This allows
7
+ * remote MCP clients to index codebases that aren't on the server filesystem.
4
8
  */
5
9
  import type { ToolModule } from "../types.js";
6
10
  /**
@@ -1,7 +1,105 @@
1
1
  /**
2
2
  * Indexing tools module - codebase indexing, status, zero-downtime reindex,
3
3
  * and alias management.
4
+ *
5
+ * index_codebase and reindex_zero_downtime read files locally and upload
6
+ * them to the RAG API in batches via POST /api/index/upload. This allows
7
+ * remote MCP clients to index codebases that aren't on the server filesystem.
4
8
  */
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { glob } from "glob";
12
+ const DEFAULT_PATTERNS = [
13
+ "**/*.ts",
14
+ "**/*.tsx",
15
+ "**/*.js",
16
+ "**/*.jsx",
17
+ "**/*.vue",
18
+ "**/*.py",
19
+ "**/*.go",
20
+ "**/*.rs",
21
+ "**/*.java",
22
+ "**/*.md",
23
+ "**/*.sql",
24
+ "**/*.yml",
25
+ "**/*.yaml",
26
+ "**/Dockerfile",
27
+ ];
28
+ const DEFAULT_EXCLUDE = [
29
+ "**/node_modules/**",
30
+ "**/dist/**",
31
+ "**/build/**",
32
+ "**/.git/**",
33
+ "**/coverage/**",
34
+ "**/.nuxt/**",
35
+ "**/.next/**",
36
+ "**/vendor/**",
37
+ "**/__pycache__/**",
38
+ "**/target/**",
39
+ "**/package-lock.json",
40
+ "**/yarn.lock",
41
+ "**/pnpm-lock.yaml",
42
+ "**/eval/results/**",
43
+ "**/eval/golden-queries.json",
44
+ ];
45
+ const BATCH_SIZE = 50;
46
+ /**
47
+ * Discover files locally using glob, read their contents, and upload
48
+ * them to the RAG API in batches.
49
+ */
50
+ async function uploadFiles(ctx, projectPath, opts) {
51
+ const patterns = opts.patterns || DEFAULT_PATTERNS;
52
+ const excludePatterns = opts.excludePatterns || DEFAULT_EXCLUDE;
53
+ // Discover files locally
54
+ const files = await glob(patterns, {
55
+ cwd: projectPath,
56
+ ignore: excludePatterns,
57
+ nodir: true,
58
+ absolute: false,
59
+ });
60
+ if (files.length === 0) {
61
+ return { totalFiles: 0, indexedFiles: 0, totalChunks: 0, errors: 0, duration: 0 };
62
+ }
63
+ let totalIndexed = 0;
64
+ let totalChunks = 0;
65
+ let totalErrors = 0;
66
+ let totalDuration = 0;
67
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
68
+ const batch = files.slice(i, i + BATCH_SIZE);
69
+ const filePayloads = [];
70
+ for (const relPath of batch) {
71
+ try {
72
+ const absPath = path.join(projectPath, relPath);
73
+ const content = fs.readFileSync(absPath, "utf-8");
74
+ filePayloads.push({ path: relPath, content });
75
+ }
76
+ catch {
77
+ totalErrors++;
78
+ }
79
+ }
80
+ if (filePayloads.length === 0)
81
+ continue;
82
+ const isFirst = i === 0;
83
+ const isLast = i + BATCH_SIZE >= files.length;
84
+ const response = await ctx.api.post("/api/index/upload", {
85
+ files: filePayloads,
86
+ force: isFirst && (opts.force ?? false),
87
+ done: isLast,
88
+ });
89
+ const data = response.data;
90
+ totalIndexed += data.filesProcessed || 0;
91
+ totalChunks += data.chunksCreated || 0;
92
+ totalErrors += data.errors || 0;
93
+ totalDuration += data.duration || 0;
94
+ }
95
+ return {
96
+ totalFiles: files.length,
97
+ indexedFiles: totalIndexed,
98
+ totalChunks,
99
+ errors: totalErrors,
100
+ duration: totalDuration,
101
+ };
102
+ }
5
103
  /**
6
104
  * Create the indexing tools module with project-specific descriptions.
7
105
  */
@@ -67,16 +165,15 @@ export function createIndexingTools(projectName) {
67
165
  ];
68
166
  const handlers = {
69
167
  index_codebase: async (args, ctx) => {
70
- const { path, force = false } = args;
71
- const response = await ctx.api.post("/api/index", {
72
- collection: `${ctx.collectionPrefix}codebase`,
73
- path: path || ctx.projectPath,
74
- force,
75
- });
76
- const data = response.data;
168
+ const { path: indexPath, force = false } = args;
169
+ const projectPath = indexPath || ctx.projectPath;
170
+ const stats = await uploadFiles(ctx, projectPath, { force });
77
171
  let result = `## Indexing ${projectName}\n\n`;
78
- result += `- **Status:** ${data.status || "started"}\n`;
79
- result += `- **Files to process:** ${data.filesToProcess ?? "N/A"}\n`;
172
+ result += `- **Total files found:** ${stats.totalFiles}\n`;
173
+ result += `- **Files indexed:** ${stats.indexedFiles}\n`;
174
+ result += `- **Chunks created:** ${stats.totalChunks}\n`;
175
+ result += `- **Errors:** ${stats.errors}\n`;
176
+ result += `- **Duration:** ${stats.duration}ms\n`;
80
177
  return result;
81
178
  },
82
179
  get_index_status: async (_args, ctx) => {
@@ -91,18 +188,19 @@ export function createIndexingTools(projectName) {
91
188
  return result;
92
189
  },
93
190
  reindex_zero_downtime: async (args, ctx) => {
94
- const { path, patterns, excludePatterns } = args;
95
- const response = await ctx.api.post("/api/reindex", {
96
- collection: `${ctx.collectionPrefix}codebase`,
97
- path: path || ctx.projectPath,
191
+ const { path: indexPath, patterns, excludePatterns } = args;
192
+ const projectPath = indexPath || ctx.projectPath;
193
+ const stats = await uploadFiles(ctx, projectPath, {
98
194
  patterns,
99
195
  excludePatterns,
196
+ force: true,
100
197
  });
101
- const data = response.data;
102
- let result = `## Zero-Downtime Reindex: ${projectName}\n\n`;
103
- result += `- **Alias:** ${data.alias || "N/A"}\n`;
104
- result += `- **Status:** ${data.status || "started"}\n`;
105
- result += `- **Message:** ${data.message || "Reindex initiated"}\n`;
198
+ let result = `## Reindex: ${projectName}\n\n`;
199
+ result += `- **Total files found:** ${stats.totalFiles}\n`;
200
+ result += `- **Files indexed:** ${stats.indexedFiles}\n`;
201
+ result += `- **Chunks created:** ${stats.totalChunks}\n`;
202
+ result += `- **Errors:** ${stats.errors}\n`;
203
+ result += `- **Duration:** ${stats.duration}ms\n`;
106
204
  return result;
107
205
  },
108
206
  list_aliases: async (_args, ctx) => {
@@ -2,7 +2,7 @@
2
2
  * Search tools module - codebase search, similarity search, grouped/hybrid search,
3
3
  * documentation search, and project statistics.
4
4
  */
5
- import { formatCodeResults, truncate, pct } from "../formatters.js";
5
+ import { formatCodeResults, formatNavigationResults, truncate } from "../formatters.js";
6
6
  /**
7
7
  * Create the search tools module with project-specific descriptions.
8
8
  */
@@ -10,7 +10,7 @@ export function createSearchTools(projectName) {
10
10
  const tools = [
11
11
  {
12
12
  name: "search_codebase",
13
- description: `Search the ${projectName} codebase for relevant code. Returns matching files with code snippets and relevance scores.`,
13
+ description: `Search the ${projectName} codebase. Returns file locations, symbols, and graph connections. Use Read tool to view the actual code at returned locations.`,
14
14
  inputSchema: {
15
15
  type: "object",
16
16
  properties: {
@@ -64,7 +64,7 @@ export function createSearchTools(projectName) {
64
64
  },
65
65
  {
66
66
  name: "grouped_search",
67
- description: `Search ${projectName} codebase with results grouped by file. Returns one best match per file instead of multiple chunks.`,
67
+ description: `Search ${projectName} codebase grouped by file. Returns file locations with symbols and connections. Use Read tool to view the actual code.`,
68
68
  inputSchema: {
69
69
  type: "object",
70
70
  properties: {
@@ -100,7 +100,7 @@ export function createSearchTools(projectName) {
100
100
  },
101
101
  {
102
102
  name: "hybrid_search",
103
- description: `Hybrid search combining keyword matching and semantic similarity for ${projectName}. Better for finding exact terms + related concepts.`,
103
+ description: `Hybrid search combining keyword matching and semantic similarity for ${projectName}. Returns file locations with symbols and connections. Use Read tool to view code.`,
104
104
  inputSchema: {
105
105
  type: "object",
106
106
  properties: {
@@ -186,7 +186,7 @@ export function createSearchTools(projectName) {
186
186
  },
187
187
  {
188
188
  name: "search_graph",
189
- description: `Search ${projectName} codebase with graph expansion. Finds semantically similar code plus connected files via import/call relationships.`,
189
+ description: `Search ${projectName} codebase with graph expansion. Returns file locations plus connected files via import/call relationships. Use Read tool to view code.`,
190
190
  inputSchema: {
191
191
  type: "object",
192
192
  properties: {
@@ -216,13 +216,14 @@ export function createSearchTools(projectName) {
216
216
  collection: `${ctx.collectionPrefix}codebase`,
217
217
  query,
218
218
  limit,
219
+ mode: "navigate",
219
220
  filters: { language, path, layer, service },
220
221
  });
221
222
  const results = response.data.results;
222
223
  if (!results || results.length === 0) {
223
224
  return "No results found for this query.";
224
225
  }
225
- return formatCodeResults(results, 500);
226
+ return formatNavigationResults(results);
226
227
  },
227
228
  search_similar: async (args, ctx) => {
228
229
  const { code, limit = 5 } = args;
@@ -246,23 +247,15 @@ export function createSearchTools(projectName) {
246
247
  query,
247
248
  groupBy,
248
249
  limit,
250
+ mode: "navigate",
249
251
  filters: hasFilters ? filters : undefined,
250
252
  });
251
253
  const groups = response.data.groups;
252
254
  if (!groups || groups.length === 0) {
253
255
  return "No results found.";
254
256
  }
255
- return groups
256
- .map((g) => {
257
- const r = g.results[0];
258
- return (`**${g[groupBy]}** (score: ${pct(r.score)})\n` +
259
- "```" +
260
- (r.language || "") +
261
- "\n" +
262
- truncate(r.content, 300) +
263
- "\n```");
264
- })
265
- .join("\n\n---\n\n");
257
+ const allResults = groups.flatMap((g) => g.results);
258
+ return formatNavigationResults(allResults);
266
259
  },
267
260
  hybrid_search: async (args, ctx) => {
268
261
  const { query, limit = 10, semanticWeight = 0.7, language, layer, service } = args;
@@ -273,20 +266,14 @@ export function createSearchTools(projectName) {
273
266
  query,
274
267
  limit,
275
268
  semanticWeight,
269
+ mode: "navigate",
276
270
  filters: hasFilters ? filters : undefined,
277
271
  });
278
272
  const results = response.data.results;
279
273
  if (!results || results.length === 0) {
280
274
  return "No results found.";
281
275
  }
282
- return results
283
- .map((r) => `**${r.file}** (combined: ${pct(r.score)}${r.semanticScore != null ? `, semantic: ${pct(r.semanticScore)}` : ''}${r.keywordScore != null ? `, keyword: ${pct(r.keywordScore)}` : ''})\n` +
284
- "```" +
285
- (r.language || "") +
286
- "\n" +
287
- truncate(r.content, 300) +
288
- "\n```")
289
- .join("\n\n---\n\n");
276
+ return formatNavigationResults(results);
290
277
  },
291
278
  search_docs: async (args, ctx) => {
292
279
  const { query, limit = 5 } = args;
@@ -329,27 +316,20 @@ export function createSearchTools(projectName) {
329
316
  query,
330
317
  limit,
331
318
  expandHops,
319
+ mode: "navigate",
332
320
  });
333
321
  const { results, graphExpanded, expandedFiles } = response.data;
322
+ if ((!results || results.length === 0) && (!graphExpanded || graphExpanded.length === 0)) {
323
+ return "No results found.";
324
+ }
334
325
  let output = "";
335
326
  if (results && results.length > 0) {
336
327
  output += "**Direct matches:**\n\n";
337
- output += results
338
- .map((r) => `**${r.file}** (score: ${pct(r.score)})\n` +
339
- "```" + (r.language || "") + "\n" +
340
- truncate(r.content, 300) + "\n```")
341
- .join("\n\n");
328
+ output += formatNavigationResults(results);
342
329
  }
343
330
  if (graphExpanded && graphExpanded.length > 0) {
344
331
  output += "\n\n---\n\n**Graph-connected files:**\n\n";
345
- output += graphExpanded
346
- .map((r) => `**${r.file}** (score: ${pct(r.score)})\n` +
347
- "```" + (r.language || "") + "\n" +
348
- truncate(r.content, 300) + "\n```")
349
- .join("\n\n");
350
- }
351
- if (!output) {
352
- return "No results found.";
332
+ output += formatNavigationResults(graphExpanded);
353
333
  }
354
334
  if (expandedFiles && expandedFiles.length > 0) {
355
335
  output += `\n\n_Graph expanded to ${expandedFiles.length} additional files._`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowley/rag-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Universal RAG MCP Server for any project",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,8 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
- "axios": "^1.6.0"
34
+ "axios": "^1.6.0",
35
+ "glob": "^11.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "^20.10.0",