@brutalist/mcp 0.8.1 → 0.9.3

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.
Files changed (35) hide show
  1. package/README.md +22 -1
  2. package/dist/brutalist-server.d.ts +46 -16
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +223 -611
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +8 -6
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +236 -156
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/domains/argument-space.d.ts +9 -0
  11. package/dist/domains/argument-space.d.ts.map +1 -1
  12. package/dist/domains/argument-space.js +27 -20
  13. package/dist/domains/argument-space.js.map +1 -1
  14. package/dist/formatting/response-formatter.d.ts +43 -0
  15. package/dist/formatting/response-formatter.d.ts.map +1 -0
  16. package/dist/formatting/response-formatter.js +277 -0
  17. package/dist/formatting/response-formatter.js.map +1 -0
  18. package/dist/generators/tool-generator.d.ts.map +1 -1
  19. package/dist/generators/tool-generator.js +3 -1
  20. package/dist/generators/tool-generator.js.map +1 -1
  21. package/dist/handlers/tool-handler.d.ts +33 -0
  22. package/dist/handlers/tool-handler.d.ts.map +1 -0
  23. package/dist/handlers/tool-handler.js +299 -0
  24. package/dist/handlers/tool-handler.js.map +1 -0
  25. package/dist/registry/argument-spaces.js +17 -17
  26. package/dist/registry/argument-spaces.js.map +1 -1
  27. package/dist/transport/http-transport.d.ts +40 -0
  28. package/dist/transport/http-transport.d.ts.map +1 -0
  29. package/dist/transport/http-transport.js +182 -0
  30. package/dist/transport/http-transport.js.map +1 -0
  31. package/dist/utils.d.ts +1 -1
  32. package/dist/utils.d.ts.map +1 -1
  33. package/dist/utils.js +13 -6
  34. package/dist/utils.js.map +1 -1
  35. package/package.json +1 -1
@@ -1,32 +1,46 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
- import { randomUUID } from "crypto";
5
- import express from "express";
6
3
  import { z } from "zod";
7
4
  import { CLIAgentOrchestrator } from './cli-agents.js';
8
5
  import { logger } from './logger.js';
9
6
  import { BASE_ROAST_SCHEMA } from './types/tool-config.js';
10
7
  import { TOOL_CONFIGS } from './tool-definitions-generated.js';
11
- import { extractPaginationParams, parseCursor, PAGINATION_DEFAULTS, ResponseChunker, createPaginationMetadata, formatPaginationStatus, estimateTokenCount } from './utils/pagination.js';
8
+ import { parseCursor, PAGINATION_DEFAULTS } from './utils/pagination.js';
12
9
  import { ResponseCache } from './utils/response-cache.js';
10
+ import { ResponseFormatter } from './formatting/response-formatter.js';
11
+ import { HttpTransport } from './transport/http-transport.js';
12
+ import { ToolHandler } from './handlers/tool-handler.js';
13
13
  // Use environment variable or fallback to manual version
14
14
  const PACKAGE_VERSION = process.env.npm_package_version || "0.6.12";
