@crowley/rag-mcp 1.2.1 → 1.3.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/schemas.d.ts +61 -60
- package/dist/schemas.js +1 -2
- package/dist/tool-middleware.js +17 -6
- package/dist/tools/agents.js +68 -0
- 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
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;
|
|
@@ -182,13 +183,22 @@ export function wrapHandler(name, handler, deps) {
|
|
|
182
183
|
}
|
|
183
184
|
const startTime = Date.now();
|
|
184
185
|
try {
|
|
186
|
+
// Validate: run PreToolUse hooks
|
|
187
|
+
const validation = await validationPipeline.validate(name, args, ctx);
|
|
188
|
+
if (!validation.allowed) {
|
|
189
|
+
return `Blocked: ${validation.reason || 'validation failed'}`;
|
|
190
|
+
}
|
|
191
|
+
const validatedArgs = validation.modifiedArgs || args;
|
|
192
|
+
const warningPrefix = validation.warnings?.length
|
|
193
|
+
? `⚠️ ${validation.warnings.join(' | ')}\n\n`
|
|
194
|
+
: '';
|
|
185
195
|
// Before: auto-enrich context
|
|
186
196
|
const contextPrefix = ctx.enrichmentEnabled && deps.enricher
|
|
187
|
-
? await deps.enricher.before(name,
|
|
197
|
+
? await deps.enricher.before(name, validatedArgs, ctx)
|
|
188
198
|
: null;
|
|
189
199
|
// Execute original handler (with timeout)
|
|
190
200
|
const timeoutMs = TOOL_TIMEOUTS[name] ?? DEFAULT_TIMEOUT_MS;
|
|
191
|
-
const result = await withTimeout(handler(
|
|
201
|
+
const result = await withTimeout(handler(validatedArgs, ctx), timeoutMs, name);
|
|
192
202
|
// Extract text for tracking/enrichment
|
|
193
203
|
const text = typeof result === "string" ? result : result.text;
|
|
194
204
|
// After: track interaction (fire-and-forget)
|
|
@@ -197,12 +207,13 @@ export function wrapHandler(name, handler, deps) {
|
|
|
197
207
|
}
|
|
198
208
|
// Track usage (fire-and-forget)
|
|
199
209
|
trackUsage(name, args, startTime, true, text, undefined, ctx);
|
|
200
|
-
// Prepend context if available
|
|
201
|
-
|
|
210
|
+
// Prepend context/warnings if available
|
|
211
|
+
const prefix = [warningPrefix, contextPrefix].filter(Boolean).join('');
|
|
212
|
+
if (prefix) {
|
|
202
213
|
if (typeof result === "string") {
|
|
203
|
-
return
|
|
214
|
+
return prefix + result;
|
|
204
215
|
}
|
|
205
|
-
return { text:
|
|
216
|
+
return { text: prefix + result.text, structured: result.structured };
|
|
206
217
|
}
|
|
207
218
|
return result;
|
|
208
219
|
}
|
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/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.3.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",
|