@crowley/rag-mcp 1.0.5 → 1.1.0

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.
Files changed (51) hide show
  1. package/dist/annotations.d.ts +16 -0
  2. package/dist/annotations.js +158 -0
  3. package/dist/context-enrichment.js +7 -0
  4. package/dist/formatters.d.ts +2 -0
  5. package/dist/formatters.js +12 -0
  6. package/dist/index.js +99 -47
  7. package/dist/schemas.d.ts +97 -0
  8. package/dist/schemas.js +128 -0
  9. package/dist/tool-middleware.d.ts +40 -0
  10. package/dist/tool-middleware.js +216 -0
  11. package/dist/tool-registry.js +2 -1
  12. package/dist/tools/advanced.d.ts +2 -2
  13. package/dist/tools/advanced.js +200 -275
  14. package/dist/tools/agents.d.ts +2 -2
  15. package/dist/tools/agents.js +59 -78
  16. package/dist/tools/analytics.d.ts +2 -2
  17. package/dist/tools/analytics.js +170 -210
  18. package/dist/tools/architecture.d.ts +2 -2
  19. package/dist/tools/architecture.js +506 -669
  20. package/dist/tools/ask.d.ts +2 -2
  21. package/dist/tools/ask.js +164 -219
  22. package/dist/tools/cache.d.ts +2 -2
  23. package/dist/tools/cache.js +63 -82
  24. package/dist/tools/clustering.d.ts +2 -2
  25. package/dist/tools/clustering.js +154 -215
  26. package/dist/tools/confluence.d.ts +2 -2
  27. package/dist/tools/confluence.js +80 -116
  28. package/dist/tools/database.d.ts +2 -2
  29. package/dist/tools/database.js +303 -380
  30. package/dist/tools/feedback.d.ts +2 -2
  31. package/dist/tools/feedback.js +143 -184
  32. package/dist/tools/guidelines.d.ts +2 -2
  33. package/dist/tools/guidelines.js +123 -135
  34. package/dist/tools/indexing.d.ts +2 -2
  35. package/dist/tools/indexing.js +100 -108
  36. package/dist/tools/memory.d.ts +2 -2
  37. package/dist/tools/memory.js +299 -485
  38. package/dist/tools/pm.d.ts +2 -2
  39. package/dist/tools/pm.js +367 -615
  40. package/dist/tools/quality.d.ts +8 -0
  41. package/dist/tools/quality.js +60 -0
  42. package/dist/tools/review.d.ts +2 -2
  43. package/dist/tools/review.js +142 -189
  44. package/dist/tools/search.d.ts +2 -2
  45. package/dist/tools/search.js +230 -305
  46. package/dist/tools/session.d.ts +2 -2
  47. package/dist/tools/session.js +288 -345
  48. package/dist/tools/suggestions.d.ts +2 -2
  49. package/dist/tools/suggestions.js +517 -512
  50. package/dist/types.d.ts +19 -2
  51. package/package.json +4 -2
