@crowley/rag-mcp 1.0.6 → 1.2.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.
- package/dist/__tests__/tool-middleware.test.d.ts +1 -0
- package/dist/__tests__/tool-middleware.test.js +83 -0
- package/dist/__tests__/tools/memory.test.d.ts +1 -0
- package/dist/__tests__/tools/memory.test.js +127 -0
- package/dist/index.js +57 -4
- package/dist/schemas.d.ts +2 -2
- package/dist/tools/memory.js +74 -17
- package/dist/tools/quality.d.ts +8 -0
- package/dist/tools/quality.js +60 -0
- package/dist/tools/suggestions.js +103 -11
- package/package.json +6 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { summarizeInput, countResults, formatToolError, TRACKING_EXCLUDE, SESSION_TOOLS, TOOL_TIMEOUTS, } from '../tool-middleware.js';
|
|
3
|
+
describe('Tool Middleware', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.resetAllMocks();
|
|
6
|
+
});
|
|
7
|
+
describe('summarizeInput', () => {
|
|
8
|
+
it('extracts query field', () => {
|
|
9
|
+
expect(summarizeInput('search', { query: 'find auth code' })).toBe('find auth code');
|
|
10
|
+
});
|
|
11
|
+
it('extracts question field', () => {
|
|
12
|
+
expect(summarizeInput('ask', { question: 'what is auth?' })).toBe('what is auth?');
|
|
13
|
+
});
|
|
14
|
+
it('extracts content field as fallback', () => {
|
|
15
|
+
expect(summarizeInput('remember', { content: 'important note' })).toBe('important note');
|
|
16
|
+
});
|
|
17
|
+
it('extracts file path as fallback', () => {
|
|
18
|
+
expect(summarizeInput('explain', { filePath: 'src/auth.ts' })).toBe('src/auth.ts');
|
|
19
|
+
});
|
|
20
|
+
it('truncates long strings to 200 chars', () => {
|
|
21
|
+
const long = 'a'.repeat(300);
|
|
22
|
+
expect(summarizeInput('search', { query: long }).length).toBeLessThanOrEqual(200);
|
|
23
|
+
});
|
|
24
|
+
it('returns tool name when no useful field', () => {
|
|
25
|
+
expect(summarizeInput('get_stats', {})).toBe('get_stats');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('countResults', () => {
|
|
29
|
+
it('returns 0 for "No results" messages', () => {
|
|
30
|
+
expect(countResults('No results found.')).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
it('returns 0 for "not found" messages', () => {
|
|
33
|
+
expect(countResults('Memory not found')).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
it('counts numbered items', () => {
|
|
36
|
+
const text = '1. First\n2. Second\n3. Third';
|
|
37
|
+
expect(countResults(text)).toBe(3);
|
|
38
|
+
});
|
|
39
|
+
it('counts bullet items', () => {
|
|
40
|
+
const text = '- item1\n- item2';
|
|
41
|
+
expect(countResults(text)).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
it('returns 1 for generic content', () => {
|
|
44
|
+
expect(countResults('Some response text')).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('formatToolError', () => {
|
|
48
|
+
const ctx = {
|
|
49
|
+
api: { defaults: { baseURL: 'http://localhost:3100' } },
|
|
50
|
+
};
|
|
51
|
+
it('formats ECONNREFUSED error', () => {
|
|
52
|
+
const err = { code: 'ECONNREFUSED' };
|
|
53
|
+
const result = formatToolError(err, ctx);
|
|
54
|
+
expect(result).toContain('Cannot connect');
|
|
55
|
+
expect(result).toContain('localhost:3100');
|
|
56
|
+
});
|
|
57
|
+
it('formats API error with status', () => {
|
|
58
|
+
const err = { response: { status: 404, data: { error: 'not found' } } };
|
|
59
|
+
const result = formatToolError(err, ctx);
|
|
60
|
+
expect(result).toContain('404');
|
|
61
|
+
});
|
|
62
|
+
it('formats generic error message', () => {
|
|
63
|
+
const err = { message: 'Something broke' };
|
|
64
|
+
const result = formatToolError(err, ctx);
|
|
65
|
+
expect(result).toContain('Something broke');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('constants', () => {
|
|
69
|
+
it('TRACKING_EXCLUDE contains meta tools', () => {
|
|
70
|
+
expect(TRACKING_EXCLUDE.has('get_tool_analytics')).toBe(true);
|
|
71
|
+
expect(TRACKING_EXCLUDE.has('get_quality_metrics')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('SESSION_TOOLS contains session management', () => {
|
|
74
|
+
expect(SESSION_TOOLS.has('start_session')).toBe(true);
|
|
75
|
+
expect(SESSION_TOOLS.has('end_session')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('TOOL_TIMEOUTS has correct tiers', () => {
|
|
78
|
+
expect(TOOL_TIMEOUTS['index_codebase']).toBe(120_000);
|
|
79
|
+
expect(TOOL_TIMEOUTS['search_codebase']).toBe(15_000);
|
|
80
|
+
expect(TOOL_TIMEOUTS['recall']).toBe(10_000);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { createMemoryTools } from '../../tools/memory';
|
|
3
|
+
function createMockCtx() {
|
|
4
|
+
return {
|
|
5
|
+
api: {
|
|
6
|
+
post: vi.fn(),
|
|
7
|
+
get: vi.fn(),
|
|
8
|
+
delete: vi.fn(),
|
|
9
|
+
patch: vi.fn(),
|
|
10
|
+
defaults: { baseURL: 'http://localhost:3100' },
|
|
11
|
+
},
|
|
12
|
+
projectName: 'testproject',
|
|
13
|
+
projectPath: '/tmp/testproject',
|
|
14
|
+
collectionPrefix: 'testproject',
|
|
15
|
+
enrichmentEnabled: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('Memory Tools', () => {
|
|
19
|
+
let tools;
|
|
20
|
+
let ctx;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.resetAllMocks();
|
|
23
|
+
tools = createMemoryTools('testproject');
|
|
24
|
+
ctx = createMockCtx();
|
|
25
|
+
});
|
|
26
|
+
function findTool(name) {
|
|
27
|
+
return tools.find(t => t.name === name);
|
|
28
|
+
}
|
|
29
|
+
describe('remember', () => {
|
|
30
|
+
it('stores memory and returns formatted result', async () => {
|
|
31
|
+
const mem = { id: 'mem-1', type: 'note', content: 'test note', createdAt: new Date().toISOString() };
|
|
32
|
+
ctx.api.post.mockResolvedValue({ data: { memory: mem } });
|
|
33
|
+
const result = await findTool('remember').handler({ content: 'test note', type: 'note', tags: ['tag1'] }, ctx);
|
|
34
|
+
expect(ctx.api.post).toHaveBeenCalledWith('/api/memory', expect.objectContaining({
|
|
35
|
+
projectName: 'testproject',
|
|
36
|
+
content: 'test note',
|
|
37
|
+
type: 'note',
|
|
38
|
+
}));
|
|
39
|
+
expect(result).toContain('Memory stored');
|
|
40
|
+
expect(result).toContain('mem-1');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('recall', () => {
|
|
44
|
+
it('returns formatted results', async () => {
|
|
45
|
+
ctx.api.post.mockResolvedValue({
|
|
46
|
+
data: {
|
|
47
|
+
results: [
|
|
48
|
+
{ memory: { type: 'insight', content: 'found it', createdAt: new Date().toISOString(), tags: [] }, score: 0.85 },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const result = await findTool('recall').handler({ query: 'find something', limit: 5 }, ctx);
|
|
53
|
+
expect(result).toContain('Recalled Memories');
|
|
54
|
+
});
|
|
55
|
+
it('returns empty message when no results', async () => {
|
|
56
|
+
ctx.api.post.mockResolvedValue({ data: { results: [] } });
|
|
57
|
+
const result = await findTool('recall').handler({ query: 'nothing' }, ctx);
|
|
58
|
+
expect(result).toContain('No memories found');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('forget', () => {
|
|
62
|
+
it('deletes by memoryId', async () => {
|
|
63
|
+
ctx.api.delete.mockResolvedValue({ data: { success: true } });
|
|
64
|
+
const result = await findTool('forget').handler({ memoryId: 'mem-1' }, ctx);
|
|
65
|
+
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining('/api/memory/mem-1'));
|
|
66
|
+
expect(result).toContain('deleted');
|
|
67
|
+
});
|
|
68
|
+
it('deletes by type', async () => {
|
|
69
|
+
ctx.api.delete.mockResolvedValue({ data: {} });
|
|
70
|
+
const result = await findTool('forget').handler({ type: 'note' }, ctx);
|
|
71
|
+
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining('/api/memory/type/note'));
|
|
72
|
+
expect(result).toContain('note');
|
|
73
|
+
});
|
|
74
|
+
it('deletes by olderThanDays', async () => {
|
|
75
|
+
ctx.api.post.mockResolvedValue({ data: { deleted: 10 } });
|
|
76
|
+
const result = await findTool('forget').handler({ olderThanDays: 30 }, ctx);
|
|
77
|
+
expect(ctx.api.post).toHaveBeenCalledWith('/api/memory/forget-older', expect.objectContaining({
|
|
78
|
+
olderThanDays: 30,
|
|
79
|
+
}));
|
|
80
|
+
expect(result).toContain('10');
|
|
81
|
+
});
|
|
82
|
+
it('returns message when nothing specified', async () => {
|
|
83
|
+
const result = await findTool('forget').handler({}, ctx);
|
|
84
|
+
expect(result).toContain('specify');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('promote_memory', () => {
|
|
88
|
+
it('promotes and returns formatted result', async () => {
|
|
89
|
+
const mem = { id: 'mem-1', type: 'insight', content: 'promoted' };
|
|
90
|
+
ctx.api.post.mockResolvedValue({ data: { memory: mem } });
|
|
91
|
+
const result = await findTool('promote_memory').handler({ memoryId: 'mem-1', reason: 'human_validated' }, ctx);
|
|
92
|
+
expect(result).toContain('promoted to durable');
|
|
93
|
+
expect(result).toContain('mem-1');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('memory_maintenance', () => {
|
|
97
|
+
it('formats maintenance results', async () => {
|
|
98
|
+
ctx.api.post.mockResolvedValue({
|
|
99
|
+
data: {
|
|
100
|
+
quarantine_cleanup: { rejected: ['q-1', 'q-2'], errors: [] },
|
|
101
|
+
feedback_maintenance: { promoted: ['f-1'], pruned: [], errors: [] },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const result = await findTool('memory_maintenance').handler({}, ctx);
|
|
105
|
+
expect(result).toContain('Maintenance Results');
|
|
106
|
+
expect(result).toContain('Quarantine Cleanup');
|
|
107
|
+
expect(result).toContain('Feedback Maintenance');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('batch_remember', () => {
|
|
111
|
+
it('stores multiple memories', async () => {
|
|
112
|
+
ctx.api.post.mockResolvedValue({
|
|
113
|
+
data: {
|
|
114
|
+
savedCount: 2,
|
|
115
|
+
memories: [
|
|
116
|
+
{ id: 'b-1', type: 'note', content: 'first' },
|
|
117
|
+
{ id: 'b-2', type: 'insight', content: 'second' },
|
|
118
|
+
],
|
|
119
|
+
errors: [],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const result = await findTool('batch_remember').handler({ items: [{ content: 'first' }, { content: 'second', type: 'insight' }] }, ctx);
|
|
123
|
+
expect(result).toContain('Saved');
|
|
124
|
+
expect(result).toContain('2');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { createCacheTools } from "./tools/cache.js";
|
|
|
34
34
|
import { createGuidelinesTools } from "./tools/guidelines.js";
|
|
35
35
|
import { createAdvancedTools } from "./tools/advanced.js";
|
|
36
36
|
import { createAgentTools } from "./tools/agents.js";
|
|
37
|
+
import { createQualityTools } from "./tools/quality.js";
|
|
37
38
|
// Configuration from environment
|
|
38
39
|
const PROJECT_NAME = process.env.PROJECT_NAME || "default";
|
|
39
40
|
const PROJECT_PATH = process.env.PROJECT_PATH || process.cwd();
|
|
@@ -76,11 +77,63 @@ const allSpecs = [
|
|
|
76
77
|
...createGuidelinesTools(PROJECT_NAME),
|
|
77
78
|
...createAdvancedTools(PROJECT_NAME),
|
|
78
79
|
...createAgentTools(PROJECT_NAME),
|
|
80
|
+
...createQualityTools(PROJECT_NAME),
|
|
79
81
|
];
|
|
82
|
+
// Core tools exposed directly to Claude (~35 tools).
|
|
83
|
+
// Hidden tools remain accessible via run_agent (agent runtime calls API directly).
|
|
84
|
+
const CORE_TOOLS = new Set([
|
|
85
|
+
// Search (6)
|
|
86
|
+
"search_codebase",
|
|
87
|
+
"hybrid_search",
|
|
88
|
+
"search_graph",
|
|
89
|
+
"find_symbol",
|
|
90
|
+
"search_docs",
|
|
91
|
+
"find_feature",
|
|
92
|
+
// Ask (2)
|
|
93
|
+
"ask_codebase",
|
|
94
|
+
"explain_code",
|
|
95
|
+
// Index (3)
|
|
96
|
+
"index_codebase",
|
|
97
|
+
"get_index_status",
|
|
98
|
+
"get_project_stats",
|
|
99
|
+
// Memory (7)
|
|
100
|
+
"remember",
|
|
101
|
+
"recall",
|
|
102
|
+
"list_memories",
|
|
103
|
+
"forget",
|
|
104
|
+
"batch_remember",
|
|
105
|
+
"promote_memory",
|
|
106
|
+
"review_memories",
|
|
107
|
+
// Architecture (6)
|
|
108
|
+
"record_adr",
|
|
109
|
+
"get_adrs",
|
|
110
|
+
"record_pattern",
|
|
111
|
+
"get_patterns",
|
|
112
|
+
"record_tech_debt",
|
|
113
|
+
"get_tech_debt",
|
|
114
|
+
// Context (3)
|
|
115
|
+
"context_briefing",
|
|
116
|
+
"smart_dispatch",
|
|
117
|
+
"setup_project",
|
|
118
|
+
// Session (2)
|
|
119
|
+
"start_session",
|
|
120
|
+
"end_session",
|
|
121
|
+
// Confluence (2)
|
|
122
|
+
"search_confluence",
|
|
123
|
+
"index_confluence",
|
|
124
|
+
// DB (4)
|
|
125
|
+
"record_table",
|
|
126
|
+
"get_table_info",
|
|
127
|
+
"check_db_schema",
|
|
128
|
+
"get_db_rules",
|
|
129
|
+
// Agents (1)
|
|
130
|
+
"run_agent",
|
|
131
|
+
]);
|
|
132
|
+
const coreSpecs = allSpecs.filter((s) => CORE_TOOLS.has(s.name));
|
|
80
133
|
// MCP Server (modern McpServer API with native Zod validation)
|
|
81
|
-
const server = new McpServer({ name: `${PROJECT_NAME}-rag`, version: "1.0
|
|
82
|
-
// Register
|
|
83
|
-
for (const spec of
|
|
134
|
+
const server = new McpServer({ name: `${PROJECT_NAME}-rag`, version: "1.1.0" }, { capabilities: { tools: {} } });
|
|
135
|
+
// Register core tools with McpServer using wrapHandler middleware
|
|
136
|
+
for (const spec of coreSpecs) {
|
|
84
137
|
const wrapped = wrapHandler(spec.name, spec.handler, { enricher, ctx });
|
|
85
138
|
server.registerTool(spec.name, {
|
|
86
139
|
description: spec.description,
|
|
@@ -121,6 +174,6 @@ async function main() {
|
|
|
121
174
|
const transport = new StdioServerTransport();
|
|
122
175
|
await server.connect(transport);
|
|
123
176
|
console.error(`${PROJECT_NAME} RAG MCP server running (collection prefix: ${COLLECTION_PREFIX})`);
|
|
124
|
-
console.error(`Registered ${allSpecs.length} tools
|
|
177
|
+
console.error(`Registered ${coreSpecs.length}/${allSpecs.length} core tools (${allSpecs.length - coreSpecs.length} hidden, accessible via run_agent)`);
|
|
125
178
|
}
|
|
126
179
|
main().catch(console.error);
|
package/dist/schemas.d.ts
CHANGED
|
@@ -83,13 +83,13 @@ export declare const MemoryRecordInput: z.ZodObject<{
|
|
|
83
83
|
importance: z.ZodOptional<z.ZodDefault<z.ZodEnum<["low", "medium", "high", "critical"]>>>;
|
|
84
84
|
context: z.ZodOptional<z.ZodString>;
|
|
85
85
|
}, "strip", z.ZodTypeAny, {
|
|
86
|
-
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "
|
|
86
|
+
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "architecture" | "tech_debt" | "convention" | "bug_fix" | "optimization";
|
|
87
87
|
content: string;
|
|
88
88
|
context?: string | undefined;
|
|
89
89
|
tags?: string[] | undefined;
|
|
90
90
|
importance?: "low" | "medium" | "high" | "critical" | undefined;
|
|
91
91
|
}, {
|
|
92
|
-
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "
|
|
92
|
+
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "architecture" | "tech_debt" | "convention" | "bug_fix" | "optimization";
|
|
93
93
|
content: string;
|
|
94
94
|
context?: string | undefined;
|
|
95
95
|
tags?: string[] | undefined;
|
package/dist/tools/memory.js
CHANGED
|
@@ -135,6 +135,7 @@ export function createMemoryTools(projectName) {
|
|
|
135
135
|
handler: async (args, ctx) => {
|
|
136
136
|
const memoryId = args.memoryId;
|
|
137
137
|
const type = args.type;
|
|
138
|
+
const olderThanDays = args.olderThanDays;
|
|
138
139
|
if (memoryId) {
|
|
139
140
|
const response = await ctx.api.delete(`/api/memory/${memoryId}?projectName=${ctx.projectName}`);
|
|
140
141
|
return response.data.success
|
|
@@ -145,7 +146,14 @@ export function createMemoryTools(projectName) {
|
|
|
145
146
|
await ctx.api.delete(`/api/memory/type/${type}?projectName=${ctx.projectName}`);
|
|
146
147
|
return `\u{1F5D1}\uFE0F Deleted all memories of type: ${type}`;
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
if (olderThanDays) {
|
|
150
|
+
const response = await ctx.api.post("/api/memory/forget-older", {
|
|
151
|
+
projectName: ctx.projectName,
|
|
152
|
+
olderThanDays,
|
|
153
|
+
});
|
|
154
|
+
return `\u{1F5D1}\uFE0F Deleted ${response.data.deleted} memories older than ${olderThanDays} days`;
|
|
155
|
+
}
|
|
156
|
+
return "Please specify memoryId, type, or olderThanDays to delete.";
|
|
149
157
|
},
|
|
150
158
|
},
|
|
151
159
|
{
|
|
@@ -191,6 +199,7 @@ export function createMemoryTools(projectName) {
|
|
|
191
199
|
handler: async (args, ctx) => {
|
|
192
200
|
const items = args.items;
|
|
193
201
|
const response = await ctx.api.post("/api/memory/batch", {
|
|
202
|
+
projectName: ctx.projectName,
|
|
194
203
|
items,
|
|
195
204
|
});
|
|
196
205
|
const { savedCount, errors, memories } = response.data;
|
|
@@ -345,33 +354,81 @@ export function createMemoryTools(projectName) {
|
|
|
345
354
|
},
|
|
346
355
|
{
|
|
347
356
|
name: "memory_maintenance",
|
|
348
|
-
description: `Run
|
|
349
|
-
schema: z.object({
|
|
357
|
+
description: `Run memory maintenance for ${projectName}: quarantine cleanup (expire old auto-memories), feedback-driven promote/prune, and compaction (merge similar durable memories).`,
|
|
358
|
+
schema: z.object({
|
|
359
|
+
operations: z.object({
|
|
360
|
+
quarantine_cleanup: z.boolean().optional().describe("Remove expired quarantine memories (default: true)"),
|
|
361
|
+
feedback_maintenance: z.boolean().optional().describe("Auto-promote/prune by feedback (default: true)"),
|
|
362
|
+
compaction: z.boolean().optional().describe("Merge similar durable memories (default: false)"),
|
|
363
|
+
compaction_dry_run: z.boolean().optional().describe("Preview compaction without changes (default: true)"),
|
|
364
|
+
}).optional().describe("Which operations to run (default: quarantine_cleanup + feedback_maintenance)"),
|
|
365
|
+
}),
|
|
350
366
|
annotations: TOOL_ANNOTATIONS["memory_maintenance"],
|
|
351
|
-
handler: async (
|
|
367
|
+
handler: async (args, ctx) => {
|
|
368
|
+
const operations = args.operations;
|
|
352
369
|
const response = await ctx.api.post("/api/memory/maintenance", {
|
|
353
370
|
projectName: ctx.projectName,
|
|
371
|
+
operations,
|
|
354
372
|
});
|
|
355
|
-
const
|
|
373
|
+
const data = response.data;
|
|
356
374
|
let result = `# \u{1F9F9} Memory Maintenance Results\n\n`;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
375
|
+
// Quarantine cleanup section
|
|
376
|
+
if (data.quarantine_cleanup) {
|
|
377
|
+
const qc = data.quarantine_cleanup;
|
|
378
|
+
result += `## Quarantine Cleanup\n`;
|
|
379
|
+
if (qc.rejected.length > 0) {
|
|
380
|
+
result += `**Expired** (${qc.rejected.length}): removed from quarantine\n`;
|
|
381
|
+
qc.rejected.slice(0, 10).forEach((id) => { result += ` \u{1F5D1}\u{FE0F} ${id}\n`; });
|
|
382
|
+
if (qc.rejected.length > 10)
|
|
383
|
+
result += ` ... and ${qc.rejected.length - 10} more\n`;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
result += `No expired quarantine memories.\n`;
|
|
387
|
+
}
|
|
388
|
+
if (qc.errors.length > 0) {
|
|
389
|
+
qc.errors.forEach((e) => { result += ` \u26A0\u{FE0F} ${e}\n`; });
|
|
390
|
+
}
|
|
360
391
|
result += `\n`;
|
|
361
392
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
393
|
+
// Feedback maintenance section
|
|
394
|
+
if (data.feedback_maintenance) {
|
|
395
|
+
const fm = data.feedback_maintenance;
|
|
396
|
+
result += `## Feedback Maintenance\n`;
|
|
397
|
+
if (fm.promoted.length > 0) {
|
|
398
|
+
result += `**Promoted** (${fm.promoted.length}): moved to durable\n`;
|
|
399
|
+
fm.promoted.forEach((id) => { result += ` \u2705 ${id}\n`; });
|
|
400
|
+
}
|
|
401
|
+
if (fm.pruned.length > 0) {
|
|
402
|
+
result += `**Pruned** (${fm.pruned.length}): removed\n`;
|
|
403
|
+
fm.pruned.forEach((id) => { result += ` \u{1F5D1}\u{FE0F} ${id}\n`; });
|
|
404
|
+
}
|
|
405
|
+
if (fm.promoted.length === 0 && fm.pruned.length === 0) {
|
|
406
|
+
result += `No feedback-based actions needed.\n`;
|
|
407
|
+
}
|
|
408
|
+
if (fm.errors.length > 0) {
|
|
409
|
+
fm.errors.forEach((e) => { result += ` \u26A0\u{FE0F} ${e}\n`; });
|
|
410
|
+
}
|
|
365
411
|
result += `\n`;
|
|
366
412
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
413
|
+
// Compaction section
|
|
414
|
+
if (data.compaction) {
|
|
415
|
+
const cp = data.compaction;
|
|
416
|
+
result += `## Compaction${cp.dryRun ? ' (dry run)' : ''}\n`;
|
|
417
|
+
if (cp.clusters.length > 0) {
|
|
418
|
+
result += `**${cp.totalClusters} cluster(s)** of similar memories found\n\n`;
|
|
419
|
+
cp.clusters.slice(0, 5).forEach((c, i) => {
|
|
420
|
+
result += `${i + 1}. ${c.originalIds.length} memories → ${truncate(c.mergedContent, 120)}\n`;
|
|
421
|
+
if (c.mergedId)
|
|
422
|
+
result += ` Merged ID: \`${c.mergedId}\`\n`;
|
|
423
|
+
});
|
|
424
|
+
if (cp.clusters.length > 5)
|
|
425
|
+
result += `... and ${cp.clusters.length - 5} more clusters\n`;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
result += `No similar memory clusters found.\n`;
|
|
429
|
+
}
|
|
370
430
|
result += `\n`;
|
|
371
431
|
}
|
|
372
|
-
if (promoted.length === 0 && pruned.length === 0) {
|
|
373
|
-
result += `No memories needed maintenance. All feedback thresholds are below auto-action levels.\n`;
|
|
374
|
-
}
|
|
375
432
|
return result;
|
|
376
433
|
},
|
|
377
434
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality tools module - LLM quality monitoring and reporting.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { TOOL_ANNOTATIONS } from "../annotations.js";
|
|
6
|
+
/**
|
|
7
|
+
* Create the quality tools module.
|
|
8
|
+
*/
|
|
9
|
+
export function createQualityTools(projectName) {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
name: "get_quality_report",
|
|
13
|
+
description: `Get LLM quality metrics for ${projectName}. Shows JSON parse rates, latency percentiles, thinking trace rates, and alerts.`,
|
|
14
|
+
schema: z.object({
|
|
15
|
+
endpoint: z.string().optional().describe("Filter by specific endpoint (e.g., '/api/ask')"),
|
|
16
|
+
}),
|
|
17
|
+
annotations: TOOL_ANNOTATIONS["get_quality_report"] || {
|
|
18
|
+
title: "Get Quality Report",
|
|
19
|
+
readOnlyHint: true,
|
|
20
|
+
openWorldHint: false,
|
|
21
|
+
},
|
|
22
|
+
handler: async (args, ctx) => {
|
|
23
|
+
const params = args.endpoint ? `?endpoint=${encodeURIComponent(args.endpoint)}` : '';
|
|
24
|
+
const response = await ctx.api.get(`/api/quality/report${params}`);
|
|
25
|
+
const data = response.data;
|
|
26
|
+
let result = `## Quality Report\n\n`;
|
|
27
|
+
result += `**Total Metrics:** ${data.total}\n\n`;
|
|
28
|
+
if (data.total === 0) {
|
|
29
|
+
result += `No quality metrics recorded yet.\n`;
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
const m = data.metrics;
|
|
33
|
+
result += `### Aggregate Metrics\n`;
|
|
34
|
+
result += `- **Avg Latency:** ${m.avgLatencyMs}ms\n`;
|
|
35
|
+
result += `- **P95 Latency:** ${m.p95LatencyMs}ms\n`;
|
|
36
|
+
result += `- **JSON Parse Rate:** ${(m.jsonParseRate * 100).toFixed(1)}%\n`;
|
|
37
|
+
result += `- **Thinking Rate:** ${(m.thinkingRate * 100).toFixed(1)}%\n`;
|
|
38
|
+
result += `- **Avg Output Length:** ${m.avgOutputLength} chars\n`;
|
|
39
|
+
result += `- **Avg Thinking Length:** ${m.avgThinkingLength} chars\n`;
|
|
40
|
+
result += `- **Avg Tokens:** ${m.avgTokens}\n\n`;
|
|
41
|
+
if (data.alerts.length > 0) {
|
|
42
|
+
result += `### ⚠ Alerts\n`;
|
|
43
|
+
for (const alert of data.alerts) {
|
|
44
|
+
result += `- ${alert}\n`;
|
|
45
|
+
}
|
|
46
|
+
result += `\n`;
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(data.byEndpoint).length > 0) {
|
|
49
|
+
result += `### By Endpoint\n`;
|
|
50
|
+
for (const [ep, stats] of Object.entries(data.byEndpoint)) {
|
|
51
|
+
result += `- **${ep}**: ${stats.count} calls, ${stats.avgLatencyMs}ms avg, `;
|
|
52
|
+
result += `JSON: ${(stats.jsonParseRate * 100).toFixed(0)}%, `;
|
|
53
|
+
result += `Thinking: ${(stats.thinkingRate * 100).toFixed(0)}%\n`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
@@ -7,6 +7,75 @@ import * as path from "path";
|
|
|
7
7
|
import { truncate, pct, PREVIEW } from "../formatters.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
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
|
+
}
|
|
10
79
|
/**
|
|
11
80
|
* Create the suggestions tools module with project-specific descriptions.
|
|
12
81
|
*/
|
|
@@ -22,9 +91,21 @@ export function createSuggestionTools(projectName) {
|
|
|
22
91
|
annotations: TOOL_ANNOTATIONS["context_briefing"],
|
|
23
92
|
handler: async (args, ctx) => {
|
|
24
93
|
const { task, files } = args;
|
|
25
|
-
//
|
|
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
|
|
26
108
|
const [memoriesRes, searchRes, patternsRes, adrsRes, graphRes] = await Promise.all([
|
|
27
|
-
// 1. Recall relevant memories
|
|
28
109
|
ctx.api
|
|
29
110
|
.post("/api/memory/recall", {
|
|
30
111
|
projectName: ctx.projectName,
|
|
@@ -33,7 +114,6 @@ export function createSuggestionTools(projectName) {
|
|
|
33
114
|
type: "all",
|
|
34
115
|
})
|
|
35
116
|
.catch(() => null),
|
|
36
|
-
// 2. Hybrid search for related code
|
|
37
117
|
ctx.api
|
|
38
118
|
.post("/api/search-hybrid", {
|
|
39
119
|
projectName: ctx.projectName,
|
|
@@ -42,7 +122,6 @@ export function createSuggestionTools(projectName) {
|
|
|
42
122
|
mode: "navigate",
|
|
43
123
|
})
|
|
44
124
|
.catch(() => null),
|
|
45
|
-
// 3. Architectural patterns
|
|
46
125
|
ctx.api
|
|
47
126
|
.post("/api/memory/recall", {
|
|
48
127
|
projectName: ctx.projectName,
|
|
@@ -52,7 +131,6 @@ export function createSuggestionTools(projectName) {
|
|
|
52
131
|
tag: "pattern",
|
|
53
132
|
})
|
|
54
133
|
.catch(() => null),
|
|
55
|
-
// 4. ADRs
|
|
56
134
|
ctx.api
|
|
57
135
|
.post("/api/memory/recall", {
|
|
58
136
|
projectName: ctx.projectName,
|
|
@@ -62,7 +140,6 @@ export function createSuggestionTools(projectName) {
|
|
|
62
140
|
tag: "adr",
|
|
63
141
|
})
|
|
64
142
|
.catch(() => null),
|
|
65
|
-
// 5. Graph dependencies (if files specified)
|
|
66
143
|
files && files.length > 0
|
|
67
144
|
? ctx.api
|
|
68
145
|
.post("/api/search-graph", {
|
|
@@ -75,7 +152,6 @@ export function createSuggestionTools(projectName) {
|
|
|
75
152
|
: Promise.resolve(null),
|
|
76
153
|
]);
|
|
77
154
|
let result = `# Context Briefing: ${task}\n\n`;
|
|
78
|
-
// Memories
|
|
79
155
|
const memories = memoriesRes?.data?.results || memoriesRes?.data?.memories || [];
|
|
80
156
|
if (memories.length > 0) {
|
|
81
157
|
result += `## Memories (${memories.length})\n`;
|
|
@@ -85,7 +161,6 @@ export function createSuggestionTools(projectName) {
|
|
|
85
161
|
}
|
|
86
162
|
result += "\n";
|
|
87
163
|
}
|
|
88
|
-
// Related code
|
|
89
164
|
const codeResults = searchRes?.data?.results || [];
|
|
90
165
|
if (codeResults.length > 0) {
|
|
91
166
|
result += `## Related Code (${codeResults.length})\n`;
|
|
@@ -97,7 +172,6 @@ export function createSuggestionTools(projectName) {
|
|
|
97
172
|
}
|
|
98
173
|
result += "\n";
|
|
99
174
|
}
|
|
100
|
-
// Patterns
|
|
101
175
|
const patterns = (patternsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("pattern"));
|
|
102
176
|
if (patterns.length > 0) {
|
|
103
177
|
result += `## Patterns (${patterns.length})\n`;
|
|
@@ -107,7 +181,6 @@ export function createSuggestionTools(projectName) {
|
|
|
107
181
|
}
|
|
108
182
|
result += "\n";
|
|
109
183
|
}
|
|
110
|
-
// ADRs
|
|
111
184
|
const adrs = (adrsRes?.data?.results || []).filter((r) => r.memory?.tags?.includes("adr"));
|
|
112
185
|
if (adrs.length > 0) {
|
|
113
186
|
result += `## ADRs (${adrs.length})\n`;
|
|
@@ -117,7 +190,6 @@ export function createSuggestionTools(projectName) {
|
|
|
117
190
|
}
|
|
118
191
|
result += "\n";
|
|
119
192
|
}
|
|
120
|
-
// Graph dependencies
|
|
121
193
|
const graphResults = graphRes?.data?.results || graphRes?.data?.directResults || [];
|
|
122
194
|
const connectedFiles = graphRes?.data?.connectedFiles || graphRes?.data?.expandedResults || [];
|
|
123
195
|
if (graphResults.length > 0 || connectedFiles.length > 0) {
|
|
@@ -136,6 +208,26 @@ export function createSuggestionTools(projectName) {
|
|
|
136
208
|
return result;
|
|
137
209
|
},
|
|
138
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);
|
|
229
|
+
},
|
|
230
|
+
},
|
|
139
231
|
{
|
|
140
232
|
name: "get_contextual_suggestions",
|
|
141
233
|
description: `Get contextual suggestions based on current work context for ${projectName}. Returns relevant suggestions, triggers, and related memories.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowley/rag-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Universal RAG MCP Server for any project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"dev": "tsc -w",
|
|
16
16
|
"start": "node dist/index.js",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"test": "vitest run --passWithNoTests",
|
|
19
|
+
"test:watch": "vitest"
|
|
18
20
|
},
|
|
19
21
|
"keywords": [
|
|
20
22
|
"mcp",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/node": "^20.10.0",
|
|
41
|
-
"typescript": "^5.3.0"
|
|
43
|
+
"typescript": "^5.3.0",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
42
45
|
}
|
|
43
46
|
}
|