15
+ /**
16
+ * BrutalistServer - Composition root for the Brutalist MCP Server
17
+ *
18
+ * This class has been refactored to follow the Single Responsibility Principle.
19
+ * Responsibilities are now delegated to specialized modules:
20
+ * - ResponseFormatter: Handles all response formatting and pagination
21
+ * - HttpTransport: Manages HTTP server and CORS
22
+ * - ToolHandler: Handles roast tool execution, caching, and conversation continuation
23
+ */
15
24
  export class BrutalistServer {
16
25
  server;
17
26
  config;
27
+ // Core dependencies
18
28
  cliOrchestrator;
19
- httpTransport;
20
29
  responseCache;
21
- actualPort;
22
- httpServer; // Store HTTP server reference for proper cleanup
23
- shutdownHandler;
30
+ // Extracted modules
31
+ formatter;
32
+ toolHandler;
33
+ httpTransport;
24
34
  // Session tracking for security
25
35
  activeSessions = new Map();
36
+ // Session cleanup configuration
37
+ MAX_SESSIONS = 10000;
38
+ SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
39
+ sessionCleanupTimer;
26
40
  constructor(config = {}) {
27
41
  this.config = {
28
42
  workingDirectory: process.cwd(),
29
- defaultTimeout: 1800000, // 30 minutes - complex codebases need time (configurable via BRUTALIST_TIMEOUT env)
43
+ defaultTimeout: 1800000, // 30 minutes - complex codebases need time
30
44
  transport: 'stdio', // Default to stdio for backward compatibility
31
45
  httpPort: 3000,
32
46
  ...config
@@ -43,20 +57,108 @@ export class BrutalistServer {
43
57
  compressionThresholdMB: 1
44
58
  });
45
59
  logger.info(`📦 Response cache initialized with ${cacheTTLHours} hour TTL`);
60
+ // Session cleanup timer - runs hourly
61
+ this.sessionCleanupTimer = setInterval(() => this.cleanupStaleSessions(), 60 * 60 * 1000);
62
+ this.sessionCleanupTimer.unref(); // Don't block Node.js exit
63
+ logger.info(`🔐 Session cleanup initialized (TTL: 24h, max: ${this.MAX_SESSIONS})`);
64
+ // Initialize extracted modules
65
+ this.formatter = new ResponseFormatter();
66
+ this.toolHandler = new ToolHandler(this.cliOrchestrator, this.responseCache, this.formatter, this.config, this.activeSessions, this.handleStreamingEvent, this.handleProgressUpdate, () => this.ensureSessionCapacity() // Session capacity management
67
+ );
68
+ // Initialize MCP server
46
69
  this.server = new McpServer({
47
70
  name: "brutalist-mcp",
48
71
  version: PACKAGE_VERSION
49
72
  }, {
50
73
  capabilities: {
51
74
  tools: {},
52
- logging: {},
53
- experimental: {
54
- streaming: true
55
- }
75
+ logging: {}
76
+ // Removed experimental.streaming - caused Zod validation errors in Claude Code client
56
77
  }
57
78
  });
58
79
  this.registerTools();
59
80
  }
81
+ async start() {
82
+ logger.info("Starting Brutalist MCP Server with CLI Agents");
83
+ // Skip CLI detection at startup - will be done lazily on first request
84
+ logger.info("CLI context will be detected on first request");
85
+ if (this.config.transport === 'http') {
86
+ await this.startHttpServer();
87
+ }
88
+ else {
89
+ await this.startStdioServer();
90
+ }
91
+ logger.info("Brutalist MCP Server started successfully");
92
+ }
93
+ async startStdioServer() {
94
+ logger.info("Starting with stdio transport");
95
+ const transport = new StdioServerTransport();
96
+ await this.server.connect(transport);
97
+ }
98
+ async startHttpServer() {
99
+ // Create and start HTTP transport
100
+ this.httpTransport = new HttpTransport(this.config, (transport) => {
101
+ // Connect MCP server to HTTP transport
102
+ this.server.connect(transport);
103
+ });
104
+ await this.httpTransport.start(PACKAGE_VERSION);
105
+ }
106
+ // Getter for actual listening port (useful for tests)
107
+ getActualPort() {
108
+ return this.httpTransport?.getActualPort();
109
+ }
110
+ // Stop the HTTP server gracefully
111
+ async stop() {
112
+ if (this.httpTransport) {
113
+ await this.httpTransport.stop();
114
+ }
115
+ }
116
+ /**
117
+ * Clean up stale sessions that exceed TTL
118
+ */
119
+ cleanupStaleSessions() {
120
+ const now = Date.now();
121
+ let cleaned = 0;
122
+ for (const [id, session] of this.activeSessions) {
123
+ if (now - session.lastActivity > this.SESSION_TTL_MS) {
124
+ this.activeSessions.delete(id);
125
+ cleaned++;
126
+ }
127
+ }
128
+ if (cleaned > 0) {
129
+ logger.info(`🧹 Cleaned ${cleaned} stale sessions (>${this.SESSION_TTL_MS / 3600000}h idle)`);
130
+ }
131
+ }
132
+ /**
133
+ * Ensure session capacity doesn't exceed MAX_SESSIONS
134
+ * Evicts oldest sessions when capacity is reached
135
+ */
136
+ ensureSessionCapacity() {
137
+ while (this.activeSessions.size >= this.MAX_SESSIONS) {
138
+ // Remove oldest session (first entry in Map)
139
+ const oldestKey = this.activeSessions.keys().next().value;
140
+ if (oldestKey) {
141
+ this.activeSessions.delete(oldestKey);
142
+ logger.debug(`♻️ Evicted oldest session to maintain capacity`);
143
+ }
144
+ else {
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ // Cleanup method for tests - remove event listeners
150
+ cleanup() {
151
+ if (this.httpTransport) {
152
+ this.httpTransport.cleanup();
153
+ }
154
+ if (this.sessionCleanupTimer) {
155
+ clearInterval(this.sessionCleanupTimer);
156
+ this.sessionCleanupTimer = undefined;
157
+ }
158
+ }
159
+ /**
160
+ * Handle streaming events from CLI agents
161
+ */
60
162
  handleStreamingEvent = (event) => {
61
163
  try {
62
164
  if (!event.sessionId) {
@@ -65,10 +167,9 @@ export class BrutalistServer {
65
167
  }
66
168
  logger.debug(`🔄 Session-scoped streaming: ${event.type} from ${event.agent} to session ${event.sessionId.substring(0, 8)}...`);
67
169
  // For HTTP transport: send session-specific notification if client supports it
68
- if (this.httpTransport) {
170
+ const httpTransportInstance = this.httpTransport?.getTransport();
171
+ if (httpTransportInstance) {
69
172
  try {
70
- // Debug: Check what capabilities the server has
71
- logger.debug(`🔍 Server capabilities check: logging=${!!this.server.server._capabilities?.logging}`);
72
173
  // Use MCP server's notification system with session context
73
174
  this.server.server.notification({
74
175
  method: "notifications/message",
@@ -122,6 +223,9 @@ export class BrutalistServer {
122
223
  });
123
224
  }
124
225
  };
226
+ /**
227
+ * Handle progress updates from CLI agents
228
+ */
125
229
  handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
126
230
  try {
127
231
  if (!sessionId) {
@@ -155,164 +259,9 @@ export class BrutalistServer {
155
259
  });
156
260
  }
157
261
  };
158
- async start() {
159
- logger.info("Starting Brutalist MCP Server with CLI Agents");
160
- // Skip CLI detection at startup - will be done lazily on first request
161
- logger.info("CLI context will be detected on first request");
162
- if (this.config.transport === 'http') {
163
- await this.startHttpServer();
164
- }
165
- else {
166
- await this.startStdioServer();
167
- }
168
- logger.info("Brutalist MCP Server started successfully");
169
- }
170
- async startStdioServer() {
171
- logger.info("Starting with stdio transport");
172
- const transport = new StdioServerTransport();
173
- await this.server.connect(transport);
174
- }
175
- async startHttpServer() {
176
- logger.info(`Starting with HTTP streaming transport on port ${this.config.httpPort}`);
177
- // Create HTTP transport with streaming support
178
- this.httpTransport = new StreamableHTTPServerTransport({
179
- sessionIdGenerator: () => randomUUID(),
180
- enableJsonResponse: false, // Force SSE streaming
181
- onsessioninitialized: (sessionId) => {
182
- logger.info(`New session initialized: ${sessionId}`);
183
- },
184
- onsessionclosed: (sessionId) => {
185
- logger.info(`Session closed: ${sessionId}`);
186
- }
187
- });
188
- // Connect the MCP server to the HTTP transport
189
- await this.server.connect(this.httpTransport);
190
- // Create Express app for HTTP handling
191
- const app = express();
192
- app.use(express.json({ limit: '10mb' })); // Add JSON size limit for security
193
- // Secure CORS implementation
194
- app.use((req, res, next) => {
195
- const origin = req.headers.origin;
196
- const isProduction = process.env.NODE_ENV === 'production';
197
- // Define safe default origins for development
198
- const defaultDevOrigins = [
199
- 'http://localhost:3000',
200
- 'http://127.0.0.1:3000',
201
- 'http://localhost:8080',
202
- 'http://127.0.0.1:8080',
203
- 'http://localhost:3001',
204
- 'http://127.0.0.1:3001'
205
- ];
206
- // Get allowed origins from config or use defaults
207
- const allowedOrigins = this.config.corsOrigins || defaultDevOrigins;
208
- const allowWildcard = this.config.allowCORSWildcard === true && !isProduction;
209
- // Determine if origin is allowed
210
- let allowedOrigin = null;
211
- if (allowWildcard) {
212
- // Only in development with explicit opt-in
213
- allowedOrigin = '*';
214
- logger.warn("⚠️ Using wildcard CORS - only safe in development!");
215
- }
216
- else if (!origin) {
217
- // No origin header (same-origin or direct server access)
218
- allowedOrigin = defaultDevOrigins[0]; // Default fallback
219
- }
220
- else if (allowedOrigins.includes(origin)) {
221
- // Explicitly allowed origin
222
- allowedOrigin = origin;
223
- }
224
- else {
225
- // Rejected origin
226
- logger.warn(`🚫 CORS rejected origin: ${origin}`);
227
- allowedOrigin = null;
228
- }
229
- // Set headers only if origin is allowed
230
- if (allowedOrigin) {
231
- res.header('Access-Control-Allow-Origin', allowedOrigin);
232
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
233
- res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
234
- // Removed Authorization header for security
235
- res.header('Access-Control-Allow-Credentials', 'false'); // Explicit false
236
- }
237
- if (req.method === 'OPTIONS') {
238
- if (allowedOrigin) {
239
- res.sendStatus(200);
240
- }
241
- else {
242
- res.sendStatus(403); // Forbidden for disallowed origins
243
- }
244
- return;
245
- }
246
- next();
247
- });
248
- // Route all MCP requests through the transport
249
- app.all('/mcp', async (req, res) => {
250
- try {
251
- await this.httpTransport.handleRequest(req, res, req.body);
252
- }
253
- catch (error) {
254
- logger.error("HTTP request handling failed", error);
255
- if (!res.headersSent) {
256
- res.status(500).json({ error: 'Internal server error' });
257
- }
258
- }
259
- });
260
- // Health check endpoint
261
- app.get('/health', (req, res) => {
262
- res.json({ status: 'ok', transport: 'http-streaming', version: PACKAGE_VERSION });
263
- });
264
- // Start the HTTP server - bind to localhost only for security
265
- const port = this.config.httpPort ?? 3000;
266
- return new Promise((resolve, reject) => {
267
- this.httpServer = app.listen(port, '127.0.0.1', () => {
268
- const actualPort = this.httpServer.address()?.port || port;
269
- this.actualPort = actualPort;
270
- logger.info(`HTTP server listening on port ${actualPort}`);
271
- logger.info(`MCP endpoint: http://localhost:${actualPort}/mcp`);
272
- logger.info(`Health check: http://localhost:${actualPort}/health`);
273
- resolve();
274
- });
275
- this.httpServer.on('error', (error) => {
276
- logger.error('HTTP server failed to start', error);
277
- reject(error);
278
- });
279
- // Handle graceful shutdown - avoid duplicate listeners
280
- if (!this.shutdownHandler) {
281
- this.shutdownHandler = () => {
282
- logger.info('Received SIGTERM, shutting down gracefully');
283
- this.httpServer?.close(() => {
284
- logger.info('HTTP server closed');
285
- process.exit(0);
286
- });
287
- };
288
- process.on('SIGTERM', this.shutdownHandler);
289
- }
290
- });
291
- }
292
- // Getter for actual listening port (useful for tests)
293
- getActualPort() {
294
- return this.actualPort;
295
- }
296
- // Stop the HTTP server gracefully
297
- async stop() {
298
- if (this.httpServer) {
299
- return new Promise((resolve) => {
300
- this.httpServer.close(() => {
301
- logger.info('HTTP server stopped');
302
- this.httpServer = undefined;
303
- this.actualPort = undefined;
304
- resolve();
305
- });
306
- });
307
- }
308
- }
309
- // Cleanup method for tests - remove event listeners
310
- cleanup() {
311
- if (this.shutdownHandler) {
312
- process.removeListener('SIGTERM', this.shutdownHandler);
313
- this.shutdownHandler = undefined;
314
- }
315
- }
262
+ /**
263
+ * Register all MCP tools
264
+ */
316
265
  registerTools() {
317
266
  // Register all roast tools using unified handler - DRY principle
318
267
  TOOL_CONFIGS.forEach(config => {
@@ -320,11 +269,14 @@ export class BrutalistServer {
320
269
  ...config.schemaExtensions,
321
270
  ...BASE_ROAST_SCHEMA
322
271
  };
323
- this.server.tool(config.name, config.description, schema, async (args, extra) => this.handleRoastTool(config, args, extra));
272
+ this.server.tool(config.name, config.description, schema, async (args, extra) => this.toolHandler.handleRoastTool(config, args, extra));
324
273
  });
325
274
  // Register special tools that don't follow the pattern
326
275
  this.registerSpecialTools();
327
276
  }
277
+ /**
278
+ * Register special tools (debate, roster)
279
+ */
328
280
  registerSpecialTools() {
329
281
  // ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
330
282
  this.server.tool("roast_cli_debate", "Deploy CLI agents in structured adversarial debate. Agents take opposing positions and systematically challenge each other's reasoning. Perfect for exploring complex topics from multiple perspectives and stress-testing ideas through rigorous intellectual discourse.", {
@@ -333,10 +285,18 @@ export class BrutalistServer {
333
285
  context: z.string().optional().describe("Additional context for the debate"),
334
286
  workingDirectory: z.string().optional().describe("Working directory for analysis"),
335
287
  models: z.object({
336
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
337
- codex: z.string().optional().describe("Codex model: gpt-5.1-codex-max (latest), gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5-codex, gpt-5, o4-mini"),
338
- gemini: z.string().optional().describe("Gemini model: gemini-3-pro-preview (latest), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite")
339
- }).optional().describe("Specific models to use for each CLI agent")
288
+ claude: z.string().optional().describe("Claude model: opus (recommended), sonnet, haiku, opusplan, or full name like claude-opus-4-5-20251101. Default: user's configured model"),
289
+ codex: z.string().optional().describe("Codex model: gpt-5.1-codex-max (recommended), gpt-5.2, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5-codex, o4-mini. Default: CLI's default"),
290
+ gemini: z.string().optional().describe("Gemini model: gemini-3-pro (recommended), gemini-3-flash, gemini-2.5-pro, gemini-2.5-flash. Default: Auto routing")
291
+ }).optional().describe("Specific models to use for each CLI agent - defaults let each CLI use its own latest model"),
292
+ // Pagination and continuation parameters
293
+ context_id: z.string().optional().describe("Context ID from previous response for pagination or conversation continuation"),
294
+ resume: z.boolean().optional().describe("Continue debate with history injection (requires context_id)"),
295
+ offset: z.number().min(0).optional().describe("Pagination offset"),
296
+ limit: z.number().min(1000).max(100000).optional().describe("Max chars/chunk (default: 90000)"),
297
+ cursor: z.string().optional().describe("Pagination cursor"),
298
+ force_refresh: z.boolean().optional().describe("Ignore cache"),
299
+ verbose: z.boolean().optional().describe("Detailed output")
340
300
  }, async (args) => {
341
301
  // CRITICAL: Prevent recursion
342
302
  if (process.env.BRUTALIST_SUBPROCESS === '1') {
@@ -348,11 +308,7 @@ export class BrutalistServer {
348
308
  }]
349
309
  };
350
310
  }
351
- return this.handleToolExecution(async () => {
352
- const debateRounds = Math.min(args.debateRounds || 2, 10); // Limit to max 10 rounds to prevent DoS
353
- const responses = await this.executeCLIDebate(args.targetPath, debateRounds, args.context, args.workingDirectory, args.models);
354
- return responses;
355
- });
311
+ return this.handleDebateToolExecution(args);
356
312
  });
357
313
  // CLI_AGENT_ROSTER: Show available brutalist critics
358
314
  this.server.tool("cli_agent_roster", "Know your weapons. Display the available CLI agent critics (Claude Code, Codex, Gemini CLI) ready to demolish your work, their capabilities, and how to deploy them for systematic destruction.", {}, async (args) => {
@@ -383,12 +339,16 @@ export class BrutalistServer {
383
339
  const cliContext = await this.cliOrchestrator.detectCLIContext();
384
340
  roster += "## Current CLI Context\n";
385
341
  roster += `**Available CLIs:** ${cliContext.availableCLIs.join(', ') || 'None detected'}\n\n`;
386
- roster += "## Pagination Support (NEW in v0.5.2)\n";
387
- roster += "**All tools now support intelligent pagination:**\n";
388
- roster += "- Analysis results are cached with 2-hour TTL\n";
389
- roster += "- Use `context_id` from response to resume conversation\n";
390
- roster += "- Smart text chunking preserves readability\n";
391
- roster += "- Example: `roast_codebase(context_id: 'a3f5c2d8', offset: 25000)`\n\n";
342
+ roster += "## Pagination & Conversation Continuation\n";
343
+ roster += "**Two distinct modes for using context_id:**\n\n";
344
+ roster += "**1. Pagination** (cached result retrieval):\n";
345
+ roster += "- `context_id` alone returns cached response at different offsets\n";
346
+ roster += "- Example: `roast_codebase(context_id: 'abc123', offset: 25000)`\n\n";
347
+ roster += "**2. Conversation Continuation** (resume dialogue with history):\n";
348
+ roster += "- `context_id` + `resume: true` + new content continues the conversation\n";
349
+ roster += "- Prior conversation is injected into CLI agent context\n";
350
+ roster += "- Example: `roast_codebase(context_id: 'abc123', resume: true, content: 'Explain issue #3 in detail')`\n\n";
351
+ roster += "**Cache TTL:** 2 hours\n\n";
392
352
  roster += "## Brutalist Philosophy\n";
393
353
  roster += "*All tools use CLI agents with brutal system prompts for maximum reality-based criticism.*\n";
394
354
  return {
@@ -396,84 +356,53 @@ export class BrutalistServer {
396
356
  };
397
357
  }
398
358
  catch (error) {
399
- return this.formatErrorResponse(error);
359
+ return this.formatter.formatErrorResponse(error);
400
360
  }
401
361
  });
402
362
  }
403
363
  /**
404
- * Unified handler for all roast tools - DRY principle
364
+ * Handle debate tool execution with caching, pagination, and conversation continuation
365
+ * Delegated mostly to ToolHandler but kept here for CLI debate-specific logic
405
366
  */
406
- async handleRoastTool(config, args, extra) {
367
+ async handleDebateToolExecution(args) {
407
368
  try {
408
- // CRITICAL: Prevent recursion - reject tool calls from brutalist-spawned subprocesses
409
- if (process.env.BRUTALIST_SUBPROCESS === '1') {
410
- logger.warn(`🚫 Rejecting tool call from brutalist subprocess (recursion prevented)`);
411
- return {
412
- content: [{
413
- type: "text",
414
- text: `ERROR: Brutalist MCP tools cannot be used from within a brutalist-spawned CLI subprocess (recursion prevented)`
415
- }]
416
- };
417
- }
418
- const progressToken = extra._meta?.progressToken;
419
- // Extract session context for security
420
- // IMPORTANT: Use consistent "anonymous" for all anonymous users to enable cache sharing
421
- const sessionId = extra?.sessionId ||
422
- extra?._meta?.sessionId ||
423
- extra?.headers?.['mcp-session-id'] ||
424
- 'anonymous'; // Consistent for cache sharing across pagination requests
425
- const requestId = `${sessionId}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
426
- logger.debug(`🔐 Processing request with session: ${sessionId.substring(0, 8)}..., request: ${requestId.substring(0, 12)}...`);
427
- // Track session activity
428
- if (!this.activeSessions.has(sessionId)) {
429
- this.activeSessions.set(sessionId, {
430
- startTime: Date.now(),
431
- requestCount: 0,
432
- lastActivity: Date.now()
433
- });
434
- }
435
- const sessionInfo = this.activeSessions.get(sessionId);
436
- sessionInfo.requestCount++;
437
- sessionInfo.lastActivity = Date.now();
438
- logger.debug(`Tool execution: ${config.name}, primaryArgField=${config.primaryArgField}`);
439
- logger.debug(`Args: ${JSON.stringify(args, null, 2)}`);
440
- // Extract pagination parameters
441
- const paginationParams = extractPaginationParams(args);
369
+ // Build pagination params
370
+ const paginationParams = {
371
+ offset: args.offset || 0,
372
+ limit: args.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT_TOKENS
373
+ };
442
374
  if (args.cursor) {
443
375
  const cursorParams = parseCursor(args.cursor);
444
376
  Object.assign(paginationParams, cursorParams);
445
377
  }
446
- // Determine if pagination was explicitly requested by the user
447
378
  const explicitPaginationRequested = args.offset !== undefined ||
448
379
  args.limit !== undefined ||
449
380
  args.cursor !== undefined ||
450
381
  args.context_id !== undefined;
451
- logger.info(`🔧 DEBUG: explicitPaginationRequested=${explicitPaginationRequested}, offset=${args.offset}, limit=${args.limit}, cursor=${args.cursor}, context_id=${args.context_id}`);
452
- // Check cache if context_id provided - detect conversation continuation
382
+ // Validate resume flag requires context_id
383
+ if (args.resume && !args.context_id) {
384
+ throw new Error(`The 'resume' flag requires a 'context_id' from a previous debate. ` +
385
+ `Run an initial debate first, then use the returned context_id with resume: true.`);
386
+ }
387
+ // Check cache if context_id provided
453
388
  let conversationHistory;
454
389
  if (args.context_id && !args.force_refresh) {
455
- const cachedResponse = await this.responseCache.getByContextId(args.context_id, sessionId);
390
+ const cachedResponse = await this.responseCache.getByContextId(args.context_id);
456
391
  if (cachedResponse) {
457
- logger.info(`🎯 Cache HIT for context_id: ${args.context_id}`);
458
- // Check if this is a continuation (new user prompt provided)
459
- // For text-based tools, check 'content' field; for filesystem tools, check primaryArgField
460
- const textContent = args.content || args.idea || args.architecture || args.research || args.product || args.security || args.infrastructure;
461
- const primaryArg = textContent || args[config.primaryArgField];
462
- // Check if content is different from the last message in conversation history
463
- const lastUserMessage = cachedResponse.conversationHistory
464
- ?.filter(msg => msg.role === 'user')
465
- .pop();
466
- const isDifferentContent = !lastUserMessage || (primaryArg && primaryArg.trim() !== lastUserMessage.content.trim());
467
- const hasNewUserPrompt = primaryArg && primaryArg.trim() !== '' && isDifferentContent;
468
- if (hasNewUserPrompt) {
469
- // CONVERSATION CONTINUATION: User is adding a new prompt to the thread
470
- logger.info(`💬 Detected conversation continuation - new prompt: "${primaryArg.substring(0, 50)}..."`);
392
+ logger.info(`🎯 Debate cache HIT for context_id: ${args.context_id}`);
393
+ if (args.resume === true) {
394
+ // CONVERSATION CONTINUATION: Continue the debate
395
+ if (!args.targetPath || args.targetPath.trim() === '') {
396
+ throw new Error(`Debate continuation (resume: true) requires a new prompt/question. ` +
397
+ `Provide your follow-up in the targetPath field.`);
398
+ }
399
+ logger.info(`💬 Debate continuation - new prompt: "${args.targetPath.substring(0, 50)}..."`);
471
400
  conversationHistory = cachedResponse.conversationHistory || [];
472
- // Don't return cached - fall through to execute new analysis with history
401
+ // Fall through to execute new debate round with history
473
402
  }
474
403
  else {
475
- // PAGINATION: Just retrieving previous response (no new prompt)
476
- logger.info(`📖 Pagination request - returning cached response`);
404
+ // PAGINATION: Return cached debate result
405
+ logger.info(`📖 Debate pagination request - returning cached response`);
477
406
  const cachedResult = {
478
407
  success: true,
479
408
  responses: [{
@@ -483,33 +412,33 @@ export class BrutalistServer {
483
412
  executionTime: 0
484
413
  }]
485
414
  };
486
- return this.formatToolResponse(cachedResult, args.verbose, paginationParams, args.context_id, explicitPaginationRequested);
415
+ return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, args.context_id, explicitPaginationRequested);
487
416
  }
488
417
  }
489
418
  else {
490
- logger.warn(`❌ Cache MISS for context_id: ${args.context_id}, session: ${sessionId}`);
419
+ logger.warn(`❌ Debate cache MISS for context_id: ${args.context_id}`);
491
420
  throw new Error(`Context ID "${args.context_id}" not found in cache. ` +
492
421
  `It may have expired (2 hour TTL) or belong to a different session. ` +
493
- `Remove context_id parameter to run a new analysis.`);
422
+ `Remove context_id parameter to run a new debate.`);
494
423
  }
495
424
  }
496
- // Generate cache key for this request
497
- const cacheKey = this.responseCache.generateCacheKey(config.cacheKeyFields.reduce((acc, field) => {
498
- acc.tool = config.name;
499
- if (args[field] !== undefined)
500
- acc[field] = args[field];
501
- return acc;
502
- }, {}));
503
- // Check if we have a cached result (unless forcing refresh)
504
- if (!args.force_refresh) {
505
- const cachedContent = await this.responseCache.get(cacheKey, sessionId);
425
+ // Generate cache key for this debate
426
+ const cacheKey = this.responseCache.generateCacheKey({
427
+ tool: 'roast_cli_debate',
428
+ targetPath: args.targetPath,
429
+ debateRounds: args.debateRounds,
430
+ context: args.context,
431
+ models: args.models
432
+ });
433
+ // Check cache for identical request (if not resuming)
434
+ if (!args.force_refresh && !args.resume) {
435
+ const cachedContent = await this.responseCache.get(cacheKey);
506
436
  if (cachedContent) {
507
- // Get existing context_id or create new alias
508
437
  const existingContextId = this.responseCache.findContextIdForKey(cacheKey);
509
438
  const contextId = existingContextId
510
439
  ? this.responseCache.createAlias(existingContextId, cacheKey)
511
440
  : this.responseCache.generateContextId(cacheKey);
512
- logger.info(`🎯 Cache hit for new request, using context_id: ${contextId}`);
441
+ logger.info(`🎯 Debate cache hit for new request, using context_id: ${contextId}`);
513
442
  const cachedResult = {
514
443
  success: true,
515
444
  responses: [{
@@ -519,72 +448,61 @@ export class BrutalistServer {
519
448
  executionTime: 0
520
449
  }]
521
450
  };
522
- return this.formatToolResponse(cachedResult, args.verbose, paginationParams, contextId, explicitPaginationRequested);
451
+ return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, contextId, explicitPaginationRequested);
523
452
  }
524
453
  }
525
- // Build context with custom builder if available
526
- let context = config.contextBuilder ? config.contextBuilder(args) : args.context;
527
- // Get the primary argument (targetPath, idea, architecture, etc.)
528
- // For text-based tools, use content field; for filesystem tools, use primaryArgField
529
- const textContent = args.content || args.idea || args.architecture || args.research || args.product || args.security || args.infrastructure;
530
- const primaryArg = textContent || args[config.primaryArgField];
531
- // If we have conversation history, inject it into the context
454
+ // Build context with conversation history if resuming
455
+ let debateContext = args.context || '';
532
456
  if (conversationHistory && conversationHistory.length > 0) {
533
- const conversationContext = conversationHistory.map(msg => {
534
- const role = msg.role === 'user' ? 'User' : 'Assistant';
535
- return `${role}: ${msg.content}`;
457
+ const previousDebate = conversationHistory.map(msg => {
458
+ const role = msg.role === 'user' ? 'User Question' : 'Debate Response';
459
+ return `${role}:\n${msg.content}`;
536
460
  }).join('\n\n---\n\n');
537
- const contextPrefix = `## Previous Conversation\n\n${conversationContext}\n\n---\n\n## New User Prompt\n\n`;
538
- context = contextPrefix + (context || '');
539
- logger.info(`💬 Injected ${conversationHistory.length} previous messages into context`);
461
+ debateContext = `## Previous Debate Context\n\n${previousDebate}\n\n---\n\n## New Follow-up Question\n\nThe user wants to continue this debate with a new question or direction.\n\n${debateContext}`;
462
+ logger.info(`💬 Injected ${conversationHistory.length} previous messages into debate context`);
540
463
  }
541
- logger.debug(`Primary arg: ${config.primaryArgField}="${primaryArg}", analysisType="${config.analysisType}"`);
542
- // Run the analysis
543
- const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.preferredCLI, args.verbose, args.models, progressToken, sessionId, requestId);
544
- // Cache the result if successful
464
+ // Execute the debate
465
+ const debateRounds = Math.min(args.debateRounds || 2, 10);
466
+ const result = await this.executeCLIDebate(args.targetPath, debateRounds, debateContext, args.workingDirectory, args.models);
467
+ // Cache the result
545
468
  let contextId;
546
469
  if (result.success && result.responses.length > 0) {
547
- const fullContent = this.extractFullContent(result);
470
+ const fullContent = this.formatter.extractFullContent(result);
548
471
  if (fullContent) {
549
- const cacheData = config.cacheKeyFields.reduce((acc, field) => {
550
- acc.tool = config.name;
551
- if (args[field] !== undefined)
552
- acc[field] = args[field];
553
- return acc;
554
- }, {});
555
- // Build updated conversation history
556
472
  const now = Date.now();
557
473
  const updatedConversation = [
558
474
  ...(conversationHistory || []),
559
- { role: 'user', content: primaryArg, timestamp: now },
475
+ { role: 'user', content: args.targetPath, timestamp: now },
560
476
  { role: 'assistant', content: fullContent, timestamp: now }
561
477
  ];
562
- // If continuing a conversation, reuse the existing context_id
563
- if (args.context_id && conversationHistory) {
564
- // Update existing cache entry with extended conversation
565
- contextId = args.context_id; // TypeScript: we know this is defined from the if condition
566
- await this.responseCache.updateByContextId(contextId, fullContent, updatedConversation, sessionId || 'anonymous');
567
- logger.info(`✅ Updated conversation ${contextId} (now ${updatedConversation.length} messages)`);
478
+ if (args.resume && args.context_id && conversationHistory) {
479
+ // Update existing cache entry
480
+ contextId = args.context_id;
481
+ await this.responseCache.updateByContextId(contextId, fullContent, updatedConversation);
482
+ logger.info(`✅ Updated debate conversation ${contextId} (now ${updatedConversation.length} messages)`);
568
483
  }
569
484
  else {
570
- // New conversation - create new context_id
571
- const { contextId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey, sessionId, requestId, updatedConversation);
485
+ // New debate - create new context_id
486
+ const { contextId: newId } = await this.responseCache.set({ tool: 'roast_cli_debate', targetPath: args.targetPath }, fullContent, cacheKey, undefined, undefined, updatedConversation);
572
487
  contextId = newId;
573
- logger.info(`✅ Cached new conversation with context ID: ${contextId} for session: ${sessionId?.substring(0, 8)}`);
488
+ logger.info(`✅ Cached new debate with context ID: ${contextId}`);
574
489
  }
575
490
  }
576
491
  }
577
- return this.formatToolResponse(result, args.verbose, paginationParams, contextId, explicitPaginationRequested);
492
+ return this.formatter.formatToolResponse(result, args.verbose, paginationParams, contextId, explicitPaginationRequested);
578
493
  }
579
494
  catch (error) {
580
- return this.formatErrorResponse(error);
495
+ return this.formatter.formatErrorResponse(error);
581
496
  }
582
497
  }
498
+ /**
499
+ * Execute CLI debate (kept in server for debate-specific logic)
500
+ */
583
501
  async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
584
502
  logger.debug("Executing CLI debate", {
585
503
  targetPath,
586
504
  debateRounds,
587
- workingDirectory,
505
+ workingDirectory
588
506
  });
589
507
  try {
590
508
  // Get CLI context
@@ -641,13 +559,6 @@ Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.
641
559
  // Subsequent rounds: Turn-based responses attacking specific arguments
642
560
  for (let round = 2; round <= debateRounds; round++) {
643
561
  logger.debug(`Starting debate round ${round}: Adversarial engagement`);
644
- // Build confrontational context from ALL previous responses
645
- const previousPositions = Array.from(fullDebateTranscript.entries())
646
- .map(([agent, outputs]) => {
647
- const latestOutput = outputs[outputs.length - 1];
648
- return `${agent.toUpperCase()} argued:\n${latestOutput}`;
649
- })
650
- .join('\n\n---\n\n');
651
562
  // Execute turn-based responses with fixed positions
652
563
  for (const [currentAgent, assignedPosition] of agentPositions.entries()) {
653
564
  const opponents = Array.from(agentPositions.entries()).filter(([a, _]) => a !== currentAgent);
@@ -702,6 +613,9 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
702
613
  throw error;
703
614
  }
704
615
  }
616
+ /**
617
+ * Synthesize debate results into formatted output
618
+ */
705
619
  synthesizeDebate(responses, targetPath, rounds, agentPositions) {
706
620
  const successfulResponses = responses.filter(r => r.success);
707
621
  if (successfulResponses.length === 0) {
@@ -776,307 +690,5 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
776
690
  }
777
691
  return synthesis;
778
692
  }
779
- async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, workingDirectory, preferredCLI, verbose, models, progressToken, sessionId, requestId) {
780
- logger.info(`🏢 Starting brutalist analysis: ${analysisType}`);
781
- logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, primaryContent=${primaryContent}`);
782
- logger.debug("Executing brutalist analysis", {
783
- primaryContent,
784
- analysisType,
785
- systemPromptSpec,
786
- workingDirectory,
787
- preferredCLI
788
- });
789
- try {
790
- // Get CLI context for execution summary
791
- logger.info(`🔧 DEBUG: About to detect CLI context`);
792
- await this.cliOrchestrator.detectCLIContext();
793
- logger.info(`🔧 DEBUG: CLI context detected successfully`);
794
- // Execute CLI agent analysis (single or multi-CLI based on preferences)
795
- logger.info(`🔍 Executing brutalist analysis with timeout: ${this.config.defaultTimeout}ms`);
796
- logger.info(`🔧 DEBUG: About to call cliOrchestrator.executeBrutalistAnalysis`);
797
- const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, {
798
- workingDirectory: workingDirectory || this.config.workingDirectory,
799
- timeout: this.config.defaultTimeout,
800
- preferredCLI,
801
- analysisType: analysisType,
802
- models,
803
- onStreamingEvent: this.handleStreamingEvent,
804
- progressToken,
805
- onProgress: progressToken && sessionId ?
806
- (progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
807
- sessionId,
808
- requestId
809
- });
810
- logger.info(`🔧 DEBUG: cliOrchestrator.executeBrutalistAnalysis returned ${responses.length} responses`);
811
- const successfulResponses = responses.filter(r => r.success);
812
- const totalExecutionTime = responses.reduce((sum, r) => sum + r.executionTime, 0);
813
- logger.info(`📊 Analysis complete: ${successfulResponses.length}/${responses.length} CLIs successful (${totalExecutionTime}ms total)`);
814
- logger.info(`🔧 DEBUG: About to synthesize feedback`);
815
- const synthesis = this.cliOrchestrator.synthesizeBrutalistFeedback(responses, analysisType);
816
- logger.info(`🔧 DEBUG: Synthesis length: ${synthesis.length} characters`);
817
- const result = {
818
- success: successfulResponses.length > 0,
819
- responses,
820
- synthesis,
821
- analysisType,
822
- targetPath: primaryContent,
823
- executionSummary: {
824
- totalCLIs: responses.length,
825
- successfulCLIs: successfulResponses.length,
826
- failedCLIs: responses.length - successfulResponses.length,
827
- totalExecutionTime,
828
- selectedCLI: responses.length === 1 ? responses[0].agent : undefined,
829
- selectionMethod: responses.length === 1 ? responses[0].selectionMethod : 'multi-cli'
830
- }
831
- };
832
- logger.info(`🔧 DEBUG: Returning result with success=${result.success}`);
833
- return result;
834
- }
835
- catch (error) {
836
- logger.error("Brutalist analysis execution failed", error);
837
- throw error;
838
- }
839
- }
840
- /**
841
- * Extract full content from analysis result for caching
842
- */
843
- extractFullContent(result) {
844
- if (result.synthesis) {
845
- return result.synthesis;
846
- }
847
- else if (result.responses && result.responses.length > 0) {
848
- const successfulResponses = result.responses.filter(r => r.success);
849
- if (successfulResponses.length > 0) {
850
- let output = `${successfulResponses.length} AI critics have systematically demolished your work.\n\n`;
851
- successfulResponses.forEach((response, index) => {
852
- output += `## Critic ${index + 1}: ${response.agent.toUpperCase()}\n`;
853
- output += `*Execution time: ${response.executionTime}ms*\n\n`;
854
- output += response.output;
855
- // Only add separator between critics, not after the last one
856
- if (index < successfulResponses.length - 1) {
857
- output += '\n\n---\n\n';
858
- }
859
- });
860
- return output;
861
- }
862
- }
863
- return null;
864
- }
865
- formatToolResponse(result, verbose = false, paginationParams, contextId, explicitPaginationRequested = false) {
866
- logger.info(`🔧 DEBUG: formatToolResponse called with synthesis length: ${result.synthesis?.length || 0}`);
867
- logger.info(`🔧 DEBUG: result.success=${result.success}, responses.length=${result.responses?.length || 0}`);
868
- logger.info(`🔧 DEBUG: pagination params:`, paginationParams);
869
- logger.info(`🔧 DEBUG: explicitPaginationRequested=${explicitPaginationRequested}`);
870
- // Get the primary content to paginate
871
- let primaryContent = '';
872
- if (result.synthesis) {
873
- primaryContent = result.synthesis;
874
- logger.info(`🔧 DEBUG: Using synthesis content (${primaryContent.length} characters)`);
875
- }
876
- else if (result.responses) {
877
- const successfulResponses = result.responses.filter(r => r.success);
878
- if (successfulResponses.length > 0) {
879
- primaryContent = successfulResponses.map(r => r.output).join('\n\n---\n\n');
880
- logger.info(`🔧 DEBUG: Using raw CLI output (${primaryContent.length} characters)`);
881
- }
882
- }
883
- // Estimate token count to determine if pagination is needed
884
- const estimatedTokens = estimateTokenCount(primaryContent);
885
- const maxTokensWithoutPagination = 25000;
886
- const needsAutoPagination = estimatedTokens > maxTokensWithoutPagination;
887
- // CRITICAL: Always apply pagination if content is too large, even if not explicitly requested
888
- // This prevents MCP protocol errors when response exceeds client token limits
889
- if (needsAutoPagination || explicitPaginationRequested) {
890
- if (needsAutoPagination && !explicitPaginationRequested) {
891
- logger.info(`🔧 AUTO-PAGINATING: ${estimatedTokens} tokens exceeds ${maxTokensWithoutPagination} limit - forcing first page`);
892
- // Force pagination params to show first chunk (use token-based limit)
893
- const forcedParams = {
894
- offset: 0,
895
- limit: PAGINATION_DEFAULTS.DEFAULT_LIMIT_TOKENS // Use token-based limit
896
- };
897
- return this.formatPaginatedResponse(primaryContent, forcedParams, result, verbose, contextId);
898
- }
899
- else if (paginationParams) {
900
- logger.info(`🔧 DEBUG: Applying pagination (explicitly requested)`);
901
- return this.formatPaginatedResponse(primaryContent, paginationParams, result, verbose, contextId);
902
- }
903
- }
904
- // Non-paginated response (only for content that fits within token limit)
905
- if (primaryContent) {
906
- logger.info(`🔧 DEBUG: Returning full response (${estimatedTokens} tokens < ${maxTokensWithoutPagination} limit)`);
907
- // Include context_id even for non-paginated responses (for future pagination/caching)
908
- let responseText = '';
909
- if (contextId) {
910
- responseText += `# Brutalist Analysis Results\n\n`;
911
- responseText += `**🔑 Context ID:** ${contextId}\n\n`;
912
- responseText += `---\n\n`;
913
- responseText += primaryContent;
914
- }
915
- else {
916
- responseText = primaryContent;
917
- }
918
- return {
919
- content: [{
920
- type: "text",
921
- text: responseText
922
- }]
923
- };
924
- }
925
- // Error handling - no successful content
926
- let errorOutput = '';
927
- if (result.responses) {
928
- const failedResponses = result.responses.filter(r => !r.success);
929
- if (failedResponses.length > 0) {
930
- errorOutput = `❌ All CLI agents failed:\n` +
931
- failedResponses.map(r => `- ${r.agent.toUpperCase()}: ${r.error}`).join('\n');
932
- }
933
- else {
934
- errorOutput = '❌ No CLI responses available';
935
- }
936
- }
937
- else {
938
- errorOutput = '❌ No analysis results';
939
- }
940
- return {
941
- content: [{
942
- type: "text",
943
- text: errorOutput
944
- }]
945
- };
946
- }
947
- formatPaginatedResponse(content, paginationParams, result, verbose, contextId) {
948
- // Using imported pagination utilities
949
- const offset = paginationParams.offset || 0;
950
- // Convert character-based limit to token-based limit (1 token ≈ 4 chars)
951
- const limitChars = paginationParams.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT;
952
- const limitTokens = Math.ceil(limitChars / 4); // Convert chars to tokens
953
- logger.info(`🔧 DEBUG: Paginating content - offset: ${offset}, limitChars: ${limitChars}, limitTokens: ${limitTokens}, total: ${content.length} chars`);
954
- // Use ResponseChunker for intelligent boundary detection (TOKEN-BASED)
955
- const chunker = new ResponseChunker(limitTokens, PAGINATION_DEFAULTS.CHUNK_OVERLAP_TOKENS);
956
- const chunks = chunker.chunkText(content);
957
- // Find the appropriate chunk based on offset
958
- let targetChunk = chunks[0]; // Default to first chunk
959
- let targetChunkIndex = 0;
960
- let currentOffset = 0;
961
- for (let i = 0; i < chunks.length; i++) {
962
- const chunk = chunks[i];
963
- if (offset >= chunk.startOffset && offset < chunk.endOffset) {
964
- targetChunk = chunk;
965
- targetChunkIndex = i;
966
- break;
967
- }
968
- currentOffset = chunk.endOffset;
969
- }
970
- const chunkContent = targetChunk.content;
971
- const actualOffset = targetChunk.startOffset;
972
- const endOffset = targetChunk.endOffset;
973
- // Create pagination metadata using actual chunk boundaries
974
- const pagination = createPaginationMetadata(content.length, paginationParams, limitTokens, chunks, targetChunkIndex);
975
- const statusLine = formatPaginationStatus(pagination);
976
- // Estimate token usage for user awareness
977
- const chunkTokens = estimateTokenCount(chunkContent);
978
- const totalTokens = estimateTokenCount(content);
979
- // Format response with pagination info
980
- let paginatedText = '';
981
- // Add header
982
- paginatedText += `# Brutalist Analysis Results\n\n`;
983
- // Show pagination metadata
984
- const needsPagination = pagination.totalChunks > 1 || pagination.hasMore;
985
- const isFirstRequest = offset === 0;
986
- // Always show context_id on first request for future pagination
987
- if (isFirstRequest && contextId) {
988
- paginatedText += `**🔑 Context ID:** ${contextId}\n`;
989
- paginatedText += `**🔢 Token Estimate:** ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
990
- }
991
- if (needsPagination) {
992
- paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
993
- if (!isFirstRequest && contextId) {
994
- paginatedText += `**🔑 Context ID:** ${contextId}\n`;
995
- }
996
- paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
997
- if (pagination.hasMore) {
998
- if (contextId) {
999
- paginatedText += `**⏭️ Continue Reading:** Use \`context_id: "${contextId}", offset: ${endOffset}\`\n\n`;
1000
- }
1001
- else {
1002
- paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
1003
- }
1004
- }
1005
- }
1006
- paginatedText += `---\n\n`;
1007
- // Add the actual content chunk
1008
- paginatedText += chunkContent;
1009
- // Add footer
1010
- if (needsPagination) {
1011
- paginatedText += `\n\n---\n\n`;
1012
- if (pagination.hasMore) {
1013
- paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
1014
- if (contextId) {
1015
- paginatedText += `🔄 To continue: Include \`context_id: "${contextId}"\` with \`offset: ${endOffset}\` in next request`;
1016
- }
1017
- else {
1018
- paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
1019
- }
1020
- }
1021
- else {
1022
- paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
1023
- }
1024
- }
1025
- // Add verbose execution details if requested
1026
- if (verbose && result.executionSummary) {
1027
- paginatedText += `\n\n### Execution Summary\n`;
1028
- paginatedText += `- **CLI Agents:** ${result.executionSummary.successfulCLIs}/${result.executionSummary.totalCLIs} successful\n`;
1029
- paginatedText += `- **Total Time:** ${result.executionSummary.totalExecutionTime}ms\n`;
1030
- if (result.executionSummary.selectedCLI) {
1031
- paginatedText += `- **Selected CLI:** ${result.executionSummary.selectedCLI}\n`;
1032
- }
1033
- }
1034
- logger.info(`🔧 DEBUG: Returning paginated chunk - ${chunkContent.length} chars (${chunkTokens} tokens)`);
1035
- return {
1036
- content: [{
1037
- type: "text",
1038
- text: paginatedText
1039
- }]
1040
- };
1041
- }
1042
- formatErrorResponse(error) {
1043
- logger.error("Tool execution failed", error);
1044
- // Sanitize error message to prevent information leakage
1045
- let sanitizedMessage = "Analysis failed";
1046
- if (error instanceof Error) {
1047
- // Only expose safe, generic error types
1048
- if (error.message.includes('timeout') || error.message.includes('Timeout')) {
1049
- sanitizedMessage = "Analysis timed out - try reducing scope or increasing timeout";
1050
- }
1051
- else if (error.message.includes('ENOENT') || error.message.includes('no such file')) {
1052
- sanitizedMessage = `DEBUG: Target path not found - Original error: ${error.message}`;
1053
- }
1054
- else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
1055
- sanitizedMessage = "Permission denied - check file access";
1056
- }
1057
- else if (error.message.includes('No CLI agents available')) {
1058
- sanitizedMessage = "No CLI agents available for analysis";
1059
- }
1060
- else {
1061
- // Generic message for other errors to prevent path/info leakage
1062
- sanitizedMessage = "Analysis failed due to internal error";
1063
- }
1064
- }
1065
- return {
1066
- content: [{
1067
- type: "text",
1068
- text: `Brutalist MCP Error: ${sanitizedMessage}`
1069
- }]
1070
- };
1071
- }
1072
- async handleToolExecution(handler) {
1073
- try {
1074
- const result = await handler();
1075
- return this.formatToolResponse(result);
1076
- }
1077
- catch (error) {
1078
- return this.formatErrorResponse(error);
1079
- }
1080
- }
1081
693
  }
1082
694
  //# sourceMappingURL=brutalist-server.js.map