@crowley/rag-mcp 1.2.0 → 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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { createMemoryTools } from '../../tools/memory';
2
+ import { createMemoryTools } from '../../tools/memory.js';
3
3
  function createMockCtx() {
4
4
  return {
5
5
  api: {
@@ -24,7 +24,7 @@ describe('Memory Tools', () => {
24
24
  ctx = createMockCtx();
25
25
  });
26
26
  function findTool(name) {
27
- return tools.find(t => t.name === name);
27
+ return tools.find((t) => t.name === name);
28
28
  }
29
29
  describe('remember', () => {
30
30
  it('stores memory and returns formatted result', async () => {
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.ZodRawShape>): ToolInputSchema;
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, "many">;
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<["decision", "insight", "pattern", "adr", "tech_debt", "todo", "architecture", "convention", "bug_fix", "optimization"]>;
23
- export declare const ResponseFormat: z.ZodDefault<z.ZodEnum<["json", "markdown"]>>;
24
- export declare const Importance: z.ZodDefault<z.ZodEnum<["low", "medium", "high", "critical"]>>;
25
- export declare const Priority: z.ZodEnum<["low", "medium", "high", "critical"]>;
26
- export declare const Severity: z.ZodEnum<["low", "medium", "high", "critical"]>;
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
41
- file_type?: string | undefined;
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
- }, "strip", z.ZodTypeAny, {
57
- file_type?: string | undefined;
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<["decision", "insight", "pattern", "adr", "tech_debt", "todo", "architecture", "convention", "bug_fix", "optimization"]>;
82
- tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
83
- importance: z.ZodOptional<z.ZodDefault<z.ZodEnum<["low", "medium", "high", "critical"]>>>;
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
- }, "strip", z.ZodTypeAny, {
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 = zodToJsonSchema(schema, { target: "openApi3" });
15
+ const jsonSchema = z.toJSONSchema(schema);
17
16
  return {
18
17
  type: "object",
19
18
  properties: (jsonSchema.properties ?? {}),
@@ -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, args, ctx)
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(args, ctx), timeoutMs, name);
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
- if (contextPrefix) {
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 contextPrefix + "\n\n" + result;
214
+ return prefix + result;
204
215
  }
205
- return { text: contextPrefix + "\n\n" + result.text, structured: result.structured };
216
+ return { text: prefix + result.text, structured: result.structured };
206
217
  }
207
218
  return result;
208
219
  }
@@ -15,9 +15,9 @@ export function createAdvancedTools(projectName) {
15
15
  description: `Consolidate duplicate memories for ${projectName}. Finds similar memories and merges them using LLM to reduce clutter.`,
16
16
  schema: z.object({
17
17
  type: z.string().optional().describe("Filter by memory type (decision, insight, context, todo, conversation, note, or all). Default: all"),
18
- threshold: z.number().optional().describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
18
+ threshold: z.coerce.number().optional().describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
19
19
  dryRun: z.boolean().optional().describe("If true, preview merge candidates without making changes (default: true)."),
20
- limit: z.number().optional().describe("Max clusters to process (default: 50)."),
20
+ limit: z.coerce.number().optional().describe("Max clusters to process (default: 50)."),
21
21
  }),
22
22
  annotations: TOOL_ANNOTATIONS["merge_memories"],
23
23
  handler: async (args, ctx) => {
@@ -65,7 +65,7 @@ export function createAdvancedTools(projectName) {
65
65
  currentFile: z.string().describe("Path of the file being edited"),
66
66
  currentCode: z.string().describe("Current code snippet or file content"),
67
67
  language: z.string().optional().describe("Programming language filter (optional)"),
68
- limit: z.number().optional().describe("Max results (default: 5)"),
68
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
69
69
  }),
70
70
  annotations: TOOL_ANNOTATIONS["get_completion_context"],
71
71
  handler: async (args, ctx) => {
@@ -108,7 +108,7 @@ export function createAdvancedTools(projectName) {
108
108
  currentFile: z.string().describe("Path of the file being edited"),
109
109
  currentCode: z.string().describe("Current code content"),
110
110
  language: z.string().optional().describe("Programming language filter (optional)"),
111
- limit: z.number().optional().describe("Max suggestions (default: 10)"),
111
+ limit: z.coerce.number().optional().describe("Max suggestions (default: 10)"),
112
112
  }),
113
113
  annotations: TOOL_ANNOTATIONS["get_import_suggestions"],
114
114
  handler: async (args, ctx) => {
@@ -147,7 +147,7 @@ export function createAdvancedTools(projectName) {
147
147
  typeName: z.string().optional().describe("Name of the type/interface/class to look up"),
148
148
  code: z.string().optional().describe("Code containing types to look up (alternative to typeName)"),
149
149
  currentFile: z.string().optional().describe("Current file to exclude from results"),
150
- limit: z.number().optional().describe("Max results per category (default: 5)"),
150
+ limit: z.coerce.number().optional().describe("Max results per category (default: 5)"),
151
151
  }),
152
152
  annotations: TOOL_ANNOTATIONS["get_type_context"],
153
153
  handler: async (args, ctx) => {
@@ -187,7 +187,7 @@ export function createAdvancedTools(projectName) {
187
187
  name: "get_behavior_patterns",
188
188
  description: `Analyze user workflow patterns for ${projectName}. Shows peak hours, tool preferences, common sequences, and session statistics.`,
189
189
  schema: z.object({
190
- days: z.number().optional().describe("Number of days to analyze (default: 7)"),
190
+ days: z.coerce.number().optional().describe("Number of days to analyze (default: 7)"),
191
191
  sessionId: z.string().optional().describe("Filter to a specific session (optional)"),
192
192
  }),
193
193
  annotations: TOOL_ANNOTATIONS["get_behavior_patterns"],
@@ -15,7 +15,7 @@ export function createAgentTools(projectName) {
15
15
  type: z.enum(["research", "review", "documentation", "refactor", "test"]).describe("Agent type: research, review, documentation, refactor, or test"),
16
16
  task: z.string().describe("The task for the agent to perform"),
17
17
  context: z.string().optional().describe("Optional additional context (code, requirements, etc.)"),
18
- maxIterations: z.number().optional().describe("Maximum ReAct iterations (default: varies by agent type)"),
18
+ maxIterations: z.coerce.number().optional().describe("Maximum ReAct iterations (default: varies by agent type)"),
19
19
  }),
20
20
  annotations: TOOL_ANNOTATIONS["run_agent"],
21
21
  handler: async (args, ctx) => {
@@ -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.`,
@@ -154,7 +154,7 @@ export function createAnalyticsTools(projectName) {
154
154
  description: `Enable scalar quantization on a ${projectName} collection to reduce memory usage.`,
155
155
  schema: z.object({
156
156
  collectionName: z.string().describe("Collection name to enable quantization on"),
157
- quantile: z.number().optional().describe("Quantile for quantization (0-1, default: 0.99)"),
157
+ quantile: z.coerce.number().optional().describe("Quantile for quantization (0-1, default: 0.99)"),
158
158
  }),
159
159
  annotations: TOOL_ANNOTATIONS["enable_quantization"],
160
160
  handler: async (args, ctx) => {
@@ -56,7 +56,7 @@ ${alternatives ? `## Alternatives Considered\n${alternatives}` : ""}`;
56
56
  schema: z.object({
57
57
  query: z.string().optional().describe("Search query (optional - returns all if empty)"),
58
58
  status: z.enum(["proposed", "accepted", "deprecated", "superseded", "all"]).optional().describe("Filter by status"),
59
- limit: z.number().optional().describe("Max results (default: 10)"),
59
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
60
60
  }),
61
61
  annotations: TOOL_ANNOTATIONS["get_adrs"],
62
62
  handler: async (args, ctx) => {
@@ -138,7 +138,7 @@ ${appliesTo ? `## Applies To\n${appliesTo}` : ""}`;
138
138
  schema: z.object({
139
139
  query: z.string().optional().describe("Search for patterns by name or description"),
140
140
  appliesTo: z.string().optional().describe("Filter by what patterns apply to (e.g., 'api', 'module')"),
141
- limit: z.number().optional().describe("Max results (default: 10)"),
141
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
142
142
  }),
143
143
  annotations: TOOL_ANNOTATIONS["get_patterns"],
144
144
  handler: async (args, ctx) => {
@@ -437,7 +437,7 @@ ${relatedAdr ? `## Related ADR\n${relatedAdr}` : ""}`;
437
437
  description: `List technical debt items for ${projectName}.`,
438
438
  schema: z.object({
439
439
  impact: z.enum(["low", "medium", "high", "critical", "all"]).optional().describe("Filter by impact"),
440
- limit: z.number().optional().describe("Max results (default: 10)"),
440
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
441
441
  }),
442
442
  annotations: TOOL_ANNOTATIONS["get_tech_debt"],
443
443
  handler: async (args, ctx) => {
package/dist/tools/ask.js CHANGED
@@ -99,7 +99,7 @@ export function createAskTools(projectName) {
99
99
  conversation: z.string().describe("The conversation text to analyze"),
100
100
  context: z.string().optional().describe("Additional context about the conversation"),
101
101
  autoSave: z.boolean().optional().describe("Automatically save extracted learnings (default: false)"),
102
- minConfidence: z.number().optional().describe("Minimum confidence threshold for learnings (0-1, default: 0.7)"),
102
+ minConfidence: z.coerce.number().optional().describe("Minimum confidence threshold for learnings (0-1, default: 0.7)"),
103
103
  }),
104
104
  annotations: TOOL_ANNOTATIONS["analyze_conversation"],
105
105
  handler: async (args, ctx) => {
@@ -15,8 +15,8 @@ export function createClusteringTools(projectName) {
15
15
  description: `Cluster code in the ${projectName} codebase by similarity. Groups related files around seed points.`,
16
16
  schema: z.object({
17
17
  seedIds: z.array(z.string()).describe("Seed point IDs to cluster around"),
18
- limit: z.number().optional().describe("Max results per cluster (default: 5)"),
19
- threshold: z.number().optional().describe("Minimum similarity threshold (0-1, default: 0.7)"),
18
+ limit: z.coerce.number().optional().describe("Max results per cluster (default: 5)"),
19
+ threshold: z.coerce.number().optional().describe("Minimum similarity threshold (0-1, default: 0.7)"),
20
20
  }),
21
21
  annotations: TOOL_ANNOTATIONS["cluster_code"],
22
22
  handler: async (args, ctx) => {
@@ -52,8 +52,8 @@ export function createClusteringTools(projectName) {
52
52
  description: `Find duplicate or near-duplicate code in ${projectName}. Groups similar files by content.`,
53
53
  schema: z.object({
54
54
  collection: z.string().optional().describe("Collection to search (default: codebase)"),
55
- limit: z.number().optional().describe("Max duplicate groups to return (default: 10)"),
56
- threshold: z.number().optional().describe("Minimum similarity threshold (0-1, default: 0.9)"),
55
+ limit: z.coerce.number().optional().describe("Max duplicate groups to return (default: 10)"),
56
+ threshold: z.coerce.number().optional().describe("Minimum similarity threshold (0-1, default: 0.9)"),
57
57
  }),
58
58
  annotations: TOOL_ANNOTATIONS["find_duplicates"],
59
59
  handler: async (args, ctx) => {
@@ -97,7 +97,7 @@ export function createClusteringTools(projectName) {
97
97
  schema: z.object({
98
98
  positiveIds: z.array(z.string()).describe("IDs of vectors to find similar code to"),
99
99
  negativeIds: z.array(z.string()).optional().describe("IDs of vectors to avoid (dissimilar)"),
100
- limit: z.number().optional().describe("Max results (default: 5)"),
100
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
101
101
  }),
102
102
  annotations: TOOL_ANNOTATIONS["recommend_similar"],
103
103
  handler: async (args, ctx) => {
@@ -129,7 +129,7 @@ export function createClusteringTools(projectName) {
129
129
  text: z.string().describe("Text to extract learnings from"),
130
130
  context: z.string().optional().describe("Additional context about the text"),
131
131
  autoSave: z.boolean().optional().describe("Automatically save extracted learnings (default: false)"),
132
- minConfidence: z.number().optional().describe("Minimum confidence threshold (0-1, default: 0.7)"),
132
+ minConfidence: z.coerce.number().optional().describe("Minimum confidence threshold (0-1, default: 0.7)"),
133
133
  }),
134
134
  annotations: TOOL_ANNOTATIONS["extract_learnings"],
135
135
  handler: async (args, ctx) => {
@@ -15,7 +15,7 @@ export function createConfluenceTools(projectName) {
15
15
  description: `Search indexed Confluence documentation for ${projectName}. Returns relevant pages with content snippets.`,
16
16
  schema: z.object({
17
17
  query: z.string().describe("Search query for Confluence content"),
18
- limit: z.number().optional().describe("Max results (default: 5)"),
18
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
19
19
  spaceKey: z.string().optional().describe("Filter by Confluence space key"),
20
20
  }),
21
21
  annotations: TOOL_ANNOTATIONS["search_confluence"],
@@ -46,7 +46,7 @@ export function createConfluenceTools(projectName) {
46
46
  schema: z.object({
47
47
  spaceKeys: z.array(z.string()).optional().describe("Specific space keys to index (indexes all accessible if empty)"),
48
48
  labels: z.array(z.string()).optional().describe("Filter pages by labels"),
49
- maxPages: z.number().optional().describe("Maximum pages to index (default: 500)"),
49
+ maxPages: z.coerce.number().optional().describe("Maximum pages to index (default: 500)"),
50
50
  force: z.boolean().optional().describe("Force re-index even if already indexed"),
51
51
  }),
52
52
  annotations: TOOL_ANNOTATIONS["index_confluence"],
@@ -138,10 +138,10 @@ export function createIndexingTools(projectName) {
138
138
  schema: z.object({}),
139
139
  outputSchema: z.object({
140
140
  status: z.string(),
141
- totalFiles: z.number().optional(),
142
- indexedFiles: z.number().optional(),
141
+ totalFiles: z.coerce.number().optional(),
142
+ indexedFiles: z.coerce.number().optional(),
143
143
  lastUpdated: z.string().optional(),
144
- vectorCount: z.number().optional(),
144
+ vectorCount: z.coerce.number().optional(),
145
145
  cached: z.boolean(),
146
146
  }),
147
147
  annotations: TOOL_ANNOTATIONS["get_index_status"],
@@ -62,7 +62,7 @@ export function createMemoryTools(projectName) {
62
62
  schema: z.object({
63
63
  query: z.string().describe("What to recall (semantic search)"),
64
64
  type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "all"]).optional().describe("Filter by memory type (default: all)"),
65
- limit: z.number().optional().describe("Max memories to retrieve (default: 5)"),
65
+ limit: z.coerce.number().optional().describe("Max memories to retrieve (default: 5)"),
66
66
  }),
67
67
  annotations: TOOL_ANNOTATIONS["recall"],
68
68
  handler: async (args, ctx) => {
@@ -89,8 +89,8 @@ export function createMemoryTools(projectName) {
89
89
  schema: z.object({
90
90
  type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "all"]).optional().describe("Filter by type"),
91
91
  tag: z.string().optional().describe("Filter by tag"),
92
- limit: z.number().optional().describe("Max results (default: 10)"),
93
- offset: z.number().optional().describe("Pagination offset (default: 0)"),
92
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
93
+ offset: z.coerce.number().optional().describe("Pagination offset (default: 0)"),
94
94
  }),
95
95
  annotations: TOOL_ANNOTATIONS["list_memories"],
96
96
  handler: async (args, ctx) => {
@@ -129,7 +129,7 @@ export function createMemoryTools(projectName) {
129
129
  schema: z.object({
130
130
  memoryId: z.string().optional().describe("Specific memory ID to delete"),
131
131
  type: z.enum(["decision", "insight", "context", "todo", "conversation", "note"]).optional().describe("Delete all memories of this type"),
132
- olderThanDays: z.number().optional().describe("Delete memories older than N days"),
132
+ olderThanDays: z.coerce.number().optional().describe("Delete memories older than N days"),
133
133
  }),
134
134
  annotations: TOOL_ANNOTATIONS["forget"],
135
135
  handler: async (args, ctx) => {
@@ -247,8 +247,8 @@ export function createMemoryTools(projectName) {
247
247
  name: "review_memories",
248
248
  description: `Get auto-extracted memories pending review in ${projectName}. Shows unvalidated learnings that need human confirmation.`,
249
249
  schema: z.object({
250
- limit: z.number().optional().describe("Max memories to return (default: 20)"),
251
- offset: z.number().optional().describe("Pagination offset (default: 0)"),
250
+ limit: z.coerce.number().optional().describe("Max memories to return (default: 20)"),
251
+ offset: z.coerce.number().optional().describe("Pagination offset (default: 0)"),
252
252
  }),
253
253
  annotations: TOOL_ANNOTATIONS["review_memories"],
254
254
  handler: async (args, ctx) => {
package/dist/tools/pm.js CHANGED
@@ -15,7 +15,7 @@ export function createPmTools(projectName) {
15
15
  description: `Search technical requirements and product documentation for ${projectName}. Finds relevant requirements, user stories, and specifications from Confluence.`,
16
16
  schema: z.object({
17
17
  query: z.string().describe("Search query for requirements (e.g., 'video inspection flow', 'payment integration')"),
18
- limit: z.number().optional().describe("Max results (default: 5)"),
18
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
19
19
  }),
20
20
  annotations: TOOL_ANNOTATIONS["search_requirements"],
21
21
  handler: async (args, ctx) => {
@@ -236,8 +236,8 @@ export function createPmTools(projectName) {
236
236
  description: `List all documented requirements/features for ${projectName} from Confluence. Groups by category or status.`,
237
237
  schema: z.object({
238
238
  category: z.string().optional().describe("Filter by category (optional)"),
239
- limit: z.number().optional().describe("Max results (default: 20)"),
240
- offset: z.number().optional().describe("Pagination offset (default: 0)"),
239
+ limit: z.coerce.number().optional().describe("Max results (default: 20)"),
240
+ offset: z.coerce.number().optional().describe("Pagination offset (default: 0)"),
241
241
  }),
242
242
  annotations: TOOL_ANNOTATIONS["list_requirements"],
243
243
  handler: async (args, ctx) => {
@@ -15,7 +15,7 @@ export function createSearchTools(projectName) {
15
15
  description: `Search the ${projectName} codebase. Returns file locations, symbols, and graph connections. Use Read tool to view the actual code at returned locations.`,
16
16
  schema: z.object({
17
17
  query: z.string().describe("Search query for finding code"),
18
- limit: z.number().optional().describe("Max results to return (default: 5)"),
18
+ limit: z.coerce.number().optional().describe("Max results to return (default: 5)"),
19
19
  language: z.string().optional().describe("Filter by language (typescript, python, vue, etc.)"),
20
20
  path: z.string().optional().describe("Filter by path pattern (e.g., 'src/modules/*')"),
21
21
  layer: z.string().optional().describe("Filter by architectural layer (api, service, util, model, middleware, test, parser, types, config, other)"),
@@ -43,7 +43,7 @@ export function createSearchTools(projectName) {
43
43
  description: "Find code similar to a given snippet.",
44
44
  schema: z.object({
45
45
  code: z.string().describe("Code snippet to find similar code for"),
46
- limit: z.number().optional().describe("Max results (default: 5)"),
46
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
47
47
  }),
48
48
  annotations: TOOL_ANNOTATIONS["search_similar"],
49
49
  handler: async (args, ctx) => {
@@ -66,7 +66,7 @@ export function createSearchTools(projectName) {
66
66
  schema: z.object({
67
67
  query: z.string().describe("Search query"),
68
68
  groupBy: z.string().optional().describe("Field to group by (default: 'file')"),
69
- limit: z.number().optional().describe("Max groups to return (default: 10)"),
69
+ limit: z.coerce.number().optional().describe("Max groups to return (default: 10)"),
70
70
  language: z.string().optional().describe("Filter by language"),
71
71
  layer: z.string().optional().describe("Filter by architectural layer (api, service, util, etc.)"),
72
72
  service: z.string().optional().describe("Filter by service/class name"),
@@ -97,8 +97,8 @@ export function createSearchTools(projectName) {
97
97
  description: `Hybrid search combining keyword matching and semantic similarity for ${projectName}. Returns file locations with symbols and connections. Use Read tool to view code.`,
98
98
  schema: z.object({
99
99
  query: z.string().describe("Search query"),
100
- limit: z.number().optional().describe("Max results (default: 10)"),
101
- semanticWeight: z.number().optional().describe("Weight for semantic vs keyword (0-1, default: 0.7)"),
100
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
101
+ semanticWeight: z.coerce.number().optional().describe("Weight for semantic vs keyword (0-1, default: 0.7)"),
102
102
  language: z.string().optional().describe("Filter by language"),
103
103
  layer: z.string().optional().describe("Filter by architectural layer (api, service, util, etc.)"),
104
104
  service: z.string().optional().describe("Filter by service/class name"),
@@ -128,7 +128,7 @@ export function createSearchTools(projectName) {
128
128
  description: `Search documentation in the ${projectName} project.`,
129
129
  schema: z.object({
130
130
  query: z.string().describe("Search query"),
131
- limit: z.number().optional().describe("Max results (default: 5)"),
131
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
132
132
  }),
133
133
  annotations: TOOL_ANNOTATIONS["search_docs"],
134
134
  handler: async (args, ctx) => {
@@ -154,11 +154,11 @@ export function createSearchTools(projectName) {
154
154
  schema: z.object({}),
155
155
  outputSchema: z.object({
156
156
  projectName: z.string(),
157
- totalFiles: z.number(),
158
- totalLines: z.number().optional(),
159
- vectorCount: z.number(),
157
+ totalFiles: z.coerce.number(),
158
+ totalLines: z.coerce.number().optional(),
159
+ vectorCount: z.coerce.number(),
160
160
  lastIndexed: z.string().optional(),
161
- languages: z.record(z.string(), z.number()).optional(),
161
+ languages: z.record(z.string(), z.coerce.number()).optional(),
162
162
  }),
163
163
  annotations: TOOL_ANNOTATIONS["get_project_stats"],
164
164
  handler: async (_args, ctx) => {
@@ -194,15 +194,15 @@ export function createSearchTools(projectName) {
194
194
  schema: z.object({
195
195
  symbol: z.string().describe("Symbol name to find (function, class, type, etc.)"),
196
196
  kind: z.string().optional().describe("Filter by kind: function, class, interface, type, enum, const"),
197
- limit: z.number().optional().describe("Max results (default: 10)"),
197
+ limit: z.coerce.number().optional().describe("Max results (default: 10)"),
198
198
  }),
199
199
  outputSchema: z.object({
200
200
  symbols: z.array(z.object({
201
201
  kind: z.string(),
202
202
  name: z.string(),
203
203
  file: z.string(),
204
- startLine: z.number(),
205
- endLine: z.number(),
204
+ startLine: z.coerce.number(),
205
+ endLine: z.coerce.number(),
206
206
  signature: z.string(),
207
207
  exported: z.boolean(),
208
208
  })),
@@ -246,8 +246,8 @@ export function createSearchTools(projectName) {
246
246
  description: `Search ${projectName} codebase with graph expansion. Returns file locations plus connected files via import/call relationships. Use Read tool to view code.`,
247
247
  schema: z.object({
248
248
  query: z.string().describe("Search query"),
249
- limit: z.number().optional().describe("Max direct results (default: 5)"),
250
- expandHops: z.number().optional().describe("Number of graph hops to expand (default: 1)"),
249
+ limit: z.coerce.number().optional().describe("Max direct results (default: 5)"),
250
+ expandHops: z.coerce.number().optional().describe("Number of graph hops to expand (default: 1)"),
251
251
  }),
252
252
  annotations: TOOL_ANNOTATIONS["search_graph"],
253
253
  handler: async (args, ctx) => {
@@ -102,7 +102,7 @@ export function createSessionTools(projectName, sharedCtx) {
102
102
  name: "analyze_usage_patterns",
103
103
  description: `Analyze tool usage patterns for ${projectName}. Shows common workflows, detected patterns, and recommendations for improving productivity.`,
104
104
  schema: z.object({
105
- days: z.number().optional().describe("Number of days to analyze (default: 7)."),
105
+ days: z.coerce.number().optional().describe("Number of days to analyze (default: 7)."),
106
106
  }),
107
107
  annotations: TOOL_ANNOTATIONS["analyze_usage_patterns"],
108
108
  handler: async (args, ctx) => {
@@ -290,7 +290,7 @@ export function createSuggestionTools(projectName) {
290
290
  schema: z.object({
291
291
  file: z.string().optional().describe("File path to find related code for"),
292
292
  code: z.string().optional().describe("Code snippet to find related code for"),
293
- limit: z.number().optional().describe("Max results (default: 5)"),
293
+ limit: z.coerce.number().optional().describe("Max results (default: 5)"),
294
294
  }),
295
295
  annotations: TOOL_ANNOTATIONS["suggest_related_code"],
296
296
  handler: async (args, ctx) => {
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 { ZodObject, ZodRawShape } from "zod";
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<ZodRawShape>;
46
- outputSchema?: ZodObject<ZodRawShape>;
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.2.0",
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": "^3.25.76",
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",