@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.
- package/README.md +22 -1
- package/dist/brutalist-server.d.ts +46 -16
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +223 -611
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +8 -6
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +236 -156
- package/dist/cli-agents.js.map +1 -1
- package/dist/domains/argument-space.d.ts +9 -0
- package/dist/domains/argument-space.d.ts.map +1 -1
- package/dist/domains/argument-space.js +27 -20
- package/dist/domains/argument-space.js.map +1 -1
- package/dist/formatting/response-formatter.d.ts +43 -0
- package/dist/formatting/response-formatter.d.ts.map +1 -0
- package/dist/formatting/response-formatter.js +277 -0
- package/dist/formatting/response-formatter.js.map +1 -0
- package/dist/generators/tool-generator.d.ts.map +1 -1
- package/dist/generators/tool-generator.js +3 -1
- package/dist/generators/tool-generator.js.map +1 -1
- package/dist/handlers/tool-handler.d.ts +33 -0
- package/dist/handlers/tool-handler.d.ts.map +1 -0
- package/dist/handlers/tool-handler.js +299 -0
- package/dist/handlers/tool-handler.js.map +1 -0
- package/dist/registry/argument-spaces.js +17 -17
- package/dist/registry/argument-spaces.js.map +1 -1
- package/dist/transport/http-transport.d.ts +40 -0
- package/dist/transport/http-transport.d.ts.map +1 -0
- package/dist/transport/http-transport.js +182 -0
- package/dist/transport/http-transport.js.map +1 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -6
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/brutalist-server.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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-
|
|
337
|
-
codex: z.string().optional().describe("Codex model: gpt-5.1-codex-max (
|
|
338
|
-
gemini: z.string().optional().describe("Gemini model: gemini-3-pro
|
|
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.
|
|
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
|
|
387
|
-
roster += "**
|
|
388
|
-
roster += "
|
|
389
|
-
roster += "-
|
|
390
|
-
roster += "-
|
|
391
|
-
roster += "
|
|
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
|
-
*
|
|
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
|
|
367
|
+
async handleDebateToolExecution(args) {
|
|
407
368
|
try {
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
|
390
|
+
const cachedResponse = await this.responseCache.getByContextId(args.context_id);
|
|
456
391
|
if (cachedResponse) {
|
|
457
|
-
logger.info(`🎯
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
//
|
|
401
|
+
// Fall through to execute new debate round with history
|
|
473
402
|
}
|
|
474
403
|
else {
|
|
475
|
-
// PAGINATION:
|
|
476
|
-
logger.info(`📖
|
|
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(`❌
|
|
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
|
|
422
|
+
`Remove context_id parameter to run a new debate.`);
|
|
494
423
|
}
|
|
495
424
|
}
|
|
496
|
-
// Generate cache key for this
|
|
497
|
-
const cacheKey = this.responseCache.generateCacheKey(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if
|
|
505
|
-
|
|
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(`🎯
|
|
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
|
|
526
|
-
let
|
|
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
|
|
534
|
-
const role = msg.role === 'user' ? 'User' : '
|
|
535
|
-
return `${role}
|
|
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
|
-
|
|
538
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
const result = await this.
|
|
544
|
-
// Cache the result
|
|
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:
|
|
475
|
+
{ role: 'user', content: args.targetPath, timestamp: now },
|
|
560
476
|
{ role: 'assistant', content: fullContent, timestamp: now }
|
|
561
477
|
];
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
contextId
|
|
566
|
-
|
|
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
|
|
571
|
-
const { contextId: newId } = await this.responseCache.set(
|
|
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
|
|
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
|