@crowley/rag-mcp 1.5.0 → 1.6.1

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.
@@ -25,7 +25,9 @@ export function formatCodeResults(results, contentLimit = PREVIEW.LONG) {
25
25
  return results
26
26
  .map((r) => `**${r.file}** (${pct(r.score)} match)\n` +
27
27
  (r.startLine ? `Lines ${r.startLine}-${r.endLine || "?"}\n` : "") +
28
- "```" + (r.language || "") + "\n" +
28
+ "```" +
29
+ (r.language || "") +
30
+ "\n" +
29
31
  truncate(r.content, contentLimit) +
30
32
  "\n```")
31
33
  .join("\n\n---\n\n");
@@ -46,7 +48,7 @@ export function formatMemoryResults(results, emptyMessage = "No memories found."
46
48
  results.forEach((r, i) => {
47
49
  const m = r.memory;
48
50
  const type = m.type;
49
- const emoji = type === "todo" && m.status === "done" ? "✅" : (typeEmojis[type] || "📝");
51
+ const emoji = type === "todo" && m.status === "done" ? "✅" : typeEmojis[type] || "📝";
50
52
  result += `### ${i + 1}. ${emoji} ${(type || "note").toUpperCase()}\n`;
51
53
  result += `**Relevance:** ${pct(r.score)}\n`;
52
54
  result += `${m.content}\n`;
@@ -64,21 +66,23 @@ export function formatMemoryResults(results, emptyMessage = "No memories found."
64
66
  export function formatNavigationResults(results) {
65
67
  if (!results?.length)
66
68
  return "No results found.";
67
- return results.map(r => {
68
- const loc = r.lines ? `:${r.lines[0]}-${r.lines[1]}` : '';
69
+ return results
70
+ .map((r) => {
71
+ const loc = r.lines ? `:${r.lines[0]}-${r.lines[1]}` : "";
69
72
  let out = `**${r.file}${loc}** (${pct(r.score)})`;
70
73
  if (r.layer)
71
74
  out += ` [${r.layer}]`;
72
75
  if (r.graphExpanded)
73
- out += ' _(graph)_';
76
+ out += " _(graph)_";
74
77
  if (r.preview)
75
78
  out += `\n\`${truncate(r.preview, 100)}\``;
76
79
  if (r.symbols?.length)
77
- out += `\nSymbols: ${r.symbols.join(', ')}`;
80
+ out += `\nSymbols: ${r.symbols.join(", ")}`;
78
81
  if (r.connections?.length)
79
- out += `\nConnections: ${r.connections.map(c => '`' + c + '`').join(', ')}`;
82
+ out += `\nConnections: ${r.connections.map((c) => "`" + c + "`").join(", ")}`;
80
83
  return out;
81
- }).join('\n\n');
84
+ })
85
+ .join("\n\n");
82
86
  }
83
87
  /** Format pagination footer for list tools */
84
88
  export function paginationFooter(count, limit, offset) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Streamable HTTP transport for MCP server.
3
+ * Enables dashboard and remote clients to connect over HTTP.
4
+ *
5
+ * Env:
6
+ * MCP_TRANSPORT — stdio | http | both (default: stdio)
7
+ * MCP_HTTP_PORT — port for HTTP transport (default: 3101)
8
+ * RAG_API_KEY — required for Bearer auth when HTTP is enabled
9
+ */
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ export interface HttpTransportConfig {
12
+ port: number;
13
+ apiKey?: string;
14
+ }
15
+ export declare function startHttpTransport(server: McpServer, config: HttpTransportConfig): Promise<void>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Streamable HTTP transport for MCP server.
3
+ * Enables dashboard and remote clients to connect over HTTP.
4
+ *
5
+ * Env:
6
+ * MCP_TRANSPORT — stdio | http | both (default: stdio)
7
+ * MCP_HTTP_PORT — port for HTTP transport (default: 3101)
8
+ * RAG_API_KEY — required for Bearer auth when HTTP is enabled
9
+ */
10
+ import { createServer, } from "node:http";
11
+ import { randomUUID } from "node:crypto";
12
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13
+ export async function startHttpTransport(server, config) {
14
+ // Per-session transport instances
15
+ const transports = new Map();
16
+ function checkAuth(req, res) {
17
+ if (!config.apiKey)
18
+ return true;
19
+ const auth = req.headers.authorization;
20
+ if (auth !== `Bearer ${config.apiKey}`) {
21
+ res.writeHead(401, { "Content-Type": "application/json" });
22
+ res.end(JSON.stringify({ error: "Unauthorized" }));
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ function parseBody(req) {
28
+ return new Promise((resolve, reject) => {
29
+ const chunks = [];
30
+ req.on("data", (chunk) => chunks.push(chunk));
31
+ req.on("end", () => {
32
+ try {
33
+ const body = Buffer.concat(chunks).toString();
34
+ resolve(body ? JSON.parse(body) : undefined);
35
+ }
36
+ catch (e) {
37
+ reject(e);
38
+ }
39
+ });
40
+ req.on("error", reject);
41
+ });
42
+ }
43
+ const httpServer = createServer(async (req, res) => {
44
+ // Only handle /mcp path
45
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
46
+ if (url.pathname !== "/mcp") {
47
+ res.writeHead(404, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify({ error: "Not found" }));
49
+ return;
50
+ }
51
+ if (!checkAuth(req, res))
52
+ return;
53
+ const sessionId = req.headers["mcp-session-id"];
54
+ if (req.method === "POST") {
55
+ const body = await parseBody(req).catch(() => undefined);
56
+ let transport = sessionId ? transports.get(sessionId) : undefined;
57
+ if (!transport) {
58
+ // New session — create transport and connect to MCP server
59
+ transport = new StreamableHTTPServerTransport({
60
+ sessionIdGenerator: () => randomUUID(),
61
+ });
62
+ transport.onclose = () => {
63
+ if (transport.sessionId) {
64
+ transports.delete(transport.sessionId);
65
+ }
66
+ };
67
+ await server.connect(transport);
68
+ if (transport.sessionId) {
69
+ transports.set(transport.sessionId, transport);
70
+ }
71
+ }
72
+ await transport.handleRequest(req, res, body);
73
+ }
74
+ else if (req.method === "GET") {
75
+ // SSE stream for server-initiated messages
76
+ if (!sessionId) {
77
+ res.writeHead(400, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: "Missing mcp-session-id header" }));
79
+ return;
80
+ }
81
+ const transport = transports.get(sessionId);
82
+ if (!transport) {
83
+ res.writeHead(404, { "Content-Type": "application/json" });
84
+ res.end(JSON.stringify({ error: "Session not found" }));
85
+ return;
86
+ }
87
+ await transport.handleRequest(req, res);
88
+ }
89
+ else if (req.method === "DELETE") {
90
+ // Close session
91
+ if (sessionId) {
92
+ const transport = transports.get(sessionId);
93
+ if (transport) {
94
+ await transport.close();
95
+ transports.delete(sessionId);
96
+ }
97
+ }
98
+ res.writeHead(200);
99
+ res.end();
100
+ }
101
+ else {
102
+ res.writeHead(405, { "Content-Type": "application/json" });
103
+ res.end(JSON.stringify({ error: "Method not allowed" }));
104
+ }
105
+ });
106
+ httpServer.listen(config.port, "127.0.0.1", () => {
107
+ console.error(`MCP HTTP transport listening on http://127.0.0.1:${config.port}/mcp`);
108
+ });
109
+ }
package/dist/index.js CHANGED
@@ -13,8 +13,16 @@
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { createApiClient } from "./api-client.js";
16
+ import { configureConnectionPool } from "./connection-pool.js";
16
17
  import { ContextEnricher } from "./context-enrichment.js";
18
+ import { startHttpTransport } from "./http-transport.js";
17
19
  import { wrapHandler } from "./tool-middleware.js";
20
+ // Phase 3: Configure undici connection pool for RAG API communication
21
+ configureConnectionPool({
22
+ connections: parseInt(process.env.MCP_POOL_CONNECTIONS || "10"),
23
+ keepAliveTimeout: parseInt(process.env.MCP_POOL_KEEPALIVE || "30000"),
24
+ pipelining: parseInt(process.env.MCP_POOL_PIPELINING || "1"),
25
+ });
18
26
  // Tool modules
19
27
  import { createSearchTools } from "./tools/search.js";
20
28
  import { createAskTools } from "./tools/ask.js";
@@ -51,6 +59,11 @@ const ctx = {
51
59
  collectionPrefix: COLLECTION_PREFIX,
52
60
  enrichmentEnabled: true,
53
61
  };
62
+ // If session ID was injected by SessionStart hook, use it
63
+ const hookSessionId = process.env.RAG_SESSION_ID;
64
+ if (hookSessionId) {
65
+ ctx.activeSessionId = hookSessionId;
66
+ }
54
67
  // Context enrichment middleware
55
68
  const enricher = new ContextEnricher({
56
69
  maxAutoRecall: 3,
@@ -169,11 +182,21 @@ async function cleanup() {
169
182
  }
170
183
  process.on("SIGINT", cleanup);
171
184
  process.on("SIGTERM", cleanup);
172
- // Start server
185
+ // Phase 4: Transport selection — stdio | http | both
186
+ const MCP_TRANSPORT = process.env.MCP_TRANSPORT || "stdio";
187
+ const MCP_HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT || "3101");
173
188
  async function main() {
174
- const transport = new StdioServerTransport();
175
- await server.connect(transport);
176
- console.error(`${PROJECT_NAME} RAG MCP server running (collection prefix: ${COLLECTION_PREFIX})`);
189
+ if (MCP_TRANSPORT === "stdio" || MCP_TRANSPORT === "both") {
190
+ const transport = new StdioServerTransport();
191
+ await server.connect(transport);
192
+ }
193
+ if (MCP_TRANSPORT === "http" || MCP_TRANSPORT === "both") {
194
+ await startHttpTransport(server, {
195
+ port: MCP_HTTP_PORT,
196
+ apiKey: RAG_API_KEY,
197
+ });
198
+ }
199
+ console.error(`${PROJECT_NAME} RAG MCP server running (transport: ${MCP_TRANSPORT}, prefix: ${COLLECTION_PREFIX})`);
177
200
  console.error(`Registered ${coreSpecs.length}/${allSpecs.length} core tools (${allSpecs.length - coreSpecs.length} hidden, accessible via run_agent)`);
178
201
  }
179
202
  main().catch(console.error);
package/dist/schemas.js CHANGED
@@ -22,10 +22,7 @@ export function zodToInputSchema(schema) {
22
22
  };
23
23
  }
24
24
  // ── Primitives ──────────────────────────────────────────────
25
- export const QueryStr = z
26
- .string()
27
- .min(1)
28
- .describe("Search query or question");
25
+ export const QueryStr = z.string().min(1).describe("Search query or question");
29
26
  export const Limit = z
30
27
  .number()
31
28
  .int()
@@ -47,10 +44,7 @@ export const FilePaths = z
47
44
  .array(FilePath)
48
45
  .min(1)
49
46
  .describe("List of file paths");
50
- export const Content = z
51
- .string()
52
- .min(1)
53
- .describe("Text content");
47
+ export const Content = z.string().min(1).describe("Text content");
54
48
  export const CollectionSuffix = z
55
49
  .string()
56
50
  .min(1)
@@ -93,10 +87,7 @@ export const SearchFilters = z
93
87
  .string()
94
88
  .optional()
95
89
  .describe("Filter by file extension (e.g. 'ts', 'py')"),
96
- directory: z
97
- .string()
98
- .optional()
99
- .describe("Filter by directory prefix"),
90
+ directory: z.string().optional().describe("Filter by directory prefix"),
100
91
  })
101
92
  .optional()
102
93
  .describe("Search filters");
@@ -69,7 +69,12 @@ export const SESSION_TOOLS = new Set([
69
69
  // ── Helpers ─────────────────────────────────────────────────
70
70
  /** Summarize tool args into a short string for analytics */
71
71
  export function summarizeInput(name, args) {
72
- const q = args.query || args.question || args.feature || args.description || args.task || "";
72
+ const q = args.query ||
73
+ args.question ||
74
+ args.feature ||
75
+ args.description ||
76
+ args.task ||
77
+ "";
73
78
  if (q && typeof q === "string")
74
79
  return q.slice(0, 200);
75
80
  const content = args.content || args.code || args.diff || "";
@@ -159,15 +164,15 @@ export function trackUsage(name, args, startTime, success, result, errorMessage,
159
164
  /** Extract file paths from tool args */
160
165
  function extractFiles(args) {
161
166
  const files = [];
162
- for (const key of ['file', 'filePath', 'currentFile', 'path']) {
167
+ for (const key of ["file", "filePath", "currentFile", "path"]) {
163
168
  const v = args[key];
164
- if (typeof v === 'string' && v.length > 0)
169
+ if (typeof v === "string" && v.length > 0)
165
170
  files.push(v);
166
171
  }
167
172
  const arr = args.affectedFiles || args.files;
168
173
  if (Array.isArray(arr)) {
169
174
  for (const f of arr) {
170
- if (typeof f === 'string')
175
+ if (typeof f === "string")
171
176
  files.push(f);
172
177
  }
173
178
  }
@@ -223,12 +228,12 @@ export function wrapHandler(name, handler, deps) {
223
228
  // Validate: run PreToolUse hooks
224
229
  const validation = await validationPipeline.validate(name, args, ctx);
225
230
  if (!validation.allowed) {
226
- return `Blocked: ${validation.reason || 'validation failed'}`;
231
+ return `Blocked: ${validation.reason || "validation failed"}`;
227
232
  }
228
233
  const validatedArgs = validation.modifiedArgs || args;
229
234
  const warningPrefix = validation.warnings?.length
230
- ? `⚠️ ${validation.warnings.join(' | ')}\n\n`
231
- : '';
235
+ ? `⚠️ ${validation.warnings.join(" | ")}\n\n`
236
+ : "";
232
237
  // Before: auto-enrich context
233
238
  const contextPrefix = ctx.enrichmentEnabled && deps.enricher
234
239
  ? await deps.enricher.before(name, validatedArgs, ctx)
@@ -247,7 +252,7 @@ export function wrapHandler(name, handler, deps) {
247
252
  // Capture in sensory buffer (fire-and-forget)
248
253
  appendToSensoryBuffer(name, args, startTime, true, text, ctx);
249
254
  // Prepend context/warnings if available
250
- const prefix = [warningPrefix, contextPrefix].filter(Boolean).join('');
255
+ const prefix = [warningPrefix, contextPrefix].filter(Boolean).join("");
251
256
  if (prefix) {
252
257
  if (typeof result === "string") {
253
258
  return prefix + result;
@@ -21,7 +21,12 @@ const SESSION_TOOLS = new Set([
21
21
  /** Summarize tool args into a short string for analytics */
22
22
  function summarizeInput(name, args) {
23
23
  // Common patterns: query, question, content, feature, code, file
24
- const q = args.query || args.question || args.feature || args.description || args.task || "";
24
+ const q = args.query ||
25
+ args.question ||
26
+ args.feature ||
27
+ args.description ||
28
+ args.task ||
29
+ "";
25
30
  if (q && typeof q === "string")
26
31
  return q.slice(0, 200);
27
32
  const content = args.content || args.code || args.diff || "";
@@ -40,7 +45,9 @@ function summarizeInput(name, args) {
40
45
  /** Count results from a tool response string */
41
46
  function countResults(result) {
42
47
  // Heuristic: count numbered list items, file matches, or "No results" = 0
43
- if (result.includes("No results") || result.includes("No matches") || result.includes("not found"))
48
+ if (result.includes("No results") ||
49
+ result.includes("No matches") ||
50
+ result.includes("not found"))
44
51
  return 0;
45
52
  const numbered = result.match(/^\d+\./gm);
46
53
  if (numbered)
@@ -166,8 +173,8 @@ export class ToolRegistry {
166
173
  // Track failed usage (fire-and-forget)
167
174
  this.trackUsage(name, args, startTime, false, "", errorMessage, ctx);
168
175
  if (err.code === "ECONNREFUSED") {
169
- return `Error: Cannot connect to RAG API at ${ctx.api.defaults.baseURL}. Is it running?\n` +
170
- `Start with: cd docker && docker-compose up -d`;
176
+ return (`Error: Cannot connect to RAG API at ${ctx.api.defaults.baseURL}. Is it running?\n` +
177
+ `Start with: cd docker && docker-compose up -d`);
171
178
  }
172
179
  if (err.response) {
173
180
  return `API Error (${err.response.status}): ${JSON.stringify(err.response.data)}`;
@@ -14,15 +14,28 @@ export function createAdvancedTools(projectName) {
14
14
  name: "merge_memories",
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
- type: z.string().optional().describe("Filter by memory type (decision, insight, context, todo, conversation, note, or all). Default: all"),
18
- threshold: z.coerce.number().optional().describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
19
- dryRun: z.boolean().optional().describe("If true, preview merge candidates without making changes (default: true)."),
20
- limit: z.coerce.number().optional().describe("Max clusters to process (default: 50)."),
17
+ type: z
18
+ .string()
19
+ .optional()
20
+ .describe("Filter by memory type (decision, insight, context, todo, conversation, note, or all). Default: all"),
21
+ threshold: z.coerce
22
+ .number()
23
+ .optional()
24
+ .describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
25
+ dryRun: z
26
+ .boolean()
27
+ .optional()
28
+ .describe("If true, preview merge candidates without making changes (default: true)."),
29
+ limit: z.coerce
30
+ .number()
31
+ .optional()
32
+ .describe("Max clusters to process (default: 50)."),
21
33
  }),
22
34
  annotations: TOOL_ANNOTATIONS["merge_memories"],
23
35
  handler: async (args, ctx) => {
24
- const { type = "all", threshold = 0.9, dryRun = true, limit = 50 } = args;
36
+ const { type = "all", threshold = 0.9, dryRun = true, limit = 50, } = args;
25
37
  const response = await ctx.api.post("/api/memory/merge", {
38
+ projectName: ctx.projectName,
26
39
  type,
27
40
  threshold,
28
41
  dryRun,
@@ -63,13 +76,21 @@ export function createAdvancedTools(projectName) {
63
76
  description: `Get code completion context for ${projectName}. Finds similar patterns, imports, and symbols from the codebase to aid code completion.`,
64
77
  schema: z.object({
65
78
  currentFile: z.string().describe("Path of the file being edited"),
66
- currentCode: z.string().describe("Current code snippet or file content"),
67
- language: z.string().optional().describe("Programming language filter (optional)"),
68
- limit: z.coerce.number().optional().describe("Max results (default: 5)"),
79
+ currentCode: z
80
+ .string()
81
+ .describe("Current code snippet or file content"),
82
+ language: z
83
+ .string()
84
+ .optional()
85
+ .describe("Programming language filter (optional)"),
86
+ limit: z.coerce
87
+ .number()
88
+ .optional()
89
+ .describe("Max results (default: 5)"),
69
90
  }),
70
91
  annotations: TOOL_ANNOTATIONS["get_completion_context"],
71
92
  handler: async (args, ctx) => {
72
- const { currentFile, currentCode, language, limit = 5 } = args;
93
+ const { currentFile, currentCode, language, limit = 5, } = args;
73
94
  const response = await ctx.api.post("/api/code/completion-context", {
74
95
  currentFile,
75
96
  currentCode,
@@ -107,12 +128,18 @@ export function createAdvancedTools(projectName) {
107
128
  schema: z.object({
108
129
  currentFile: z.string().describe("Path of the file being edited"),
109
130
  currentCode: z.string().describe("Current code content"),
110
- language: z.string().optional().describe("Programming language filter (optional)"),
111
- limit: z.coerce.number().optional().describe("Max suggestions (default: 10)"),
131
+ language: z
132
+ .string()
133
+ .optional()
134
+ .describe("Programming language filter (optional)"),
135
+ limit: z.coerce
136
+ .number()
137
+ .optional()
138
+ .describe("Max suggestions (default: 10)"),
112
139
  }),
113
140
  annotations: TOOL_ANNOTATIONS["get_import_suggestions"],
114
141
  handler: async (args, ctx) => {
115
- const { currentFile, currentCode, language, limit = 10 } = args;
142
+ const { currentFile, currentCode, language, limit = 10, } = args;
116
143
  const response = await ctx.api.post("/api/code/import-suggestions", {
117
144
  currentFile,
118
145
  currentCode,
@@ -144,14 +171,26 @@ export function createAdvancedTools(projectName) {
144
171
  name: "get_type_context",
145
172
  description: `Look up type/interface/class definitions and usage in ${projectName}. Finds where a type is defined and how it's used across the codebase.`,
146
173
  schema: z.object({
147
- typeName: z.string().optional().describe("Name of the type/interface/class to look up"),
148
- code: z.string().optional().describe("Code containing types to look up (alternative to typeName)"),
149
- currentFile: z.string().optional().describe("Current file to exclude from results"),
150
- limit: z.coerce.number().optional().describe("Max results per category (default: 5)"),
174
+ typeName: z
175
+ .string()
176
+ .optional()
177
+ .describe("Name of the type/interface/class to look up"),
178
+ code: z
179
+ .string()
180
+ .optional()
181
+ .describe("Code containing types to look up (alternative to typeName)"),
182
+ currentFile: z
183
+ .string()
184
+ .optional()
185
+ .describe("Current file to exclude from results"),
186
+ limit: z.coerce
187
+ .number()
188
+ .optional()
189
+ .describe("Max results per category (default: 5)"),
151
190
  }),
152
191
  annotations: TOOL_ANNOTATIONS["get_type_context"],
153
192
  handler: async (args, ctx) => {
154
- const { typeName, code, currentFile, limit = 5 } = args;
193
+ const { typeName, code, currentFile, limit = 5, } = args;
155
194
  if (!typeName && !code) {
156
195
  return "Error: Either typeName or code is required.";
157
196
  }
@@ -187,8 +226,14 @@ export function createAdvancedTools(projectName) {
187
226
  name: "get_behavior_patterns",
188
227
  description: `Analyze user workflow patterns for ${projectName}. Shows peak hours, tool preferences, common sequences, and session statistics.`,
189
228
  schema: z.object({
190
- days: z.coerce.number().optional().describe("Number of days to analyze (default: 7)"),
191
- sessionId: z.string().optional().describe("Filter to a specific session (optional)"),
229
+ days: z.coerce
230
+ .number()
231
+ .optional()
232
+ .describe("Number of days to analyze (default: 7)"),
233
+ sessionId: z
234
+ .string()
235
+ .optional()
236
+ .describe("Filter to a specific session (optional)"),
192
237
  }),
193
238
  annotations: TOOL_ANNOTATIONS["get_behavior_patterns"],
194
239
  handler: async (args, ctx) => {
@@ -12,10 +12,18 @@ export function createAgentTools(projectName) {
12
12
  name: "run_agent",
13
13
  description: `Run a specialized agent for ${projectName}. Agents autonomously research, review, or analyze using multiple tool calls. Returns result + reasoning trace.`,
14
14
  schema: z.object({
15
- type: z.enum(["research", "review", "documentation", "refactor", "test"]).describe("Agent type: research, review, documentation, refactor, or test"),
15
+ type: z
16
+ .enum(["research", "review", "documentation", "refactor", "test"])
17
+ .describe("Agent type: research, review, documentation, refactor, or test"),
16
18
  task: z.string().describe("The task for the agent to perform"),
17
- context: z.string().optional().describe("Optional additional context (code, requirements, etc.)"),
18
- maxIterations: z.coerce.number().optional().describe("Maximum ReAct iterations (default: varies by agent type)"),
19
+ context: z
20
+ .string()
21
+ .optional()
22
+ .describe("Optional additional context (code, requirements, etc.)"),
23
+ maxIterations: z.coerce
24
+ .number()
25
+ .optional()
26
+ .describe("Maximum ReAct iterations (default: varies by agent type)"),
19
27
  }),
20
28
  annotations: TOOL_ANNOTATIONS["run_agent"],
21
29
  handler: async (args, ctx) => {
@@ -63,16 +71,37 @@ export function createAgentTools(projectName) {
63
71
  name: "tribunal_debate",
64
72
  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
73
  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)"),
74
+ topic: z
75
+ .string()
76
+ .describe("The debate topic (e.g., 'Should we use REST or gRPC for the new API?')"),
77
+ positions: z
78
+ .array(z.string())
79
+ .min(2)
80
+ .max(4)
81
+ .describe("Positions to debate (2-4 options, e.g., ['REST', 'gRPC'])"),
82
+ context: z
83
+ .string()
84
+ .optional()
85
+ .describe("Additional context for the debate"),
86
+ maxRounds: z.coerce
87
+ .number()
88
+ .optional()
89
+ .describe("Number of rebuttal rounds (default: 1, max: 3)"),
90
+ useCodeContext: z
91
+ .boolean()
92
+ .optional()
93
+ .describe("Fetch relevant code, ADRs, and patterns as evidence (default: false)"),
94
+ autoRecord: z
95
+ .boolean()
96
+ .optional()
97
+ .describe("Save verdict as a decision in project memory (default: false)"),
72
98
  }),
73
- annotations: TOOL_ANNOTATIONS["tribunal_debate"] || { priority: 0.4, readOnlyHint: true },
99
+ annotations: TOOL_ANNOTATIONS["tribunal_debate"] || {
100
+ priority: 0.4,
101
+ readOnlyHint: true,
102
+ },
74
103
  handler: async (args, ctx) => {
75
- const { topic, positions, context, maxRounds, useCodeContext, autoRecord } = args;
104
+ const { topic, positions, context, maxRounds, useCodeContext, autoRecord, } = args;
76
105
  const response = await ctx.api.post("/api/tribunal/debate", {
77
106
  projectName: ctx.projectName,
78
107
  topic,
@@ -87,7 +116,7 @@ export function createAgentTools(projectName) {
87
116
  let result = `## Tribunal Debate: ${data.topic}\n`;
88
117
  result += `**Status:** ${data.status}`;
89
118
  result += ` | **Duration:** ${Math.round(data.durationMs / 1000)}s`;
90
- result += ` | **Cost:** ~$${data.cost?.estimatedUsd?.toFixed(3) || '?'}\n\n`;
119
+ result += ` | **Cost:** ~$${data.cost?.estimatedUsd?.toFixed(3) || "?"}\n\n`;
91
120
  // Phases summary
92
121
  if (data.phases) {
93
122
  result += `### Phases\n`;
@@ -100,7 +129,7 @@ export function createAgentTools(projectName) {
100
129
  if (data.arguments && data.arguments.length > 0) {
101
130
  result += `### Arguments\n`;
102
131
  for (const arg of data.arguments) {
103
- const label = arg.round === 0 ? 'Initial' : `Rebuttal R${arg.round}`;
132
+ const label = arg.round === 0 ? "Initial" : `Rebuttal R${arg.round}`;
104
133
  result += `#### ${arg.position} (${label})\n${arg.content}\n\n`;
105
134
  }
106
135
  }
@@ -72,7 +72,9 @@ export function createAnalyticsTools(projectName) {
72
72
  name: "get_analytics",
73
73
  description: `Get detailed analytics for a ${projectName} collection. Shows vectors, storage, language breakdown, and more.`,
74
74
  schema: z.object({
75
- collectionName: z.string().describe("Collection name to get analytics for (e.g., 'codebase', 'docs', 'memory')"),
75
+ collectionName: z
76
+ .string()
77
+ .describe("Collection name to get analytics for (e.g., 'codebase', 'docs', 'memory')"),
76
78
  }),
77
79
  annotations: TOOL_ANNOTATIONS["get_analytics"],
78
80
  handler: async (args, ctx) => {
@@ -125,7 +127,9 @@ export function createAnalyticsTools(projectName) {
125
127
  name: "list_backups",
126
128
  description: `List backup snapshots for a ${projectName} collection.`,
127
129
  schema: z.object({
128
- collectionName: z.string().describe("Collection name to list backups for"),
130
+ collectionName: z
131
+ .string()
132
+ .describe("Collection name to list backups for"),
129
133
  }),
130
134
  annotations: TOOL_ANNOTATIONS["list_backups"],
131
135
  handler: async (args, ctx) => {
@@ -153,8 +157,13 @@ export function createAnalyticsTools(projectName) {
153
157
  name: "enable_quantization",
154
158
  description: `Enable scalar quantization on a ${projectName} collection to reduce memory usage.`,
155
159
  schema: z.object({
156
- collectionName: z.string().describe("Collection name to enable quantization on"),
157
- quantile: z.coerce.number().optional().describe("Quantile for quantization (0-1, default: 0.99)"),
160
+ collectionName: z
161
+ .string()
162
+ .describe("Collection name to enable quantization on"),
163
+ quantile: z.coerce
164
+ .number()
165
+ .optional()
166
+ .describe("Quantile for quantization (0-1, default: 0.99)"),
158
167
  }),
159
168
  annotations: TOOL_ANNOTATIONS["enable_quantization"],
160
169
  handler: async (args, ctx) => {
@@ -195,7 +204,10 @@ export function createAnalyticsTools(projectName) {
195
204
  name: "get_prediction_stats",
196
205
  description: `Get predictive loader stats for ${projectName}. Shows prediction accuracy, hit rates, and strategy breakdown.`,
197
206
  schema: z.object({
198
- sessionId: z.string().optional().describe("Session ID to get stats for. If omitted, returns aggregate stats."),
207
+ sessionId: z
208
+ .string()
209
+ .optional()
210
+ .describe("Session ID to get stats for. If omitted, returns aggregate stats."),
199
211
  }),
200
212
  annotations: TOOL_ANNOTATIONS["get_prediction_stats"],
201
213
  handler: async (args, ctx) => {