@crowley/rag-mcp 1.0.4 → 1.0.5

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,9 +26,9 @@ export declare class ContextEnricher {
26
26
  */
27
27
  before(name: string, args: Record<string, unknown>, ctx: ToolContext): Promise<string | null>;
28
28
  /**
29
- * After hook: fire-and-forget session activity tracking.
29
+ * After hook: fire-and-forget session activity tracking + implicit feedback.
30
30
  */
31
- after(name: string, _args: Record<string, unknown>, _result: string, ctx: ToolContext): void;
31
+ after(name: string, args: Record<string, unknown>, result: string, ctx: ToolContext): void;
32
32
  /**
33
33
  * Extract a semantic query string from tool arguments.
34
34
  */
@@ -18,6 +18,7 @@ export const DEFAULT_ENRICHABLE_TOOLS = new Set([
18
18
  "suggest_implementation",
19
19
  "suggest_related_code",
20
20
  "check_architecture",
21
+ "context_briefing",
21
22
  "run_agent",
22
23
  ]);
23
24
  export const DEFAULT_SKIP_TOOLS = new Set([
@@ -79,21 +80,36 @@ export class ContextEnricher {
79
80
  }
80
81
  }
81
82
  /**
82
- * After hook: fire-and-forget session activity tracking.
83
+ * After hook: fire-and-forget session activity tracking + implicit feedback.
83
84
  */
84
- after(name, _args, _result, ctx) {
85
- if (!ctx.activeSessionId)
86
- return;
87
- // Fire-and-forget: track tool usage in session
88
- ctx.api
89
- .post(`/api/session/${ctx.activeSessionId}/activity`, {
90
- projectName: ctx.projectName,
91
- type: "tool",
92
- value: name,
93
- })
94
- .catch(() => {
95
- // Silently ignore tracking errors
96
- });
85
+ after(name, args, result, ctx) {
86
+ // Session activity tracking
87
+ if (ctx.activeSessionId) {
88
+ ctx.api
89
+ .post(`/api/session/${ctx.activeSessionId}/activity`, {
90
+ projectName: ctx.projectName,
91
+ type: "tool",
92
+ value: name,
93
+ })
94
+ .catch(() => { });
95
+ }
96
+ // Implicit positive feedback for enrichable search tools
97
+ if (this.config.enrichableTools.has(name)) {
98
+ const query = this.extractQuery(args);
99
+ if (query &&
100
+ result &&
101
+ !result.includes("No results") &&
102
+ !result.includes("not found") &&
103
+ !result.includes("No relevant context found")) {
104
+ ctx.api
105
+ .post("/api/feedback/search", {
106
+ projectName: ctx.projectName,
107
+ query,
108
+ feedbackType: "helpful",
109
+ })
110
+ .catch(() => { });
111
+ }
112
+ }
97
113
  }
98
114
  /**
99
115
  * Extract a semantic query string from tool arguments.
package/dist/index.js CHANGED
@@ -99,6 +99,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
99
99
  content: [{ type: "text", text: result }],
100
100
  };
101
101
  });
102
+ // Graceful shutdown: close active session on exit
103
+ async function cleanup() {
104
+ if (ctx.activeSessionId) {
105
+ try {
106
+ await api.post(`/api/session/${ctx.activeSessionId}/end`, {
107
+ projectName: PROJECT_NAME,
108
+ summary: "Session ended by MCP server shutdown",
109
+ autoSaveLearnings: true,
110
+ });
111
+ }
112
+ catch {
113
+ // Best-effort, don't block shutdown
114
+ }
115
+ }
116
+ process.exit(0);
117
+ }
118
+ process.on("SIGINT", cleanup);
119
+ process.on("SIGTERM", cleanup);
102
120
  // Start server
103
121
  async function main() {
104
122
  const transport = new StdioServerTransport();
@@ -291,6 +291,7 @@ ${alternatives ? `## Alternatives Considered\n${alternatives}` : ""}`;
291
291
  query: query || "architecture decision ADR",
292
292
  type: "decision",
293
293
  limit,
294
+ tag: "adr",
294
295
  });
295
296
  const results = response.data.results || [];
296
297
  const adrs = results.filter((r) => r.memory.tags?.includes("adr") &&
@@ -349,6 +350,7 @@ ${appliesTo ? `## Applies To\n${appliesTo}` : ""}`;
349
350
  query: query || "architectural pattern structure",
350
351
  type: "context",
351
352
  limit,
353
+ tag: "pattern",
352
354
  });
353
355
  const results = response.data.results || [];
354
356
  const patterns = results.filter((r) => {
@@ -383,6 +385,7 @@ ${appliesTo ? `## Applies To\n${appliesTo}` : ""}`;
383
385
  query: patternQuery,
384
386
  type: "context",
385
387
  limit: 5,
388
+ tag: "pattern",
386
389
  });
387
390
  // Get relevant ADRs
388
391
  const adrsResponse = await ctx.api.post("/api/memory/recall", {
@@ -390,6 +393,7 @@ ${appliesTo ? `## Applies To\n${appliesTo}` : ""}`;
390
393
  query: patternQuery,
391
394
  type: "decision",
392
395
  limit: 5,
396
+ tag: "adr",
393
397
  });
394
398
  // Search similar code in codebase
395
399
  let similarCode = [];
@@ -499,6 +503,7 @@ Provide a structured analysis:
499
503
  query: `${type} ${feature} pattern structure`,
500
504
  type: "context",
501
505
  limit: 5,
506
+ tag: "pattern",
502
507
  });
503
508
  // Get relevant ADRs
504
509
  const adrsResponse = await ctx.api.post("/api/memory/recall", {
@@ -506,6 +511,7 @@ Provide a structured analysis:
506
511
  query: `${type} ${feature}`,
507
512
  type: "decision",
508
513
  limit: 3,
514
+ tag: "adr",
509
515
  });
510
516
  // Get similar implementations
511
517
  const codeResponse = await ctx.api.post("/api/search", {
@@ -602,6 +608,7 @@ ${relatedAdr ? `## Related ADR\n${relatedAdr}` : ""}`;
602
608
  query: "technical debt violation issue",
603
609
  type: "insight",
604
610
  limit: limit * 2, // Fetch extra to account for filtering
611
+ tag: "tech-debt",
605
612
  });
606
613
  const results = response.data.results || [];
607
614
  const debts = results
@@ -649,6 +656,7 @@ ${relatedAdr ? `## Related ADR\n${relatedAdr}` : ""}`;
649
656
  query: "pattern structure organization",
650
657
  type: "context",
651
658
  limit: 10,
659
+ tag: "pattern",
652
660
  });
653
661
  // Get codebase structure
654
662
  const codeResponse = await ctx.api.post("/api/search", {
@@ -100,6 +100,9 @@ async function uploadFiles(ctx, projectPath, opts) {
100
100
  duration: totalDuration,
101
101
  };
102
102
  }
103
+ // In-memory cache for get_index_status (30 min TTL)
104
+ let _statusCache = null;
105
+ const STATUS_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
103
106
  /**
104
107
  * Create the indexing tools module with project-specific descriptions.
105
108
  */
@@ -125,7 +128,7 @@ export function createIndexingTools(projectName) {
125
128
  },
126
129
  {
127
130
  name: "get_index_status",
128
- description: `Get the indexing status for ${projectName} codebase.`,
131
+ description: `Get the indexing status for ${projectName} codebase. Results cached for 30 minutes.`,
129
132
  inputSchema: {
130
133
  type: "object",
131
134
  properties: {},
@@ -168,6 +171,7 @@ export function createIndexingTools(projectName) {
168
171
  const { path: indexPath, force = false } = args;
169
172
  const projectPath = indexPath || ctx.projectPath;
170
173
  const stats = await uploadFiles(ctx, projectPath, { force });
174
+ _statusCache = null; // Invalidate status cache after indexing
171
175
  let result = `## Indexing ${projectName}\n\n`;
172
176
  result += `- **Total files found:** ${stats.totalFiles}\n`;
173
177
  result += `- **Files indexed:** ${stats.indexedFiles}\n`;
@@ -177,6 +181,11 @@ export function createIndexingTools(projectName) {
177
181
  return result;
178
182
  },
179
183
  get_index_status: async (_args, ctx) => {
184
+ // Return cached result if still valid
185
+ if (_statusCache && Date.now() < _statusCache.expiresAt) {
186
+ const remainingMin = Math.round((_statusCache.expiresAt - Date.now()) / 60000);
187
+ return _statusCache.data + `\n_Cached (expires in ${remainingMin}min)_`;
188
+ }
180
189
  const response = await ctx.api.get(`/api/index/status/${ctx.collectionPrefix}codebase`);
181
190
  const data = response.data;
182
191
  let result = `## Index Status: ${projectName}\n\n`;
@@ -185,6 +194,8 @@ export function createIndexingTools(projectName) {
185
194
  result += `- **Indexed Files:** ${data.indexedFiles ?? "N/A"}\n`;
186
195
  result += `- **Last Updated:** ${data.lastUpdated ? new Date(data.lastUpdated).toLocaleString() : "Never"}\n`;
187
196
  result += `- **Vector Count:** ${data.vectorCount ?? "N/A"}\n`;
197
+ // Cache for 30 minutes
198
+ _statusCache = { data: result, expiresAt: Date.now() + STATUS_CACHE_TTL };
188
199
  return result;
189
200
  },
190
201
  reindex_zero_downtime: async (args, ctx) => {
@@ -195,6 +206,7 @@ export function createIndexingTools(projectName) {
195
206
  excludePatterns,
196
207
  force: true,
197
208
  });
209
+ _statusCache = null; // Invalidate status cache after reindex
198
210
  let result = `## Reindex: ${projectName}\n\n`;
199
211
  result += `- **Total files found:** ${stats.totalFiles}\n`;
200
212
  result += `- **Files indexed:** ${stats.indexedFiles}\n`;
@@ -2,12 +2,33 @@
2
2
  * Suggestions tools module - contextual suggestions, related code,
3
3
  * implementation suggestions, test suggestions, and code context.
4
4
  */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
5
7
  import { truncate, pct, PREVIEW } from "../formatters.js";
6
8
  /**
7
9
  * Create the suggestions tools module with project-specific descriptions.
8
10
  */
9
11
  export function createSuggestionTools(projectName) {
10
12
  const tools = [
13
+ {
14
+ name: "context_briefing",
15
+ description: `REQUIRED before code changes. Parallel lookup of recall + search + patterns + ADRs + graph for ${projectName}. One call replaces 5 separate RAG lookups.`,
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ task: {
20
+ type: "string",
21
+ description: "What you will implement/change",
22
+ },
23
+ files: {
24
+ type: "array",
25
+ items: { type: "string" },
26
+ description: "Files you plan to modify",
27
+ },
28
+ },
29
+ required: ["task"],
30
+ },
31
+ },
11
32
  {
12
33
  name: "get_contextual_suggestions",
13
34
  description: `Get contextual suggestions based on current work context for ${projectName}. Returns relevant suggestions, triggers, and related memories.`,
@@ -116,8 +137,153 @@ export function createSuggestionTools(projectName) {
116
137
  },
117
138
  },
118
139
  },
140
+ {
141
+ name: "setup_project",
142
+ description: "Configure Claude Code for RAG integration. Creates/updates .mcp.json, adds RAG instructions to CLAUDE.md, and configures permissions. Call after index_codebase on a new project.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ projectPath: {
147
+ type: "string",
148
+ description: "Absolute path to project root",
149
+ },
150
+ projectName: {
151
+ type: "string",
152
+ description: "Project name in Qdrant (collection prefix)",
153
+ },
154
+ ragApiUrl: {
155
+ type: "string",
156
+ description: "RAG API URL (default: from MCP env)",
157
+ },
158
+ ragApiKey: {
159
+ type: "string",
160
+ description: "RAG API key (default: from MCP env)",
161
+ },
162
+ updateClaudeMd: {
163
+ type: "boolean",
164
+ description: "Add RAG section to CLAUDE.md (default: true)",
165
+ },
166
+ },
167
+ required: ["projectPath", "projectName"],
168
+ },
169
+ },
119
170
  ];
120
171
  const handlers = {
172
+ context_briefing: async (args, ctx) => {
173
+ const { task, files } = args;
174
+ // 5 parallel lookups
175
+ const [memoriesRes, searchRes, patternsRes, adrsRes, graphRes] = await Promise.all([
176
+ // 1. Recall relevant memories
177
+ ctx.api
178
+ .post("/api/memory/recall", {
179
+ projectName: ctx.projectName,
180
+ query: task,
181
+ limit: 5,
182
+ type: "all",
183
+ })
184
+ .catch(() => null),
185
+ // 2. Hybrid search for related code
186
+ ctx.api
187
+ .post("/api/search-hybrid", {
188
+ projectName: ctx.projectName,
189
+ query: task,
190
+ limit: 5,
191
+ mode: "navigate",
192
+ })
193
+ .catch(() => null),
194
+ // 3. Architectural patterns
195
+ ctx.api
196
+ .post("/api/memory/recall", {
197
+ projectName: ctx.projectName,
198
+ query: task,
199
+ type: "context",
200
+ limit: 5,
201
+ tag: "pattern",
202
+ })
203
+ .catch(() => null),
204
+ // 4. ADRs
205
+ ctx.api
206
+ .post("/api/memory/recall", {
207
+ projectName: ctx.projectName,
208
+ query: task,
209
+ type: "decision",
210
+ limit: 3,
211
+ tag: "adr",
212
+ })
213
+ .catch(() => null),
214
+ // 5. Graph dependencies (if files specified)
215
+ files && files.length > 0
216
+ ? ctx.api
217
+ .post("/api/search-graph", {
218
+ projectName: ctx.projectName,
219
+ query: files[0],
220
+ expandHops: 1,
221
+ limit: 5,
222
+ })
223
+ .catch(() => null)
224
+ : Promise.resolve(null),
225
+ ]);
226
+ let result = `# Context Briefing: ${task}\n\n`;
227
+ // Memories
228
+ const memories = memoriesRes?.data?.results || memoriesRes?.data?.memories || [];
229
+ if (memories.length > 0) {
230
+ result += `## Memories (${memories.length})\n`;
231
+ for (const m of memories) {
232
+ const mem = m.memory || m;
233
+ result += `- [${mem.type || "note"}] ${truncate(mem.content || "", 150)}\n`;
234
+ }
235
+ result += "\n";
236
+ }
237
+ // Related code
238
+ const codeResults = searchRes?.data?.results || [];
239
+ if (codeResults.length > 0) {
240
+ result += `## Related Code (${codeResults.length})\n`;
241
+ for (const r of codeResults) {
242
+ result += `- \`${r.file}\``;
243
+ if (r.symbols?.length)
244
+ result += ` — ${r.symbols.join(", ")}`;
245
+ result += "\n";
246
+ }
247
+ result += "\n";
248
+ }
249
+ // Patterns
250
+ const patterns = (patternsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("pattern"));
251
+ if (patterns.length > 0) {
252
+ result += `## Patterns (${patterns.length})\n`;
253
+ for (const p of patterns) {
254
+ const name = p.memory?.metadata?.patternName || p.memory?.relatedTo || "Pattern";
255
+ result += `- **${name}**: ${truncate(p.memory?.content || "", 120)}\n`;
256
+ }
257
+ result += "\n";
258
+ }
259
+ // ADRs
260
+ const adrs = (adrsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("adr"));
261
+ if (adrs.length > 0) {
262
+ result += `## ADRs (${adrs.length})\n`;
263
+ for (const a of adrs) {
264
+ const title = a.memory?.metadata?.adrTitle || a.memory?.relatedTo || "ADR";
265
+ result += `- **${title}**: ${truncate(a.memory?.content || "", 120)}\n`;
266
+ }
267
+ result += "\n";
268
+ }
269
+ // Graph dependencies
270
+ const graphResults = graphRes?.data?.results || graphRes?.data?.directResults || [];
271
+ const connectedFiles = graphRes?.data?.connectedFiles || graphRes?.data?.expandedResults || [];
272
+ if (graphResults.length > 0 || connectedFiles.length > 0) {
273
+ result += `## Dependencies\n`;
274
+ for (const g of graphResults) {
275
+ result += `- \`${g.file}\`\n`;
276
+ }
277
+ for (const c of connectedFiles) {
278
+ result += `- \`${c.file}\` (connected)\n`;
279
+ }
280
+ result += "\n";
281
+ }
282
+ if (result.endsWith(`# Context Briefing: ${task}\n\n`)) {
283
+ result += "_No relevant context found. Proceed with implementation._\n";
284
+ }
285
+ return result;
286
+ },
121
287
  get_contextual_suggestions: async (args, ctx) => {
122
288
  const { currentFile, currentCode, recentFiles, task } = args;
123
289
  const response = await ctx.api.post("/api/suggestions", {
@@ -296,6 +462,116 @@ export function createSuggestionTools(projectName) {
296
462
  }
297
463
  return result;
298
464
  },
465
+ setup_project: async (args, ctx) => {
466
+ const { projectPath, projectName: targetProject, ragApiUrl, ragApiKey, updateClaudeMd = true, } = args;
467
+ const apiUrl = ragApiUrl || process.env.RAG_API_URL || "http://localhost:3100";
468
+ const apiKey = ragApiKey || process.env.RAG_API_KEY;
469
+ const serverName = `${targetProject}-rag`;
470
+ const changes = [];
471
+ // 1. Create/update .mcp.json
472
+ const mcpJsonPath = path.join(projectPath, ".mcp.json");
473
+ let mcpConfig = {};
474
+ try {
475
+ const existing = fs.readFileSync(mcpJsonPath, "utf-8");
476
+ mcpConfig = JSON.parse(existing);
477
+ }
478
+ catch {
479
+ // File doesn't exist or invalid JSON
480
+ }
481
+ if (!mcpConfig.mcpServers)
482
+ mcpConfig.mcpServers = {};
483
+ const serverEnv = {
484
+ RAG_API_URL: apiUrl,
485
+ PROJECT_NAME: targetProject,
486
+ PROJECT_PATH: projectPath,
487
+ };
488
+ if (apiKey)
489
+ serverEnv.RAG_API_KEY = apiKey;
490
+ mcpConfig.mcpServers[serverName] = {
491
+ command: "npx",
492
+ args: ["-y", "@crowley/rag-mcp@latest"],
493
+ env: serverEnv,
494
+ };
495
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
496
+ changes.push(`.mcp.json — added \`${serverName}\` server`);
497
+ // 2. Update CLAUDE.md with RAG section
498
+ if (updateClaudeMd) {
499
+ const claudeMdPath = path.join(projectPath, "CLAUDE.md");
500
+ let claudeMd = "";
501
+ try {
502
+ claudeMd = fs.readFileSync(claudeMdPath, "utf-8");
503
+ }
504
+ catch {
505
+ // File doesn't exist
506
+ }
507
+ const ragSection = `\n## RAG Integration
508
+
509
+ You MUST call \`context_briefing\` before making any code changes.
510
+ This single tool performs all RAG lookups in parallel (recall, search, patterns, ADRs, graph).
511
+
512
+ Example: \`context_briefing(task: "describe your change", files: ["path/to/file.ts"])\`
513
+
514
+ After completing significant changes:
515
+ - \`remember\` — save important context for future sessions
516
+ - \`record_adr\` — document architectural decisions
517
+ `;
518
+ if (claudeMd.includes("## RAG")) {
519
+ changes.push("CLAUDE.md — RAG section already exists, skipped");
520
+ }
521
+ else {
522
+ claudeMd = claudeMd ? claudeMd.trimEnd() + "\n" + ragSection : `# CLAUDE.md\n${ragSection}`;
523
+ fs.writeFileSync(claudeMdPath, claudeMd);
524
+ changes.push("CLAUDE.md — added RAG Integration section");
525
+ }
526
+ }
527
+ // 3. Create/update .claude/settings.local.json permissions
528
+ const claudeDir = path.join(projectPath, ".claude");
529
+ const settingsPath = path.join(claudeDir, "settings.local.json");
530
+ let settings = {};
531
+ try {
532
+ const existing = fs.readFileSync(settingsPath, "utf-8");
533
+ settings = JSON.parse(existing);
534
+ }
535
+ catch {
536
+ // File doesn't exist or invalid JSON
537
+ }
538
+ if (!settings.permissions)
539
+ settings.permissions = {};
540
+ if (!settings.permissions.allow)
541
+ settings.permissions.allow = [];
542
+ const mcpPermission = `mcp__${serverName}__*`;
543
+ if (!settings.permissions.allow.includes(mcpPermission)) {
544
+ settings.permissions.allow.push(mcpPermission);
545
+ if (!fs.existsSync(claudeDir))
546
+ fs.mkdirSync(claudeDir, { recursive: true });
547
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
548
+ changes.push(`.claude/settings.local.json — added \`${mcpPermission}\` permission`);
549
+ }
550
+ else {
551
+ changes.push(".claude/settings.local.json — permission already exists, skipped");
552
+ }
553
+ // 4. Check index status
554
+ let indexInfo = "";
555
+ try {
556
+ const statusRes = await ctx.api.get(`/api/index/status/${targetProject}_codebase`);
557
+ const data = statusRes.data;
558
+ indexInfo = `\n## Index Status\n- **Vectors:** ${data.vectorCount ?? "N/A"}\n- **Status:** ${data.status || "unknown"}\n`;
559
+ }
560
+ catch {
561
+ indexInfo = "\n## Index Status\n_Not indexed yet. Run `index_codebase` first._\n";
562
+ }
563
+ let result = `# Project Setup: ${targetProject}\n\n`;
564
+ result += `## Files Updated\n`;
565
+ for (const c of changes) {
566
+ result += `- ${c}\n`;
567
+ }
568
+ result += indexInfo;
569
+ result += `\n## Next Steps\n`;
570
+ result += `1. Restart Claude Code to load the new MCP server\n`;
571
+ result += `2. Run \`index_codebase\` if not indexed yet\n`;
572
+ result += `3. Use \`context_briefing\` before code changes\n`;
573
+ return result;
574
+ },
299
575
  };
300
576
  return { tools, handlers };
301
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowley/rag-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Universal RAG MCP Server for any project",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",