@@ -5,506 +5,511 @@
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { truncate, pct, PREVIEW } from "../formatters.js";
8
+ import { z } from "zod";
9
+ import { TOOL_ANNOTATIONS } from "../annotations.js";
10
+ /**
11
+ * Format smart dispatch result into readable markdown.
12
+ */
13
+ function formatSmartDispatchResult(task, data) {
14
+ let result = `# Context Briefing: ${task}\n`;
15
+ result += `_Routing: ${data.reasoning} (${data.plan?.join(", ")}) [${data.timing?.totalMs}ms]_\n\n`;
16
+ const ctx = data.context || {};
17
+ if (ctx.memories?.length > 0) {
18
+ result += `## Memories (${ctx.memories.length})\n`;
19
+ for (const m of ctx.memories) {
20
+ const mem = m.memory || m;
21
+ result += `- [${mem.type || "note"}] ${(mem.content || "").slice(0, 150)}\n`;
22
+ }
23
+ result += "\n";
24
+ }
25
+ if (ctx.codeResults?.length > 0) {
26
+ result += `## Related Code (${ctx.codeResults.length})\n`;
27
+ for (const r of ctx.codeResults) {
28
+ result += `- \`${r.file}\``;
29
+ if (r.symbols?.length)
30
+ result += ` — ${r.symbols.join(", ")}`;
31
+ result += "\n";
32
+ }
33
+ result += "\n";
34
+ }
35
+ if (ctx.patterns?.length > 0) {
36
+ result += `## Patterns (${ctx.patterns.length})\n`;
37
+ for (const p of ctx.patterns) {
38
+ const mem = p.memory || p;
39
+ const name = mem.metadata?.patternName || mem.relatedTo || "Pattern";
40
+ result += `- **${name}**: ${(mem.content || "").slice(0, 120)}\n`;
41
+ }
42
+ result += "\n";
43
+ }
44
+ if (ctx.adrs?.length > 0) {
45
+ result += `## ADRs (${ctx.adrs.length})\n`;
46
+ for (const a of ctx.adrs) {
47
+ const mem = a.memory || a;
48
+ const title = mem.metadata?.adrTitle || mem.relatedTo || "ADR";
49
+ result += `- **${title}**: ${(mem.content || "").slice(0, 120)}\n`;
50
+ }
51
+ result += "\n";
52
+ }
53
+ if (ctx.graphDeps?.length > 0) {
54
+ result += `## Dependencies (${ctx.graphDeps.length})\n`;
55
+ for (const g of ctx.graphDeps) {
56
+ result += `- \`${g.file}\`\n`;
57
+ }
58
+ result += "\n";
59
+ }
60
+ if (ctx.docs?.length > 0) {
61
+ result += `## Docs (${ctx.docs.length})\n`;
62
+ for (const d of ctx.docs) {
63
+ result += `- \`${d.file}\`: ${(d.content || "").slice(0, 100)}\n`;
64
+ }
65
+ result += "\n";
66
+ }
67
+ if (ctx.symbols?.length > 0) {
68
+ result += `## Symbols (${ctx.symbols.length})\n`;
69
+ for (const s of ctx.symbols) {
70
+ result += `- \`${s.name || s.symbol}\` [${s.kind || "unknown"}] in \`${s.file || "?"}\`\n`;
71
+ }
72
+ result += "\n";
73
+ }
74
+ if (result.endsWith(`_Routing: ${data.reasoning} (${data.plan?.join(", ")}) [${data.timing?.totalMs}ms]_\n\n`)) {
75
+ result += "_No relevant context found. Proceed with implementation._\n";
76
+ }
77
+ return result;
78
+ }
8
79
  /**
9
80
  * Create the suggestions tools module with project-specific descriptions.
10
81
  */
