@crowley/rag-mcp 1.2.1 → 1.5.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/context-enrichment.d.ts +6 -0
- package/dist/context-enrichment.js +63 -6
- package/dist/schemas.d.ts +61 -60
- package/dist/schemas.js +1 -2
- package/dist/tool-middleware.js +58 -6
- package/dist/tools/agents.js +68 -0
- package/dist/tools/memory.js +4 -1
- package/dist/types.d.ts +3 -3
- package/dist/validation-hooks.d.ts +40 -0
- package/dist/validation-hooks.js +108 -0
- package/package.json +2 -3
|
@@ -19,7 +19,13 @@ export declare const DEFAULT_ENRICHABLE_TOOLS: Set<string>;
|
|
|
19
19
|
export declare const DEFAULT_SKIP_TOOLS: Set<string>;
|
|
20
20
|
export declare class ContextEnricher {
|
|
21
21
|
private config;
|
|
22
|
+
private cache;
|
|
23
|
+
private static CACHE_TTL_MS;
|
|
22
24
|
constructor(config?: Partial<EnrichmentConfig>);
|
|
25
|
+
/**
|
|
26
|
+
* Clear enrichment cache (call on session end).
|
|
27
|
+
*/
|
|
28
|
+
clearCache(): void;
|
|
23
29
|
/**
|
|
24
30
|
* Before hook: auto-recall relevant memories/patterns/ADRs.
|
|
25
31
|
* Returns a context prefix string or null if nothing relevant found.
|
|
@@ -45,6 +45,8 @@ export const DEFAULT_SKIP_TOOLS = new Set([
|
|
|
45
45
|
]);
|
|
46
46
|
export class ContextEnricher {
|
|
47
47
|
config;
|
|
48
|
+
cache = new Map();
|
|
49
|
+
static CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
48
50
|
constructor(config = {}) {
|
|
49
51
|
this.config = {
|
|
50
52
|
enrichableTools: config.enrichableTools ?? DEFAULT_ENRICHABLE_TOOLS,
|
|
@@ -54,6 +56,12 @@ export class ContextEnricher {
|
|
|
54
56
|
timeoutMs: config.timeoutMs ?? 2000,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Clear enrichment cache (call on session end).
|
|
61
|
+
*/
|
|
62
|
+
clearCache() {
|
|
63
|
+
this.cache.clear();
|
|
64
|
+
}
|
|
57
65
|
/**
|
|
58
66
|
* Before hook: auto-recall relevant memories/patterns/ADRs.
|
|
59
67
|
* Returns a context prefix string or null if nothing relevant found.
|
|
@@ -68,11 +76,29 @@ export class ContextEnricher {
|
|
|
68
76
|
const query = this.extractQuery(args);
|
|
69
77
|
if (!query)
|
|
70
78
|
return null;
|
|
79
|
+
// Check per-session cache
|
|
80
|
+
const cacheKey = `${ctx.activeSessionId || 'no-session'}:${query.slice(0, 100)}`;
|
|
81
|
+
const cached = this.cache.get(cacheKey);
|
|
82
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
83
|
+
return cached.result;
|
|
84
|
+
}
|
|
71
85
|
try {
|
|
72
86
|
const memories = await this.recallWithTimeout(query, ctx);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
const result = memories.length === 0 ? null : this.formatContext(memories);
|
|
88
|
+
// Store in cache
|
|
89
|
+
this.cache.set(cacheKey, {
|
|
90
|
+
result,
|
|
91
|
+
expiresAt: Date.now() + ContextEnricher.CACHE_TTL_MS,
|
|
92
|
+
});
|
|
93
|
+
// Evict expired entries lazily (every 50 calls)
|
|
94
|
+
if (this.cache.size > 100) {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
for (const [key, entry] of this.cache) {
|
|
97
|
+
if (now > entry.expiresAt)
|
|
98
|
+
this.cache.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
76
102
|
}
|
|
77
103
|
catch {
|
|
78
104
|
// Enrichment should never break tool calls
|
|
@@ -150,8 +176,10 @@ export class ContextEnricher {
|
|
|
150
176
|
const controller = new AbortController();
|
|
151
177
|
const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
152
178
|
try {
|
|
153
|
-
// Parallel recall: general memories + decisions/ADRs
|
|
154
|
-
const
|
|
179
|
+
// Parallel recall: general memories + decisions/ADRs + LTM (when enabled)
|
|
180
|
+
const graphRecallEnabled = process.env.GRAPH_RECALL_ENABLED === "true";
|
|
181
|
+
const consolidationEnabled = process.env.CONSOLIDATION_ENABLED === "true";
|
|
182
|
+
const recalls = [
|
|
155
183
|
ctx.api
|
|
156
184
|
.post("/api/memory/recall-durable", {
|
|
157
185
|
projectName: ctx.projectName,
|
|
@@ -168,7 +196,19 @@ export class ContextEnricher {
|
|
|
168
196
|
type: "decision",
|
|
169
197
|
}, { signal: controller.signal })
|
|
170
198
|
.catch(() => null),
|
|
171
|
-
]
|
|
199
|
+
];
|
|
200
|
+
// Phase 2+4: also recall from LTM (episodic+semantic with Ebbinghaus decay)
|
|
201
|
+
if (consolidationEnabled) {
|
|
202
|
+
recalls.push(ctx.api
|
|
203
|
+
.post("/api/memory/recall-ltm", {
|
|
204
|
+
projectName: ctx.projectName,
|
|
205
|
+
query,
|
|
206
|
+
limit: this.config.maxAutoRecall,
|
|
207
|
+
graphRecall: graphRecallEnabled,
|
|
208
|
+
}, { signal: controller.signal })
|
|
209
|
+
.catch(() => null));
|
|
210
|
+
}
|
|
211
|
+
const [memoriesRes, decisionsRes, ltmRes] = await Promise.all(recalls);
|
|
172
212
|
const memories = [];
|
|
173
213
|
const seenIds = new Set();
|
|
174
214
|
// Process general memories
|
|
@@ -197,6 +237,23 @@ export class ContextEnricher {
|
|
|
197
237
|
}
|
|
198
238
|
}
|
|
199
239
|
}
|
|
240
|
+
// Process LTM results (episodic + semantic)
|
|
241
|
+
if (ltmRes?.data?.results) {
|
|
242
|
+
for (const r of ltmRes.data.results) {
|
|
243
|
+
const mem = r.memory;
|
|
244
|
+
const id = mem?.id;
|
|
245
|
+
if (r.score >= this.config.minRelevance && id && !seenIds.has(id)) {
|
|
246
|
+
seenIds.add(id);
|
|
247
|
+
memories.push({
|
|
248
|
+
type: mem?.subtype || mem?.type || "insight",
|
|
249
|
+
content: mem?.content || "",
|
|
250
|
+
score: r.score,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Sort by score and limit
|
|
256
|
+
memories.sort((a, b) => b.score - a.score);
|
|
200
257
|
return memories.slice(0, this.config.maxAutoRecall + 2);
|
|
201
258
|
}
|
|
202
259
|
finally {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -11,40 +11,57 @@ import type { ToolInputSchema } from "./types.js";
|
|
|
11
11
|
* Used during Phase 2 migration while ToolRegistry still expects raw JSON Schema.
|
|
12
12
|
* Phase 3 passes Zod schemas directly to McpServer.registerTool().
|
|
13
13
|
*/
|
|
14
|
-
export declare function zodToInputSchema(schema: z.ZodObject<z.
|
|
14
|
+
export declare function zodToInputSchema(schema: z.ZodObject<Record<string, z.ZodType>>): ToolInputSchema;
|
|
15
15
|
export declare const QueryStr: z.ZodString;
|
|
16
16
|
export declare const Limit: z.ZodDefault<z.ZodNumber>;
|
|
17
17
|
export declare const Offset: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
export declare const FilePath: z.ZodString;
|
|
19
|
-
export declare const FilePaths: z.ZodArray<z.ZodString
|
|
19
|
+
export declare const FilePaths: z.ZodArray<z.ZodString>;
|
|
20
20
|
export declare const Content: z.ZodString;
|
|
21
21
|
export declare const CollectionSuffix: z.ZodString;
|
|
22
|
-
export declare const MemoryType: z.ZodEnum<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
export declare const MemoryType: z.ZodEnum<{
|
|
23
|
+
decision: "decision";
|
|
24
|
+
insight: "insight";
|
|
25
|
+
todo: "todo";
|
|
26
|
+
pattern: "pattern";
|
|
27
|
+
adr: "adr";
|
|
28
|
+
architecture: "architecture";
|
|
29
|
+
tech_debt: "tech_debt";
|
|
30
|
+
convention: "convention";
|
|
31
|
+
bug_fix: "bug_fix";
|
|
32
|
+
optimization: "optimization";
|
|
33
|
+
}>;
|
|
34
|
+
export declare const ResponseFormat: z.ZodDefault<z.ZodEnum<{
|
|
35
|
+
markdown: "markdown";
|
|
36
|
+
json: "json";
|
|
37
|
+
}>>;
|
|
38
|
+
export declare const Importance: z.ZodDefault<z.ZodEnum<{
|
|
39
|
+
low: "low";
|
|
40
|
+
medium: "medium";
|
|
41
|
+
high: "high";
|
|
42
|
+
critical: "critical";
|
|
43
|
+
}>>;
|
|
44
|
+
export declare const Priority: z.ZodEnum<{
|
|
45
|
+
low: "low";
|
|
46
|
+
medium: "medium";
|
|
47
|
+
high: "high";
|
|
48
|
+
critical: "critical";
|
|
49
|
+
}>;
|
|
50
|
+
export declare const Severity: z.ZodEnum<{
|
|
51
|
+
low: "low";
|
|
52
|
+
medium: "medium";
|
|
53
|
+
high: "high";
|
|
54
|
+
critical: "critical";
|
|
55
|
+
}>;
|
|
27
56
|
export declare const PaginationParams: z.ZodObject<{
|
|
28
57
|
limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
29
58
|
offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
30
|
-
},
|
|
31
|
-
limit?: number | undefined;
|
|
32
|
-
offset?: number | undefined;
|
|
33
|
-
}, {
|
|
34
|
-
limit?: number | undefined;
|
|
35
|
-
offset?: number | undefined;
|
|
36
|
-
}>;
|
|
59
|
+
}, z.core.$strip>;
|
|
37
60
|
export declare const SearchFilters: z.ZodOptional<z.ZodObject<{
|
|
38
61
|
file_type: z.ZodOptional<z.ZodString>;
|
|
39
62
|
directory: z.ZodOptional<z.ZodString>;
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
directory?: string | undefined;
|
|
43
|
-
}, {
|
|
44
|
-
file_type?: string | undefined;
|
|
45
|
-
directory?: string | undefined;
|
|
46
|
-
}>>;
|
|
47
|
-
export declare const Tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
63
|
+
}, z.core.$strip>>;
|
|
64
|
+
export declare const Tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
48
65
|
export declare const Confidence: z.ZodOptional<z.ZodNumber>;
|
|
49
66
|
/** Base shape for search tools: query + optional limit + optional filters */
|
|
50
67
|
export declare const SearchInput: z.ZodObject<{
|
|
@@ -53,45 +70,29 @@ export declare const SearchInput: z.ZodObject<{
|
|
|
53
70
|
filters: z.ZodOptional<z.ZodObject<{
|
|
54
71
|
file_type: z.ZodOptional<z.ZodString>;
|
|
55
72
|
directory: z.ZodOptional<z.ZodString>;
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
directory?: string | undefined;
|
|
59
|
-
}, {
|
|
60
|
-
file_type?: string | undefined;
|
|
61
|
-
directory?: string | undefined;
|
|
62
|
-
}>>;
|
|
63
|
-
}, "strip", z.ZodTypeAny, {
|
|
64
|
-
query: string;
|
|
65
|
-
limit?: number | undefined;
|
|
66
|
-
filters?: {
|
|
67
|
-
file_type?: string | undefined;
|
|
68
|
-
directory?: string | undefined;
|
|
69
|
-
} | undefined;
|
|
70
|
-
}, {
|
|
71
|
-
query: string;
|
|
72
|
-
limit?: number | undefined;
|
|
73
|
-
filters?: {
|
|
74
|
-
file_type?: string | undefined;
|
|
75
|
-
directory?: string | undefined;
|
|
76
|
-
} | undefined;
|
|
77
|
-
}>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>;
|
|
78
75
|
/** Base shape for memory record tools */
|
|
79
76
|
export declare const MemoryRecordInput: z.ZodObject<{
|
|
80
77
|
content: z.ZodString;
|
|
81
|
-
type: z.ZodEnum<
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
type: z.ZodEnum<{
|
|
79
|
+
decision: "decision";
|
|
80
|
+
insight: "insight";
|
|
81
|
+
todo: "todo";
|
|
82
|
+
pattern: "pattern";
|
|
83
|
+
adr: "adr";
|
|
84
|
+
architecture: "architecture";
|
|
85
|
+
tech_debt: "tech_debt";
|
|
86
|
+
convention: "convention";
|
|
87
|
+
bug_fix: "bug_fix";
|
|
88
|
+
optimization: "optimization";
|
|
89
|
+
}>;
|
|
90
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
91
|
+
importance: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
92
|
+
low: "low";
|
|
93
|
+
medium: "medium";
|
|
94
|
+
high: "high";
|
|
95
|
+
critical: "critical";
|
|
96
|
+
}>>>;
|
|
84
97
|
context: z.ZodOptional<z.ZodString>;
|
|
85
|
-
},
|
|
86
|
-
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "architecture" | "tech_debt" | "convention" | "bug_fix" | "optimization";
|
|
87
|
-
content: string;
|
|
88
|
-
context?: string | undefined;
|
|
89
|
-
tags?: string[] | undefined;
|
|
90
|
-
importance?: "low" | "medium" | "high" | "critical" | undefined;
|
|
91
|
-
}, {
|
|
92
|
-
type: "decision" | "insight" | "todo" | "adr" | "pattern" | "architecture" | "tech_debt" | "convention" | "bug_fix" | "optimization";
|
|
93
|
-
content: string;
|
|
94
|
-
context?: string | undefined;
|
|
95
|
-
tags?: string[] | undefined;
|
|
96
|
-
importance?: "low" | "medium" | "high" | "critical" | undefined;
|
|
97
|
-
}>;
|
|
98
|
+
}, z.core.$strip>;
|
package/dist/schemas.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* from raw JSON Schema objects to Zod-based inputSchema definitions.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
9
8
|
// ── JSON Schema conversion ──────────────────────────────────
|
|
10
9
|
/**
|
|
11
10
|
* Convert a Zod object schema to the MCP ToolInputSchema format.
|
|
@@ -13,7 +12,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
13
12
|
* Phase 3 passes Zod schemas directly to McpServer.registerTool().
|
|
14
13
|
*/
|
|
15
14
|
export function zodToInputSchema(schema) {
|
|
16
|
-
const jsonSchema =
|
|
15
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
17
16
|
return {
|
|
18
17
|
type: "object",
|
|
19
18
|
properties: (jsonSchema.properties ?? {}),
|
package/dist/tool-middleware.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* During Phase 2 migration, ToolRegistry continues to use its own copy.
|
|
9
9
|
* Phase 3 replaces ToolRegistry with wrapHandler() + McpServer.registerTool().
|
|
10
10
|
*/
|
|
11
|
+
import { validationPipeline } from "./validation-hooks.js";
|
|
11
12
|
// ── Timeouts ────────────────────────────────────────────────
|
|
12
13
|
/** Default tool timeout in milliseconds */
|
|
13
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -154,6 +155,43 @@ export function trackUsage(name, args, startTime, success, result, errorMessage,
|
|
|
154
155
|
})
|
|
155
156
|
.catch(() => { });
|
|
156
157
|
}
|
|
158
|
+
// ── Sensory buffer ─────────────────────────────────────────
|
|
159
|
+
/** Extract file paths from tool args */
|
|
160
|
+
function extractFiles(args) {
|
|
161
|
+
const files = [];
|
|
162
|
+
for (const key of ['file', 'filePath', 'currentFile', 'path']) {
|
|
163
|
+
const v = args[key];
|
|
164
|
+
if (typeof v === 'string' && v.length > 0)
|
|
165
|
+
files.push(v);
|
|
166
|
+
}
|
|
167
|
+
const arr = args.affectedFiles || args.files;
|
|
168
|
+
if (Array.isArray(arr)) {
|
|
169
|
+
for (const f of arr) {
|
|
170
|
+
if (typeof f === 'string')
|
|
171
|
+
files.push(f);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return files.slice(0, 20);
|
|
175
|
+
}
|
|
176
|
+
/** Fire-and-forget: capture tool event in sensory buffer */
|
|
177
|
+
function appendToSensoryBuffer(name, args, startTime, success, resultText, ctx) {
|
|
178
|
+
if (!ctx.activeSessionId)
|
|
179
|
+
return;
|
|
180
|
+
if (TRACKING_EXCLUDE.has(name))
|
|
181
|
+
return;
|
|
182
|
+
ctx.api
|
|
183
|
+
.post("/api/sensory/append", {
|
|
184
|
+
projectName: ctx.projectName,
|
|
185
|
+
sessionId: ctx.activeSessionId,
|
|
186
|
+
toolName: name,
|
|
187
|
+
inputSummary: summarizeInput(name, args).slice(0, 500),
|
|
188
|
+
outputSummary: (resultText || "").slice(0, 500),
|
|
189
|
+
filesTouched: extractFiles(args),
|
|
190
|
+
success,
|
|
191
|
+
durationMs: Date.now() - startTime,
|
|
192
|
+
})
|
|
193
|
+
.catch(() => { });
|
|
194
|
+
}
|
|
157
195
|
// ── Error formatting ────────────────────────────────────────
|
|
158
196
|
/** Format an error caught during tool execution */
|
|
159
197
|
export function formatToolError(error, ctx) {
|
|
@@ -182,13 +220,22 @@ export function wrapHandler(name, handler, deps) {
|
|
|
182
220
|
}
|
|
183
221
|
const startTime = Date.now();
|
|
184
222
|
try {
|
|
223
|
+
// Validate: run PreToolUse hooks
|
|
224
|
+
const validation = await validationPipeline.validate(name, args, ctx);
|
|
225
|
+
if (!validation.allowed) {
|
|
226
|
+
return `Blocked: ${validation.reason || 'validation failed'}`;
|
|
227
|
+
}
|
|
228
|
+
const validatedArgs = validation.modifiedArgs || args;
|
|
229
|
+
const warningPrefix = validation.warnings?.length
|
|
230
|
+
? `⚠️ ${validation.warnings.join(' | ')}\n\n`
|
|
231
|
+
: '';
|
|
185
232
|
// Before: auto-enrich context
|
|
186
233
|
const contextPrefix = ctx.enrichmentEnabled && deps.enricher
|
|
187
|
-
? await deps.enricher.before(name,
|
|
234
|
+
? await deps.enricher.before(name, validatedArgs, ctx)
|
|
188
235
|
: null;
|
|
189
236
|
// Execute original handler (with timeout)
|
|
190
237
|
const timeoutMs = TOOL_TIMEOUTS[name] ?? DEFAULT_TIMEOUT_MS;
|
|
191
|
-
const result = await withTimeout(handler(
|
|
238
|
+
const result = await withTimeout(handler(validatedArgs, ctx), timeoutMs, name);
|
|
192
239
|
// Extract text for tracking/enrichment
|
|
193
240
|
const text = typeof result === "string" ? result : result.text;
|
|
194
241
|
// After: track interaction (fire-and-forget)
|
|
@@ -197,12 +244,15 @@ export function wrapHandler(name, handler, deps) {
|
|
|
197
244
|
}
|
|
198
245
|
// Track usage (fire-and-forget)
|
|
199
246
|
trackUsage(name, args, startTime, true, text, undefined, ctx);
|
|
200
|
-
//
|
|
201
|
-
|
|
247
|
+
// Capture in sensory buffer (fire-and-forget)
|
|
248
|
+
appendToSensoryBuffer(name, args, startTime, true, text, ctx);
|
|
249
|
+
// Prepend context/warnings if available
|
|
250
|
+
const prefix = [warningPrefix, contextPrefix].filter(Boolean).join('');
|
|
251
|
+
if (prefix) {
|
|
202
252
|
if (typeof result === "string") {
|
|
203
|
-
return
|
|
253
|
+
return prefix + result;
|
|
204
254
|
}
|
|
205
|
-
return { text:
|
|
255
|
+
return { text: prefix + result.text, structured: result.structured };
|
|
206
256
|
}
|
|
207
257
|
return result;
|
|
208
258
|
}
|
|
@@ -210,6 +260,8 @@ export function wrapHandler(name, handler, deps) {
|
|
|
210
260
|
const errorMessage = formatToolError(error, ctx);
|
|
211
261
|
// Track failed usage (fire-and-forget)
|
|
212
262
|
trackUsage(name, args, startTime, false, "", errorMessage, ctx);
|
|
263
|
+
// Capture failure in sensory buffer (fire-and-forget)
|
|
264
|
+
appendToSensoryBuffer(name, args, startTime, false, errorMessage, ctx);
|
|
213
265
|
return errorMessage;
|
|
214
266
|
}
|
|
215
267
|
};
|
package/dist/tools/agents.js
CHANGED
|
@@ -59,6 +59,74 @@ export function createAgentTools(projectName) {
|
|
|
59
59
|
return result;
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: "tribunal_debate",
|
|
64
|
+
description: `Run an adversarial debate on a topic for ${projectName}. Multiple advocates argue positions, a judge renders a verdict. Use for architecture decisions, tech choices, or code approach trade-offs.`,
|
|
65
|
+
schema: z.object({
|
|
66
|
+
topic: z.string().describe("The debate topic (e.g., 'Should we use REST or gRPC for the new API?')"),
|
|
67
|
+
positions: z.array(z.string()).min(2).max(4).describe("Positions to debate (2-4 options, e.g., ['REST', 'gRPC'])"),
|
|
68
|
+
context: z.string().optional().describe("Additional context for the debate"),
|
|
69
|
+
maxRounds: z.coerce.number().optional().describe("Number of rebuttal rounds (default: 1, max: 3)"),
|
|
70
|
+
useCodeContext: z.boolean().optional().describe("Fetch relevant code, ADRs, and patterns as evidence (default: false)"),
|
|
71
|
+
autoRecord: z.boolean().optional().describe("Save verdict as a decision in project memory (default: false)"),
|
|
72
|
+
}),
|
|
73
|
+
annotations: TOOL_ANNOTATIONS["tribunal_debate"] || { priority: 0.4, readOnlyHint: true },
|
|
74
|
+
handler: async (args, ctx) => {
|
|
75
|
+
const { topic, positions, context, maxRounds, useCodeContext, autoRecord } = args;
|
|
76
|
+
const response = await ctx.api.post("/api/tribunal/debate", {
|
|
77
|
+
projectName: ctx.projectName,
|
|
78
|
+
topic,
|
|
79
|
+
positions,
|
|
80
|
+
context,
|
|
81
|
+
maxRounds,
|
|
82
|
+
useCodeContext,
|
|
83
|
+
autoRecord,
|
|
84
|
+
});
|
|
85
|
+
const data = response.data;
|
|
86
|
+
// Format result as markdown
|
|
87
|
+
let result = `## Tribunal Debate: ${data.topic}\n`;
|
|
88
|
+
result += `**Status:** ${data.status}`;
|
|
89
|
+
result += ` | **Duration:** ${Math.round(data.durationMs / 1000)}s`;
|
|
90
|
+
result += ` | **Cost:** ~$${data.cost?.estimatedUsd?.toFixed(3) || '?'}\n\n`;
|
|
91
|
+
// Phases summary
|
|
92
|
+
if (data.phases) {
|
|
93
|
+
result += `### Phases\n`;
|
|
94
|
+
for (const phase of data.phases) {
|
|
95
|
+
result += `- **${phase.name}**: ${Math.round(phase.durationMs / 1000)}s, ${phase.tokens} tokens\n`;
|
|
96
|
+
}
|
|
97
|
+
result += `\n`;
|
|
98
|
+
}
|
|
99
|
+
// Arguments
|
|
100
|
+
if (data.arguments && data.arguments.length > 0) {
|
|
101
|
+
result += `### Arguments\n`;
|
|
102
|
+
for (const arg of data.arguments) {
|
|
103
|
+
const label = arg.round === 0 ? 'Initial' : `Rebuttal R${arg.round}`;
|
|
104
|
+
result += `#### ${arg.position} (${label})\n${arg.content}\n\n`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Verdict
|
|
108
|
+
if (data.verdict) {
|
|
109
|
+
result += `### Verdict\n`;
|
|
110
|
+
result += `**Recommendation:** ${data.verdict.recommendation}\n`;
|
|
111
|
+
result += `**Confidence:** ${data.verdict.confidence}\n\n`;
|
|
112
|
+
if (data.verdict.scores) {
|
|
113
|
+
result += `**Scores:**\n`;
|
|
114
|
+
for (const s of data.verdict.scores) {
|
|
115
|
+
result += `- ${s.position}: ${s.score}/10\n`;
|
|
116
|
+
}
|
|
117
|
+
result += `\n`;
|
|
118
|
+
}
|
|
119
|
+
result += `**Reasoning:**\n${data.verdict.reasoning}\n\n`;
|
|
120
|
+
result += `**Trade-offs:**\n${data.verdict.tradeoffs}\n\n`;
|
|
121
|
+
result += `**Dissent:**\n${data.verdict.dissent}\n\n`;
|
|
122
|
+
result += `**Conditions:**\n${data.verdict.conditions}\n`;
|
|
123
|
+
}
|
|
124
|
+
if (data.error) {
|
|
125
|
+
result += `\n**Error:** ${data.error}\n`;
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
62
130
|
{
|
|
63
131
|
name: "get_agent_types",
|
|
64
132
|
description: `List available agent types for ${projectName} with descriptions.`,
|
package/dist/tools/memory.js
CHANGED
|
@@ -61,19 +61,22 @@ export function createMemoryTools(projectName) {
|
|
|
61
61
|
description: "Retrieve relevant memories based on context. Searches agent memory for past decisions, insights, and notes related to the query.",
|
|
62
62
|
schema: z.object({
|
|
63
63
|
query: z.string().describe("What to recall (semantic search)"),
|
|
64
|
-
type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "all"]).optional().describe("Filter by memory type (default: all)"),
|
|
64
|
+
type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "procedure", "all"]).optional().describe("Filter by memory type (default: all)"),
|
|
65
65
|
limit: z.coerce.number().optional().describe("Max memories to retrieve (default: 5)"),
|
|
66
|
+
graphRecall: z.boolean().optional().describe("Enable graph-aware recall with spreading activation (default: false)"),
|
|
66
67
|
}),
|
|
67
68
|
annotations: TOOL_ANNOTATIONS["recall"],
|
|
68
69
|
handler: async (args, ctx) => {
|
|
69
70
|
const query = args.query;
|
|
70
71
|
const type = args.type || "all";
|
|
71
72
|
const limit = args.limit || 5;
|
|
73
|
+
const graphRecall = args.graphRecall || false;
|
|
72
74
|
const response = await ctx.api.post("/api/memory/recall", {
|
|
73
75
|
projectName: ctx.projectName,
|
|
74
76
|
query,
|
|
75
77
|
type,
|
|
76
78
|
limit,
|
|
79
|
+
graphRecall,
|
|
77
80
|
});
|
|
78
81
|
const results = response.data.results || [];
|
|
79
82
|
if (results.length === 0) {
|
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared types for the MCP server tool modules.
|
|
3
3
|
*/
|
|
4
4
|
import type { AxiosInstance } from "axios";
|
|
5
|
-
import type {
|
|
5
|
+
import type { z } from "zod";
|
|
6
6
|
import type { ToolAnnotations } from "./annotations.js";
|
|
7
7
|
/** MCP tool input schema shape (raw JSON Schema) */
|
|
8
8
|
export interface ToolInputSchema {
|
|
@@ -42,8 +42,8 @@ export interface ToolModule {
|
|
|
42
42
|
export interface ToolSpec {
|
|
43
43
|
name: string;
|
|
44
44
|
description: string;
|
|
45
|
-
schema: ZodObject<
|
|
46
|
-
outputSchema?: ZodObject<
|
|
45
|
+
schema: z.ZodObject<Record<string, z.ZodType>>;
|
|
46
|
+
outputSchema?: z.ZodObject<Record<string, z.ZodType>>;
|
|
47
47
|
annotations?: ToolAnnotations;
|
|
48
48
|
handler: ToolHandler;
|
|
49
49
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreToolUse Validation Hooks
|
|
3
|
+
*
|
|
4
|
+
* Validation pipeline that runs before tool execution.
|
|
5
|
+
* Hooks can block execution, modify args, or add warnings.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by claude-quanta-plugin's PreToolUse hooks pattern.
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolContext } from "./types.js";
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
allowed: boolean;
|
|
12
|
+
reason?: string;
|
|
13
|
+
warnings?: string[];
|
|
14
|
+
modifiedArgs?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export type ValidationHook = (toolName: string, args: Record<string, unknown>, ctx: ToolContext) => Promise<ValidationResult> | ValidationResult;
|
|
17
|
+
/**
|
|
18
|
+
* Prevent destructive operations without explicit confirmation.
|
|
19
|
+
* Blocks: index_codebase with force=true, forget (memory deletion).
|
|
20
|
+
*/
|
|
21
|
+
export declare const destructiveGuard: ValidationHook;
|
|
22
|
+
/**
|
|
23
|
+
* Validate required fields for critical tools.
|
|
24
|
+
*/
|
|
25
|
+
export declare const requiredFieldsValidator: ValidationHook;
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize inputs — trim strings, enforce reasonable limits.
|
|
28
|
+
*/
|
|
29
|
+
export declare const inputSanitizer: ValidationHook;
|
|
30
|
+
export declare class ValidationPipeline {
|
|
31
|
+
private hooks;
|
|
32
|
+
constructor(hooks?: ValidationHook[]);
|
|
33
|
+
addHook(hook: ValidationHook): void;
|
|
34
|
+
/**
|
|
35
|
+
* Run all hooks in sequence. First rejection stops the pipeline.
|
|
36
|
+
* Args can be modified by hooks (each hook sees the potentially modified args).
|
|
37
|
+
*/
|
|
38
|
+
validate(toolName: string, args: Record<string, unknown>, ctx: ToolContext): Promise<ValidationResult>;
|
|
39
|
+
}
|
|
40
|
+
export declare const validationPipeline: ValidationPipeline;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreToolUse Validation Hooks
|
|
3
|
+
*
|
|
4
|
+
* Validation pipeline that runs before tool execution.
|
|
5
|
+
* Hooks can block execution, modify args, or add warnings.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by claude-quanta-plugin's PreToolUse hooks pattern.
|
|
8
|
+
*/
|
|
9
|
+
// ── Built-in Hooks ──────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Prevent destructive operations without explicit confirmation.
|
|
12
|
+
* Blocks: index_codebase with force=true, forget (memory deletion).
|
|
13
|
+
*/
|
|
14
|
+
export const destructiveGuard = (toolName, args) => {
|
|
15
|
+
if (toolName === 'index_codebase' && args.force === true) {
|
|
16
|
+
return {
|
|
17
|
+
allowed: true,
|
|
18
|
+
warnings: ['Force reindex will delete and rebuild the entire index. This may take several minutes.'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (toolName === 'forget') {
|
|
22
|
+
return {
|
|
23
|
+
allowed: true,
|
|
24
|
+
warnings: ['This will permanently delete a memory entry.'],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { allowed: true };
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Validate required fields for critical tools.
|
|
31
|
+
*/
|
|
32
|
+
export const requiredFieldsValidator = (toolName, args) => {
|
|
33
|
+
// Search tools must have a query
|
|
34
|
+
const searchTools = ['search_codebase', 'hybrid_search', 'search_docs', 'find_feature', 'ask_codebase'];
|
|
35
|
+
if (searchTools.includes(toolName)) {
|
|
36
|
+
const query = args.query || args.question;
|
|
37
|
+
if (!query || (typeof query === 'string' && query.trim().length < 3)) {
|
|
38
|
+
return { allowed: false, reason: `${toolName} requires a query of at least 3 characters` };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Memory tools must have content
|
|
42
|
+
if (toolName === 'remember' && (!args.content || (typeof args.content === 'string' && args.content.trim().length < 10))) {
|
|
43
|
+
return { allowed: false, reason: 'remember requires content of at least 10 characters' };
|
|
44
|
+
}
|
|
45
|
+
return { allowed: true };
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize inputs — trim strings, enforce reasonable limits.
|
|
49
|
+
*/
|
|
50
|
+
export const inputSanitizer = (toolName, args) => {
|
|
51
|
+
const modified = { ...args };
|
|
52
|
+
let changed = false;
|
|
53
|
+
// Trim string values
|
|
54
|
+
for (const [key, value] of Object.entries(modified)) {
|
|
55
|
+
if (typeof value === 'string' && value !== value.trim()) {
|
|
56
|
+
modified[key] = value.trim();
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Cap limit params to prevent excessive results
|
|
61
|
+
if (typeof modified.limit === 'number' && modified.limit > 50) {
|
|
62
|
+
modified.limit = 50;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
return changed
|
|
66
|
+
? { allowed: true, modifiedArgs: modified }
|
|
67
|
+
: { allowed: true };
|
|
68
|
+
};
|
|
69
|
+
// ── Validation Pipeline ─────────────────────────────────────
|
|
70
|
+
export class ValidationPipeline {
|
|
71
|
+
hooks = [];
|
|
72
|
+
constructor(hooks) {
|
|
73
|
+
this.hooks = hooks || [
|
|
74
|
+
destructiveGuard,
|
|
75
|
+
requiredFieldsValidator,
|
|
76
|
+
inputSanitizer,
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
addHook(hook) {
|
|
80
|
+
this.hooks.push(hook);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run all hooks in sequence. First rejection stops the pipeline.
|
|
84
|
+
* Args can be modified by hooks (each hook sees the potentially modified args).
|
|
85
|
+
*/
|
|
86
|
+
async validate(toolName, args, ctx) {
|
|
87
|
+
let currentArgs = { ...args };
|
|
88
|
+
const allWarnings = [];
|
|
89
|
+
for (const hook of this.hooks) {
|
|
90
|
+
const result = await hook(toolName, currentArgs, ctx);
|
|
91
|
+
if (!result.allowed) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
if (result.warnings) {
|
|
95
|
+
allWarnings.push(...result.warnings);
|
|
96
|
+
}
|
|
97
|
+
if (result.modifiedArgs) {
|
|
98
|
+
currentArgs = result.modifiedArgs;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
allowed: true,
|
|
103
|
+
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
|
104
|
+
modifiedArgs: currentArgs !== args ? currentArgs : undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export const validationPipeline = new ValidationPipeline();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowley/rag-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Universal RAG MCP Server for any project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"axios": "^1.6.0",
|
|
37
37
|
"glob": "^11.0.0",
|
|
38
|
-
"zod": "^
|
|
39
|
-
"zod-to-json-schema": "^3.25.1"
|
|
38
|
+
"zod": "^4.0.0"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
42
41
|
"@types/node": "^20.10.0",
|