11
82
  export function createSuggestionTools(projectName) {
12
- const tools = [
83
+ return [
13
84
  {
14
85
  name: "context_briefing",
15
86
  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"],
87
+ schema: z.object({
88
+ task: z.string().describe("What you will implement/change"),
89
+ files: z.array(z.string()).optional().describe("Files you plan to modify"),
90
+ }),
91
+ annotations: TOOL_ANNOTATIONS["context_briefing"],
92
+ handler: async (args, ctx) => {
93
+ const { task, files } = args;
94
+ // Use smart_dispatch for intelligent routing
95
+ try {
96
+ const dispatchRes = await ctx.api.post("/api/smart-dispatch", {
97
+ projectName: ctx.projectName,
98
+ task,
99
+ files,
100
+ });
101
+ const data = dispatchRes.data;
102
+ return formatSmartDispatchResult(task, data);
103
+ }
104
+ catch {
105
+ // Fallback to legacy 5-parallel-lookups if smart-dispatch unavailable
106
+ }
107
+ // Legacy fallback: 5 parallel lookups
108
+ const [memoriesRes, searchRes, patternsRes, adrsRes, graphRes] = await Promise.all([
109
+ ctx.api
110
+ .post("/api/memory/recall", {
111
+ projectName: ctx.projectName,
112
+ query: task,
113
+ limit: 5,
114
+ type: "all",
115
+ })
116
+ .catch(() => null),
117
+ ctx.api
118
+ .post("/api/search-hybrid", {
119
+ projectName: ctx.projectName,
120
+ query: task,
121
+ limit: 5,
122
+ mode: "navigate",
123
+ })
124
+ .catch(() => null),
125
+ ctx.api
126
+ .post("/api/memory/recall", {
127
+ projectName: ctx.projectName,
128
+ query: task,
129
+ type: "context",
130
+ limit: 5,
131
+ tag: "pattern",
132
+ })
133
+ .catch(() => null),
134
+ ctx.api
135
+ .post("/api/memory/recall", {
136
+ projectName: ctx.projectName,
137
+ query: task,
138
+ type: "decision",
139
+ limit: 3,
140
+ tag: "adr",
141
+ })
142
+ .catch(() => null),
143
+ files && files.length > 0
144
+ ? ctx.api
145
+ .post("/api/search-graph", {
146
+ projectName: ctx.projectName,
147
+ query: files[0],
148
+ expandHops: 1,
149
+ limit: 5,
150
+ })
151
+ .catch(() => null)
152
+ : Promise.resolve(null),
153
+ ]);
154
+ let result = `# Context Briefing: ${task}\n\n`;
155
+ const memories = memoriesRes?.data?.results || memoriesRes?.data?.memories || [];
156
+ if (memories.length > 0) {
157
+ result += `## Memories (${memories.length})\n`;
158
+ for (const m of memories) {
159
+ const mem = m.memory || m;
160
+ result += `- [${mem.type || "note"}] ${truncate(mem.content || "", 150)}\n`;
161
+ }
162
+ result += "\n";
163
+ }
164
+ const codeResults = searchRes?.data?.results || [];
165
+ if (codeResults.length > 0) {
166
+ result += `## Related Code (${codeResults.length})\n`;
167
+ for (const r of codeResults) {
168
+ result += `- \`${r.file}\``;
169
+ if (r.symbols?.length)
170
+ result += ` — ${r.symbols.join(", ")}`;
171
+ result += "\n";
172
+ }
173
+ result += "\n";
174
+ }
175
+ const patterns = (patternsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("pattern"));
176
+ if (patterns.length > 0) {
177
+ result += `## Patterns (${patterns.length})\n`;
178
+ for (const p of patterns) {
179
+ const name = p.memory?.metadata?.patternName || p.memory?.relatedTo || "Pattern";
180
+ result += `- **${name}**: ${truncate(p.memory?.content || "", 120)}\n`;
181
+ }
182
+ result += "\n";
183
+ }
184
+ const adrs = (adrsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("adr"));
185
+ if (adrs.length > 0) {
186
+ result += `## ADRs (${adrs.length})\n`;
187
+ for (const a of adrs) {
188
+ const title = a.memory?.metadata?.adrTitle || a.memory?.relatedTo || "ADR";
189
+ result += `- **${title}**: ${truncate(a.memory?.content || "", 120)}\n`;
190
+ }
191
+ result += "\n";
192
+ }
193
+ const graphResults = graphRes?.data?.results || graphRes?.data?.directResults || [];
194
+ const connectedFiles = graphRes?.data?.connectedFiles || graphRes?.data?.expandedResults || [];
195
+ if (graphResults.length > 0 || connectedFiles.length > 0) {
196
+ result += `## Dependencies\n`;
197
+ for (const g of graphResults) {
198
+ result += `- \`${g.file}\`\n`;
199
+ }
200
+ for (const c of connectedFiles) {
201
+ result += `- \`${c.file}\` (connected)\n`;
202
+ }
203
+ result += "\n";
204
+ }
205
+ if (result.endsWith(`# Context Briefing: ${task}\n\n`)) {
206
+ result += "_No relevant context found. Proceed with implementation._\n";
207
+ }
208
+ return result;
209
+ },
210
+ },
211
+ {
212
+ name: "smart_dispatch",
213
+ description: `Intelligent task routing for ${projectName}. LLM analyzes your task and runs only the needed lookups (2-5 of 7 available) in parallel. More efficient than context_briefing for narrow tasks.`,
214
+ schema: z.object({
215
+ task: z.string().describe("What you will implement/change"),
216
+ files: z.array(z.string()).optional().describe("Files you plan to modify"),
217
+ intent: z.enum(["code", "research", "debug", "review", "architecture"]).optional().describe("Task intent for better routing"),
218
+ }),
219
+ annotations: TOOL_ANNOTATIONS["context_briefing"], // Same annotations as context_briefing
220
+ handler: async (args, ctx) => {
221
+ const { task, files, intent } = args;
222
+ const response = await ctx.api.post("/api/smart-dispatch", {
223
+ projectName: ctx.projectName,
224
+ task,
225
+ files,
226
+ intent,
227
+ });
228
+ return formatSmartDispatchResult(task, response.data);
30
229
  },
31
230
  },
32
231
  {
33
232
  name: "get_contextual_suggestions",
34
233
  description: `Get contextual suggestions based on current work context for ${projectName}. Returns relevant suggestions, triggers, and related memories.`,
35
- inputSchema: {
36
- type: "object",
37
- properties: {
38
- currentFile: {
39
- type: "string",
40
- description: "Currently active file path",
41
- },
42
- currentCode: {
43
- type: "string",
44
- description: "Currently selected or visible code",
45
- },
46
- recentFiles: {
47
- type: "array",
48
- items: { type: "string" },
49
- description: "Recently opened file paths",
50
- },
51
- task: {
52
- type: "string",
53
- description: "Current task description",
54
- },
55
- },
234
+ schema: z.object({
235
+ currentFile: z.string().optional().describe("Currently active file path"),
236
+ currentCode: z.string().optional().describe("Currently selected or visible code"),
237
+ recentFiles: z.array(z.string()).optional().describe("Recently opened file paths"),
238
+ task: z.string().optional().describe("Current task description"),
239
+ }),
240
+ annotations: TOOL_ANNOTATIONS["get_contextual_suggestions"],
241
+ handler: async (args, ctx) => {
242
+ const { currentFile, currentCode, recentFiles, task } = args;
243
+ const response = await ctx.api.post("/api/suggestions", {
244
+ projectName: ctx.projectName,
245
+ currentFile,
246
+ currentCode,
247
+ recentFiles,
248
+ task,
249
+ });
250
+ const data = response.data;
251
+ let result = `## Contextual Suggestions\n\n`;
252
+ if (data.relevanceScore !== undefined) {
253
+ result += `**Relevance Score:** ${pct(data.relevanceScore)}\n\n`;
254
+ }
255
+ if (data.triggers && data.triggers.length > 0) {
256
+ result += `### Triggers\n`;
257
+ for (const t of data.triggers) {
258
+ result += `- **${t.type}:** ${t.value}`;
259
+ if (t.confidence)
260
+ result += ` (${pct(t.confidence)})`;
261
+ result += "\n";
262
+ }
263
+ result += "\n";
264
+ }
265
+ if (data.suggestions && data.suggestions.length > 0) {
266
+ result += `### Suggestions\n`;
267
+ for (const s of data.suggestions) {
268
+ result += `- **${s.title}** [${s.type}]\n`;
269
+ if (s.description)
270
+ result += ` ${s.description}\n`;
271
+ if (s.reason)
272
+ result += ` *Reason: ${s.reason}*\n`;
273
+ if (s.relevance !== undefined)
274
+ result += ` Relevance: ${pct(s.relevance)}\n`;
275
+ }
276
+ result += "\n";
277
+ }
278
+ if (data.relatedMemories && data.relatedMemories.length > 0) {
279
+ result += `### Related Memories\n`;
280
+ for (const m of data.relatedMemories) {
281
+ result += `- ${m.content || m.title || JSON.stringify(m)}\n`;
282
+ }
283
+ }
284
+ return result;
56
285
  },
57
286
  },
58
287
  {
59
288
  name: "suggest_related_code",
60
289
  description: `Find code related to a given file or snippet in ${projectName}. Shows similar implementations and related modules.`,
61
- inputSchema: {
62
- type: "object",
63
- properties: {
64
- file: {
65
- type: "string",
66
- description: "File path to find related code for",
67
- },
68
- code: {
69
- type: "string",
70
- description: "Code snippet to find related code for",
71
- },
72
- limit: {
73
- type: "number",
74
- description: "Max results (default: 5)",
75
- default: 5,
76
- },
77
- },
290
+ schema: z.object({
291
+ file: z.string().optional().describe("File path to find related code for"),
292
+ code: z.string().optional().describe("Code snippet to find related code for"),
293
+ limit: z.number().optional().describe("Max results (default: 5)"),
294
+ }),
295
+ annotations: TOOL_ANNOTATIONS["suggest_related_code"],
296
+ handler: async (args, ctx) => {
297
+ const { file, code, limit = 5 } = args;
298
+ const response = await ctx.api.post("/api/code/related", {
299
+ projectName: ctx.projectName,
300
+ file,
301
+ code,
302
+ limit,
303
+ });
304
+ const results = response.data.results || response.data;
305
+ if (!results || results.length === 0) {
306
+ return "No related code found.";
307
+ }
308
+ let result = `## Related Code\n\n`;
309
+ for (const r of results) {
310
+ result += `### ${r.file}\n`;
311
+ result += `**Score:** ${pct(r.score)}`;
312
+ if (r.reason)
313
+ result += ` | **Reason:** ${r.reason}`;
314
+ if (r.line)
315
+ result += ` | Line ${r.line}`;
316
+ result += "\n";
317
+ if (r.content || r.code) {
318
+ result += "```\n" + truncate(r.content || r.code, PREVIEW.MEDIUM) + "\n```\n";
319
+ }
320
+ result += "\n";
321
+ }
322
+ return result;
78
323
  },
79
324
  },
80
325
  {
81
326
  name: "suggest_implementation",
82
327
  description: `Get implementation suggestions for a feature in ${projectName}. Shows similar patterns and adaptation hints.`,
83
- inputSchema: {
84
- type: "object",
85
- properties: {
86
- description: {
87
- type: "string",
88
- description: "Description of what to implement",
89
- },
90
- currentFile: {
91
- type: "string",
92
- description: "Current file for context",
93
- },
94
- language: {
95
- type: "string",
96
- description: "Target programming language",
97
- },
98
- },
99
- required: ["description"],
328
+ schema: z.object({
329
+ description: z.string().describe("Description of what to implement"),
330
+ currentFile: z.string().optional().describe("Current file for context"),
331
+ language: z.string().optional().describe("Target programming language"),
332
+ }),
333
+ annotations: TOOL_ANNOTATIONS["suggest_implementation"],
334
+ handler: async (args, ctx) => {
335
+ const { description, currentFile, language } = args;
336
+ const response = await ctx.api.post("/api/code/suggest-implementation", {
337
+ projectName: ctx.projectName,
338
+ description,
339
+ currentFile,
340
+ language,
341
+ });
342
+ const data = response.data;
343
+ const patterns = data.patterns || data.results || [];
344
+ if (!patterns || patterns.length === 0) {
345
+ return "No implementation suggestions found.";
346
+ }
347
+ const patternIcons = {
348
+ similar_structure: "\ud83d\udcd0",
349
+ same_domain: "\ud83c\udfaf",
350
+ related_import: "\ud83d\udce6",
351
+ test_pattern: "\ud83e\uddea",
352
+ };
353
+ let result = `## Implementation Suggestions\n\n`;
354
+ for (const p of patterns) {
355
+ const icon = patternIcons[p.pattern || p.type] || "\ud83d\udcd0";
356
+ result += `### ${icon} ${p.file || p.name || "Pattern"}\n`;
357
+ if (p.adaptationHints || p.hints) {
358
+ result += `**Adaptation:** ${p.adaptationHints || p.hints}\n`;
359
+ }
360
+ if (p.content || p.code) {
361
+ result += "```\n" + truncate(p.content || p.code, 400) + "\n```\n";
362
+ }
363
+ result += "\n";
364
+ }
365
+ return result;
100
366
  },
101
367
  },
102
368
  {
103
369
  name: "suggest_tests",
104
370
  description: `Get test suggestions for code in ${projectName}. Shows recommended test types, frameworks, and example patterns.`,
105
- inputSchema: {
106
- type: "object",
107
- properties: {
108
- file: {
109
- type: "string",
110
- description: "File to suggest tests for",
111
- },
112
- code: {
113
- type: "string",
114
- description: "Code to suggest tests for",
115
- },
116
- framework: {
117
- type: "string",
118
- description: "Test framework preference (jest, mocha, pytest, etc.)",
119
- },
120
- },
371
+ schema: z.object({
372
+ file: z.string().optional().describe("File to suggest tests for"),
373
+ code: z.string().optional().describe("Code to suggest tests for"),
374
+ framework: z.string().optional().describe("Test framework preference (jest, mocha, pytest, etc.)"),
375
+ }),
376
+ annotations: TOOL_ANNOTATIONS["suggest_tests"],
377
+ handler: async (args, ctx) => {
378
+ const { file, code, framework } = args;
379
+ const response = await ctx.api.post("/api/code/suggest-tests", {
380
+ projectName: ctx.projectName,
381
+ file,
382
+ code,
383
+ framework,
384
+ });
385
+ const data = response.data;
386
+ const tests = data.tests || data.suggestions || data.results || [];
387
+ if (!tests || tests.length === 0) {
388
+ return "No test suggestions found.";
389
+ }
390
+ const typeIcons = {
391
+ unit: "\ud83d\udd2c",
392
+ integration: "\ud83d\udd17",
393
+ e2e: "\ud83c\udf10",
394
+ };
395
+ let result = `## Test Suggestions\n\n`;
396
+ for (const t of tests) {
397
+ const icon = typeIcons[t.type] || "\ud83d\udd2c";
398
+ result += `### ${icon} ${t.name || t.title || t.type || "Test"}\n`;
399
+ if (t.framework)
400
+ result += `**Framework:** ${t.framework}\n`;
401
+ if (t.coverage)
402
+ result += `**Coverage:** ${t.coverage}\n`;
403
+ if (t.content || t.code) {
404
+ result += "```\n" + truncate(t.content || t.code, PREVIEW.LONG) + "\n```\n";
405
+ }
406
+ result += "\n";
407
+ }
408
+ return result;
121
409
  },
122
410
  },
123
411
  {
124
412
  name: "get_code_context",
125
413
  description: `Get full context for a code file in ${projectName}. Shows imports, related code, and test patterns.`,
126
- inputSchema: {
127
- type: "object",
128
- properties: {
129
- file: {
130
- type: "string",
131
- description: "File path to get context for",
132
- },
133
- code: {
134
- type: "string",
135
- description: "Code snippet for context",
136
- },
137
- },
138
- },
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
- },
170
- ];
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", {
414
+ schema: z.object({
415
+ file: z.string().optional().describe("File path to get context for"),
416
+ code: z.string().optional().describe("Code snippet for context"),
417
+ }),
418
+ annotations: TOOL_ANNOTATIONS["get_code_context"],
419
+ handler: async (args, ctx) => {
420
+ const { file, code } = args;
421
+ const response = await ctx.api.post("/api/code/context", {
188
422
  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(", ")}`;
423
+ file,
424
+ code,
425
+ });
426
+ const data = response.data;
427
+ let result = `## Code Context\n\n`;
428
+ if (data.imports && data.imports.length > 0) {
429
+ result += `### Imports\n`;
430
+ for (const imp of data.imports) {
431
+ result += `- ${imp}\n`;
432
+ }
245
433
  result += "\n";
246
434
  }
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
- },
287
- get_contextual_suggestions: async (args, ctx) => {
288
- const { currentFile, currentCode, recentFiles, task } = args;
289
- const response = await ctx.api.post("/api/suggestions", {
290
- projectName: ctx.projectName,
291
- currentFile,
292
- currentCode,
293
- recentFiles,
294
- task,
295
- });
296
- const data = response.data;
297
- let result = `## Contextual Suggestions\n\n`;
298
- if (data.relevanceScore !== undefined) {
299
- result += `**Relevance Score:** ${pct(data.relevanceScore)}\n\n`;
300
- }
301
- if (data.triggers && data.triggers.length > 0) {
302
- result += `### Triggers\n`;
303
- for (const t of data.triggers) {
304
- result += `- **${t.type}:** ${t.value}`;
305
- if (t.confidence)
306
- result += ` (${pct(t.confidence)})`;
435
+ if (data.relatedCode && data.relatedCode.length > 0) {
436
+ result += `### Related Code\n`;
437
+ for (const r of data.relatedCode) {
438
+ result += `- **${r.file}** (${pct(r.score)})`;
439
+ if (r.reason)
440
+ result += ` - ${r.reason}`;
441
+ result += "\n";
442
+ }
307
443
  result += "\n";
308
444
  }
309
- result += "\n";
310
- }
311
- if (data.suggestions && data.suggestions.length > 0) {
312
- result += `### Suggestions\n`;
313
- for (const s of data.suggestions) {
314
- result += `- **${s.title}** [${s.type}]\n`;
315
- if (s.description)
316
- result += ` ${s.description}\n`;
317
- if (s.reason)
318
- result += ` *Reason: ${s.reason}*\n`;
319
- if (s.relevance !== undefined)
320
- result += ` Relevance: ${pct(s.relevance)}\n`;
321
- }
322
- result += "\n";
323
- }
324
- if (data.relatedMemories && data.relatedMemories.length > 0) {
325
- result += `### Related Memories\n`;
326
- for (const m of data.relatedMemories) {
327
- result += `- ${m.content || m.title || JSON.stringify(m)}\n`;
328
- }
329
- }
330
- return result;
331
- },
332
- suggest_related_code: async (args, ctx) => {
333
- const { file, code, limit = 5 } = args;
334
- const response = await ctx.api.post("/api/code/related", {
335
- projectName: ctx.projectName,
336
- file,
337
- code,
338
- limit,
339
- });
340
- const results = response.data.results || response.data;
341
- if (!results || results.length === 0) {
342
- return "No related code found.";
343
- }
344
- let result = `## Related Code\n\n`;
345
- for (const r of results) {
346
- result += `### ${r.file}\n`;
347
- result += `**Score:** ${pct(r.score)}`;
348
- if (r.reason)
349
- result += ` | **Reason:** ${r.reason}`;
350
- if (r.line)
351
- result += ` | Line ${r.line}`;
352
- result += "\n";
353
- if (r.content || r.code) {
354
- result += "```\n" + truncate(r.content || r.code, PREVIEW.MEDIUM) + "\n```\n";
355
- }
356
- result += "\n";
357
- }
358
- return result;
359
- },
360
- suggest_implementation: async (args, ctx) => {
361
- const { description, currentFile, language } = args;
362
- const response = await ctx.api.post("/api/code/suggest-implementation", {
363
- projectName: ctx.projectName,
364
- description,
365
- currentFile,
366
- language,
367
- });
368
- const data = response.data;
369
- const patterns = data.patterns || data.results || [];
370
- if (!patterns || patterns.length === 0) {
371
- return "No implementation suggestions found.";
372
- }
373
- const patternIcons = {
374
- similar_structure: "\ud83d\udcd0",
375
- same_domain: "\ud83c\udfaf",
376
- related_import: "\ud83d\udce6",
377
- test_pattern: "\ud83e\uddea",
378
- };
379
- let result = `## Implementation Suggestions\n\n`;
380
- for (const p of patterns) {
381
- const icon = patternIcons[p.pattern || p.type] || "\ud83d\udcd0";
382
- result += `### ${icon} ${p.file || p.name || "Pattern"}\n`;
383
- if (p.adaptationHints || p.hints) {
384
- result += `**Adaptation:** ${p.adaptationHints || p.hints}\n`;
445
+ if (data.testPatterns && data.testPatterns.length > 0) {
446
+ result += `### Test Patterns\n`;
447
+ for (const t of data.testPatterns) {
448
+ result += `- **${t.file}**`;
449
+ if (t.type)
450
+ result += ` [${t.type}]`;
451
+ if (t.framework)
452
+ result += ` (${t.framework})`;
453
+ result += "\n";
454
+ }
385
455
  }
386
- if (p.content || p.code) {
387
- result += "```\n" + truncate(p.content || p.code, 400) + "\n```\n";
388
- }
389
- result += "\n";
390
- }
391
- return result;
392
- },
393
- suggest_tests: async (args, ctx) => {
394
- const { file, code, framework } = args;
395
- const response = await ctx.api.post("/api/code/suggest-tests", {
396
- projectName: ctx.projectName,
397
- file,
398
- code,
399
- framework,
400
- });
401
- const data = response.data;
402
- const tests = data.tests || data.suggestions || data.results || [];
403
- if (!tests || tests.length === 0) {
404
- return "No test suggestions found.";
405
- }
406
- const typeIcons = {
407
- unit: "\ud83d\udd2c",
408
- integration: "\ud83d\udd17",
409
- e2e: "\ud83c\udf10",
410
- };
411
- let result = `## Test Suggestions\n\n`;
412
- for (const t of tests) {
413
- const icon = typeIcons[t.type] || "\ud83d\udd2c";
414
- result += `### ${icon} ${t.name || t.title || t.type || "Test"}\n`;
415
- if (t.framework)
416
- result += `**Framework:** ${t.framework}\n`;
417
- if (t.coverage)
418
- result += `**Coverage:** ${t.coverage}\n`;
419
- if (t.content || t.code) {
420
- result += "```\n" + truncate(t.content || t.code, PREVIEW.LONG) + "\n```\n";
421
- }
422
- result += "\n";
423
- }
424
- return result;
425
- },
426
- get_code_context: async (args, ctx) => {
427
- const { file, code } = args;
428
- const response = await ctx.api.post("/api/code/context", {
429
- projectName: ctx.projectName,
430
- file,
431
- code,
432
- });
433
- const data = response.data;
434
- let result = `## Code Context\n\n`;
435
- if (data.imports && data.imports.length > 0) {
436
- result += `### Imports\n`;
437
- for (const imp of data.imports) {
438
- result += `- ${imp}\n`;
439
- }
440
- result += "\n";
441
- }
442
- if (data.relatedCode && data.relatedCode.length > 0) {
443
- result += `### Related Code\n`;
444
- for (const r of data.relatedCode) {
445
- result += `- **${r.file}** (${pct(r.score)})`;
446
- if (r.reason)
447
- result += ` - ${r.reason}`;
448
- result += "\n";
449
- }
450
- result += "\n";
451
- }
452
- if (data.testPatterns && data.testPatterns.length > 0) {
453
- result += `### Test Patterns\n`;
454
- for (const t of data.testPatterns) {
455
- result += `- **${t.file}**`;
456
- if (t.type)
457
- result += ` [${t.type}]`;
458
- if (t.framework)
459
- result += ` (${t.framework})`;
460
- result += "\n";
461
- }
462
- }
463
- return result;
456
+ return result;
457
+ },
464
458
  },
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 = "";
459
+ {
460
+ name: "setup_project",
461
+ 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.",
462
+ schema: z.object({
463
+ projectPath: z.string().describe("Absolute path to project root"),
464
+ projectName: z.string().describe("Project name in Qdrant (collection prefix)"),
465
+ ragApiUrl: z.string().optional().describe("RAG API URL (default: from MCP env)"),
466
+ ragApiKey: z.string().optional().describe("RAG API key (default: from MCP env)"),
467
+ updateClaudeMd: z.boolean().optional().describe("Add RAG section to CLAUDE.md (default: true)"),
468
+ }),
469
+ annotations: TOOL_ANNOTATIONS["setup_project"],
470
+ handler: async (args, ctx) => {
471
+ const { projectPath, projectName: targetProject, ragApiUrl, ragApiKey, updateClaudeMd = true, } = args;
472
+ const apiUrl = ragApiUrl || process.env.RAG_API_URL || "http://localhost:3100";
473
+ const apiKey = ragApiKey || process.env.RAG_API_KEY;
474
+ const serverName = `${targetProject}-rag`;
475
+ const changes = [];
476
+ // 1. Create/update .mcp.json
477
+ const mcpJsonPath = path.join(projectPath, ".mcp.json");
478
+ let mcpConfig = {};
501
479
  try {
502
- claudeMd = fs.readFileSync(claudeMdPath, "utf-8");
480
+ const existing = fs.readFileSync(mcpJsonPath, "utf-8");
481
+ mcpConfig = JSON.parse(existing);
503
482
  }
504
483
  catch {
505
- // File doesn't exist
484
+ // File doesn't exist or invalid JSON
506
485
  }
507
- const ragSection = `\n## RAG Integration
486
+ if (!mcpConfig.mcpServers)
487
+ mcpConfig.mcpServers = {};
488
+ const serverEnv = {
489
+ RAG_API_URL: apiUrl,
490
+ PROJECT_NAME: targetProject,
491
+ PROJECT_PATH: projectPath,
492
+ };
493
+ if (apiKey)
494
+ serverEnv.RAG_API_KEY = apiKey;
495
+ mcpConfig.mcpServers[serverName] = {
496
+ command: "npx",
497
+ args: ["-y", "@crowley/rag-mcp@latest"],
498
+ env: serverEnv,
499
+ };
500
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
501
+ changes.push(`.mcp.json — added \`${serverName}\` server`);
502
+ // 2. Update CLAUDE.md with RAG section
503
+ if (updateClaudeMd) {
504
+ const claudeMdPath = path.join(projectPath, "CLAUDE.md");
505
+ let claudeMd = "";
506
+ try {
507
+ claudeMd = fs.readFileSync(claudeMdPath, "utf-8");
508
+ }
509
+ catch {
510
+ // File doesn't exist
511
+ }
512
+ const ragSection = `\n## RAG Integration
508
513
 
509
514
  You MUST call \`context_briefing\` before making any code changes.
510
515
  This single tool performs all RAG lookups in parallel (recall, search, patterns, ADRs, graph).
@@ -515,63 +520,63 @@ After completing significant changes:
515
520
  - \`remember\` — save important context for future sessions
516
521
  - \`record_adr\` — document architectural decisions
517
522
  `;
518
- if (claudeMd.includes("## RAG")) {
519
- changes.push("CLAUDE.md — RAG section already exists, skipped");
523
+ if (claudeMd.includes("## RAG")) {
524
+ changes.push("CLAUDE.md — RAG section already exists, skipped");
525
+ }
526
+ else {
527
+ claudeMd = claudeMd ? claudeMd.trimEnd() + "\n" + ragSection : `# CLAUDE.md\n${ragSection}`;
528
+ fs.writeFileSync(claudeMdPath, claudeMd);
529
+ changes.push("CLAUDE.md — added RAG Integration section");
530
+ }
531
+ }
532
+ // 3. Create/update .claude/settings.local.json permissions
533
+ const claudeDir = path.join(projectPath, ".claude");
534
+ const settingsPath = path.join(claudeDir, "settings.local.json");
535
+ let settings = {};
536
+ try {
537
+ const existing = fs.readFileSync(settingsPath, "utf-8");
538
+ settings = JSON.parse(existing);
539
+ }
540
+ catch {
541
+ // File doesn't exist or invalid JSON
542
+ }
543
+ if (!settings.permissions)
544
+ settings.permissions = {};
545
+ if (!settings.permissions.allow)
546
+ settings.permissions.allow = [];
547
+ const mcpPermission = `mcp__${serverName}__*`;
548
+ if (!settings.permissions.allow.includes(mcpPermission)) {
549
+ settings.permissions.allow.push(mcpPermission);
550
+ if (!fs.existsSync(claudeDir))
551
+ fs.mkdirSync(claudeDir, { recursive: true });
552
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
553
+ changes.push(`.claude/settings.local.json — added \`${mcpPermission}\` permission`);
520
554
  }
521
555
  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");
556
+ changes.push(".claude/settings.local.json permission already exists, skipped");
525
557
  }
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;
558
+ // 4. Check index status
559
+ let indexInfo = "";
560
+ try {
561
+ const statusRes = await ctx.api.get(`/api/index/status/${targetProject}_codebase`);
562
+ const data = statusRes.data;
563
+ indexInfo = `\n## Index Status\n- **Vectors:** ${data.vectorCount ?? "N/A"}\n- **Status:** ${data.status || "unknown"}\n`;
564
+ }
565
+ catch {
566
+ indexInfo = "\n## Index Status\n_Not indexed yet. Run `index_codebase` first._\n";
567
+ }
568
+ let result = `# Project Setup: ${targetProject}\n\n`;
569
+ result += `## Files Updated\n`;
570
+ for (const c of changes) {
571
+ result += `- ${c}\n`;
572
+ }
573
+ result += indexInfo;
574
+ result += `\n## Next Steps\n`;
575
+ result += `1. Restart Claude Code to load the new MCP server\n`;
576
+ result += `2. Run \`index_codebase\` if not indexed yet\n`;
577
+ result += `3. Use \`context_briefing\` before code changes\n`;
578
+ return result;
579
+ },
574
580
  },
575
- };
576
- return { tools, handlers };
581
+ ];
577
582
  }