@daniel.stefan/metalink 1.3.2 → 1.3.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.
@@ -1,34 +1,35 @@
1
1
  /**
2
2
  * HTTP Server - Express-based API for MetaLink
3
3
  */
4
- import express from 'express';
5
- import rateLimit from 'express-rate-limit';
6
- import path from 'path';
7
- import { fileURLToPath } from 'url';
8
- import fs from 'fs';
9
- import { randomUUID, createHmac } from 'crypto';
10
- import { ServerManager } from './manager.js';
4
+ import express from "express";
5
+ import rateLimit from "express-rate-limit";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import fs from "fs";
9
+ import { randomUUID, createHmac } from "crypto";
10
+ import { ServerManager } from "./manager.js";
11
11
  import { version } from "../index.js";
12
- import { calculateTotalTokens } from './token-calculator.js';
13
- import { globalMetrics, MetricsPersistence, MetricsAggregator } from '../metrics/index.js';
14
- import { logger, generateRequestId, getOrCreateRequestId } from '../logging/index.js';
15
- import { getPromptsList, getPrompt } from './prompts.js';
16
- import { getResourcesList, getResourceTemplatesList, readResource } from './resources.js';
12
+ import { calculateTotalTokens } from "./token-calculator.js";
13
+ import { globalMetrics, MetricsPersistence, MetricsAggregator, } from "../metrics/index.js";
14
+ import { logger, generateRequestId, getOrCreateRequestId, } from "../logging/index.js";
15
+ import { getPromptsList, getPrompt } from "./prompts.js";
16
+ import { getResourcesList, getResourceTemplatesList, readResource, } from "./resources.js";
17
+ import { formatDescribeToolResponse, } from "../utils/toon-formatter.js";
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
  // MCP Protocol Version - MUST be set in all HTTP responses per spec 2025-06-18
19
- const MCP_PROTOCOL_VERSION = '2025-06-18';
20
+ const MCP_PROTOCOL_VERSION = "2025-06-18";
20
21
  // Custom error class for invalid parameters (JSON-RPC error code -32602)
21
22
  class InvalidParamsError extends Error {
22
23
  constructor(message) {
23
24
  super(message);
24
- this.name = 'InvalidParamsError';
25
+ this.name = "InvalidParamsError";
25
26
  }
26
27
  }
27
28
  // Custom error class for method not found (JSON-RPC error code -32601)
28
29
  class MethodNotFoundError extends Error {
29
30
  constructor(method) {
30
31
  super(`Method not found: ${method}`);
31
- this.name = 'MethodNotFoundError';
32
+ this.name = "MethodNotFoundError";
32
33
  }
33
34
  }
34
35
  // Custom error class for session not initialized (triggers HTTP 404 for MCP spec compliance)
@@ -37,7 +38,7 @@ class MethodNotFoundError extends Error {
37
38
  class SessionNotInitializedError extends Error {
38
39
  constructor(method) {
39
40
  super(`Session must be initialized before calling ${method}. Call initialize first.`);
40
- this.name = 'SessionNotInitializedError';
41
+ this.name = "SessionNotInitializedError";
41
42
  }
42
43
  }
43
44
  export class HttpServer {
@@ -91,12 +92,12 @@ export class HttpServer {
91
92
  // Phase 2: Set config loader for tool safety classification
92
93
  this.serverManager.setConfigLoader(this.configLoader);
93
94
  // Phase 4 - v1.4.0: Initialize metrics persistence and aggregation
94
- const metricsDir = process.env.METALINK_METRICS_DIR || '~/.config/metalink';
95
+ const metricsDir = process.env.METALINK_METRICS_DIR || "~/.config/metalink";
95
96
  this.metricsPersistence = new MetricsPersistence(metricsDir);
96
97
  this.metricsAggregator = new MetricsAggregator();
97
98
  // Session persistence path (v1.1.24+)
98
- const configDir = (process.env.METALINK_CONFIG_DIR || '~/.config/metalink').replace(/^~/, process.env.HOME || '~');
99
- this.sessionPersistPath = path.join(configDir, 'sessions.json');
99
+ const configDir = (process.env.METALINK_CONFIG_DIR || "~/.config/metalink").replace(/^~/, process.env.HOME || "~");
100
+ this.sessionPersistPath = path.join(configDir, "sessions.json");
100
101
  // Load persisted metrics on startup
101
102
  this.loadPersistedMetrics();
102
103
  // Load persisted sessions on startup (v1.1.24+)
@@ -119,14 +120,14 @@ export class HttpServer {
119
120
  * Pagination support - Encode cursor data to opaque base64 string
120
121
  */
121
122
  encodeCursor(data) {
122
- return Buffer.from(JSON.stringify(data)).toString('base64');
123
+ return Buffer.from(JSON.stringify(data)).toString("base64");
123
124
  }
124
125
  /**
125
126
  * Pagination support - Decode cursor from base64 string
126
127
  */
127
128
  decodeCursor(cursor) {
128
129
  try {
129
- const decoded = Buffer.from(cursor, 'base64').toString('utf-8');
130
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
130
131
  return JSON.parse(decoded);
131
132
  }
132
133
  catch {
@@ -142,7 +143,7 @@ export class HttpServer {
142
143
  return value;
143
144
  }
144
145
  // Truncate and add indicator
145
- const truncated = json.substring(0, maxChars - 20) + '... [truncated]';
146
+ const truncated = json.substring(0, maxChars - 20) + "... [truncated]";
146
147
  try {
147
148
  return JSON.parse(truncated);
148
149
  }
@@ -151,6 +152,16 @@ export class HttpServer {
151
152
  return truncated;
152
153
  }
153
154
  }
155
+ /**
156
+ * Detect if current client is likely an LLM based on request headers
157
+ * Used for auto-detection of TOON format
158
+ */
159
+ isLLMClient() {
160
+ // This is a heuristic based on common LLM client patterns
161
+ // In practice, you'd track this per-session or per-request
162
+ // For now, we default to false and let explicit config override
163
+ return false;
164
+ }
154
165
  /**
155
166
  * Pagination support - Apply pagination to tool result
156
167
  *
@@ -177,15 +188,15 @@ export class HttpServer {
177
188
  // More results available - create cursor
178
189
  const nextCursor = this.encodeCursor({
179
190
  offset: endOffset,
180
- type: 'array'
191
+ type: "array",
181
192
  });
182
193
  return {
183
194
  result: result.slice(startOffset, endOffset),
184
195
  _pagination: {
185
196
  nextCursor,
186
197
  totalAvailable,
187
- truncated: true
188
- }
198
+ truncated: true,
199
+ },
189
200
  };
190
201
  }
191
202
  else if (startOffset > 0) {
@@ -194,8 +205,8 @@ export class HttpServer {
194
205
  result: result.slice(startOffset),
195
206
  _pagination: {
196
207
  totalAvailable,
197
- truncated: false
198
- }
208
+ truncated: false,
209
+ },
199
210
  };
200
211
  }
201
212
  }
@@ -205,23 +216,23 @@ export class HttpServer {
205
216
  // Generate cursor for continuation
206
217
  const nextCursor = this.encodeCursor({
207
218
  offset: maxChars,
208
- type: 'chars'
219
+ type: "chars",
209
220
  });
210
221
  return {
211
222
  result: this.truncateToChars(result, maxChars),
212
223
  _pagination: {
213
224
  nextCursor,
214
225
  totalAvailable: json.length,
215
- truncated: true
216
- }
226
+ truncated: true,
227
+ },
217
228
  };
218
229
  }
219
230
  // No pagination needed
220
231
  return {
221
232
  result,
222
233
  _pagination: {
223
- truncated: false
224
- }
234
+ truncated: false,
235
+ },
225
236
  };
226
237
  }
227
238
  /**
@@ -230,19 +241,19 @@ export class HttpServer {
230
241
  setupMiddleware() {
231
242
  // Request ID middleware - generate or extract correlation ID
232
243
  this.app.use((req, _res, next) => {
233
- const requestId = getOrCreateRequestId(req.headers['x-request-id']);
244
+ const requestId = getOrCreateRequestId(req.headers["x-request-id"]);
234
245
  req.requestId = requestId;
235
246
  next();
236
247
  });
237
248
  // Debug middleware - structured logging for all requests
238
249
  this.app.use((req, _res, next) => {
239
250
  const requestId = req.requestId;
240
- logger.debug('Incoming request', {
251
+ logger.debug("Incoming request", {
241
252
  requestId,
242
253
  method: req.method,
243
254
  path: req.path,
244
255
  url: req.url,
245
- userAgent: req.headers['user-agent'],
256
+ userAgent: req.headers["user-agent"],
246
257
  });
247
258
  next();
248
259
  });
@@ -251,14 +262,14 @@ export class HttpServer {
251
262
  const startTime = Date.now();
252
263
  const requestId = req.requestId;
253
264
  globalMetrics.recordApiRequest();
254
- res.on('finish', () => {
265
+ res.on("finish", () => {
255
266
  const latency = Date.now() - startTime;
256
267
  globalMetrics.recordApiResponse(latency);
257
268
  if (res.statusCode >= 400) {
258
269
  globalMetrics.recordApiError();
259
270
  }
260
271
  // Log response with correlation ID
261
- logger.info('Request completed', {
272
+ logger.info("Request completed", {
262
273
  requestId,
263
274
  method: req.method,
264
275
  path: req.path,
@@ -271,7 +282,7 @@ export class HttpServer {
271
282
  // Origin header validation to prevent DNS rebinding attacks
272
283
  this.app.use((req, res, next) => {
273
284
  // Only validate POST requests (where state changes can occur)
274
- if (req.method === 'POST') {
285
+ if (req.method === "POST") {
275
286
  const origin = req.headers.origin;
276
287
  const requestId = req.requestId;
277
288
  // Allow requests with no Origin header (non-browser clients, Electron apps)
@@ -284,38 +295,38 @@ export class HttpServer {
284
295
  const originUrl = new URL(origin);
285
296
  const hostname = originUrl.hostname;
286
297
  // Allow localhost, 127.0.0.1, and IPv6 loopback
287
- const allowedHosts = ['localhost', '127.0.0.1', '[::1]', '::1'];
298
+ const allowedHosts = ["localhost", "127.0.0.1", "[::1]", "::1"];
288
299
  if (!allowedHosts.includes(hostname)) {
289
- logger.warn('Rejected request from unauthorized origin', {
300
+ logger.warn("Rejected request from unauthorized origin", {
290
301
  requestId,
291
302
  origin,
292
303
  hostname,
293
- security: 'dns_rebinding_protection',
304
+ security: "dns_rebinding_protection",
294
305
  });
295
306
  res.status(403).json({
296
- jsonrpc: '2.0',
307
+ jsonrpc: "2.0",
297
308
  error: {
298
309
  code: -32600,
299
- message: 'Invalid Request: Origin not allowed'
300
- }
310
+ message: "Invalid Request: Origin not allowed",
311
+ },
301
312
  });
302
313
  return;
303
314
  }
304
315
  }
305
316
  catch (err) {
306
317
  // Invalid origin URL
307
- logger.warn('Rejected request with malformed origin', {
318
+ logger.warn("Rejected request with malformed origin", {
308
319
  requestId,
309
320
  origin,
310
- error: err instanceof Error ? err.message : 'Unknown error',
311
- security: 'dns_rebinding_protection',
321
+ error: err instanceof Error ? err.message : "Unknown error",
322
+ security: "dns_rebinding_protection",
312
323
  });
313
324
  res.status(403).json({
314
- jsonrpc: '2.0',
325
+ jsonrpc: "2.0",
315
326
  error: {
316
327
  code: -32600,
317
- message: 'Invalid Request: Malformed origin'
318
- }
328
+ message: "Invalid Request: Malformed origin",
329
+ },
319
330
  });
320
331
  return;
321
332
  }
@@ -323,23 +334,26 @@ export class HttpServer {
323
334
  next();
324
335
  });
325
336
  // Raw body parser for MCP endpoints (must come before JSON parser)
326
- this.app.use('/mcp', express.raw({ type: 'application/json' }));
327
- this.app.use('/mcp/rpc', express.raw({ type: 'application/json' }));
328
- this.app.use('/rpc', express.raw({ type: 'application/json' }));
329
- this.app.use('/rpc/rpc', express.raw({ type: 'application/json' }));
337
+ this.app.use("/mcp", express.raw({ type: "application/json" }));
338
+ this.app.use("/mcp/rpc", express.raw({ type: "application/json" }));
339
+ this.app.use("/rpc", express.raw({ type: "application/json" }));
340
+ this.app.use("/rpc/rpc", express.raw({ type: "application/json" }));
330
341
  // JSON body parser for other endpoints
331
342
  this.app.use(express.json());
332
343
  // Rate limiting
333
344
  const daemonConfig = this.config.daemon;
334
- if (daemonConfig && daemonConfig.rateLimit && daemonConfig.rateLimit.enabled) {
345
+ if (daemonConfig &&
346
+ daemonConfig.rateLimit &&
347
+ daemonConfig.rateLimit.enabled) {
335
348
  const limiter = rateLimit({
336
349
  windowMs: daemonConfig.rateLimit.windowMs || 60000,
337
350
  max: daemonConfig.rateLimit.max || 500,
338
351
  keyGenerator: (req) => {
339
- if (daemonConfig.rateLimit && daemonConfig.rateLimit.keyGenerator === 'user') {
340
- return req.userId || req.ip || '';
352
+ if (daemonConfig.rateLimit &&
353
+ daemonConfig.rateLimit.keyGenerator === "user") {
354
+ return req.userId || req.ip || "";
341
355
  }
342
- return req.ip || '';
356
+ return req.ip || "";
343
357
  },
344
358
  });
345
359
  this.app.use(limiter);
@@ -351,7 +365,7 @@ export class HttpServer {
351
365
  // Request logging
352
366
  this.app.use((req, res, next) => {
353
367
  const start = Date.now();
354
- res.on('finish', () => {
368
+ res.on("finish", () => {
355
369
  const duration = Date.now() - start;
356
370
  console.log(`[${req.method}] ${req.path} ${res.statusCode} ${duration}ms`);
357
371
  });
@@ -364,29 +378,30 @@ export class HttpServer {
364
378
  authMiddleware(req, res, next) {
365
379
  const authHeader = req.headers.authorization;
366
380
  if (!authHeader) {
367
- res.status(401).json({ error: 'Missing Authorization header' });
381
+ res.status(401).json({ error: "Missing Authorization header" });
368
382
  return;
369
383
  }
370
- const [scheme] = authHeader.split(' ');
371
- if (scheme !== 'Bearer') {
372
- res.status(401).json({ error: 'Invalid Authorization scheme' });
384
+ const [scheme] = authHeader.split(" ");
385
+ if (scheme !== "Bearer") {
386
+ res.status(401).json({ error: "Invalid Authorization scheme" });
373
387
  return;
374
388
  }
375
389
  const token = authHeader.substring(7);
376
390
  const secret = this.config?.daemon?.auth?.secret;
377
391
  if (!secret) {
378
- console.warn('[Auth] No auth secret configured, rejecting request');
379
- res.status(500).json({ error: 'Auth not configured' });
392
+ console.warn("[Auth] No auth secret configured, rejecting request");
393
+ res.status(500).json({ error: "Auth not configured" });
380
394
  return;
381
395
  }
382
- const expectedToken = createHmac('sha256', secret)
383
- .update('metalink-auth-token').digest('hex');
396
+ const expectedToken = createHmac("sha256", secret)
397
+ .update("metalink-auth-token")
398
+ .digest("hex");
384
399
  if (token !== expectedToken) {
385
- res.status(401).json({ error: 'Invalid authentication token' });
400
+ res.status(401).json({ error: "Invalid authentication token" });
386
401
  return;
387
402
  }
388
403
  req.authenticated = true;
389
- req.userId = 'authenticated-user';
404
+ req.userId = "authenticated-user";
390
405
  next();
391
406
  }
392
407
  /**
@@ -395,41 +410,41 @@ export class HttpServer {
395
410
  setupRoutes() {
396
411
  const api = express.Router();
397
412
  // Server endpoints
398
- api.get('/servers', this.listServers.bind(this));
399
- api.get('/servers/available/list', this.listAvailableServers.bind(this));
400
- api.post('/servers/:name/start', this.startServer.bind(this));
401
- api.post('/servers/:name/stop', this.stopServer.bind(this));
402
- api.post('/servers/:name/restart', this.restartServer.bind(this));
403
- api.post('/servers/:name/enable', this.enableServer.bind(this));
404
- api.post('/servers/:name/disable', this.disableServer.bind(this));
405
- api.get('/servers/:name/status', this.getServerStatus.bind(this));
406
- api.get('/servers/:name/info', this.getServerInfo.bind(this));
407
- api.post('/servers/:name/refresh-tools', this.refreshServerTools.bind(this)); // v1.4.2: Force refresh tools
413
+ api.get("/servers", this.listServers.bind(this));
414
+ api.get("/servers/available/list", this.listAvailableServers.bind(this));
415
+ api.post("/servers/:name/start", this.startServer.bind(this));
416
+ api.post("/servers/:name/stop", this.stopServer.bind(this));
417
+ api.post("/servers/:name/restart", this.restartServer.bind(this));
418
+ api.post("/servers/:name/enable", this.enableServer.bind(this));
419
+ api.post("/servers/:name/disable", this.disableServer.bind(this));
420
+ api.get("/servers/:name/status", this.getServerStatus.bind(this));
421
+ api.get("/servers/:name/info", this.getServerInfo.bind(this));
422
+ api.post("/servers/:name/refresh-tools", this.refreshServerTools.bind(this)); // v1.4.2: Force refresh tools
408
423
  // Tool endpoints
409
- api.get('/servers/:name/tools', this.getServerTools.bind(this));
410
- api.post('/servers/:name/tools/:toolName/execute', this.executeTool.bind(this));
424
+ api.get("/servers/:name/tools", this.getServerTools.bind(this));
425
+ api.post("/servers/:name/tools/:toolName/execute", this.executeTool.bind(this));
411
426
  // Configuration endpoints
412
- api.get('/config', this.getConfig.bind(this));
413
- api.put('/config', this.updateConfig.bind(this));
427
+ api.get("/config", this.getConfig.bind(this));
428
+ api.put("/config", this.updateConfig.bind(this));
414
429
  // Registry management endpoints
415
- api.post('/registry/servers', this.addServer.bind(this));
416
- api.delete('/registry/servers/:name', this.removeServer.bind(this));
417
- api.post('/registry/validate', this.validateServer.bind(this));
430
+ api.post("/registry/servers", this.addServer.bind(this));
431
+ api.delete("/registry/servers/:name", this.removeServer.bind(this));
432
+ api.post("/registry/validate", this.validateServer.bind(this));
418
433
  // Health endpoints
419
- api.get('/health', this.healthCheck.bind(this));
434
+ api.get("/health", this.healthCheck.bind(this));
420
435
  // Version endpoint
421
- api.get('/version', this.getVersion.bind(this));
436
+ api.get("/version", this.getVersion.bind(this));
422
437
  // Metrics endpoints (Phase 4 - v1.4.0)
423
- api.get('/metrics', this.getMetrics.bind(this));
424
- api.get('/metrics/prometheus', this.getPrometheusMetrics.bind(this));
425
- api.get('/metrics/api', this.getApiMetrics.bind(this));
426
- api.get('/metrics/servers/:name', this.getServerMetrics.bind(this));
427
- api.get('/metrics/hourly', this.getHourlyMetrics.bind(this));
428
- api.get('/metrics/daily', this.getDailyMetrics.bind(this));
429
- api.get('/metrics/weekly', this.getWeeklyMetrics.bind(this));
430
- api.get('/metrics/errors', this.getErrorAnalytics.bind(this));
431
- api.get('/metrics/errors/:toolName', this.getToolErrorMetrics.bind(this));
432
- api.get('/metrics/tools', this.getToolMetrics.bind(this)); // Test 187: Tool-specific granular metrics
438
+ api.get("/metrics", this.getMetrics.bind(this));
439
+ api.get("/metrics/prometheus", this.getPrometheusMetrics.bind(this));
440
+ api.get("/metrics/api", this.getApiMetrics.bind(this));
441
+ api.get("/metrics/servers/:name", this.getServerMetrics.bind(this));
442
+ api.get("/metrics/hourly", this.getHourlyMetrics.bind(this));
443
+ api.get("/metrics/daily", this.getDailyMetrics.bind(this));
444
+ api.get("/metrics/weekly", this.getWeeklyMetrics.bind(this));
445
+ api.get("/metrics/errors", this.getErrorAnalytics.bind(this));
446
+ api.get("/metrics/errors/:toolName", this.getToolErrorMetrics.bind(this));
447
+ api.get("/metrics/tools", this.getToolMetrics.bind(this)); // Test 187: Tool-specific granular metrics
433
448
  // Safety endpoints (Phase 1 - v1.2.0)
434
449
  api.get("/safety", this.getSafetyRules.bind(this));
435
450
  api.get("/safety/check/:server/:tool", this.checkToolSafety.bind(this));
@@ -440,77 +455,77 @@ export class HttpServer {
440
455
  api.delete("/safety/rules/:rule", this.removeRule.bind(this));
441
456
  api.post("/safety/reset", this.resetSafetyRules.bind(this));
442
457
  api.post("/safety/import", this.importSafetyRules.bind(this));
443
- this.app.use('/api/v1', api);
458
+ this.app.use("/api/v1", api);
444
459
  // Also register /api/metrics endpoint (without /v1 prefix) for backward compatibility with tests
445
- this.app.get('/api/metrics', this.getMetrics.bind(this));
460
+ this.app.get("/api/metrics", this.getMetrics.bind(this));
446
461
  // MCP endpoint (JSON-RPC over HTTP) - Primary endpoint per MCP 2025-06-18 spec
447
462
  // GET /mcp for streaming/SSE connections (Claude Code HTTP transport)
448
- this.app.get('/mcp', this.handleMcpRequest.bind(this));
463
+ this.app.get("/mcp", this.handleMcpRequest.bind(this));
449
464
  // POST endpoints for standard JSON-RPC
450
- this.app.post('/mcp', this.handleMcpRequest.bind(this));
465
+ this.app.post("/mcp", this.handleMcpRequest.bind(this));
451
466
  // DELETE /mcp for session cleanup (per MCP spec)
452
- this.app.delete('/mcp', this.handleMcpRequest.bind(this));
467
+ this.app.delete("/mcp", this.handleMcpRequest.bind(this));
453
468
  // Legacy SSE endpoints for backward compatibility (2025-03-26 and earlier)
454
- this.app.get('/sse', this.handleMcpRequest.bind(this)); // Legacy SSE endpoint
455
- this.app.post('/messages', this.handleMcpRequest.bind(this)); // Legacy JSON-RPC endpoint
469
+ this.app.get("/sse", this.handleMcpRequest.bind(this)); // Legacy SSE endpoint
470
+ this.app.post("/messages", this.handleMcpRequest.bind(this)); // Legacy JSON-RPC endpoint
456
471
  // Alternative MCP endpoint paths for compatibility with various clients
457
472
  // Grok HTTP transport expects /mcp/rpc path
458
- this.app.post('/mcp/rpc', this.handleMcpRequest.bind(this));
473
+ this.app.post("/mcp/rpc", this.handleMcpRequest.bind(this));
459
474
  // Root-level /rpc for clients that don't use /mcp prefix (e.g., Grok CLI)
460
- this.app.post('/rpc', this.handleMcpRequest.bind(this));
475
+ this.app.post("/rpc", this.handleMcpRequest.bind(this));
461
476
  // Grok appends /rpc to URL, creating /rpc/rpc when given http://.../rpc
462
- this.app.post('/rpc/rpc', this.handleMcpRequest.bind(this));
477
+ this.app.post("/rpc/rpc", this.handleMcpRequest.bind(this));
463
478
  // Prometheus metrics endpoint (standard /metrics path for scraping)
464
- this.app.get('/metrics', this.getPrometheusMetrics.bind(this));
479
+ this.app.get("/metrics", this.getPrometheusMetrics.bind(this));
465
480
  // Serve dashboard static files (if available)
466
- const dashboardPath = path.join(__dirname, '../../../dashboard/dist');
481
+ const dashboardPath = path.join(__dirname, "../../../dashboard/dist");
467
482
  const dashboardExists = fs.existsSync(dashboardPath);
468
- const indexPath = path.join(dashboardPath, 'index.html');
483
+ const indexPath = path.join(dashboardPath, "index.html");
469
484
  const indexExists = dashboardExists && fs.existsSync(indexPath);
470
- console.log('[HTTP] dashboardPath:', dashboardPath);
471
- console.log('[HTTP] dashboard exists:', dashboardExists);
472
- console.log('[HTTP] index.html exists:', indexExists);
485
+ console.log("[HTTP] dashboardPath:", dashboardPath);
486
+ console.log("[HTTP] dashboard exists:", dashboardExists);
487
+ console.log("[HTTP] index.html exists:", indexExists);
473
488
  // Serve dashboard - explicit root route
474
489
  if (indexExists) {
475
- console.log('[HTTP] Registering GET / route for dashboard');
476
- this.app.get('/', (_req, res) => {
477
- console.log('[HTTP] Serving root path from:', indexPath);
490
+ console.log("[HTTP] Registering GET / route for dashboard");
491
+ this.app.get("/", (_req, res) => {
492
+ console.log("[HTTP] Serving root path from:", indexPath);
478
493
  res.sendFile(indexPath);
479
494
  });
480
- console.log('[HTTP] GET / route registered successfully');
495
+ console.log("[HTTP] GET / route registered successfully");
481
496
  }
482
497
  else {
483
- console.log('[HTTP] Skipping GET / - index.html not found');
498
+ console.log("[HTTP] Skipping GET / - index.html not found");
484
499
  }
485
500
  if (dashboardExists) {
486
- console.log('[HTTP] Registering static file middleware for:', dashboardPath);
501
+ console.log("[HTTP] Registering static file middleware for:", dashboardPath);
487
502
  // Serve static assets from dashboard
488
503
  this.app.use(express.static(dashboardPath, {
489
- etag: false
504
+ etag: false,
490
505
  }));
491
- console.log('[HTTP] Static middleware registered');
506
+ console.log("[HTTP] Static middleware registered");
492
507
  }
493
508
  // SPA fallback - serve index.html for client-side routing (only if dashboard exists)
494
509
  if (indexExists) {
495
- console.log('[HTTP] Registering fallback route (*)');
496
- this.app.get('*', (_req, res) => {
497
- console.log('[HTTP] Fallback route hit for:', _req.path);
510
+ console.log("[HTTP] Registering fallback route (*)");
511
+ this.app.get("*", (_req, res) => {
512
+ console.log("[HTTP] Fallback route hit for:", _req.path);
498
513
  res.sendFile(indexPath);
499
514
  });
500
- console.log('[HTTP] Fallback route registered');
515
+ console.log("[HTTP] Fallback route registered");
501
516
  }
502
517
  else {
503
- console.log('[HTTP] Skipping fallback route - dashboard not available');
518
+ console.log("[HTTP] Skipping fallback route - dashboard not available");
504
519
  }
505
520
  // SECURITY: Error handler with sanitization to prevent credential leakage
506
521
  // OWASP Reference: A3:2017-Sensitive Data Exposure
507
522
  this.app.use((err, _req, res, _next) => {
508
523
  // Log full error internally for debugging (not exposed to client)
509
- console.error('API Error:', err);
524
+ console.error("API Error:", err);
510
525
  // SECURITY: Sanitize error message before sending to client
511
526
  const sanitizedMessage = this.sanitizeErrorMessage(err.message);
512
527
  res.status(500).json({
513
- error: 'Internal Server Error',
528
+ error: "Internal Server Error",
514
529
  message: sanitizedMessage,
515
530
  });
516
531
  });
@@ -528,7 +543,7 @@ export class HttpServer {
528
543
  */
529
544
  sanitizeErrorMessage(message) {
530
545
  if (!message)
531
- return 'An error occurred';
546
+ return "An error occurred";
532
547
  // Patterns that indicate sensitive data that should be masked
533
548
  const sensitivePatterns = [
534
549
  // API keys and tokens (various formats)
@@ -556,11 +571,11 @@ export class HttpServer {
556
571
  ];
557
572
  let sanitized = message;
558
573
  for (const pattern of sensitivePatterns) {
559
- sanitized = sanitized.replace(pattern, '[REDACTED]');
574
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
560
575
  }
561
576
  // Additional safety: truncate very long messages that might contain dumps
562
577
  if (sanitized.length > 500) {
563
- sanitized = sanitized.substring(0, 500) + '... [truncated]';
578
+ sanitized = sanitized.substring(0, 500) + "... [truncated]";
564
579
  }
565
580
  return sanitized;
566
581
  }
@@ -568,52 +583,52 @@ export class HttpServer {
568
583
  * Setup Server-Sent Events
569
584
  */
570
585
  setupSSE() {
571
- this.app.get('/api/v1/events', (req, res) => {
586
+ this.app.get("/api/v1/events", (req, res) => {
572
587
  // Check authentication
573
588
  if (this.config.daemon?.auth?.enabled && !req.authenticated) {
574
- res.status(401).json({ error: 'Unauthorized' });
589
+ res.status(401).json({ error: "Unauthorized" });
575
590
  return;
576
591
  }
577
- res.setHeader('Content-Type', 'text/event-stream');
578
- res.setHeader('Cache-Control', 'no-cache');
579
- res.setHeader('Connection', 'keep-alive');
592
+ res.setHeader("Content-Type", "text/event-stream");
593
+ res.setHeader("Cache-Control", "no-cache");
594
+ res.setHeader("Connection", "keep-alive");
580
595
  // Send initial connection message
581
596
  res.write('data: {"type":"connected"}\n\n');
582
597
  this.eventClients.add(res);
583
598
  // Clean up on disconnect
584
- req.on('close', () => {
599
+ req.on("close", () => {
585
600
  this.eventClients.delete(res);
586
601
  });
587
602
  });
588
603
  // Broadcast events from server manager
589
- this.serverManager.on('server:started', (data) => {
604
+ this.serverManager.on("server:started", (data) => {
590
605
  this.broadcastEvent({
591
- type: 'server:started',
606
+ type: "server:started",
592
607
  data,
593
608
  });
594
609
  });
595
- this.serverManager.on('server:stopped', (data) => {
610
+ this.serverManager.on("server:stopped", (data) => {
596
611
  this.broadcastEvent({
597
- type: 'server:stopped',
612
+ type: "server:stopped",
598
613
  data,
599
614
  });
600
615
  });
601
- this.serverManager.on('server:error', (data) => {
616
+ this.serverManager.on("server:error", (data) => {
602
617
  this.broadcastEvent({
603
- type: 'server:error',
618
+ type: "server:error",
604
619
  data,
605
620
  });
606
621
  });
607
- this.serverManager.on('health:check', (data) => {
622
+ this.serverManager.on("health:check", (data) => {
608
623
  this.broadcastEvent({
609
- type: 'health:check',
624
+ type: "health:check",
610
625
  data,
611
626
  });
612
627
  });
613
628
  // Listen for server removal events (from removeServer method)
614
- this.serverManager.on('server:removed', (data) => {
629
+ this.serverManager.on("server:removed", (data) => {
615
630
  this.broadcastEvent({
616
- type: 'server:removed',
631
+ type: "server:removed",
617
632
  data,
618
633
  });
619
634
  });
@@ -635,12 +650,13 @@ export class HttpServer {
635
650
  try {
636
651
  const servers = this.configLoader.getServers();
637
652
  const serverInfos = servers.map((config) => {
638
- const isStdio = config.transport === 'stdio' || config.transport === undefined;
653
+ const isStdio = config.transport === "stdio" || config.transport === undefined;
639
654
  const tools = this.serverManager.getServerTools(config.name);
640
655
  const baseInfo = {
641
656
  name: config.name,
642
657
  env: config.env || {},
643
- status: (this.serverManager.getServerStatus(config.name)?.status || 'stopped'),
658
+ status: (this.serverManager.getServerStatus(config.name)?.status ||
659
+ "stopped"),
644
660
  process: this.serverManager.getServerStatus(config.name),
645
661
  toolCount: tools.length || 0,
646
662
  tokenEstimate: calculateTotalTokens(tools),
@@ -661,7 +677,7 @@ export class HttpServer {
661
677
  }
662
678
  catch (error) {
663
679
  res.status(500).json({
664
- error: error instanceof Error ? error.message : 'Failed to list servers',
680
+ error: error instanceof Error ? error.message : "Failed to list servers",
665
681
  });
666
682
  }
667
683
  }
@@ -672,15 +688,16 @@ export class HttpServer {
672
688
  try {
673
689
  const allServers = this.configLoader.getAllServers();
674
690
  const exposedServers = this.configLoader.getServers();
675
- const exposedNames = new Set(exposedServers.map(s => s.name));
691
+ const exposedNames = new Set(exposedServers.map((s) => s.name));
676
692
  const serverInfos = allServers.map((config) => {
677
- const isStdio = config.transport === 'stdio' || config.transport === undefined;
693
+ const isStdio = config.transport === "stdio" || config.transport === undefined;
678
694
  const tools = this.serverManager.getServerTools(config.name);
679
695
  const baseInfo = {
680
696
  name: config.name,
681
- type: isStdio ? 'stdio' : 'http',
697
+ type: isStdio ? "stdio" : "http",
682
698
  env: config.env || {},
683
- status: (this.serverManager.getServerStatus(config.name)?.status || 'stopped'),
699
+ status: (this.serverManager.getServerStatus(config.name)?.status ||
700
+ "stopped"),
684
701
  process: this.serverManager.getServerStatus(config.name),
685
702
  enabled: exposedNames.has(config.name),
686
703
  toolCount: tools.length || 0,
@@ -691,7 +708,7 @@ export class HttpServer {
691
708
  ...baseInfo,
692
709
  command: config.command,
693
710
  args: config.args || [],
694
- fullCommand: `${config.command} ${(config.args || []).join(' ')}`.trim(),
711
+ fullCommand: `${config.command} ${(config.args || []).join(" ")}`.trim(),
695
712
  }
696
713
  : {
697
714
  ...baseInfo,
@@ -703,7 +720,9 @@ export class HttpServer {
703
720
  }
704
721
  catch (error) {
705
722
  res.status(500).json({
706
- error: error instanceof Error ? error.message : 'Failed to list available servers',
723
+ error: error instanceof Error
724
+ ? error.message
725
+ : "Failed to list available servers",
707
726
  });
708
727
  }
709
728
  }
@@ -720,18 +739,18 @@ export class HttpServer {
720
739
  }
721
740
  // Get current EXPOSE_SERVERS list
722
741
  const currentExposed = process.env.EXPOSE_SERVERS
723
- ? process.env.EXPOSE_SERVERS.split(',').map(s => s.trim())
724
- : (this.config.base_servers || []);
742
+ ? process.env.EXPOSE_SERVERS.split(",").map((s) => s.trim())
743
+ : this.config.base_servers || [];
725
744
  // Add server if not already exposed
726
745
  if (!currentExposed.includes(name)) {
727
746
  currentExposed.push(name);
728
- process.env.EXPOSE_SERVERS = currentExposed.join(',');
747
+ process.env.EXPOSE_SERVERS = currentExposed.join(",");
729
748
  }
730
749
  res.json({ success: true, message: `Server ${name} enabled` });
731
750
  }
732
751
  catch (error) {
733
752
  res.status(400).json({
734
- error: error instanceof Error ? error.message : 'Failed to enable server',
753
+ error: error instanceof Error ? error.message : "Failed to enable server",
735
754
  });
736
755
  }
737
756
  }
@@ -743,19 +762,19 @@ export class HttpServer {
743
762
  const { name } = req.params;
744
763
  // Get current EXPOSE_SERVERS list
745
764
  const currentExposed = process.env.EXPOSE_SERVERS
746
- ? process.env.EXPOSE_SERVERS.split(',').map(s => s.trim())
747
- : (this.config.base_servers || []);
765
+ ? process.env.EXPOSE_SERVERS.split(",").map((s) => s.trim())
766
+ : this.config.base_servers || [];
748
767
  // Remove server if exposed
749
768
  const index = currentExposed.indexOf(name);
750
769
  if (index >= 0) {
751
770
  currentExposed.splice(index, 1);
752
- process.env.EXPOSE_SERVERS = currentExposed.join(',');
771
+ process.env.EXPOSE_SERVERS = currentExposed.join(",");
753
772
  }
754
773
  res.json({ success: true, message: `Server ${name} disabled` });
755
774
  }
756
775
  catch (error) {
757
776
  res.status(400).json({
758
- error: error instanceof Error ? error.message : 'Failed to disable server',
777
+ error: error instanceof Error ? error.message : "Failed to disable server",
759
778
  });
760
779
  }
761
780
  }
@@ -767,7 +786,9 @@ export class HttpServer {
767
786
  const { name } = req.params;
768
787
  const config = this.configLoader.getServer(name);
769
788
  if (!config) {
770
- res.status(404).json({ error: `Server ${name} not found in configuration` });
789
+ res
790
+ .status(404)
791
+ .json({ error: `Server ${name} not found in configuration` });
771
792
  return;
772
793
  }
773
794
  const process = await this.serverManager.startServer(config);
@@ -778,7 +799,7 @@ export class HttpServer {
778
799
  }
779
800
  catch (error) {
780
801
  res.status(400).json({
781
- error: error instanceof Error ? error.message : 'Failed to start server',
802
+ error: error instanceof Error ? error.message : "Failed to start server",
782
803
  });
783
804
  }
784
805
  }
@@ -796,7 +817,7 @@ export class HttpServer {
796
817
  }
797
818
  catch (error) {
798
819
  res.status(400).json({
799
- error: error instanceof Error ? error.message : 'Failed to stop server',
820
+ error: error instanceof Error ? error.message : "Failed to stop server",
800
821
  });
801
822
  }
802
823
  }
@@ -808,7 +829,9 @@ export class HttpServer {
808
829
  const { name } = req.params;
809
830
  const config = this.configLoader.getServer(name);
810
831
  if (!config) {
811
- res.status(404).json({ error: `Server ${name} not found in configuration` });
832
+ res
833
+ .status(404)
834
+ .json({ error: `Server ${name} not found in configuration` });
812
835
  return;
813
836
  }
814
837
  const process = await this.serverManager.restartServer(config);
@@ -819,7 +842,7 @@ export class HttpServer {
819
842
  }
820
843
  catch (error) {
821
844
  res.status(400).json({
822
- error: error instanceof Error ? error.message : 'Failed to restart server',
845
+ error: error instanceof Error ? error.message : "Failed to restart server",
823
846
  });
824
847
  }
825
848
  }
@@ -832,7 +855,9 @@ export class HttpServer {
832
855
  const { name } = req.params;
833
856
  const config = this.configLoader.getServer(name);
834
857
  if (!config) {
835
- res.status(404).json({ error: `Server ${name} not found in configuration` });
858
+ res
859
+ .status(404)
860
+ .json({ error: `Server ${name} not found in configuration` });
836
861
  return;
837
862
  }
838
863
  console.log(`[HTTP] Force refreshing tools for ${name}...`);
@@ -841,12 +866,14 @@ export class HttpServer {
841
866
  success: true,
842
867
  serverName: name,
843
868
  toolCount: tools.length,
844
- tools: tools.map(t => t.name),
869
+ tools: tools.map((t) => t.name),
845
870
  });
846
871
  }
847
872
  catch (error) {
848
873
  res.status(400).json({
849
- error: error instanceof Error ? error.message : 'Failed to refresh server tools',
874
+ error: error instanceof Error
875
+ ? error.message
876
+ : "Failed to refresh server tools",
850
877
  });
851
878
  }
852
879
  }
@@ -870,11 +897,11 @@ export class HttpServer {
870
897
  server: name,
871
898
  status: {
872
899
  pid: 0,
873
- status: 'stopped',
900
+ status: "stopped",
874
901
  uptime: 0,
875
902
  lastHealthCheck: 0,
876
- errorCount: 0
877
- }
903
+ errorCount: 0,
904
+ },
878
905
  });
879
906
  return;
880
907
  }
@@ -882,7 +909,9 @@ export class HttpServer {
882
909
  }
883
910
  catch (error) {
884
911
  res.status(500).json({
885
- error: error instanceof Error ? error.message : 'Failed to get server status',
912
+ error: error instanceof Error
913
+ ? error.message
914
+ : "Failed to get server status",
886
915
  });
887
916
  }
888
917
  }
@@ -893,7 +922,7 @@ export class HttpServer {
893
922
  async getServerInfo(req, res) {
894
923
  try {
895
924
  const { name } = req.params;
896
- const forceDiscovery = req.query.forceDiscovery === 'true';
925
+ const forceDiscovery = req.query.forceDiscovery === "true";
897
926
  // Get server configuration from registry
898
927
  const serverConfig = this.configLoader.getServer(name);
899
928
  if (!serverConfig) {
@@ -923,16 +952,17 @@ export class HttpServer {
923
952
  const exposedServers = this.configLoader.getServers();
924
953
  const enabled = exposedServers.some((s) => s.name === name);
925
954
  // Build response - different fields for stdio vs HTTP servers
926
- const isStdio = serverConfig.transport === 'stdio' || serverConfig.transport === undefined;
955
+ const isStdio = serverConfig.transport === "stdio" ||
956
+ serverConfig.transport === undefined;
927
957
  const baseResponse = {
928
958
  name: serverConfig.name,
929
- status: runtimeStatus?.status || 'stopped',
959
+ status: runtimeStatus?.status || "stopped",
930
960
  enabled,
931
961
  pid: runtimeStatus?.pid,
932
962
  uptime: runtimeStatus?.uptime,
933
963
  lastHealthCheck: runtimeStatus?.lastHealthCheck,
934
964
  errorCount: runtimeStatus?.errorCount,
935
- tools: tools.map(t => t.name),
965
+ tools: tools.map((t) => t.name),
936
966
  toolCount: tools.length,
937
967
  };
938
968
  const response = isStdio
@@ -953,7 +983,7 @@ export class HttpServer {
953
983
  }
954
984
  catch (error) {
955
985
  res.status(500).json({
956
- error: error instanceof Error ? error.message : 'Failed to get server info',
986
+ error: error instanceof Error ? error.message : "Failed to get server info",
957
987
  });
958
988
  }
959
989
  }
@@ -964,7 +994,7 @@ export class HttpServer {
964
994
  try {
965
995
  const { name } = req.params;
966
996
  const status = this.serverManager.getServerStatus(name);
967
- if (!status || status.status !== 'running') {
997
+ if (!status || status.status !== "running") {
968
998
  res.status(400).json({ error: `Server ${name} is not running` });
969
999
  return;
970
1000
  }
@@ -974,7 +1004,7 @@ export class HttpServer {
974
1004
  }
975
1005
  catch (error) {
976
1006
  res.status(500).json({
977
- error: error instanceof Error ? error.message : 'Failed to get server tools',
1007
+ error: error instanceof Error ? error.message : "Failed to get server tools",
978
1008
  });
979
1009
  }
980
1010
  }
@@ -986,7 +1016,7 @@ export class HttpServer {
986
1016
  const { name, toolName } = req.params;
987
1017
  const { arguments: args } = req.body;
988
1018
  const status = this.serverManager.getServerStatus(name);
989
- if (!status || status.status !== 'running') {
1019
+ if (!status || status.status !== "running") {
990
1020
  res.status(400).json({ error: `Server ${name} is not running` });
991
1021
  return;
992
1022
  }
@@ -1001,7 +1031,7 @@ export class HttpServer {
1001
1031
  catch (error) {
1002
1032
  const response = {
1003
1033
  success: false,
1004
- error: error instanceof Error ? error.message : 'Failed to execute tool',
1034
+ error: error instanceof Error ? error.message : "Failed to execute tool",
1005
1035
  };
1006
1036
  res.status(500).json(response);
1007
1037
  }
@@ -1016,7 +1046,9 @@ export class HttpServer {
1016
1046
  }
1017
1047
  catch (error) {
1018
1048
  res.status(500).json({
1019
- error: error instanceof Error ? error.message : 'Failed to get configuration',
1049
+ error: error instanceof Error
1050
+ ? error.message
1051
+ : "Failed to get configuration",
1020
1052
  });
1021
1053
  }
1022
1054
  }
@@ -1027,8 +1059,10 @@ export class HttpServer {
1027
1059
  try {
1028
1060
  // Extract config updates from request body
1029
1061
  const updates = req.body;
1030
- if (!updates || typeof updates !== 'object') {
1031
- res.status(400).json({ error: 'Invalid configuration: expected object' });
1062
+ if (!updates || typeof updates !== "object") {
1063
+ res
1064
+ .status(400)
1065
+ .json({ error: "Invalid configuration: expected object" });
1032
1066
  return;
1033
1067
  }
1034
1068
  // Handle specific config sections that can be updated
@@ -1037,11 +1071,13 @@ export class HttpServer {
1037
1071
  }
1038
1072
  // Reload configuration to pick up any file-based changes
1039
1073
  await this.configLoader.reload();
1040
- res.json({ success: true, message: 'Configuration updated' });
1074
+ res.json({ success: true, message: "Configuration updated" });
1041
1075
  }
1042
1076
  catch (error) {
1043
1077
  res.status(400).json({
1044
- error: error instanceof Error ? error.message : 'Failed to update configuration',
1078
+ error: error instanceof Error
1079
+ ? error.message
1080
+ : "Failed to update configuration",
1045
1081
  });
1046
1082
  }
1047
1083
  }
@@ -1065,23 +1101,23 @@ export class HttpServer {
1065
1101
  if (isActive) {
1066
1102
  const tools = this.serverManager.getServerTools(serverName);
1067
1103
  baseServerChecks[serverName] = {
1068
- status: 'healthy',
1069
- tools_count: tools.length
1104
+ status: "healthy",
1105
+ tools_count: tools.length,
1070
1106
  };
1071
1107
  healthyCount++;
1072
1108
  }
1073
1109
  else {
1074
1110
  baseServerChecks[serverName] = {
1075
- status: 'unhealthy',
1076
- error: 'Server not running'
1111
+ status: "unhealthy",
1112
+ error: "Server not running",
1077
1113
  };
1078
1114
  degradedCount++;
1079
1115
  }
1080
1116
  }
1081
1117
  catch (error) {
1082
1118
  baseServerChecks[serverName] = {
1083
- status: 'unhealthy',
1084
- error: error instanceof Error ? error.message : 'Unknown error'
1119
+ status: "unhealthy",
1120
+ error: error instanceof Error ? error.message : "Unknown error",
1085
1121
  };
1086
1122
  degradedCount++;
1087
1123
  }
@@ -1091,17 +1127,17 @@ export class HttpServer {
1091
1127
  let httpStatusCode;
1092
1128
  if (degradedCount === 0) {
1093
1129
  // All base servers are healthy
1094
- overallStatus = 'healthy';
1130
+ overallStatus = "healthy";
1095
1131
  httpStatusCode = 200;
1096
1132
  }
1097
1133
  else if (healthyCount > 0) {
1098
1134
  // Some base servers are down but not all
1099
- overallStatus = 'degraded';
1135
+ overallStatus = "degraded";
1100
1136
  httpStatusCode = 200; // Still operational
1101
1137
  }
1102
1138
  else {
1103
1139
  // All base servers are down or critical failure
1104
- overallStatus = 'unhealthy';
1140
+ overallStatus = "unhealthy";
1105
1141
  httpStatusCode = 503; // Service Unavailable
1106
1142
  }
1107
1143
  const health = {
@@ -1110,25 +1146,25 @@ export class HttpServer {
1110
1146
  uptime_seconds: uptimeSeconds,
1111
1147
  checks: {
1112
1148
  daemon: {
1113
- status: 'healthy'
1149
+ status: "healthy",
1114
1150
  },
1115
- base_servers: baseServerChecks
1116
- }
1151
+ base_servers: baseServerChecks,
1152
+ },
1117
1153
  };
1118
1154
  res.status(httpStatusCode).json(health);
1119
1155
  }
1120
1156
  catch (error) {
1121
1157
  // Critical daemon error
1122
1158
  res.status(503).json({
1123
- status: 'unhealthy',
1159
+ status: "unhealthy",
1124
1160
  version,
1125
1161
  uptime_seconds: Math.floor((Date.now() - this.startTime) / 1000),
1126
1162
  checks: {
1127
1163
  daemon: {
1128
- status: 'unhealthy',
1129
- error: error instanceof Error ? error.message : 'Health check failed'
1130
- }
1131
- }
1164
+ status: "unhealthy",
1165
+ error: error instanceof Error ? error.message : "Health check failed",
1166
+ },
1167
+ },
1132
1168
  });
1133
1169
  }
1134
1170
  }
@@ -1141,7 +1177,7 @@ export class HttpServer {
1141
1177
  }
1142
1178
  catch (error) {
1143
1179
  res.status(500).json({
1144
- error: error instanceof Error ? error.message : 'Failed to get version',
1180
+ error: error instanceof Error ? error.message : "Failed to get version",
1145
1181
  });
1146
1182
  }
1147
1183
  }
@@ -1161,7 +1197,7 @@ export class HttpServer {
1161
1197
  }
1162
1198
  catch (error) {
1163
1199
  res.status(500).json({
1164
- error: error instanceof Error ? error.message : 'Failed to get metrics',
1200
+ error: error instanceof Error ? error.message : "Failed to get metrics",
1165
1201
  });
1166
1202
  }
1167
1203
  }
@@ -1170,11 +1206,13 @@ export class HttpServer {
1170
1206
  */
1171
1207
  async getPrometheusMetrics(_req, res) {
1172
1208
  try {
1173
- res.set('Content-Type', 'text/plain');
1209
+ res.set("Content-Type", "text/plain");
1174
1210
  res.send(globalMetrics.exportPrometheus());
1175
1211
  }
1176
1212
  catch (error) {
1177
- res.status(500).send(`# Error exporting metrics: ${error instanceof Error ? error.message : 'Unknown error'}`);
1213
+ res
1214
+ .status(500)
1215
+ .send(`# Error exporting metrics: ${error instanceof Error ? error.message : "Unknown error"}`);
1178
1216
  }
1179
1217
  }
1180
1218
  /**
@@ -1186,7 +1224,7 @@ export class HttpServer {
1186
1224
  }
1187
1225
  catch (error) {
1188
1226
  res.status(500).json({
1189
- error: error instanceof Error ? error.message : 'Failed to get API metrics',
1227
+ error: error instanceof Error ? error.message : "Failed to get API metrics",
1190
1228
  });
1191
1229
  }
1192
1230
  }
@@ -1198,14 +1236,18 @@ export class HttpServer {
1198
1236
  const { name } = req.params;
1199
1237
  const serverMetrics = globalMetrics.getServerMetrics(name);
1200
1238
  if (!serverMetrics) {
1201
- res.status(404).json({ error: `Server '${name}' not found or no metrics available` });
1239
+ res.status(404).json({
1240
+ error: `Server '${name}' not found or no metrics available`,
1241
+ });
1202
1242
  return;
1203
1243
  }
1204
1244
  res.json(serverMetrics);
1205
1245
  }
1206
1246
  catch (error) {
1207
1247
  res.status(500).json({
1208
- error: error instanceof Error ? error.message : 'Failed to get server metrics',
1248
+ error: error instanceof Error
1249
+ ? error.message
1250
+ : "Failed to get server metrics",
1209
1251
  });
1210
1252
  }
1211
1253
  }
@@ -1214,18 +1256,20 @@ export class HttpServer {
1214
1256
  */
1215
1257
  async getHourlyMetrics(req, res) {
1216
1258
  try {
1217
- const hours = parseInt(req.query.hours || '24');
1259
+ const hours = parseInt(req.query.hours || "24");
1218
1260
  const allMetrics = globalMetrics.getMetrics();
1219
1261
  const hourlyData = this.metricsAggregator.aggregateHourly(allMetrics, hours);
1220
1262
  res.json({
1221
- period: 'hourly',
1263
+ period: "hourly",
1222
1264
  hours,
1223
1265
  data: hourlyData,
1224
1266
  });
1225
1267
  }
1226
1268
  catch (error) {
1227
1269
  res.status(500).json({
1228
- error: error instanceof Error ? error.message : 'Failed to get hourly metrics',
1270
+ error: error instanceof Error
1271
+ ? error.message
1272
+ : "Failed to get hourly metrics",
1229
1273
  });
1230
1274
  }
1231
1275
  }
@@ -1234,18 +1278,20 @@ export class HttpServer {
1234
1278
  */
1235
1279
  async getDailyMetrics(req, res) {
1236
1280
  try {
1237
- const days = parseInt(req.query.days || '7');
1281
+ const days = parseInt(req.query.days || "7");
1238
1282
  const allMetrics = globalMetrics.getMetrics();
1239
1283
  const dailyData = this.metricsAggregator.aggregateDaily(allMetrics, days);
1240
1284
  res.json({
1241
- period: 'daily',
1285
+ period: "daily",
1242
1286
  days,
1243
1287
  data: dailyData,
1244
1288
  });
1245
1289
  }
1246
1290
  catch (error) {
1247
1291
  res.status(500).json({
1248
- error: error instanceof Error ? error.message : 'Failed to get daily metrics',
1292
+ error: error instanceof Error
1293
+ ? error.message
1294
+ : "Failed to get daily metrics",
1249
1295
  });
1250
1296
  }
1251
1297
  }
@@ -1254,18 +1300,20 @@ export class HttpServer {
1254
1300
  */
1255
1301
  async getWeeklyMetrics(req, res) {
1256
1302
  try {
1257
- const weeks = parseInt(req.query.weeks || '4');
1303
+ const weeks = parseInt(req.query.weeks || "4");
1258
1304
  const allMetrics = globalMetrics.getMetrics();
1259
1305
  const weeklyData = this.metricsAggregator.aggregateWeekly(allMetrics, weeks);
1260
1306
  res.json({
1261
- period: 'weekly',
1307
+ period: "weekly",
1262
1308
  weeks,
1263
1309
  data: weeklyData,
1264
1310
  });
1265
1311
  }
1266
1312
  catch (error) {
1267
1313
  res.status(500).json({
1268
- error: error instanceof Error ? error.message : 'Failed to get weekly metrics',
1314
+ error: error instanceof Error
1315
+ ? error.message
1316
+ : "Failed to get weekly metrics",
1269
1317
  });
1270
1318
  }
1271
1319
  }
@@ -1279,7 +1327,9 @@ export class HttpServer {
1279
1327
  }
1280
1328
  catch (error) {
1281
1329
  res.status(500).json({
1282
- error: error instanceof Error ? error.message : 'Failed to get error analytics',
1330
+ error: error instanceof Error
1331
+ ? error.message
1332
+ : "Failed to get error analytics",
1283
1333
  });
1284
1334
  }
1285
1335
  }
@@ -1294,7 +1344,7 @@ export class HttpServer {
1294
1344
  const metrics = globalMetrics.getToolErrorMetrics(toolName, serverName);
1295
1345
  if (!metrics) {
1296
1346
  res.status(404).json({
1297
- error: `No metrics found for tool '${toolName}'${serverName ? ` on server '${serverName}'` : ''}`
1347
+ error: `No metrics found for tool '${toolName}'${serverName ? ` on server '${serverName}'` : ""}`,
1298
1348
  });
1299
1349
  return;
1300
1350
  }
@@ -1302,7 +1352,9 @@ export class HttpServer {
1302
1352
  }
1303
1353
  catch (error) {
1304
1354
  res.status(500).json({
1305
- error: error instanceof Error ? error.message : 'Failed to get tool error metrics',
1355
+ error: error instanceof Error
1356
+ ? error.message
1357
+ : "Failed to get tool error metrics",
1306
1358
  });
1307
1359
  }
1308
1360
  }
@@ -1316,12 +1368,16 @@ export class HttpServer {
1316
1368
  async getToolMetrics(req, res) {
1317
1369
  try {
1318
1370
  const { serverName } = req.query;
1319
- const slow = req.query.slow ? parseInt(req.query.slow, 10) : undefined;
1320
- const errorsThreshold = req.query.errors ? parseFloat(req.query.errors) : undefined;
1371
+ const slow = req.query.slow
1372
+ ? parseInt(req.query.slow, 10)
1373
+ : undefined;
1374
+ const errorsThreshold = req.query.errors
1375
+ ? parseFloat(req.query.errors)
1376
+ : undefined;
1321
1377
  let metrics = globalMetrics.getAllToolMetrics();
1322
1378
  // Filter by server if requested
1323
1379
  if (serverName) {
1324
- metrics = metrics.filter(m => m.serverName === serverName);
1380
+ metrics = metrics.filter((m) => m.serverName === serverName);
1325
1381
  }
1326
1382
  // Return slowest tools if requested
1327
1383
  if (slow !== undefined) {
@@ -1338,7 +1394,7 @@ export class HttpServer {
1338
1394
  }
1339
1395
  catch (error) {
1340
1396
  res.status(500).json({
1341
- error: error instanceof Error ? error.message : 'Failed to get tool metrics',
1397
+ error: error instanceof Error ? error.message : "Failed to get tool metrics",
1342
1398
  });
1343
1399
  }
1344
1400
  }
@@ -1350,15 +1406,15 @@ export class HttpServer {
1350
1406
  const data = await this.metricsPersistence.load();
1351
1407
  if (data) {
1352
1408
  globalMetrics.restoreMetrics(data);
1353
- console.log('[MetricsPersistence] Successfully restored metrics from disk');
1409
+ console.log("[MetricsPersistence] Successfully restored metrics from disk");
1354
1410
  }
1355
1411
  else {
1356
- console.log('[MetricsPersistence] No persisted metrics found, starting fresh');
1412
+ console.log("[MetricsPersistence] No persisted metrics found, starting fresh");
1357
1413
  }
1358
1414
  }
1359
1415
  catch (error) {
1360
- console.error('[MetricsPersistence] Failed to load metrics:', error);
1361
- console.log('[MetricsPersistence] Starting with fresh metrics');
1416
+ console.error("[MetricsPersistence] Failed to load metrics:", error);
1417
+ console.log("[MetricsPersistence] Starting with fresh metrics");
1362
1418
  }
1363
1419
  }
1364
1420
  /**
@@ -1369,12 +1425,12 @@ export class HttpServer {
1369
1425
  timestamp: Date.now(),
1370
1426
  metrics: globalMetrics.getMetrics(),
1371
1427
  serverMetrics: globalMetrics.getAllServerMetrics(),
1372
- toolMetrics: Array.from(globalMetrics['toolMetrics'].values()), // Access private field for persistence
1428
+ toolMetrics: Array.from(globalMetrics["toolMetrics"].values()), // Access private field for persistence
1373
1429
  apiMetrics: globalMetrics.getApiMetrics(),
1374
1430
  });
1375
1431
  // Save every 60 seconds
1376
1432
  this.metricsPersistence.startPeriodicWrites(getMetricsData, 60000);
1377
- console.log('[MetricsPersistence] Started periodic writes (60s interval)');
1433
+ console.log("[MetricsPersistence] Started periodic writes (60s interval)");
1378
1434
  }
1379
1435
  // === Session Management (Streamable HTTP - MCP 2025-03-26) ===
1380
1436
  /**
@@ -1390,7 +1446,7 @@ export class HttpServer {
1390
1446
  events.push({
1391
1447
  id: eventId,
1392
1448
  timestamp: Date.now(),
1393
- data: eventData
1449
+ data: eventData,
1394
1450
  });
1395
1451
  // Keep only last N events to avoid memory bloat
1396
1452
  if (events.length > this.MAX_EVENTS_PER_SESSION) {
@@ -1406,7 +1462,7 @@ export class HttpServer {
1406
1462
  if (!lastEventId)
1407
1463
  return events;
1408
1464
  // Return all events after lastEventId
1409
- return events.filter(e => e.id > lastEventId);
1465
+ return events.filter((e) => e.id > lastEventId);
1410
1466
  }
1411
1467
  createSession(clientInfo) {
1412
1468
  const sessionId = randomUUID();
@@ -1417,10 +1473,10 @@ export class HttpServer {
1417
1473
  clientInfo,
1418
1474
  initialized: false,
1419
1475
  lastEventId: 0,
1420
- protocolVersion: '2025-06-18'
1476
+ protocolVersion: "2025-06-18",
1421
1477
  });
1422
- const clientName = clientInfo?.name || 'unknown';
1423
- const clientVersion = clientInfo?.version || 'unknown';
1478
+ const clientName = clientInfo?.name || "unknown";
1479
+ const clientVersion = clientInfo?.version || "unknown";
1424
1480
  console.log(`[MCP] Session created: ${sessionId} | Client: ${clientName}/${clientVersion}`);
1425
1481
  // Persist sessions to disk (v1.1.24+)
1426
1482
  this.persistSessions();
@@ -1497,16 +1553,16 @@ export class HttpServer {
1497
1553
  const data = {
1498
1554
  version: this.SESSION_PERSIST_VERSION,
1499
1555
  persistedAt: Date.now(),
1500
- sessions: sessionsObj
1556
+ sessions: sessionsObj,
1501
1557
  };
1502
1558
  // Atomic write: write to temp file, then rename
1503
1559
  const tempPath = `${this.sessionPersistPath}.tmp`;
1504
- fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
1560
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf8");
1505
1561
  fs.renameSync(tempPath, this.sessionPersistPath);
1506
1562
  console.log(`[SessionPersistence] Saved ${this.sessions.size} sessions to disk`);
1507
1563
  }
1508
1564
  catch (error) {
1509
- console.error('[SessionPersistence] Failed to persist sessions:', error);
1565
+ console.error("[SessionPersistence] Failed to persist sessions:", error);
1510
1566
  // Non-fatal: sessions still work in memory
1511
1567
  }
1512
1568
  }
@@ -1517,10 +1573,10 @@ export class HttpServer {
1517
1573
  loadSessions() {
1518
1574
  try {
1519
1575
  if (!fs.existsSync(this.sessionPersistPath)) {
1520
- console.log('[SessionPersistence] No persisted sessions found, starting fresh');
1576
+ console.log("[SessionPersistence] No persisted sessions found, starting fresh");
1521
1577
  return;
1522
1578
  }
1523
- const content = fs.readFileSync(this.sessionPersistPath, 'utf8');
1579
+ const content = fs.readFileSync(this.sessionPersistPath, "utf8");
1524
1580
  const data = JSON.parse(content);
1525
1581
  // Version check for future migrations
1526
1582
  if (data.version !== this.SESSION_PERSIST_VERSION) {
@@ -1548,12 +1604,12 @@ export class HttpServer {
1548
1604
  }
1549
1605
  catch (error) {
1550
1606
  const errCode = error.code;
1551
- if (errCode === 'ENOENT') {
1552
- console.log('[SessionPersistence] No persisted sessions found, starting fresh');
1607
+ if (errCode === "ENOENT") {
1608
+ console.log("[SessionPersistence] No persisted sessions found, starting fresh");
1553
1609
  }
1554
1610
  else {
1555
- console.error('[SessionPersistence] Failed to load sessions:', error);
1556
- console.log('[SessionPersistence] Starting with fresh sessions');
1611
+ console.error("[SessionPersistence] Failed to load sessions:", error);
1612
+ console.log("[SessionPersistence] Starting with fresh sessions");
1557
1613
  }
1558
1614
  }
1559
1615
  }
@@ -1563,26 +1619,37 @@ export class HttpServer {
1563
1619
  */
1564
1620
  async handleStreamableRequest(request, req) {
1565
1621
  const { method, params, id } = request;
1566
- const sessionId = req?.headers['mcp-session-id'];
1622
+ const sessionId = req?.headers["mcp-session-id"];
1567
1623
  // Validate JSON-RPC 2.0 structure
1568
- if (!request.jsonrpc || request.jsonrpc !== '2.0') {
1569
- return [{
1570
- jsonrpc: '2.0',
1624
+ if (!request.jsonrpc || request.jsonrpc !== "2.0") {
1625
+ return [
1626
+ {
1627
+ jsonrpc: "2.0",
1571
1628
  id,
1572
- error: { code: -32600, message: 'Invalid Request: jsonrpc field must be "2.0"' }
1573
- }, sessionId];
1629
+ error: {
1630
+ code: -32600,
1631
+ message: 'Invalid Request: jsonrpc field must be "2.0"',
1632
+ },
1633
+ },
1634
+ sessionId,
1635
+ ];
1574
1636
  }
1575
1637
  try {
1576
1638
  let result;
1577
1639
  let session;
1578
1640
  let responseSessionId;
1579
1641
  // Handle session-aware methods
1580
- if (method === 'initialize') {
1642
+ if (method === "initialize") {
1581
1643
  const initParams = params;
1582
1644
  // Protocol version negotiation per spec 2025-11-25
1583
1645
  const requestedVersion = initParams.protocolVersion;
1584
- const supportedVersions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']; // List in order of preference
1585
- let negotiatedVersion = '2025-11-25'; // Default to latest
1646
+ const supportedVersions = [
1647
+ "2025-11-25",
1648
+ "2025-06-18",
1649
+ "2025-03-26",
1650
+ "2024-11-05",
1651
+ ]; // List in order of preference
1652
+ let negotiatedVersion = "2025-11-25"; // Default to latest
1586
1653
  if (requestedVersion) {
1587
1654
  if (supportedVersions.includes(requestedVersion)) {
1588
1655
  // Client requested supported version
@@ -1593,8 +1660,9 @@ export class HttpServer {
1593
1660
  // Client requested unsupported version - offer fallback chain
1594
1661
  console.log(`[MCP] Unsupported protocol version requested: ${requestedVersion}. Offering fallback to ${negotiatedVersion}`);
1595
1662
  // Return error with supported versions for client to retry with fallback
1596
- return [{
1597
- jsonrpc: '2.0',
1663
+ return [
1664
+ {
1665
+ jsonrpc: "2.0",
1598
1666
  id,
1599
1667
  error: {
1600
1668
  code: -32602,
@@ -1602,10 +1670,12 @@ export class HttpServer {
1602
1670
  data: {
1603
1671
  requested: requestedVersion,
1604
1672
  supported: supportedVersions,
1605
- recommended: negotiatedVersion
1606
- }
1607
- }
1608
- }, undefined];
1673
+ recommended: negotiatedVersion,
1674
+ },
1675
+ },
1676
+ },
1677
+ undefined,
1678
+ ];
1609
1679
  }
1610
1680
  }
1611
1681
  // Create or retrieve session
@@ -1623,7 +1693,7 @@ export class HttpServer {
1623
1693
  clientInfo: initParams.clientInfo,
1624
1694
  initialized: false,
1625
1695
  lastEventId: 0,
1626
- protocolVersion: '2025-06-18'
1696
+ protocolVersion: "2025-06-18",
1627
1697
  });
1628
1698
  newSessionId = sessionId;
1629
1699
  }
@@ -1634,7 +1704,7 @@ export class HttpServer {
1634
1704
  session = this.getSession(newSessionId);
1635
1705
  session.initialized = true;
1636
1706
  session.protocolVersion = negotiatedVersion;
1637
- session.userAgent = req?.headers?.['user-agent'] || 'unknown';
1707
+ session.userAgent = req?.headers?.["user-agent"] || "unknown";
1638
1708
  session.lastEventId = 0;
1639
1709
  responseSessionId = newSessionId;
1640
1710
  // Store client capabilities for per-client feature adaptation
@@ -1643,12 +1713,14 @@ export class HttpServer {
1643
1713
  }
1644
1714
  // Extract callback URL from request headers for bidirectional communication (MCP callback protocol)
1645
1715
  if (req) {
1646
- const callbackUrl = req.headers['x-callback-url'];
1647
- const callbackPort = req.headers['x-callback-port'];
1716
+ const callbackUrl = req.headers["x-callback-url"];
1717
+ const callbackPort = req.headers["x-callback-port"];
1648
1718
  if (callbackUrl || callbackPort) {
1649
1719
  const url = callbackUrl || `http://127.0.0.1:${callbackPort}/mcp`;
1650
1720
  session.callbackUrl = url;
1651
- session.callbackPort = callbackPort ? parseInt(callbackPort, 10) : undefined;
1721
+ session.callbackPort = callbackPort
1722
+ ? parseInt(callbackPort, 10)
1723
+ : undefined;
1652
1724
  console.log(`[MCP] Callback registered: ${url}`);
1653
1725
  }
1654
1726
  }
@@ -1658,20 +1730,23 @@ export class HttpServer {
1658
1730
  capabilities: {
1659
1731
  tools: { listChanged: true },
1660
1732
  prompts: { listChanged: false },
1661
- resources: { listChanged: false }
1733
+ resources: { listChanged: false },
1662
1734
  },
1663
1735
  serverInfo: {
1664
- name: 'metalink',
1665
- version
1666
- }
1736
+ name: "metalink",
1737
+ version,
1738
+ },
1667
1739
  };
1668
- return [{
1669
- jsonrpc: '2.0',
1740
+ return [
1741
+ {
1742
+ jsonrpc: "2.0",
1670
1743
  id,
1671
- result
1672
- }, responseSessionId];
1744
+ result,
1745
+ },
1746
+ responseSessionId,
1747
+ ];
1673
1748
  }
1674
- else if (method === 'notifications/initialized') {
1749
+ else if (method === "notifications/initialized") {
1675
1750
  return [null, sessionId]; // No response for notifications
1676
1751
  }
1677
1752
  else {
@@ -1685,99 +1760,114 @@ export class HttpServer {
1685
1760
  // Per MCP spec, should return HTTP 404, but we can't from this function.
1686
1761
  // Caller handles HTTP 404 at line ~2067. This is defensive programming only.
1687
1762
  console.error(`[MCP] Session validation fallback triggered for: ${sessionId.substring(0, 8)}...`);
1688
- return [{
1689
- jsonrpc: '2.0',
1763
+ return [
1764
+ {
1765
+ jsonrpc: "2.0",
1690
1766
  id,
1691
- error: { code: -32603, message: 'Invalid or expired session' }
1692
- }, sessionId];
1767
+ error: { code: -32603, message: "Invalid or expired session" },
1768
+ },
1769
+ sessionId,
1770
+ ];
1693
1771
  }
1694
1772
  }
1695
1773
  // Handle other MCP methods
1696
- if (method === 'ping') {
1774
+ if (method === "ping") {
1697
1775
  // MCP spec: ping method for keepalive/health checks
1698
1776
  result = {};
1699
1777
  }
1700
- else if (method === 'roots/list') {
1778
+ else if (method === "roots/list") {
1701
1779
  result = { roots: [] };
1702
1780
  }
1703
- else if (method === 'prompts/list') {
1781
+ else if (method === "prompts/list") {
1704
1782
  result = { prompts: getPromptsList() };
1705
1783
  }
1706
- else if (method === 'resources/list') {
1784
+ else if (method === "resources/list") {
1707
1785
  result = { resources: getResourcesList() };
1708
1786
  }
1709
- else if (method === 'resources/templates/list') {
1787
+ else if (method === "resources/templates/list") {
1710
1788
  result = { resourceTemplates: getResourceTemplatesList() };
1711
1789
  }
1712
- else if (method === 'prompts/get') {
1790
+ else if (method === "prompts/get") {
1713
1791
  const promptParams = params;
1714
1792
  if (!promptParams.name) {
1715
- throw new InvalidParamsError('Missing required parameter: name');
1793
+ throw new InvalidParamsError("Missing required parameter: name");
1716
1794
  }
1717
1795
  try {
1718
1796
  result = getPrompt(promptParams.name, promptParams.arguments || {});
1719
1797
  }
1720
1798
  catch (err) {
1721
- throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown prompt error');
1799
+ throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown prompt error");
1722
1800
  }
1723
1801
  }
1724
- else if (method === 'resources/read') {
1802
+ else if (method === "resources/read") {
1725
1803
  const resourceParams = params;
1726
1804
  if (!resourceParams.uri) {
1727
- throw new InvalidParamsError('Missing required parameter: uri');
1805
+ throw new InvalidParamsError("Missing required parameter: uri");
1728
1806
  }
1729
1807
  try {
1730
1808
  result = await readResource(resourceParams.uri, this.serverManager, this.configLoader);
1731
1809
  }
1732
1810
  catch (err) {
1733
- throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown resource error');
1811
+ throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown resource error");
1734
1812
  }
1735
1813
  }
1736
- else if (method === 'tools/list') {
1814
+ else if (method === "tools/list") {
1737
1815
  // Auto-reinitialize session if not initialized (transparent to client)
1738
1816
  // This handles stale sessions from server restarts without client needing to retry
1739
1817
  if (!session?.initialized) {
1740
- const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
1818
+ const newSessionId = this.createSession({
1819
+ name: "auto-reinit",
1820
+ version: "1.0",
1821
+ });
1741
1822
  const newSession = this.getSession(newSessionId);
1742
1823
  newSession.initialized = true;
1743
- newSession.protocolVersion = '2025-06-18';
1824
+ newSession.protocolVersion = "2025-06-18";
1744
1825
  responseSessionId = newSessionId;
1745
1826
  console.log(`[MCP] Auto-reinitialized session for tools/list: ${newSessionId}`);
1746
1827
  }
1747
1828
  result = await this.mcpListTools();
1748
1829
  }
1749
- else if (method === 'tools/call') {
1830
+ else if (method === "tools/call") {
1750
1831
  // Auto-reinitialize session if not initialized (transparent to client)
1751
1832
  // This handles stale sessions from server restarts without client needing to retry
1752
1833
  let effectiveSessionId = sessionId;
1753
1834
  if (!session?.initialized) {
1754
- const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
1835
+ const newSessionId = this.createSession({
1836
+ name: "auto-reinit",
1837
+ version: "1.0",
1838
+ });
1755
1839
  const newSession = this.getSession(newSessionId);
1756
1840
  newSession.initialized = true;
1757
- newSession.protocolVersion = '2025-06-18';
1841
+ newSession.protocolVersion = "2025-06-18";
1758
1842
  responseSessionId = newSessionId;
1759
1843
  effectiveSessionId = newSessionId;
1760
1844
  console.log(`[MCP] Auto-reinitialized session for tools/call: ${newSessionId}`);
1761
1845
  }
1762
1846
  const callParams = params;
1763
1847
  if (!callParams.name) {
1764
- throw new InvalidParamsError('Missing required parameter: name');
1848
+ throw new InvalidParamsError("Missing required parameter: name");
1765
1849
  }
1766
1850
  result = await this.mcpCallTool(callParams.name, callParams.arguments, effectiveSessionId);
1767
1851
  }
1768
1852
  else {
1769
- return [{
1770
- jsonrpc: '2.0',
1853
+ return [
1854
+ {
1855
+ jsonrpc: "2.0",
1771
1856
  id,
1772
- error: { code: -32601, message: `Unknown method: ${method}` }
1773
- }, sessionId];
1857
+ error: { code: -32601, message: `Unknown method: ${method}` },
1858
+ },
1859
+ sessionId,
1860
+ ];
1774
1861
  }
1775
1862
  }
1776
- return [{
1777
- jsonrpc: '2.0',
1863
+ return [
1864
+ {
1865
+ jsonrpc: "2.0",
1778
1866
  id,
1779
- result
1780
- }, responseSessionId];
1867
+ result,
1868
+ },
1869
+ responseSessionId,
1870
+ ];
1781
1871
  }
1782
1872
  catch (error) {
1783
1873
  // SessionNotInitializedError must bubble up to trigger HTTP 404
@@ -1787,14 +1877,17 @@ export class HttpServer {
1787
1877
  }
1788
1878
  // Check if this is an invalid parameters error (JSON-RPC -32602)
1789
1879
  const isInvalidParams = error instanceof InvalidParamsError;
1790
- return [{
1791
- jsonrpc: '2.0',
1880
+ return [
1881
+ {
1882
+ jsonrpc: "2.0",
1792
1883
  id,
1793
1884
  error: {
1794
1885
  code: isInvalidParams ? -32602 : -32603,
1795
- message: error instanceof Error ? error.message : 'Internal error'
1796
- }
1797
- }, sessionId];
1886
+ message: error instanceof Error ? error.message : "Internal error",
1887
+ },
1888
+ },
1889
+ sessionId,
1890
+ ];
1798
1891
  }
1799
1892
  }
1800
1893
  /**
@@ -1802,8 +1895,8 @@ export class HttpServer {
1802
1895
  */
1803
1896
  async handleMcpRequest(req, res) {
1804
1897
  const requestStartTime = Date.now();
1805
- const userAgent = req.headers['user-agent'] || 'unknown';
1806
- const sessionId = req.headers['mcp-session-id'];
1898
+ const userAgent = req.headers["user-agent"] || "unknown";
1899
+ const sessionId = req.headers["mcp-session-id"];
1807
1900
  const requestId = req.requestId || generateRequestId();
1808
1901
  // Create child logger with request context
1809
1902
  const reqLogger = logger.child({
@@ -1813,56 +1906,62 @@ export class HttpServer {
1813
1906
  userAgent,
1814
1907
  });
1815
1908
  // Log incoming MCP request
1816
- reqLogger.info('MCP request received', {
1909
+ reqLogger.info("MCP request received", {
1817
1910
  path: req.path,
1818
- protocol: req.headers['mcp-protocol-version'],
1911
+ protocol: req.headers["mcp-protocol-version"],
1819
1912
  });
1820
1913
  try {
1821
1914
  // Handle GET requests per MCP spec 2025-06-18 - SSE streaming for server-sent events
1822
- if (req.method === 'GET') {
1823
- const sessionId = req.headers['mcp-session-id'];
1915
+ if (req.method === "GET") {
1916
+ const sessionId = req.headers["mcp-session-id"];
1824
1917
  // If no session ID, handle health check with JSON response
1825
1918
  if (!sessionId) {
1826
1919
  const responseTime = Date.now() - requestStartTime;
1827
- reqLogger.info('Health check request', {
1920
+ reqLogger.info("Health check request", {
1828
1921
  accept: req.headers.accept,
1829
- protocolVersion: req.headers['mcp-protocol-version'],
1922
+ protocolVersion: req.headers["mcp-protocol-version"],
1830
1923
  });
1831
1924
  // Health check - always return JSON regardless of Accept header
1832
- res.setHeader('Content-Type', 'application/json');
1833
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
1834
- res.setHeader('X-Request-Id', requestId);
1835
- res.status(200).json({ status: 'ready', protocol: MCP_PROTOCOL_VERSION });
1836
- reqLogger.info('Health check response sent', {
1925
+ res.setHeader("Content-Type", "application/json");
1926
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
1927
+ res.setHeader("X-Request-Id", requestId);
1928
+ res
1929
+ .status(200)
1930
+ .json({ status: "ready", protocol: MCP_PROTOCOL_VERSION });
1931
+ reqLogger.info("Health check response sent", {
1837
1932
  status: 200,
1838
1933
  duration_ms: responseTime,
1839
1934
  });
1840
1935
  return;
1841
1936
  }
1842
1937
  // Session-based requests - set up SSE streaming with Last-Event-ID resumption support
1843
- res.setHeader('Content-Type', 'text/event-stream');
1844
- res.setHeader('Cache-Control', 'no-cache');
1845
- res.setHeader('Connection', 'keep-alive');
1846
- res.setHeader('X-Accel-Buffering', 'no');
1847
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
1938
+ res.setHeader("Content-Type", "text/event-stream");
1939
+ res.setHeader("Cache-Control", "no-cache");
1940
+ res.setHeader("Connection", "keep-alive");
1941
+ res.setHeader("X-Accel-Buffering", "no");
1942
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
1848
1943
  const session = this.getSession(sessionId);
1849
1944
  if (!session?.initialized) {
1850
- res.status(404).json({ error: 'Session not found or not initialized' });
1945
+ res
1946
+ .status(404)
1947
+ .json({ error: "Session not found or not initialized" });
1851
1948
  return;
1852
1949
  }
1853
- res.setHeader('Mcp-Session-Id', sessionId);
1950
+ res.setHeader("Mcp-Session-Id", sessionId);
1854
1951
  // Check for Last-Event-ID header (per MCP 2025-06-18 spec for resumption)
1855
- const lastEventIdHeader = req.headers['last-event-id'];
1856
- const lastEventId = lastEventIdHeader ? parseInt(String(lastEventIdHeader), 10) : undefined;
1952
+ const lastEventIdHeader = req.headers["last-event-id"];
1953
+ const lastEventId = lastEventIdHeader
1954
+ ? parseInt(String(lastEventIdHeader), 10)
1955
+ : undefined;
1857
1956
  if (lastEventId !== undefined) {
1858
- reqLogger.info('SSE stream resumption requested', {
1957
+ reqLogger.info("SSE stream resumption requested", {
1859
1958
  lastEventId,
1860
- transport: 'sse',
1959
+ transport: "sse",
1861
1960
  });
1862
1961
  }
1863
1962
  else {
1864
- reqLogger.info('New SSE stream connection', {
1865
- transport: 'sse',
1963
+ reqLogger.info("New SSE stream connection", {
1964
+ transport: "sse",
1866
1965
  });
1867
1966
  }
1868
1967
  // Track SSE connection for server-sent messages
@@ -1871,9 +1970,9 @@ export class HttpServer {
1871
1970
  if (lastEventId !== undefined) {
1872
1971
  const bufferedEvents = this.getEventsForResumption(sessionId, lastEventId);
1873
1972
  if (bufferedEvents.length > 0) {
1874
- reqLogger.debug('Sending buffered events for resumption', {
1973
+ reqLogger.debug("Sending buffered events for resumption", {
1875
1974
  eventCount: bufferedEvents.length,
1876
- transport: 'sse',
1975
+ transport: "sse",
1877
1976
  });
1878
1977
  for (const event of bufferedEvents) {
1879
1978
  res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
@@ -1883,8 +1982,10 @@ export class HttpServer {
1883
1982
  else {
1884
1983
  // Send initial endpoint event ONLY on new connection (not on resumption)
1885
1984
  // Resuming connections should only receive buffered events, not duplicate endpoint events
1886
- const endpointEventId = this.trackSseEvent(sessionId, { type: 'endpoint' });
1887
- res.write(`id: ${endpointEventId}\ndata: ${JSON.stringify({ type: 'endpoint' })}\n\n`);
1985
+ const endpointEventId = this.trackSseEvent(sessionId, {
1986
+ type: "endpoint",
1987
+ });
1988
+ res.write(`id: ${endpointEventId}\ndata: ${JSON.stringify({ type: "endpoint" })}\n\n`);
1888
1989
  // Update session's last event ID
1889
1990
  if (session) {
1890
1991
  session.lastEventId = endpointEventId;
@@ -1895,22 +1996,22 @@ export class HttpServer {
1895
1996
  // This is critical for clients like mcp-remote that rely on async iterators
1896
1997
  const keepaliveInterval = setInterval(() => {
1897
1998
  try {
1898
- res.write(':keepalive\n\n');
1999
+ res.write(":keepalive\n\n");
1899
2000
  }
1900
2001
  catch (error) {
1901
- reqLogger.debug('SSE keepalive write failed', {
1902
- error: error instanceof Error ? error.message : 'Unknown error',
1903
- transport: 'sse',
2002
+ reqLogger.debug("SSE keepalive write failed", {
2003
+ error: error instanceof Error ? error.message : "Unknown error",
2004
+ transport: "sse",
1904
2005
  });
1905
2006
  clearInterval(keepaliveInterval);
1906
2007
  }
1907
2008
  }, 15000); // Send keepalive every 15 seconds
1908
2009
  // Clean up on disconnect
1909
- req.on('close', () => {
2010
+ req.on("close", () => {
1910
2011
  clearInterval(keepaliveInterval);
1911
2012
  this.sseConnections.delete(sessionId);
1912
- reqLogger.info('SSE connection closed', {
1913
- transport: 'sse',
2013
+ reqLogger.info("SSE connection closed", {
2014
+ transport: "sse",
1914
2015
  });
1915
2016
  });
1916
2017
  // Keep connection open for server-sent events
@@ -1922,33 +2023,33 @@ export class HttpServer {
1922
2023
  // Only use SSE if:
1923
2024
  // 1. Client explicitly requests ONLY SSE ("text/event-stream")
1924
2025
  // 2. OR client doesn't offer JSON as an option
1925
- const acceptHeader = req.headers.accept || 'application/json';
1926
- const hasJSON = acceptHeader.includes('application/json');
1927
- const hasSSE = acceptHeader.includes('text/event-stream');
2026
+ const acceptHeader = req.headers.accept || "application/json";
2027
+ const hasJSON = acceptHeader.includes("application/json");
2028
+ const hasSSE = acceptHeader.includes("text/event-stream");
1928
2029
  // Use SSE only if SSE is available AND JSON is NOT preferred
1929
2030
  const useSSE = hasSSE && !hasJSON;
1930
2031
  // Debug: Log all headers to understand mcp-remote communication
1931
- console.log('[MCP] POST request headers:', {
1932
- 'x-callback-url': req.headers['x-callback-url'],
1933
- 'x-callback-port': req.headers['x-callback-port'],
1934
- 'mcp-session-id': req.headers['mcp-session-id'],
1935
- 'user-agent': req.headers['user-agent'],
1936
- 'content-type': req.headers['content-type'],
1937
- 'accept': req.headers['accept']
2032
+ console.log("[MCP] POST request headers:", {
2033
+ "x-callback-url": req.headers["x-callback-url"],
2034
+ "x-callback-port": req.headers["x-callback-port"],
2035
+ "mcp-session-id": req.headers["mcp-session-id"],
2036
+ "user-agent": req.headers["user-agent"],
2037
+ "content-type": req.headers["content-type"],
2038
+ accept: req.headers["accept"],
1938
2039
  });
1939
2040
  console.log(`[MCP] SSE mode: ${useSSE} (based on Accept header: "${acceptHeader}")`);
1940
2041
  // Parse raw body with robust error handling
1941
- let text = '';
2042
+ let text = "";
1942
2043
  try {
1943
2044
  if (!req.body) {
1944
2045
  // Empty body - valid for some requests
1945
- text = '';
2046
+ text = "";
1946
2047
  }
1947
- else if (typeof req.body === 'string') {
2048
+ else if (typeof req.body === "string") {
1948
2049
  text = req.body;
1949
2050
  }
1950
2051
  else if (Buffer.isBuffer(req.body)) {
1951
- text = req.body.toString('utf-8');
2052
+ text = req.body.toString("utf-8");
1952
2053
  }
1953
2054
  else {
1954
2055
  // Unknown body type - try to convert
@@ -1956,20 +2057,20 @@ export class HttpServer {
1956
2057
  }
1957
2058
  }
1958
2059
  catch (parseError) {
1959
- console.error('[MCP] Body parsing error:', parseError);
2060
+ console.error("[MCP] Body parsing error:", parseError);
1960
2061
  res.status(400).json({
1961
- jsonrpc: '2.0',
2062
+ jsonrpc: "2.0",
1962
2063
  error: {
1963
2064
  code: -32700,
1964
- message: 'Invalid request body'
2065
+ message: "Invalid request body",
1965
2066
  },
1966
- id: null
2067
+ id: null,
1967
2068
  });
1968
2069
  return;
1969
2070
  }
1970
2071
  if (!text || text.trim().length === 0) {
1971
- res.setHeader('Content-Type', 'application/json');
1972
- res.send('');
2072
+ res.setHeader("Content-Type", "application/json");
2073
+ res.send("");
1973
2074
  return;
1974
2075
  }
1975
2076
  // Parse JSON-RPC requests (support both batch arrays and newline-delimited)
@@ -1993,7 +2094,7 @@ export class HttpServer {
1993
2094
  }
1994
2095
  catch (parseError) {
1995
2096
  // Fallback to newline-delimited format
1996
- const lines = text.split('\n').filter((line) => line.trim());
2097
+ const lines = text.split("\n").filter((line) => line.trim());
1997
2098
  console.log(`[MCP Request] Newline-delimited format with ${lines.length} lines`);
1998
2099
  for (const line of lines) {
1999
2100
  try {
@@ -2001,9 +2102,9 @@ export class HttpServer {
2001
2102
  }
2002
2103
  catch (lineError) {
2003
2104
  responses.push({
2004
- jsonrpc: '2.0',
2005
- error: { code: -32700, message: 'Parse error' },
2006
- id: null
2105
+ jsonrpc: "2.0",
2106
+ error: { code: -32700, message: "Parse error" },
2107
+ id: null,
2007
2108
  });
2008
2109
  }
2009
2110
  }
@@ -2013,15 +2114,16 @@ export class HttpServer {
2013
2114
  console.log(`[MCP Request] Processing ${requests.length} request(s)`);
2014
2115
  requests.forEach((req, idx) => {
2015
2116
  const truncatedParams = JSON.stringify(req.params || {}).substring(0, 200);
2016
- console.log(`[MCP Request] ${idx + 1}: method="${req.method}" id="${req.id}" params=${truncatedParams}${JSON.stringify(req.params || {}).length > 200 ? '...' : ''}`);
2117
+ console.log(`[MCP Request] ${idx + 1}: method="${req.method}" id="${req.id}" params=${truncatedParams}${JSON.stringify(req.params || {}).length > 200 ? "..." : ""}`);
2017
2118
  });
2018
2119
  }
2019
2120
  for (const request of requests) {
2020
2121
  try {
2021
2122
  // Detect protocol version and route appropriately
2022
- if (request.method === 'initialize') {
2023
- const protocolVersion = request.params?.protocolVersion;
2024
- if (protocolVersion === '2024-11-05') {
2123
+ if (request.method === "initialize") {
2124
+ const protocolVersion = request.params
2125
+ ?.protocolVersion;
2126
+ if (protocolVersion === "2024-11-05") {
2025
2127
  // Legacy protocol path
2026
2128
  const response = await this.handleJsonRpcRequest(request);
2027
2129
  if (response)
@@ -2039,19 +2141,22 @@ export class HttpServer {
2039
2141
  }
2040
2142
  else {
2041
2143
  // For non-initialize requests, detect by session presence in header
2042
- const headerSessionId = req.headers['mcp-session-id'];
2144
+ const headerSessionId = req.headers["mcp-session-id"];
2043
2145
  if (headerSessionId) {
2044
2146
  // Session ID provided - check if it exists
2045
2147
  if (!this.getSession(headerSessionId)) {
2046
2148
  // Session doesn't exist - auto-create and initialize (transparent recovery)
2047
2149
  // This handles stale sessions from server restarts without client needing to reinitialize
2048
- const newSessionId = this.createSession({ name: 'auto-reinit', version: '1.0' });
2150
+ const newSessionId = this.createSession({
2151
+ name: "auto-reinit",
2152
+ version: "1.0",
2153
+ });
2049
2154
  const newSession = this.getSession(newSessionId);
2050
2155
  newSession.initialized = true;
2051
- newSession.protocolVersion = '2025-06-18';
2156
+ newSession.protocolVersion = "2025-06-18";
2052
2157
  console.log(`[MCP] Auto-reinitialized stale session ${headerSessionId.substring(0, 8)}... → ${newSessionId.substring(0, 8)}...`);
2053
2158
  // Update header for downstream processing (hacky but works)
2054
- req.headers['mcp-session-id'] = newSessionId;
2159
+ req.headers["mcp-session-id"] = newSessionId;
2055
2160
  }
2056
2161
  // Has valid session (original or auto-created) → use Streamable HTTP
2057
2162
  const [response, sId] = await this.handleStreamableRequest(request, req);
@@ -2071,9 +2176,9 @@ export class HttpServer {
2071
2176
  }
2072
2177
  catch (parseError) {
2073
2178
  responses.push({
2074
- jsonrpc: '2.0',
2075
- error: { code: -32700, message: 'Parse error' },
2076
- id: null
2179
+ jsonrpc: "2.0",
2180
+ error: { code: -32700, message: "Parse error" },
2181
+ id: null,
2077
2182
  });
2078
2183
  }
2079
2184
  }
@@ -2088,17 +2193,19 @@ export class HttpServer {
2088
2193
  // Callback port is independent - it's for fallback/bidirectional, not transport selection
2089
2194
  const shouldUseSSE = useSSE;
2090
2195
  // Check if session has callback URL registered
2091
- const session = lastSessionId ? this.getSession(lastSessionId) : undefined;
2196
+ const session = lastSessionId
2197
+ ? this.getSession(lastSessionId)
2198
+ : undefined;
2092
2199
  const hasCallback = session?.callbackUrl !== undefined;
2093
2200
  const responseTime = Date.now() - requestStartTime;
2094
2201
  if (shouldUseSSE) {
2095
2202
  // Server-Sent Events format
2096
2203
  console.log(`[MCP] Sending ${responses.length} response(s) in SSE format`);
2097
- res.setHeader('Content-Type', 'text/event-stream');
2098
- res.setHeader('Cache-Control', 'no-cache');
2099
- res.setHeader('Connection', 'keep-alive');
2100
- res.setHeader('X-Accel-Buffering', 'no');
2101
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2204
+ res.setHeader("Content-Type", "text/event-stream");
2205
+ res.setHeader("Cache-Control", "no-cache");
2206
+ res.setHeader("Connection", "keep-alive");
2207
+ res.setHeader("X-Accel-Buffering", "no");
2208
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2102
2209
  for (const response of responses) {
2103
2210
  const dataStr = `data: ${JSON.stringify(response)}\n\n`;
2104
2211
  console.log(`[MCP] Writing SSE data: ${dataStr.substring(0, 100)}...`);
@@ -2110,22 +2217,22 @@ export class HttpServer {
2110
2217
  console.log(`[MCP Response] ${new Date().toISOString()} | SSE | Responses: ${responses.length} | Time: ${responseTime}ms | User-Agent: ${userAgent}`);
2111
2218
  responses.forEach((resp, idx) => {
2112
2219
  const truncated = JSON.stringify(resp).substring(0, 300);
2113
- console.log(`[MCP Response] SSE ${idx + 1}: ${truncated}${JSON.stringify(resp).length > 300 ? '...' : ''}`);
2220
+ console.log(`[MCP Response] SSE ${idx + 1}: ${truncated}${JSON.stringify(resp).length > 300 ? "..." : ""}`);
2114
2221
  });
2115
2222
  }
2116
2223
  else {
2117
2224
  // Standard JSON (array for batch, newline-delimited for multiple single requests)
2118
- reqLogger.debug('Sending JSON response', {
2225
+ reqLogger.debug("Sending JSON response", {
2119
2226
  responseCount: responses.length,
2120
2227
  batch: isBatchRequest,
2121
- transport: 'json',
2228
+ transport: "json",
2122
2229
  });
2123
- res.setHeader('Content-Type', 'application/json');
2124
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2125
- res.setHeader('X-Request-Id', requestId);
2230
+ res.setHeader("Content-Type", "application/json");
2231
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2232
+ res.setHeader("X-Request-Id", requestId);
2126
2233
  // Set Mcp-Session-Id header if session exists
2127
2234
  if (lastSessionId) {
2128
- res.setHeader('Mcp-Session-Id', lastSessionId);
2235
+ res.setHeader("Mcp-Session-Id", lastSessionId);
2129
2236
  }
2130
2237
  let responseText;
2131
2238
  if (isBatchRequest) {
@@ -2134,14 +2241,14 @@ export class HttpServer {
2134
2241
  }
2135
2242
  else {
2136
2243
  // Newline-delimited format (backward compatibility)
2137
- responseText = responses.map(r => JSON.stringify(r)).join('\n');
2244
+ responseText = responses.map((r) => JSON.stringify(r)).join("\n");
2138
2245
  }
2139
2246
  res.write(responseText);
2140
2247
  res.end();
2141
- reqLogger.info('MCP response sent', {
2248
+ reqLogger.info("MCP response sent", {
2142
2249
  responseCount: responses.length,
2143
2250
  duration_ms: responseTime,
2144
- transport: 'json',
2251
+ transport: "json",
2145
2252
  batch: isBatchRequest,
2146
2253
  });
2147
2254
  }
@@ -2151,23 +2258,25 @@ export class HttpServer {
2151
2258
  // SessionNotInitializedError returns HTTP 404 per MCP spec
2152
2259
  // This triggers client auto-reinitialize handlers
2153
2260
  if (error instanceof SessionNotInitializedError) {
2154
- reqLogger.info('Session not initialized - returning HTTP 404 for client auto-reinitialize', {
2261
+ reqLogger.info("Session not initialized - returning HTTP 404 for client auto-reinitialize", {
2155
2262
  error: error.message,
2156
2263
  duration_ms: responseTime,
2157
2264
  });
2158
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2159
- res.setHeader('X-Request-Id', requestId);
2265
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2266
+ res.setHeader("X-Request-Id", requestId);
2160
2267
  res.status(404).json({ error: error.message });
2161
2268
  return;
2162
2269
  }
2163
- reqLogger.error('MCP request failed', {
2164
- error: error instanceof Error ? error.message : 'Internal error',
2270
+ reqLogger.error("MCP request failed", {
2271
+ error: error instanceof Error ? error.message : "Internal error",
2165
2272
  stack: error instanceof Error ? error.stack : undefined,
2166
2273
  duration_ms: responseTime,
2167
2274
  });
2168
- res.setHeader('MCP-Protocol-Version', MCP_PROTOCOL_VERSION);
2169
- res.setHeader('X-Request-Id', requestId);
2170
- res.status(500).json({ error: error instanceof Error ? error.message : 'Internal error' });
2275
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
2276
+ res.setHeader("X-Request-Id", requestId);
2277
+ res.status(500).json({
2278
+ error: error instanceof Error ? error.message : "Internal error",
2279
+ });
2171
2280
  }
2172
2281
  }
2173
2282
  /**
@@ -2176,80 +2285,83 @@ export class HttpServer {
2176
2285
  async handleJsonRpcRequest(request) {
2177
2286
  const { method, params, id } = request;
2178
2287
  // Validate JSON-RPC 2.0 structure
2179
- if (!request.jsonrpc || request.jsonrpc !== '2.0') {
2288
+ if (!request.jsonrpc || request.jsonrpc !== "2.0") {
2180
2289
  return {
2181
- jsonrpc: '2.0',
2290
+ jsonrpc: "2.0",
2182
2291
  id,
2183
- error: { code: -32600, message: 'Invalid Request: jsonrpc field must be "2.0"' }
2292
+ error: {
2293
+ code: -32600,
2294
+ message: 'Invalid Request: jsonrpc field must be "2.0"',
2295
+ },
2184
2296
  };
2185
2297
  }
2186
2298
  try {
2187
2299
  let result;
2188
2300
  // Handle MCP methods
2189
- if (method === 'notifications/initialized') {
2301
+ if (method === "notifications/initialized") {
2190
2302
  return null; // No response for notifications
2191
2303
  }
2192
- if (method === 'initialize') {
2304
+ if (method === "initialize") {
2193
2305
  result = {
2194
- protocolVersion: '2024-11-05',
2306
+ protocolVersion: "2024-11-05",
2195
2307
  capabilities: {
2196
2308
  tools: { listChanged: true },
2197
2309
  prompts: { listChanged: false },
2198
2310
  resources: { listChanged: false },
2199
2311
  },
2200
2312
  serverInfo: {
2201
- name: 'metalink',
2313
+ name: "metalink",
2202
2314
  version,
2203
2315
  },
2204
2316
  };
2205
2317
  }
2206
- else if (method === 'ping') {
2318
+ else if (method === "ping") {
2207
2319
  // MCP spec: ping method for keepalive/health checks
2208
2320
  result = {};
2209
2321
  }
2210
- else if (method === 'roots/list') {
2322
+ else if (method === "roots/list") {
2211
2323
  result = { roots: [] };
2212
2324
  }
2213
- else if (method === 'prompts/list') {
2325
+ else if (method === "prompts/list") {
2214
2326
  result = { prompts: getPromptsList() };
2215
2327
  }
2216
- else if (method === 'resources/list') {
2328
+ else if (method === "resources/list") {
2217
2329
  result = { resources: getResourcesList() };
2218
2330
  }
2219
- else if (method === 'resources/templates/list') {
2331
+ else if (method === "resources/templates/list") {
2220
2332
  result = { resourceTemplates: getResourceTemplatesList() };
2221
2333
  }
2222
- else if (method === 'prompts/get') {
2334
+ else if (method === "prompts/get") {
2223
2335
  const promptParams = params;
2224
2336
  if (!promptParams.name) {
2225
- throw new InvalidParamsError('Missing required parameter: name');
2337
+ throw new InvalidParamsError("Missing required parameter: name");
2226
2338
  }
2227
2339
  try {
2228
2340
  result = getPrompt(promptParams.name, promptParams.arguments || {});
2229
2341
  }
2230
2342
  catch (err) {
2231
- throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown prompt error');
2343
+ throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown prompt error");
2232
2344
  }
2233
2345
  }
2234
- else if (method === 'resources/read') {
2346
+ else if (method === "resources/read") {
2235
2347
  const resourceParams = params;
2236
2348
  if (!resourceParams.uri) {
2237
- throw new InvalidParamsError('Missing required parameter: uri');
2349
+ throw new InvalidParamsError("Missing required parameter: uri");
2238
2350
  }
2239
2351
  try {
2240
2352
  result = await readResource(resourceParams.uri, this.serverManager, this.configLoader);
2241
2353
  }
2242
2354
  catch (err) {
2243
- throw new InvalidParamsError(err instanceof Error ? err.message : 'Unknown resource error');
2355
+ throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown resource error");
2244
2356
  }
2245
2357
  }
2246
- else if (method === 'tools/list') {
2358
+ else if (method === "tools/list") {
2247
2359
  result = await this.mcpListTools();
2248
2360
  }
2249
- else if (method === 'tools/call') {
2361
+ else if (method === "tools/call") {
2250
2362
  const callParams = params;
2251
2363
  if (!callParams.name) {
2252
- throw new InvalidParamsError('Missing required parameter: name');
2364
+ throw new InvalidParamsError("Missing required parameter: name");
2253
2365
  }
2254
2366
  result = await this.mcpCallTool(callParams.name, callParams.arguments);
2255
2367
  }
@@ -2257,7 +2369,7 @@ export class HttpServer {
2257
2369
  throw new MethodNotFoundError(method);
2258
2370
  }
2259
2371
  return {
2260
- jsonrpc: '2.0',
2372
+ jsonrpc: "2.0",
2261
2373
  id,
2262
2374
  result,
2263
2375
  };
@@ -2276,11 +2388,11 @@ export class HttpServer {
2276
2388
  errorCode = -32602; // Invalid params
2277
2389
  }
2278
2390
  return {
2279
- jsonrpc: '2.0',
2391
+ jsonrpc: "2.0",
2280
2392
  id,
2281
2393
  error: {
2282
2394
  code: errorCode,
2283
- message: error instanceof Error ? error.message : 'Internal error',
2395
+ message: error instanceof Error ? error.message : "Internal error",
2284
2396
  },
2285
2397
  };
2286
2398
  }
@@ -2334,124 +2446,130 @@ export class HttpServer {
2334
2446
  * Legacy mode: Set dynamicToolExposure=true to expose all base server tools
2335
2447
  */
2336
2448
  tools.push({
2337
- name: 'search_tools',
2449
+ name: "search_tools",
2338
2450
  description: 'Search tools by keyword across all available servers, or list all tools from a specific server. Can also search by server name to find all tools from matching servers. When exactly 1 tool matches, automatically includes full schema (inputSchema, requiredParams, example) to save a describe_tool call. Each result includes annotations.safety ("safe" or "risky") - use execute_tool for safe, execute_tool_confirm for risky.',
2339
2451
  inputSchema: {
2340
- type: 'object',
2452
+ type: "object",
2341
2453
  properties: {
2342
2454
  query: {
2343
- type: 'string',
2344
- description: 'Optional keyword to search for in server names, tool names, and descriptions. If query matches a server name, returns all tools from that server.',
2455
+ type: "string",
2456
+ description: "Optional keyword to search for in server names, tool names, and descriptions. If query matches a server name, returns all tools from that server.",
2345
2457
  },
2346
2458
  server_name: {
2347
- type: 'string',
2348
- description: 'Optional server name to filter results. If provided, only returns tools from this specific server.',
2459
+ type: "string",
2460
+ description: "Optional server name to filter results. If provided, only returns tools from this specific server.",
2349
2461
  },
2350
2462
  },
2351
2463
  required: [],
2352
2464
  },
2353
2465
  }, {
2354
- name: 'describe_tool',
2355
- description: 'Get detailed schema for a specific tool including validation hints. Returns inputSchema, requiredParams, and optional validation warnings/suggestions when schema is incomplete or incorrect.',
2466
+ name: "describe_tool",
2467
+ description: "Get detailed schema for a specific tool including validation hints. Returns inputSchema, requiredParams, and optional validation warnings/suggestions when schema is incomplete or incorrect.",
2356
2468
  inputSchema: {
2357
- type: 'object',
2469
+ type: "object",
2358
2470
  properties: {
2359
2471
  server_name: {
2360
- type: 'string',
2472
+ type: "string",
2361
2473
  description: 'Server name (e.g., "memory", "jira-basic-auth")',
2362
2474
  },
2363
2475
  tool_name: {
2364
- type: 'string',
2476
+ type: "string",
2365
2477
  description: 'Tool name (e.g., "create_entities", "search_issues")',
2366
2478
  },
2367
2479
  },
2368
- required: ['server_name', 'tool_name'],
2480
+ required: ["server_name", "tool_name"],
2369
2481
  },
2370
2482
  }, {
2371
- name: 'execute_tool',
2483
+ name: "execute_tool",
2372
2484
  description: this.getExecuteToolDescription(),
2373
2485
  inputSchema: {
2374
- type: 'object',
2486
+ type: "object",
2375
2487
  properties: {
2376
2488
  server_name: {
2377
- type: 'string',
2489
+ type: "string",
2378
2490
  description: 'Server name (e.g., "jira-basic-auth", "memory")',
2379
- example: 'jira-basic-auth'
2491
+ example: "jira-basic-auth",
2380
2492
  },
2381
2493
  tool_name: {
2382
- type: 'string',
2494
+ type: "string",
2383
2495
  description: 'Tool name (e.g., "confluence_search", "create_entities")',
2384
- example: 'confluence_search'
2496
+ example: "confluence_search",
2385
2497
  },
2386
2498
  arguments: {
2387
- type: 'object',
2388
- description: 'Tool-specific parameters. Call describe_tool(server_name, tool_name) first to discover required parameters, then nest those parameters here.',
2499
+ type: "object",
2500
+ description: "Tool-specific parameters. Call describe_tool(server_name, tool_name) first to discover required parameters, then nest those parameters here.",
2389
2501
  example: {
2390
- cql: 'type=page ORDER BY created DESC',
2391
- limit: 10
2502
+ cql: "type=page ORDER BY created DESC",
2503
+ limit: 10,
2392
2504
  },
2393
- additionalProperties: true
2505
+ additionalProperties: true,
2394
2506
  },
2395
2507
  max_results: {
2396
- type: 'number',
2397
- description: 'Optional: Limit array results to N items (for pagination)',
2398
- example: 50
2508
+ type: "number",
2509
+ description: "Optional: Limit array results to N items (for pagination)",
2510
+ example: 50,
2399
2511
  },
2400
2512
  max_result_chars: {
2401
- type: 'number',
2402
- description: 'Optional: Limit total response size to N characters (default: 50000)',
2403
- example: 10000
2513
+ type: "number",
2514
+ description: "Optional: Limit total response size to N characters (default: 50000)",
2515
+ example: 10000,
2404
2516
  },
2405
2517
  cursor: {
2406
- type: 'string',
2407
- description: 'Optional: Opaque cursor from previous response to continue pagination'
2408
- }
2518
+ type: "string",
2519
+ description: "Optional: Opaque cursor from previous response to continue pagination",
2520
+ },
2409
2521
  },
2410
- required: ['server_name', 'tool_name', 'arguments'],
2411
- additionalProperties: false
2412
- }
2522
+ required: ["server_name", "tool_name", "arguments"],
2523
+ additionalProperties: false,
2524
+ },
2413
2525
  }, {
2414
- name: 'execute_tool_confirm',
2526
+ name: "execute_tool_confirm",
2415
2527
  description: 'Execute risky tool (requires user confirmation). IMPORTANT: Check annotations.safety in search_tools results - only use this for "risky" tools, use execute_tool for "safe" tools. Safety annotation is authoritative. Required for external services without explicit safe rules.',
2416
2528
  inputSchema: {
2417
- type: 'object',
2529
+ type: "object",
2418
2530
  properties: {
2419
2531
  server_name: {
2420
- type: 'string',
2532
+ type: "string",
2421
2533
  description: 'Server name (e.g., "jira-basic-auth", "memory")',
2422
- example: 'memory'
2534
+ example: "memory",
2423
2535
  },
2424
2536
  tool_name: {
2425
- type: 'string',
2537
+ type: "string",
2426
2538
  description: 'Tool name (e.g., "create_issue", "delete_entities")',
2427
- example: 'create_entities'
2539
+ example: "create_entities",
2428
2540
  },
2429
2541
  arguments: {
2430
- type: 'object',
2431
- description: 'Tool arguments object. REQUIRED. All tool-specific parameters MUST be nested inside this object, not at the top level.',
2542
+ type: "object",
2543
+ description: "Tool arguments object. REQUIRED. All tool-specific parameters MUST be nested inside this object, not at the top level.",
2432
2544
  example: {
2433
- entities: [{ name: 'test', entityType: 'concept', observations: ['example data'] }]
2545
+ entities: [
2546
+ {
2547
+ name: "test",
2548
+ entityType: "concept",
2549
+ observations: ["example data"],
2550
+ },
2551
+ ],
2434
2552
  },
2435
- additionalProperties: true
2553
+ additionalProperties: true,
2436
2554
  },
2437
2555
  max_results: {
2438
- type: 'number',
2439
- description: 'Optional: Limit array results to N items (for pagination)',
2440
- example: 50
2556
+ type: "number",
2557
+ description: "Optional: Limit array results to N items (for pagination)",
2558
+ example: 50,
2441
2559
  },
2442
2560
  max_result_chars: {
2443
- type: 'number',
2444
- description: 'Optional: Limit total response size to N characters (default: 50000)',
2445
- example: 10000
2561
+ type: "number",
2562
+ description: "Optional: Limit total response size to N characters (default: 50000)",
2563
+ example: 10000,
2446
2564
  },
2447
2565
  cursor: {
2448
- type: 'string',
2449
- description: 'Optional: Opaque cursor from previous response to continue pagination'
2450
- }
2566
+ type: "string",
2567
+ description: "Optional: Opaque cursor from previous response to continue pagination",
2568
+ },
2451
2569
  },
2452
- required: ['server_name', 'tool_name', 'arguments'],
2453
- additionalProperties: false
2454
- }
2570
+ required: ["server_name", "tool_name", "arguments"],
2571
+ additionalProperties: false,
2572
+ },
2455
2573
  });
2456
2574
  // OPTIONAL: Base server tools - controlled by config options (v1.3.57+)
2457
2575
  // Three modes:
@@ -2471,7 +2589,10 @@ export class HttpServer {
2471
2589
  tools.push({
2472
2590
  name: `${serverName}-${tool.name}`,
2473
2591
  description: tool.description || `Tool from ${serverName} server`,
2474
- inputSchema: tool.inputSchema || { type: 'object', properties: {} },
2592
+ inputSchema: tool.inputSchema || {
2593
+ type: "object",
2594
+ properties: {},
2595
+ },
2475
2596
  });
2476
2597
  }
2477
2598
  }
@@ -2500,7 +2621,7 @@ export class HttpServer {
2500
2621
  async mcpCallTool(name, args, sessionId) {
2501
2622
  // Track metrics for tool call (Phase 4 - v1.4.0)
2502
2623
  const startTime = Date.now();
2503
- globalMetrics.incrementCounter(`tool_calls_${name}`, 'calls');
2624
+ globalMetrics.incrementCounter(`tool_calls_${name}`, "calls");
2504
2625
  // SECURITY: Extract server name for rate limiting
2505
2626
  // For meta-tools (execute_tool, search_tools), extract from args
2506
2627
  // For direct calls (server-tool), extract from name
@@ -2508,7 +2629,7 @@ export class HttpServer {
2508
2629
  // Record tool call for error rate tracking
2509
2630
  globalMetrics.recordToolCall(name, serverName || undefined);
2510
2631
  // Log tool call with structured context
2511
- logger.info('Tool call initiated', {
2632
+ logger.info("Tool call initiated", {
2512
2633
  tool: name,
2513
2634
  server: serverName || undefined,
2514
2635
  sessionId,
@@ -2519,21 +2640,21 @@ export class HttpServer {
2519
2640
  }
2520
2641
  // SECURITY: Discovery endpoint rate limiting (P1)
2521
2642
  // search_tools and describe_tool have separate rate limits per session
2522
- if (name === 'search_tools' || name === 'describe_tool') {
2523
- const rateLimitKey = sessionId || 'anonymous';
2643
+ if (name === "search_tools" || name === "describe_tool") {
2644
+ const rateLimitKey = sessionId || "anonymous";
2524
2645
  this.checkDiscoveryRateLimit(rateLimitKey);
2525
2646
  }
2526
2647
  try {
2527
2648
  const result = await this.executeToolCall(name, args);
2528
2649
  // Record successful execution metrics
2529
2650
  const latency = Date.now() - startTime;
2530
- globalMetrics.setGauge(`tool_latency_${name}`, latency, 'ms');
2651
+ globalMetrics.setGauge(`tool_latency_${name}`, latency, "ms");
2531
2652
  // Record granular tool-specific metrics (Test 187)
2532
2653
  if (serverName) {
2533
- globalMetrics.recordToolExecution(serverName, name.replace(`${serverName}-`, ''), latency);
2654
+ globalMetrics.recordToolExecution(serverName, name.replace(`${serverName}-`, ""), latency);
2534
2655
  }
2535
2656
  // Log successful tool execution
2536
- logger.info('Tool call completed', {
2657
+ logger.info("Tool call completed", {
2537
2658
  tool: name,
2538
2659
  server: serverName || undefined,
2539
2660
  duration_ms: latency,
@@ -2542,16 +2663,16 @@ export class HttpServer {
2542
2663
  }
2543
2664
  catch (error) {
2544
2665
  // Record error metrics with detailed tracking
2545
- globalMetrics.incrementCounter(`tool_errors_${name}`, 'errors');
2666
+ globalMetrics.incrementCounter(`tool_errors_${name}`, "errors");
2546
2667
  globalMetrics.recordToolError(error, name, serverName || undefined);
2547
2668
  // Record granular tool-specific error (Test 187)
2548
2669
  if (serverName) {
2549
2670
  const errorMessage = error instanceof Error ? error.message : String(error);
2550
- globalMetrics.recordToolFailure(serverName, name.replace(`${serverName}-`, ''), errorMessage);
2671
+ globalMetrics.recordToolFailure(serverName, name.replace(`${serverName}-`, ""), errorMessage);
2551
2672
  }
2552
2673
  // Log tool execution error
2553
2674
  const latency = Date.now() - startTime;
2554
- logger.error('Tool call failed', {
2675
+ logger.error("Tool call failed", {
2555
2676
  tool: name,
2556
2677
  server: serverName || undefined,
2557
2678
  error: error instanceof Error ? error.message : String(error),
@@ -2569,18 +2690,22 @@ export class HttpServer {
2569
2690
  */
2570
2691
  extractServerNameForRateLimit(name, args) {
2571
2692
  // Meta-tools with explicit server_name argument
2572
- if (name === 'execute_tool' || name === 'execute_tool_confirm' ||
2573
- name === 'describe_tool' || name === 'list_tools') {
2693
+ if (name === "execute_tool" ||
2694
+ name === "execute_tool_confirm" ||
2695
+ name === "describe_tool" ||
2696
+ name === "list_tools") {
2574
2697
  const callArgs = args;
2575
2698
  return callArgs?.server_name || null;
2576
2699
  }
2577
2700
  // Discovery tools - no server-specific rate limiting
2578
- if (name === 'search_tools' || name === 'list_available_servers' || name === 'list_servers') {
2701
+ if (name === "search_tools" ||
2702
+ name === "list_available_servers" ||
2703
+ name === "list_servers") {
2579
2704
  return null;
2580
2705
  }
2581
2706
  // Direct tool calls: "server-toolName" format
2582
- if (name.includes('-')) {
2583
- const parts = name.split('-');
2707
+ if (name.includes("-")) {
2708
+ const parts = name.split("-");
2584
2709
  return parts[0];
2585
2710
  }
2586
2711
  return null;
@@ -2611,7 +2736,7 @@ export class HttpServer {
2611
2736
  const waitTime = Math.ceil((limiter.resetTime - now) / 1000);
2612
2737
  console.warn(`[SECURITY] Rate limit exceeded for server '${serverName}': ` +
2613
2738
  `${limiter.count} calls in window, max ${this.TOOL_RATE_LIMIT_MAX_CALLS}`);
2614
- globalMetrics.incrementCounter(`rate_limit_exceeded_${serverName}`, 'rate_limits');
2739
+ globalMetrics.incrementCounter(`rate_limit_exceeded_${serverName}`, "rate_limits");
2615
2740
  throw new Error(`Rate limit exceeded for server '${serverName}'. ` +
2616
2741
  `Maximum ${this.TOOL_RATE_LIMIT_MAX_CALLS} tool calls per minute. ` +
2617
2742
  `Please wait ${waitTime} seconds before retrying.`);
@@ -2643,7 +2768,7 @@ export class HttpServer {
2643
2768
  const waitTime = Math.ceil((limiter.resetTime - now) / 1000);
2644
2769
  console.warn(`[SECURITY] Discovery rate limit exceeded for session '${sessionId}': ` +
2645
2770
  `${limiter.count} calls in window, max ${this.DISCOVERY_RATE_LIMIT_MAX_CALLS}`);
2646
- globalMetrics.incrementCounter('discovery_rate_limit_exceeded', 'rate_limits');
2771
+ globalMetrics.incrementCounter("discovery_rate_limit_exceeded", "rate_limits");
2647
2772
  throw new Error(`Discovery rate limit exceeded. ` +
2648
2773
  `Maximum ${this.DISCOVERY_RATE_LIMIT_MAX_CALLS} discovery calls per minute. ` +
2649
2774
  `Please wait ${waitTime} seconds before retrying.`);
@@ -2656,68 +2781,73 @@ export class HttpServer {
2656
2781
  async executeToolCall(name, args) {
2657
2782
  const callArgs = args;
2658
2783
  switch (name) {
2659
- case 'list_available_servers': {
2784
+ case "list_available_servers": {
2660
2785
  const allServers = this.configLoader.getAllServers();
2661
2786
  const enabledServers = this.configLoader.getServers();
2662
- const enabledNames = new Set(enabledServers.map(s => s.name));
2787
+ const enabledNames = new Set(enabledServers.map((s) => s.name));
2663
2788
  return {
2664
2789
  content: [
2665
2790
  {
2666
- type: 'text',
2791
+ type: "text",
2667
2792
  text: JSON.stringify({
2668
- servers: allServers.map(s => {
2669
- const isStdio = s.transport === 'stdio' || s.transport === undefined;
2793
+ servers: allServers.map((s) => {
2794
+ const isStdio = s.transport === "stdio" || s.transport === undefined;
2670
2795
  return {
2671
2796
  name: s.name,
2672
- ...(isStdio ? { command: s.command } : { url: s.url }),
2797
+ ...(isStdio
2798
+ ? { command: s.command }
2799
+ : { url: s.url }),
2673
2800
  enabled: enabledNames.has(s.name),
2674
2801
  };
2675
2802
  }),
2676
- }, null, 2)
2677
- }
2678
- ]
2803
+ }, null, 2),
2804
+ },
2805
+ ],
2679
2806
  };
2680
2807
  }
2681
- case 'list_servers': {
2808
+ case "list_servers": {
2682
2809
  const servers = this.configLoader.getServers();
2683
2810
  return {
2684
2811
  content: [
2685
2812
  {
2686
- type: 'text',
2813
+ type: "text",
2687
2814
  text: JSON.stringify({
2688
- servers: servers.map(s => {
2689
- const isStdio = s.transport === 'stdio' || s.transport === undefined;
2815
+ servers: servers.map((s) => {
2816
+ const isStdio = s.transport === "stdio" || s.transport === undefined;
2690
2817
  return {
2691
2818
  name: s.name,
2692
- ...(isStdio ? { command: s.command } : { url: s.url }),
2693
- status: this.serverManager.getServerStatus(s.name)?.status || 'stopped',
2819
+ ...(isStdio
2820
+ ? { command: s.command }
2821
+ : { url: s.url }),
2822
+ status: this.serverManager.getServerStatus(s.name)?.status ||
2823
+ "stopped",
2694
2824
  toolCount: this.serverManager.getServerTools(s.name).length || 0,
2695
2825
  };
2696
2826
  }),
2697
- }, null, 2)
2698
- }
2699
- ]
2827
+ }, null, 2),
2828
+ },
2829
+ ],
2700
2830
  };
2701
2831
  }
2702
- case 'list_tools': {
2832
+ case "list_tools": {
2703
2833
  const serverName = callArgs?.server_name;
2704
2834
  if (!serverName)
2705
- throw new InvalidParamsError('server_name required');
2835
+ throw new InvalidParamsError("server_name required");
2706
2836
  // Get tools from server manager
2707
2837
  const tools = this.serverManager.getServerTools(serverName);
2708
2838
  return {
2709
2839
  content: [
2710
2840
  {
2711
- type: 'text',
2841
+ type: "text",
2712
2842
  text: JSON.stringify({
2713
2843
  server: serverName,
2714
2844
  tools: tools || [],
2715
- }, null, 2)
2716
- }
2717
- ]
2845
+ }, null, 2),
2846
+ },
2847
+ ],
2718
2848
  };
2719
2849
  }
2720
- case 'execute_tool': {
2850
+ case "execute_tool": {
2721
2851
  const args = callArgs;
2722
2852
  // DEBUG: Log raw incoming args to diagnose Raycast/Grok issues
2723
2853
  console.log(`[DEBUG execute_tool] RAW INCOMING ARGS: ${JSON.stringify(args)}`);
@@ -2732,7 +2862,7 @@ export class HttpServer {
2732
2862
  const toolArgs = fixedArgs.arguments || {};
2733
2863
  // SAFETY CHECK: Verify this tool is classified as 'safe' (with argument inspection)
2734
2864
  const safetyResult = this.serverManager.classifyToolSafety(serverName, toolName, toolArgs);
2735
- if (safetyResult.safety === 'risky') {
2865
+ if (safetyResult.safety === "risky") {
2736
2866
  throw new InvalidParamsError(`Tool ${serverName}:${toolName} is classified as RISKY and requires user confirmation.\n` +
2737
2867
  `Use 'execute_tool_confirm' instead for this tool.\n` +
2738
2868
  `Classification reason: ${safetyResult.reason}`);
@@ -2746,7 +2876,7 @@ export class HttpServer {
2746
2876
  const missingArgs = this.serverManager.detectMissingArguments(fixedArgs, toolSchema);
2747
2877
  if (missingArgs) {
2748
2878
  const inputSchema = toolSchema.inputSchema;
2749
- const requiredParams = inputSchema?.required?.join(', ') || 'unknown';
2879
+ const requiredParams = inputSchema?.required?.join(", ") || "unknown";
2750
2880
  const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2751
2881
  `\n` +
2752
2882
  `Required: ${requiredParams}\n` +
@@ -2762,7 +2892,7 @@ export class HttpServer {
2762
2892
  const inputSchema2 = toolSchema.inputSchema;
2763
2893
  const requiredParams2 = inputSchema2?.required || [];
2764
2894
  // Check if required params are missing from arguments
2765
- const missingRequiredParams = requiredParams2.filter(param => !(param in args2));
2895
+ const missingRequiredParams = requiredParams2.filter((param) => !(param in args2));
2766
2896
  if (missingRequiredParams.length > 0) {
2767
2897
  // Generate example values for each required param
2768
2898
  const exampleArgs = {};
@@ -2772,9 +2902,9 @@ export class HttpServer {
2772
2902
  exampleArgs[param] = propSchema.example;
2773
2903
  else if (propSchema?.default)
2774
2904
  exampleArgs[param] = propSchema.default;
2775
- else if (propSchema?.type === 'string')
2905
+ else if (propSchema?.type === "string")
2776
2906
  exampleArgs[param] = `<${param}>`;
2777
- else if (propSchema?.type === 'number')
2907
+ else if (propSchema?.type === "number")
2778
2908
  exampleArgs[param] = 10;
2779
2909
  else
2780
2910
  exampleArgs[param] = `<${param}>`;
@@ -2782,7 +2912,7 @@ export class HttpServer {
2782
2912
  const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2783
2913
  `\n` +
2784
2914
  `You provided: ${JSON.stringify(args2)}\n` +
2785
- `Missing: ${missingRequiredParams.join(', ')}\n` +
2915
+ `Missing: ${missingRequiredParams.join(", ")}\n` +
2786
2916
  `\n` +
2787
2917
  `✅ CORRECT FORMAT:\n` +
2788
2918
  ` {"server_name": "${serverName}", "tool_name": "${toolName}", "arguments": ${JSON.stringify(exampleArgs)}}\n` +
@@ -2803,24 +2933,28 @@ export class HttpServer {
2803
2933
  const paginationParams = {
2804
2934
  max_results: fixedArgs.max_results,
2805
2935
  max_result_chars: fixedArgs.max_result_chars,
2806
- cursor: fixedArgs.cursor
2936
+ cursor: fixedArgs.cursor,
2807
2937
  };
2808
2938
  // Only paginate if at least one pagination param is provided
2809
- if (paginationParams.max_results || paginationParams.max_result_chars || paginationParams.cursor) {
2939
+ if (paginationParams.max_results ||
2940
+ paginationParams.max_result_chars ||
2941
+ paginationParams.cursor) {
2810
2942
  const paginated = this.paginateResult(result, paginationParams);
2811
2943
  // Return result with pagination metadata merged in
2812
- if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
2944
+ if (typeof result === "object" &&
2945
+ result !== null &&
2946
+ !Array.isArray(result)) {
2813
2947
  return {
2814
2948
  ...result,
2815
2949
  result: paginated.result,
2816
- _pagination: paginated._pagination
2950
+ _pagination: paginated._pagination,
2817
2951
  };
2818
2952
  }
2819
2953
  else {
2820
2954
  // If result is not an object (e.g., primitive or array), wrap it
2821
2955
  return {
2822
2956
  result: paginated.result,
2823
- _pagination: paginated._pagination
2957
+ _pagination: paginated._pagination,
2824
2958
  };
2825
2959
  }
2826
2960
  }
@@ -2832,14 +2966,16 @@ export class HttpServer {
2832
2966
  const inputSchema = toolSchema.inputSchema;
2833
2967
  // Check if error is parameter-related
2834
2968
  const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
2835
- const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
2836
- errorStr.includes('required') || errorStr.includes('missing');
2969
+ const isParamError = errorStr.includes("null") ||
2970
+ errorStr.includes("undefined") ||
2971
+ errorStr.includes("required") ||
2972
+ errorStr.includes("missing");
2837
2973
  if (isParamError && inputSchema) {
2838
2974
  const requiredParams = inputSchema.required || [];
2839
2975
  const availableParams = Object.keys(inputSchema.properties || {});
2840
- const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
2841
- const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
2842
- const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
2976
+ const optionalParams = availableParams.filter((p) => !requiredParams.includes(p));
2977
+ const requiredList = requiredParams.length > 0 ? requiredParams.join(", ") : "none";
2978
+ const optionalList = optionalParams.length > 0 ? optionalParams.join(", ") : "none";
2843
2979
  const hint = `\n\n💡 Hint: This tool expects:\n` +
2844
2980
  ` Required: ${requiredList}\n` +
2845
2981
  ` Optional: ${optionalList}\n` +
@@ -2849,7 +2985,7 @@ export class HttpServer {
2849
2985
  throw new Error(errorMsg);
2850
2986
  }
2851
2987
  }
2852
- case 'execute_tool_confirm': {
2988
+ case "execute_tool_confirm": {
2853
2989
  const args = callArgs;
2854
2990
  // Phase 2a: Detect and fix Raycast format issues
2855
2991
  const fixedArgs = this.serverManager.detectAndFixRaycastFormat(args);
@@ -2872,7 +3008,7 @@ export class HttpServer {
2872
3008
  const missingArgs = this.serverManager.detectMissingArguments(fixedArgs, toolSchema);
2873
3009
  if (missingArgs) {
2874
3010
  const inputSchema = toolSchema.inputSchema;
2875
- const requiredParams = inputSchema?.required?.join(', ') || 'unknown';
3011
+ const requiredParams = inputSchema?.required?.join(", ") || "unknown";
2876
3012
  const errorMsg = `❌ Missing required parameters for ${serverName}:${toolName}\n` +
2877
3013
  `\n` +
2878
3014
  `Required: ${requiredParams}\n` +
@@ -2898,24 +3034,28 @@ export class HttpServer {
2898
3034
  const paginationParams = {
2899
3035
  max_results: fixedArgs.max_results,
2900
3036
  max_result_chars: fixedArgs.max_result_chars,
2901
- cursor: fixedArgs.cursor
3037
+ cursor: fixedArgs.cursor,
2902
3038
  };
2903
3039
  // Only paginate if at least one pagination param is provided
2904
- if (paginationParams.max_results || paginationParams.max_result_chars || paginationParams.cursor) {
3040
+ if (paginationParams.max_results ||
3041
+ paginationParams.max_result_chars ||
3042
+ paginationParams.cursor) {
2905
3043
  const paginated = this.paginateResult(result, paginationParams);
2906
3044
  // Return result with pagination metadata merged in
2907
- if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
3045
+ if (typeof result === "object" &&
3046
+ result !== null &&
3047
+ !Array.isArray(result)) {
2908
3048
  return {
2909
3049
  ...result,
2910
3050
  result: paginated.result,
2911
- _pagination: paginated._pagination
3051
+ _pagination: paginated._pagination,
2912
3052
  };
2913
3053
  }
2914
3054
  else {
2915
3055
  // If result is not an object (e.g., primitive or array), wrap it
2916
3056
  return {
2917
3057
  result: paginated.result,
2918
- _pagination: paginated._pagination
3058
+ _pagination: paginated._pagination,
2919
3059
  };
2920
3060
  }
2921
3061
  }
@@ -2927,14 +3067,16 @@ export class HttpServer {
2927
3067
  const inputSchema = toolSchema.inputSchema;
2928
3068
  // Check if error is parameter-related
2929
3069
  const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
2930
- const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
2931
- errorStr.includes('required') || errorStr.includes('missing');
3070
+ const isParamError = errorStr.includes("null") ||
3071
+ errorStr.includes("undefined") ||
3072
+ errorStr.includes("required") ||
3073
+ errorStr.includes("missing");
2932
3074
  if (isParamError && inputSchema) {
2933
3075
  const requiredParams = inputSchema.required || [];
2934
3076
  const availableParams = Object.keys(inputSchema.properties || {});
2935
- const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
2936
- const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
2937
- const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
3077
+ const optionalParams = availableParams.filter((p) => !requiredParams.includes(p));
3078
+ const requiredList = requiredParams.length > 0 ? requiredParams.join(", ") : "none";
3079
+ const optionalList = optionalParams.length > 0 ? optionalParams.join(", ") : "none";
2938
3080
  const hint = `\n\n💡 Hint: This tool expects:\n` +
2939
3081
  ` Required: ${requiredList}\n` +
2940
3082
  ` Optional: ${optionalList}\n` +
@@ -2947,18 +3089,18 @@ export class HttpServer {
2947
3089
  // ===== 4-TOOL SPEAKEASY APPROACH (v1.3.52) =====
2948
3090
  // Discovery tools: search_tools, describe_tool
2949
3091
  // Execution tools: execute_tool, execute_tool_confirm
2950
- case 'search_tools': {
3092
+ case "search_tools": {
2951
3093
  // FIX v1.3.52: Search ALL servers using CACHED schemas (no auto-start)
2952
3094
  // UPDATE v1.3.56: Support optional server_name filter and server name matching
2953
3095
  // - query (optional): Search term for server names, tool names, and descriptions
2954
3096
  // - server_name (optional): Filter to specific server
2955
3097
  // - At least one parameter required
2956
3098
  try {
2957
- const query = callArgs?.query || '';
2958
- const serverNameFilter = callArgs?.server_name || '';
3099
+ const query = callArgs?.query || "";
3100
+ const serverNameFilter = callArgs?.server_name || "";
2959
3101
  // Validate: at least one parameter provided
2960
3102
  if (!query && !serverNameFilter) {
2961
- throw new InvalidParamsError('At least one parameter required: query or server_name');
3103
+ throw new InvalidParamsError("At least one parameter required: query or server_name");
2962
3104
  }
2963
3105
  const results = [];
2964
3106
  // Get all servers or filter to specific server
@@ -2971,12 +3113,16 @@ export class HttpServer {
2971
3113
  throw new InvalidParamsError(`Server '${serverNameFilter}' not found in registry`);
2972
3114
  }
2973
3115
  // Multi-keyword matching: split query into individual words
2974
- const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 0);
3116
+ const keywords = query
3117
+ .toLowerCase()
3118
+ .split(/\s+/)
3119
+ .filter((k) => k.length > 0);
2975
3120
  // Step 1: Check if any keyword matches a server name
2976
3121
  let serverKeyword;
2977
3122
  if (keywords.length > 0 && !serverNameFilter) {
2978
3123
  for (const keyword of keywords) {
2979
- const matchedServer = allServers.find((s) => s.name.toLowerCase().includes(keyword) || keyword.includes(s.name.toLowerCase()));
3124
+ const matchedServer = allServers.find((s) => s.name.toLowerCase().includes(keyword) ||
3125
+ keyword.includes(s.name.toLowerCase()));
2980
3126
  if (matchedServer) {
2981
3127
  serverKeyword = keyword;
2982
3128
  serversToSearch = [matchedServer];
@@ -2986,47 +3132,47 @@ export class HttpServer {
2986
3132
  }
2987
3133
  // Step 2: Get remaining keywords (exclude server name keyword)
2988
3134
  const searchKeywords = serverKeyword
2989
- ? keywords.filter(k => k !== serverKeyword)
3135
+ ? keywords.filter((k) => k !== serverKeyword)
2990
3136
  : keywords;
2991
3137
  for (const serverConfig of serversToSearch) {
2992
3138
  const serverName = serverConfig.name.toLowerCase();
2993
3139
  const tools = this.serverManager.getServerTools(serverConfig.name);
2994
3140
  for (const tool of tools) {
2995
3141
  const toolName = tool.name.toLowerCase();
2996
- const toolDesc = (tool.description || '').toLowerCase();
3142
+ const toolDesc = (tool.description || "").toLowerCase();
2997
3143
  // Determine match type
2998
- let matchType = 'all';
3144
+ let matchType = "all";
2999
3145
  let shouldInclude = false;
3000
3146
  if (keywords.length === 0) {
3001
3147
  // No query, just listing all tools from filtered server
3002
- matchType = 'all';
3148
+ matchType = "all";
3003
3149
  shouldInclude = true;
3004
3150
  }
3005
3151
  else if (serverKeyword && searchKeywords.length === 0) {
3006
3152
  // Only server name in query - return ALL tools from this server
3007
- matchType = 'server';
3153
+ matchType = "server";
3008
3154
  shouldInclude = true;
3009
3155
  }
3010
3156
  else if (searchKeywords.length > 0) {
3011
3157
  // Multi-keyword matching: check if ANY keyword matches tool name or description
3012
- const nameMatches = searchKeywords.some(keyword => toolName.includes(keyword));
3013
- const descMatches = searchKeywords.some(keyword => toolDesc.includes(keyword));
3158
+ const nameMatches = searchKeywords.some((keyword) => toolName.includes(keyword));
3159
+ const descMatches = searchKeywords.some((keyword) => toolDesc.includes(keyword));
3014
3160
  if (nameMatches && descMatches) {
3015
- matchType = 'all';
3161
+ matchType = "all";
3016
3162
  shouldInclude = true;
3017
3163
  }
3018
3164
  else if (nameMatches) {
3019
- matchType = 'name';
3165
+ matchType = "name";
3020
3166
  shouldInclude = true;
3021
3167
  }
3022
3168
  else if (descMatches) {
3023
- matchType = 'description';
3169
+ matchType = "description";
3024
3170
  shouldInclude = true;
3025
3171
  }
3026
3172
  else if (serverKeyword) {
3027
3173
  // Server matched but no tool/description keywords matched
3028
3174
  // Still include if we're filtering by server (be permissive)
3029
- matchType = 'server';
3175
+ matchType = "server";
3030
3176
  shouldInclude = true;
3031
3177
  }
3032
3178
  }
@@ -3037,7 +3183,7 @@ export class HttpServer {
3037
3183
  const requiredArray = inputSchema?.required;
3038
3184
  const allParams = properties ? Object.keys(properties) : [];
3039
3185
  const requiredParams = requiredArray || [];
3040
- const optionalParams = allParams.filter(p => !requiredParams.includes(p));
3186
+ const optionalParams = allParams.filter((p) => !requiredParams.includes(p));
3041
3187
  // Generate example arguments from inputSchema
3042
3188
  const exampleArgs = {};
3043
3189
  if (properties) {
@@ -3050,34 +3196,38 @@ export class HttpServer {
3050
3196
  else if (prop.default !== undefined) {
3051
3197
  exampleArgs[key] = prop.default;
3052
3198
  }
3053
- else if (prop.type === 'string') {
3199
+ else if (prop.type === "string") {
3054
3200
  // Generate meaningful examples for common param names
3055
- if (key === 'query')
3056
- exampleArgs[key] = 'search term';
3057
- else if (key === 'cql')
3058
- exampleArgs[key] = 'type=page ORDER BY created DESC';
3059
- else if (key === 'jql')
3060
- exampleArgs[key] = 'project = PROJ ORDER BY created DESC';
3061
- else if (key === 'timezone')
3062
- exampleArgs[key] = 'UTC';
3063
- else if (key === 'url')
3064
- exampleArgs[key] = 'https://example.com';
3201
+ if (key === "query")
3202
+ exampleArgs[key] = "search term";
3203
+ else if (key === "cql")
3204
+ exampleArgs[key] = "type=page ORDER BY created DESC";
3205
+ else if (key === "jql")
3206
+ exampleArgs[key] =
3207
+ "project = PROJ ORDER BY created DESC";
3208
+ else if (key === "timezone")
3209
+ exampleArgs[key] = "UTC";
3210
+ else if (key === "url")
3211
+ exampleArgs[key] = "https://example.com";
3065
3212
  else
3066
3213
  exampleArgs[key] = `<${key}>`;
3067
3214
  }
3068
- else if (prop.type === 'number' || prop.type === 'integer') {
3069
- if (key === 'limit' || key === 'max_results' || key === 'maxResults')
3215
+ else if (prop.type === "number" ||
3216
+ prop.type === "integer") {
3217
+ if (key === "limit" ||
3218
+ key === "max_results" ||
3219
+ key === "maxResults")
3070
3220
  exampleArgs[key] = 10;
3071
3221
  else
3072
3222
  exampleArgs[key] = 1;
3073
3223
  }
3074
- else if (prop.type === 'boolean') {
3224
+ else if (prop.type === "boolean") {
3075
3225
  exampleArgs[key] = true;
3076
3226
  }
3077
- else if (prop.type === 'array') {
3227
+ else if (prop.type === "array") {
3078
3228
  exampleArgs[key] = [];
3079
3229
  }
3080
- else if (prop.type === 'object') {
3230
+ else if (prop.type === "object") {
3081
3231
  exampleArgs[key] = {};
3082
3232
  }
3083
3233
  }
@@ -3094,7 +3244,7 @@ export class HttpServer {
3094
3244
  safetyAnnotation = {
3095
3245
  safety: classification.safety,
3096
3246
  safetyReason: classification.reason,
3097
- requiresConfirmation: classification.safety === 'risky',
3247
+ requiresConfirmation: classification.safety === "risky",
3098
3248
  };
3099
3249
  }
3100
3250
  // v1.3.x: Check for argument inspection rules
@@ -3103,15 +3253,15 @@ export class HttpServer {
3103
3253
  const fullToolName = `${serverConfig.name}:${tool.name}`;
3104
3254
  const argInspectionRule = argInspectionRules.find((rule) => rule.tool === fullToolName);
3105
3255
  let toolNote = "Use describe_tool to get full schema before execution";
3106
- if (argInspectionRule && safetyAnnotation.safety === 'risky') {
3256
+ if (argInspectionRule && safetyAnnotation.safety === "risky") {
3107
3257
  // Add argument inspection hint for risky tools with auto-approval patterns
3108
3258
  safetyAnnotation.argumentInspection = {
3109
3259
  enabled: true,
3110
3260
  field: argInspectionRule.argumentField,
3111
3261
  safePatterns: argInspectionRule.safeCommandPatterns || [],
3112
- note: `Auto-approved when ${argInspectionRule.argumentField} matches safe patterns (e.g., ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(', ')})`
3262
+ note: `Auto-approved when ${argInspectionRule.argumentField} matches safe patterns (e.g., ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(", ")})`,
3113
3263
  };
3114
- toolNote = `Risky by default, but AUTO-APPROVED for read-only operations. Check ${argInspectionRule.argumentField} - patterns like ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(', ')} are safe.`;
3264
+ toolNote = `Risky by default, but AUTO-APPROVED for read-only operations. Check ${argInspectionRule.argumentField} - patterns like ${(argInspectionRule.safeCommandPatterns || []).slice(0, 3).join(", ")} are safe.`;
3115
3265
  }
3116
3266
  results.push({
3117
3267
  server: serverConfig.name,
@@ -3122,14 +3272,16 @@ export class HttpServer {
3122
3272
  matchType,
3123
3273
  // v1.3.x: Top-level safety fields for easier access
3124
3274
  safety: safetyAnnotation.safety,
3125
- execute_with: safetyAnnotation.safety === 'safe' ? 'execute_tool' : 'execute_tool_confirm',
3275
+ execute_with: safetyAnnotation.safety === "safe"
3276
+ ? "execute_tool"
3277
+ : "execute_tool_confirm",
3126
3278
  annotations: safetyAnnotation,
3127
3279
  _schema: {
3128
3280
  inputSchema,
3129
3281
  requiredParams,
3130
3282
  optionalParams,
3131
- exampleArgs
3132
- }
3283
+ exampleArgs,
3284
+ },
3133
3285
  });
3134
3286
  }
3135
3287
  }
@@ -3147,12 +3299,16 @@ export class HttpServer {
3147
3299
  result.example = {
3148
3300
  server_name: result.server,
3149
3301
  tool_name: result.tool,
3150
- arguments: schema.exampleArgs
3302
+ arguments: schema.exampleArgs,
3151
3303
  };
3152
3304
  // Anthropic Advanced Tool Use: Include tool use examples if available
3153
3305
  // https://www.anthropic.com/engineering/advanced-tool-use
3154
- const tool = this.serverManager.getServerTools(result.server).find(t => t.name === result.tool);
3155
- if (tool?.inputExamples && Array.isArray(tool.inputExamples) && tool.inputExamples.length > 0) {
3306
+ const tool = this.serverManager
3307
+ .getServerTools(result.server)
3308
+ .find((t) => t.name === result.tool);
3309
+ if (tool?.inputExamples &&
3310
+ Array.isArray(tool.inputExamples) &&
3311
+ tool.inputExamples.length > 0) {
3156
3312
  result.inputExamples = tool.inputExamples;
3157
3313
  }
3158
3314
  // Remove the note since we're providing full schema
@@ -3164,7 +3320,7 @@ export class HttpServer {
3164
3320
  else {
3165
3321
  // Include inputSchema in all results for Raycast compatibility
3166
3322
  // v1.1.67: Always include inputSchema even when not auto-describing
3167
- results.forEach(r => {
3323
+ results.forEach((r) => {
3168
3324
  if (r._schema) {
3169
3325
  r.inputSchema = r._schema.inputSchema;
3170
3326
  r.requiredParams = r._schema.requiredParams;
@@ -3176,30 +3332,30 @@ export class HttpServer {
3176
3332
  return {
3177
3333
  content: [
3178
3334
  {
3179
- type: 'text',
3335
+ type: "text",
3180
3336
  text: JSON.stringify({
3181
3337
  query: query || undefined,
3182
3338
  server_name: serverNameFilter || undefined,
3183
3339
  count: results.length,
3184
3340
  autoDescribed,
3185
- tools: results.slice(0, 50)
3186
- }, null, 2)
3187
- }
3188
- ]
3341
+ tools: results.slice(0, 50),
3342
+ }, null, 2),
3343
+ },
3344
+ ],
3189
3345
  };
3190
3346
  }
3191
3347
  catch (error) {
3192
3348
  throw new Error(`Failed to search tools: ${error instanceof Error ? error.message : String(error)}`);
3193
3349
  }
3194
3350
  }
3195
- case 'describe_tool': {
3351
+ case "describe_tool": {
3196
3352
  try {
3197
3353
  const serverName = callArgs?.server_name;
3198
3354
  const toolName = callArgs?.tool_name;
3199
3355
  // RAYCAST DEBUG: Log what client is requesting
3200
3356
  console.log(`[describe_tool] REQUEST: server="${serverName}" tool="${toolName}"`);
3201
3357
  if (!serverName || !toolName) {
3202
- throw new InvalidParamsError('server_name and tool_name are required');
3358
+ throw new InvalidParamsError("server_name and tool_name are required");
3203
3359
  }
3204
3360
  // Get server config
3205
3361
  const allServers = this.configLoader.getAllServers();
@@ -3209,15 +3365,15 @@ export class HttpServer {
3209
3365
  }
3210
3366
  // Discover tools from the server
3211
3367
  const toolSchemas = await this.serverManager.discoverToolSchemas(serverName, serverConfig);
3212
- const toolSchema = toolSchemas.find(t => t.name === toolName);
3368
+ const toolSchema = toolSchemas.find((t) => t.name === toolName);
3213
3369
  if (!toolSchema) {
3214
- const availableTools = toolSchemas.map(t => t.name).join(', ');
3370
+ const availableTools = toolSchemas.map((t) => t.name).join(", ");
3215
3371
  throw new Error(`Tool '${toolName}' not found in server '${serverName}'.\n` +
3216
3372
  `Available tools: ${availableTools}\n` +
3217
3373
  `Use search_tools with a keyword to find tools across all servers.`);
3218
3374
  }
3219
3375
  // v1.4.0: Validate schema and collect hints
3220
- const { SchemaValidator } = await import('./schema-validator.js');
3376
+ const { SchemaValidator } = await import("./schema-validator.js");
3221
3377
  const validator = new SchemaValidator();
3222
3378
  const validationResult = validator.validateToolSchema(toolSchema);
3223
3379
  // Generate example arguments from inputSchema
@@ -3235,44 +3391,46 @@ export class HttpServer {
3235
3391
  else if (prop.default !== undefined) {
3236
3392
  exampleArgs[key] = prop.default;
3237
3393
  }
3238
- else if (prop.type === 'string') {
3239
- exampleArgs[key] = prop.description ? `<${key}>` : 'example';
3394
+ else if (prop.type === "string") {
3395
+ exampleArgs[key] = prop.description ? `<${key}>` : "example";
3240
3396
  }
3241
- else if (prop.type === 'number') {
3397
+ else if (prop.type === "number") {
3242
3398
  exampleArgs[key] = 10;
3243
3399
  }
3244
- else if (prop.type === 'boolean') {
3400
+ else if (prop.type === "boolean") {
3245
3401
  exampleArgs[key] = true;
3246
3402
  }
3247
- else if (prop.type === 'array') {
3403
+ else if (prop.type === "array") {
3248
3404
  exampleArgs[key] = [];
3249
3405
  }
3250
- else if (prop.type === 'object') {
3406
+ else if (prop.type === "object") {
3251
3407
  exampleArgs[key] = {};
3252
3408
  }
3253
3409
  }
3254
3410
  }
3255
3411
  // Calculate optional params (all params not in required array)
3256
3412
  const allParams = properties ? Object.keys(properties) : [];
3257
- const optionalParams = allParams.filter(p => !(requiredArray || []).includes(p));
3413
+ const optionalParams = allParams.filter((p) => !(requiredArray || []).includes(p));
3258
3414
  // Build tool object matching search_tools auto-describe format for Raycast compatibility
3259
3415
  const toolObj = {
3260
3416
  server: serverName,
3261
3417
  tool: toolName,
3262
3418
  name: `${serverName}-${toolName}`,
3263
- description: toolSchema.description || '',
3419
+ description: toolSchema.description || "",
3264
3420
  inputSchema: toolSchema.inputSchema || {},
3265
3421
  requiredParams: requiredArray || [],
3266
3422
  optionalParams: optionalParams,
3267
3423
  example: {
3268
3424
  server_name: serverName,
3269
3425
  tool_name: toolName,
3270
- arguments: exampleArgs
3271
- }
3426
+ arguments: exampleArgs,
3427
+ },
3272
3428
  };
3273
3429
  // Anthropic Advanced Tool Use: Include tool use examples if available
3274
3430
  // https://www.anthropic.com/engineering/advanced-tool-use
3275
- if (toolSchema.inputExamples && Array.isArray(toolSchema.inputExamples) && toolSchema.inputExamples.length > 0) {
3431
+ if (toolSchema.inputExamples &&
3432
+ Array.isArray(toolSchema.inputExamples) &&
3433
+ toolSchema.inputExamples.length > 0) {
3276
3434
  toolObj.inputExamples = toolSchema.inputExamples;
3277
3435
  }
3278
3436
  // Phase 2: Include safety annotations if available
@@ -3284,17 +3442,38 @@ export class HttpServer {
3284
3442
  server: serverName,
3285
3443
  tools: [toolObj],
3286
3444
  count: 1,
3287
- detailLevel: 'full' // Always full for describe_tool
3445
+ detailLevel: "full", // Always full for describe_tool
3288
3446
  };
3289
3447
  // RAYCAST DEBUG: Log what schema is returned
3290
3448
  console.log(`[describe_tool] RESPONSE: ${serverName}:${toolName} - requiredParams=${JSON.stringify(requiredArray || [])} - hasInputSchema=${!!toolSchema.inputSchema}`);
3449
+ // Check if TOON format is enabled
3450
+ const toonConfig = this.configLoader.getConfig()?.toon;
3451
+ const useToon = toonConfig?.enabled &&
3452
+ (toonConfig?.format === "toon" ||
3453
+ (toonConfig?.format === "auto" && this.isLLMClient()));
3454
+ let responseText;
3455
+ if (useToon) {
3456
+ // Format as TOON (Token-Optimized Object Notation)
3457
+ const toonOptions = {
3458
+ includeDescriptions: toonConfig?.includeDescriptions ?? true,
3459
+ includeTypes: toonConfig?.includeTypes ?? true,
3460
+ includeExamples: toonConfig?.includeExamples ?? true,
3461
+ maxDescriptionLength: toonConfig?.maxDescriptionLength ?? 100,
3462
+ };
3463
+ responseText = formatDescribeToolResponse(serverName, toolSchema, toonOptions);
3464
+ console.log(`[describe_tool] Using TOON format (~${toonConfig?.format === "toon" ? "50%" : "auto-detected"} token savings)`);
3465
+ }
3466
+ else {
3467
+ // Default JSON format
3468
+ responseText = JSON.stringify(responseObj, null, 2);
3469
+ }
3291
3470
  return {
3292
3471
  content: [
3293
3472
  {
3294
- type: 'text',
3295
- text: JSON.stringify(responseObj, null, 2)
3296
- }
3297
- ]
3473
+ type: "text",
3474
+ text: responseText,
3475
+ },
3476
+ ],
3298
3477
  };
3299
3478
  }
3300
3479
  catch (error) {
@@ -3530,8 +3709,8 @@ export class HttpServer {
3530
3709
  default: {
3531
3710
  // Handle base server tools in format "server-toolName"
3532
3711
  // Support hyphenated server names like "duckduckgo-mcp"
3533
- if (name.includes('-')) {
3534
- const lastHyphenIndex = name.lastIndexOf('-');
3712
+ if (name.includes("-")) {
3713
+ const lastHyphenIndex = name.lastIndexOf("-");
3535
3714
  if (lastHyphenIndex === -1) {
3536
3715
  throw new InvalidParamsError(`Invalid tool name format: ${name}`);
3537
3716
  }
@@ -3569,30 +3748,32 @@ export class HttpServer {
3569
3748
  const args2 = args || {};
3570
3749
  console.log(`[MetaLink] direct tool call: ${name} with args ${JSON.stringify(args2)}`);
3571
3750
  // Phase 3: Special handling for memory-search_nodes - use fallback directly
3572
- if (serverName === 'memory' && toolName === 'search_nodes') {
3751
+ if (serverName === "memory" && toolName === "search_nodes") {
3573
3752
  console.log(`[MetaLink] Intercepting memory-search_nodes - using read_graph with client-side filtering`);
3574
3753
  try {
3575
- const graphResult = await this.serverManager.callTool('memory', 'read_graph', {});
3576
- const query = (args2.query || '').toLowerCase();
3754
+ const graphResult = await this.serverManager.callTool("memory", "read_graph", {});
3755
+ const query = (args2.query || "").toLowerCase();
3577
3756
  // Parse the nested response
3578
- if (graphResult && typeof graphResult === 'object') {
3757
+ if (graphResult && typeof graphResult === "object") {
3579
3758
  const resultObj = graphResult;
3580
3759
  if (resultObj.content && Array.isArray(resultObj.content)) {
3581
3760
  const textContent = resultObj.content[0]?.text;
3582
3761
  if (textContent) {
3583
3762
  const graphData = JSON.parse(textContent);
3584
3763
  // Filter entities by query
3585
- const filteredEntities = graphData.entities?.filter((e) => query === '' ||
3764
+ const filteredEntities = graphData.entities?.filter((e) => query === "" ||
3586
3765
  e.name.toLowerCase().includes(query) ||
3587
3766
  e.observations?.some((obs) => obs.toLowerCase().includes(query))) || [];
3588
3767
  return {
3589
- content: [{
3590
- type: 'text',
3768
+ content: [
3769
+ {
3770
+ type: "text",
3591
3771
  text: JSON.stringify({
3592
3772
  entities: filteredEntities,
3593
- relations: graphData.relations || []
3594
- }, null, 2)
3595
- }]
3773
+ relations: graphData.relations || [],
3774
+ }, null, 2),
3775
+ },
3776
+ ],
3596
3777
  };
3597
3778
  }
3598
3779
  }
@@ -3612,17 +3793,25 @@ export class HttpServer {
3612
3793
  // Enhanced error with inputSchema for debugging
3613
3794
  const errorMsg = `Tool execution failed for ${name}: ${toolError instanceof Error ? toolError.message : String(toolError)}`;
3614
3795
  // Check if error is parameter-related
3615
- const errorStr = (toolError instanceof Error ? toolError.message : String(toolError)).toLowerCase();
3616
- const isParamError = errorStr.includes('null') || errorStr.includes('undefined') ||
3617
- errorStr.includes('required') || errorStr.includes('missing');
3796
+ const errorStr = (toolError instanceof Error
3797
+ ? toolError.message
3798
+ : String(toolError)).toLowerCase();
3799
+ const isParamError = errorStr.includes("null") ||
3800
+ errorStr.includes("undefined") ||
3801
+ errorStr.includes("required") ||
3802
+ errorStr.includes("missing");
3618
3803
  if (isParamError && toolSchema) {
3619
3804
  const inputSchema = toolSchema.inputSchema;
3620
3805
  if (inputSchema) {
3621
3806
  const requiredParams = inputSchema.required || [];
3622
3807
  const availableParams = Object.keys(inputSchema.properties || {});
3623
- const optionalParams = availableParams.filter(p => !requiredParams.includes(p));
3624
- const requiredList = requiredParams.length > 0 ? requiredParams.join(', ') : 'none';
3625
- const optionalList = optionalParams.length > 0 ? optionalParams.join(', ') : 'none';
3808
+ const optionalParams = availableParams.filter((p) => !requiredParams.includes(p));
3809
+ const requiredList = requiredParams.length > 0
3810
+ ? requiredParams.join(", ")
3811
+ : "none";
3812
+ const optionalList = optionalParams.length > 0
3813
+ ? optionalParams.join(", ")
3814
+ : "none";
3626
3815
  const hint = `\n\n💡 Hint: This tool expects:\n` +
3627
3816
  ` Required: ${requiredList}\n` +
3628
3817
  ` Optional: ${optionalList}\n` +
@@ -3656,7 +3845,7 @@ export class HttpServer {
3656
3845
  await this.serverManager.ensureServerStarted(server.name, server);
3657
3846
  const tools = this.serverManager.getServerTools(server.name);
3658
3847
  discoveredToolCount = tools.length;
3659
- console.log(`[AddServer] Discovered ${discoveredToolCount} tools for '${server.name}': ${tools.map(t => t.name).join(', ')}`);
3848
+ console.log(`[AddServer] Discovered ${discoveredToolCount} tools for '${server.name}': ${tools.map((t) => t.name).join(", ")}`);
3660
3849
  }
3661
3850
  catch (discoveryError) {
3662
3851
  console.warn(`[AddServer] Tool discovery failed for '${server.name}' (non-fatal):`, discoveryError);
@@ -3664,7 +3853,7 @@ export class HttpServer {
3664
3853
  }
3665
3854
  // Broadcast server:added event
3666
3855
  this.broadcastEvent({
3667
- type: 'server:added',
3856
+ type: "server:added",
3668
3857
  data: {
3669
3858
  server,
3670
3859
  timestamp: Date.now(),
@@ -3678,7 +3867,7 @@ export class HttpServer {
3678
3867
  });
3679
3868
  }
3680
3869
  catch (error) {
3681
- const message = error instanceof Error ? error.message : 'Failed to add server';
3870
+ const message = error instanceof Error ? error.message : "Failed to add server";
3682
3871
  res.status(400).json({
3683
3872
  error: message,
3684
3873
  });
@@ -3700,9 +3889,9 @@ export class HttpServer {
3700
3889
  }
3701
3890
  // Check if server is running and require force if so
3702
3891
  const status = this.serverManager.getServerStatus(name);
3703
- if (status?.status === 'running') {
3892
+ if (status?.status === "running") {
3704
3893
  // Check for force flag in query params
3705
- const force = req.query.force === 'true';
3894
+ const force = req.query.force === "true";
3706
3895
  if (!force) {
3707
3896
  res.status(409).json({
3708
3897
  error: `Server '${name}' is currently running. Use ?force=true to remove it anyway.`,
@@ -3722,7 +3911,7 @@ export class HttpServer {
3722
3911
  await this.configLoader.removeServerFromRegistry(name, { timeout: 5000 });
3723
3912
  // Broadcast server:removed event
3724
3913
  this.broadcastEvent({
3725
- type: 'server:removed',
3914
+ type: "server:removed",
3726
3915
  data: {
3727
3916
  name,
3728
3917
  timestamp: Date.now(),
@@ -3734,7 +3923,7 @@ export class HttpServer {
3734
3923
  });
3735
3924
  }
3736
3925
  catch (error) {
3737
- const message = error instanceof Error ? error.message : 'Failed to remove server';
3926
+ const message = error instanceof Error ? error.message : "Failed to remove server";
3738
3927
  res.status(400).json({
3739
3928
  error: message,
3740
3929
  });
@@ -3745,7 +3934,7 @@ export class HttpServer {
3745
3934
  */
3746
3935
  async validateServer(req, res) {
3747
3936
  try {
3748
- const { RegistryManager } = await import('../config/registry.js');
3937
+ const { RegistryManager } = await import("../config/registry.js");
3749
3938
  const registry = new RegistryManager();
3750
3939
  // Validate configuration
3751
3940
  const validation = await registry.validateServer(req.body, {
@@ -3754,7 +3943,7 @@ export class HttpServer {
3754
3943
  if (validation.valid) {
3755
3944
  res.json({
3756
3945
  valid: true,
3757
- message: 'Server configuration is valid',
3946
+ message: "Server configuration is valid",
3758
3947
  });
3759
3948
  }
3760
3949
  else {
@@ -3765,7 +3954,7 @@ export class HttpServer {
3765
3954
  }
3766
3955
  }
3767
3956
  catch (error) {
3768
- const message = error instanceof Error ? error.message : 'Validation error';
3957
+ const message = error instanceof Error ? error.message : "Validation error";
3769
3958
  res.status(400).json({
3770
3959
  error: message,
3771
3960
  });
@@ -3786,7 +3975,7 @@ export class HttpServer {
3786
3975
  // NOTE: Intentionally NOT resolving - keeps event loop alive for background mode
3787
3976
  });
3788
3977
  // Handle server errors
3789
- this.server.on('error', (err) => {
3978
+ this.server.on("error", (err) => {
3790
3979
  reject(err);
3791
3980
  });
3792
3981
  });
@@ -3808,14 +3997,15 @@ export class HttpServer {
3808
3997
  }
3809
3998
  notifyToolsListChanged() {
3810
3999
  const now = Date.now();
3811
- if (now - this.lastToolsListNotification < this.TOOLS_LIST_NOTIFICATION_THROTTLE_MS) {
3812
- console.log('[MCP] Throttling tools/list_changed notification');
4000
+ if (now - this.lastToolsListNotification <
4001
+ this.TOOLS_LIST_NOTIFICATION_THROTTLE_MS) {
4002
+ console.log("[MCP] Throttling tools/list_changed notification");
3813
4003
  return;
3814
4004
  }
3815
4005
  this.lastToolsListNotification = now;
3816
4006
  const notification = {
3817
- jsonrpc: '2.0',
3818
- method: 'notifications/tools/list_changed'
4007
+ jsonrpc: "2.0",
4008
+ method: "notifications/tools/list_changed",
3819
4009
  };
3820
4010
  let sentCount = 0;
3821
4011
  for (const [sessionId, conn] of this.sseConnections) {
@@ -3846,14 +4036,14 @@ export class HttpServer {
3846
4036
  console.log(`[CALLBACK] Posting response to ${session.callbackUrl}`);
3847
4037
  // POST response to callback URL
3848
4038
  const callbackResponse = await fetch(session.callbackUrl, {
3849
- method: 'POST',
4039
+ method: "POST",
3850
4040
  headers: {
3851
- 'Content-Type': 'application/json',
3852
- 'Mcp-Session-Id': session.id,
3853
- 'MCP-Protocol-Version': MCP_PROTOCOL_VERSION
4041
+ "Content-Type": "application/json",
4042
+ "Mcp-Session-Id": session.id,
4043
+ "MCP-Protocol-Version": MCP_PROTOCOL_VERSION,
3854
4044
  },
3855
4045
  body: JSON.stringify(response),
3856
- signal: AbortSignal.timeout(10000) // 10 second timeout
4046
+ signal: AbortSignal.timeout(10000), // 10 second timeout
3857
4047
  });
3858
4048
  if (!callbackResponse.ok) {
3859
4049
  console.warn(`[CALLBACK] Warning: callback returned ${callbackResponse.status}`);
@@ -3873,7 +4063,7 @@ export class HttpServer {
3873
4063
  */
3874
4064
  async getSafetyRules(req, res) {
3875
4065
  try {
3876
- const includePatterns = req.query.include_patterns === 'true';
4066
+ const includePatterns = req.query.include_patterns === "true";
3877
4067
  const rules = this.configLoader.getToolSafetyRules();
3878
4068
  const response = {
3879
4069
  safeToolOverrides: rules.safeToolOverrides || [],
@@ -3889,7 +4079,7 @@ export class HttpServer {
3889
4079
  }
3890
4080
  catch (error) {
3891
4081
  res.status(500).json({
3892
- error: error instanceof Error ? error.message : 'Failed to get safety rules',
4082
+ error: error instanceof Error ? error.message : "Failed to get safety rules",
3893
4083
  });
3894
4084
  }
3895
4085
  }
@@ -3901,7 +4091,7 @@ export class HttpServer {
3901
4091
  const { server, tool } = req.params;
3902
4092
  if (!server || !tool) {
3903
4093
  res.status(400).json({
3904
- error: 'Missing required parameters: server and tool',
4094
+ error: "Missing required parameters: server and tool",
3905
4095
  });
3906
4096
  return;
3907
4097
  }
@@ -3912,12 +4102,14 @@ export class HttpServer {
3912
4102
  fullName: `${server}:${tool}`,
3913
4103
  safety: result.safety,
3914
4104
  reason: result.reason,
3915
- requiresConfirmation: result.safety === 'risky',
4105
+ requiresConfirmation: result.safety === "risky",
3916
4106
  });
3917
4107
  }
3918
4108
  catch (error) {
3919
4109
  res.status(500).json({
3920
- error: error instanceof Error ? error.message : 'Failed to check tool safety',
4110
+ error: error instanceof Error
4111
+ ? error.message
4112
+ : "Failed to check tool safety",
3921
4113
  });
3922
4114
  }
3923
4115
  }
@@ -3927,9 +4119,9 @@ export class HttpServer {
3927
4119
  async addSafeToolOverride(req, res) {
3928
4120
  try {
3929
4121
  const { tool, reason } = req.body;
3930
- if (!tool || typeof tool !== 'string') {
4122
+ if (!tool || typeof tool !== "string") {
3931
4123
  res.status(400).json({
3932
- error: 'Missing or invalid required parameter: tool (string)',
4124
+ error: "Missing or invalid required parameter: tool (string)",
3933
4125
  });
3934
4126
  return;
3935
4127
  }
@@ -3937,7 +4129,7 @@ export class HttpServer {
3937
4129
  const toolPattern = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_*-]+$/;
3938
4130
  if (!toolPattern.test(tool)) {
3939
4131
  res.status(400).json({
3940
- error: 'Invalid tool format. Expected: server:tool or server:*',
4132
+ error: "Invalid tool format. Expected: server:tool or server:*",
3941
4133
  });
3942
4134
  return;
3943
4135
  }
@@ -3951,7 +4143,9 @@ export class HttpServer {
3951
4143
  }
3952
4144
  catch (error) {
3953
4145
  res.status(500).json({
3954
- error: error instanceof Error ? error.message : 'Failed to add safe tool override',
4146
+ error: error instanceof Error
4147
+ ? error.message
4148
+ : "Failed to add safe tool override",
3955
4149
  });
3956
4150
  }
3957
4151
  }
@@ -3961,9 +4155,9 @@ export class HttpServer {
3961
4155
  async addRiskyToolOverride(req, res) {
3962
4156
  try {
3963
4157
  const { tool, reason } = req.body;
3964
- if (!tool || typeof tool !== 'string') {
4158
+ if (!tool || typeof tool !== "string") {
3965
4159
  res.status(400).json({
3966
- error: 'Missing or invalid required parameter: tool (string)',
4160
+ error: "Missing or invalid required parameter: tool (string)",
3967
4161
  });
3968
4162
  return;
3969
4163
  }
@@ -3971,7 +4165,7 @@ export class HttpServer {
3971
4165
  const toolPattern = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_*-]+$/;
3972
4166
  if (!toolPattern.test(tool)) {
3973
4167
  res.status(400).json({
3974
- error: 'Invalid tool format. Expected: server:tool or server:*',
4168
+ error: "Invalid tool format. Expected: server:tool or server:*",
3975
4169
  });
3976
4170
  return;
3977
4171
  }
@@ -3985,7 +4179,9 @@ export class HttpServer {
3985
4179
  }
3986
4180
  catch (error) {
3987
4181
  res.status(500).json({
3988
- error: error instanceof Error ? error.message : 'Failed to add risky tool override',
4182
+ error: error instanceof Error
4183
+ ? error.message
4184
+ : "Failed to add risky tool override",
3989
4185
  });
3990
4186
  }
3991
4187
  }
@@ -3995,9 +4191,9 @@ export class HttpServer {
3995
4191
  async addSafePattern(req, res) {
3996
4192
  try {
3997
4193
  const { pattern, reason } = req.body;
3998
- if (!pattern || typeof pattern !== 'string') {
4194
+ if (!pattern || typeof pattern !== "string") {
3999
4195
  res.status(400).json({
4000
- error: 'Missing or invalid required parameter: pattern (string)',
4196
+ error: "Missing or invalid required parameter: pattern (string)",
4001
4197
  });
4002
4198
  return;
4003
4199
  }
@@ -4021,7 +4217,7 @@ export class HttpServer {
4021
4217
  }
4022
4218
  catch (error) {
4023
4219
  res.status(500).json({
4024
- error: error instanceof Error ? error.message : 'Failed to add safe pattern',
4220
+ error: error instanceof Error ? error.message : "Failed to add safe pattern",
4025
4221
  });
4026
4222
  }
4027
4223
  }
@@ -4031,9 +4227,9 @@ export class HttpServer {
4031
4227
  async addRiskyPattern(req, res) {
4032
4228
  try {
4033
4229
  const { pattern, reason } = req.body;
4034
- if (!pattern || typeof pattern !== 'string') {
4230
+ if (!pattern || typeof pattern !== "string") {
4035
4231
  res.status(400).json({
4036
- error: 'Missing or invalid required parameter: pattern (string)',
4232
+ error: "Missing or invalid required parameter: pattern (string)",
4037
4233
  });
4038
4234
  return;
4039
4235
  }
@@ -4057,7 +4253,9 @@ export class HttpServer {
4057
4253
  }
4058
4254
  catch (error) {
4059
4255
  res.status(500).json({
4060
- error: error instanceof Error ? error.message : 'Failed to add risky pattern',
4256
+ error: error instanceof Error
4257
+ ? error.message
4258
+ : "Failed to add risky pattern",
4061
4259
  });
4062
4260
  }
4063
4261
  }
@@ -4069,7 +4267,7 @@ export class HttpServer {
4069
4267
  const { rule } = req.params;
4070
4268
  if (!rule) {
4071
4269
  res.status(400).json({
4072
- error: 'Missing required parameter: rule',
4270
+ error: "Missing required parameter: rule",
4073
4271
  });
4074
4272
  return;
4075
4273
  }
@@ -4078,26 +4276,26 @@ export class HttpServer {
4078
4276
  // Auto-detect rule type
4079
4277
  const rules = this.configLoader.getToolSafetyRules();
4080
4278
  let removed = false;
4081
- let type = '';
4279
+ let type = "";
4082
4280
  if (rules.safeToolOverrides?.includes(decodedRule)) {
4083
4281
  await this.configLoader.removeSafeToolOverride(decodedRule);
4084
4282
  removed = true;
4085
- type = 'safe_tool_override';
4283
+ type = "safe_tool_override";
4086
4284
  }
4087
4285
  else if (rules.riskyToolOverrides?.includes(decodedRule)) {
4088
4286
  await this.configLoader.removeRiskyToolOverride(decodedRule);
4089
4287
  removed = true;
4090
- type = 'risky_tool_override';
4288
+ type = "risky_tool_override";
4091
4289
  }
4092
4290
  else if (rules.safePatterns?.includes(decodedRule)) {
4093
4291
  await this.configLoader.removeSafePattern(decodedRule);
4094
4292
  removed = true;
4095
- type = 'safe_pattern';
4293
+ type = "safe_pattern";
4096
4294
  }
4097
4295
  else if (rules.riskyPatterns?.includes(decodedRule)) {
4098
4296
  await this.configLoader.removeRiskyPattern(decodedRule);
4099
4297
  removed = true;
4100
- type = 'risky_pattern';
4298
+ type = "risky_pattern";
4101
4299
  }
4102
4300
  if (!removed) {
4103
4301
  res.status(404).json({
@@ -4114,7 +4312,7 @@ export class HttpServer {
4114
4312
  }
4115
4313
  catch (error) {
4116
4314
  res.status(500).json({
4117
- error: error instanceof Error ? error.message : 'Failed to remove rule',
4315
+ error: error instanceof Error ? error.message : "Failed to remove rule",
4118
4316
  });
4119
4317
  }
4120
4318
  }
@@ -4126,19 +4324,21 @@ export class HttpServer {
4126
4324
  const { force } = req.body;
4127
4325
  if (!force) {
4128
4326
  res.status(400).json({
4129
- error: 'Reset requires explicit confirmation. Set force: true',
4327
+ error: "Reset requires explicit confirmation. Set force: true",
4130
4328
  });
4131
4329
  return;
4132
4330
  }
4133
4331
  await this.configLoader.resetToDefaults();
4134
4332
  res.json({
4135
4333
  success: true,
4136
- message: 'Reset all safety rules to defaults',
4334
+ message: "Reset all safety rules to defaults",
4137
4335
  });
4138
4336
  }
4139
4337
  catch (error) {
4140
4338
  res.status(500).json({
4141
- error: error instanceof Error ? error.message : 'Failed to reset safety rules',
4339
+ error: error instanceof Error
4340
+ ? error.message
4341
+ : "Failed to reset safety rules",
4142
4342
  });
4143
4343
  }
4144
4344
  }
@@ -4148,30 +4348,52 @@ export class HttpServer {
4148
4348
  async importSafetyRules(req, res) {
4149
4349
  try {
4150
4350
  const { rules, merge = true } = req.body; // Default to merge mode
4151
- if (!rules || typeof rules !== 'object') {
4351
+ if (!rules || typeof rules !== "object") {
4152
4352
  res.status(400).json({
4153
- error: 'Missing or invalid required parameter: rules (object)',
4353
+ error: "Missing or invalid required parameter: rules (object)",
4154
4354
  });
4155
4355
  return;
4156
4356
  }
4157
4357
  // Get current rules to preserve argumentInspectionRules
4158
4358
  const currentRules = this.configLoader.getToolSafetyRules();
4159
4359
  // Extract arrays from the rules object
4160
- const safeToolOverrides = Array.isArray(rules.safeToolOverrides) ? rules.safeToolOverrides : [];
4161
- const riskyToolOverrides = Array.isArray(rules.riskyToolOverrides) ? rules.riskyToolOverrides : [];
4162
- const safePatterns = Array.isArray(rules.safePatterns) ? rules.safePatterns : [];
4163
- const riskyPatterns = Array.isArray(rules.riskyPatterns) ? rules.riskyPatterns : [];
4360
+ const safeToolOverrides = Array.isArray(rules.safeToolOverrides)
4361
+ ? rules.safeToolOverrides
4362
+ : [];
4363
+ const riskyToolOverrides = Array.isArray(rules.riskyToolOverrides)
4364
+ ? rules.riskyToolOverrides
4365
+ : [];
4366
+ const safePatterns = Array.isArray(rules.safePatterns)
4367
+ ? rules.safePatterns
4368
+ : [];
4369
+ const riskyPatterns = Array.isArray(rules.riskyPatterns)
4370
+ ? rules.riskyPatterns
4371
+ : [];
4164
4372
  // Preserve argumentInspectionRules unless explicitly provided in import
4165
4373
  const argumentInspectionRules = Array.isArray(rules.argumentInspectionRules)
4166
4374
  ? rules.argumentInspectionRules
4167
- : (currentRules.argumentInspectionRules || []);
4375
+ : currentRules.argumentInspectionRules || [];
4168
4376
  if (merge) {
4169
4377
  // Merge mode: Add new rules to existing ones (default)
4170
4378
  // Merge tool overrides (avoid duplicates)
4171
- const mergedSafeTools = [...new Set([...(currentRules.safeToolOverrides || []), ...safeToolOverrides])];
4172
- const mergedRiskyTools = [...new Set([...(currentRules.riskyToolOverrides || []), ...riskyToolOverrides])];
4173
- const mergedSafePatterns = [...new Set([...(currentRules.safePatterns || []), ...safePatterns])];
4174
- const mergedRiskyPatterns = [...new Set([...(currentRules.riskyPatterns || []), ...riskyPatterns])];
4379
+ const mergedSafeTools = [
4380
+ ...new Set([
4381
+ ...(currentRules.safeToolOverrides || []),
4382
+ ...safeToolOverrides,
4383
+ ]),
4384
+ ];
4385
+ const mergedRiskyTools = [
4386
+ ...new Set([
4387
+ ...(currentRules.riskyToolOverrides || []),
4388
+ ...riskyToolOverrides,
4389
+ ]),
4390
+ ];
4391
+ const mergedSafePatterns = [
4392
+ ...new Set([...(currentRules.safePatterns || []), ...safePatterns]),
4393
+ ];
4394
+ const mergedRiskyPatterns = [
4395
+ ...new Set([...(currentRules.riskyPatterns || []), ...riskyPatterns]),
4396
+ ];
4175
4397
  await this.configLoader.setToolSafetyRules({
4176
4398
  safeToolOverrides: mergedSafeTools,
4177
4399
  riskyToolOverrides: mergedRiskyTools,
@@ -4181,7 +4403,7 @@ export class HttpServer {
4181
4403
  });
4182
4404
  res.json({
4183
4405
  success: true,
4184
- message: 'Safety rules imported and merged successfully',
4406
+ message: "Safety rules imported and merged successfully",
4185
4407
  imported: {
4186
4408
  safeTools: mergedSafeTools.length,
4187
4409
  riskyTools: mergedRiskyTools.length,
@@ -4201,7 +4423,7 @@ export class HttpServer {
4201
4423
  });
4202
4424
  res.json({
4203
4425
  success: true,
4204
- message: 'Safety rules imported successfully (replace mode)',
4426
+ message: "Safety rules imported successfully (replace mode)",
4205
4427
  imported: {
4206
4428
  safeTools: safeToolOverrides.length,
4207
4429
  riskyTools: riskyToolOverrides.length,
@@ -4213,7 +4435,9 @@ export class HttpServer {
4213
4435
  }
4214
4436
  catch (error) {
4215
4437
  res.status(500).json({
4216
- error: error instanceof Error ? error.message : 'Failed to import safety rules',
4438
+ error: error instanceof Error
4439
+ ? error.message
4440
+ : "Failed to import safety rules",
4217
4441
  });
4218
4442
  }
4219
4443
  }
@@ -4228,10 +4452,10 @@ export class HttpServer {
4228
4452
  apiMetrics: globalMetrics.getApiMetrics(),
4229
4453
  };
4230
4454
  await this.metricsPersistence.save(finalMetrics);
4231
- console.log('[MetricsPersistence] Saved final metrics on shutdown');
4455
+ console.log("[MetricsPersistence] Saved final metrics on shutdown");
4232
4456
  }
4233
4457
  catch (error) {
4234
- console.error('[MetricsPersistence] Failed to save metrics on shutdown:', error);
4458
+ console.error("[MetricsPersistence] Failed to save metrics on shutdown:", error);
4235
4459
  }
4236
4460
  await this.serverManager.cleanup();
4237
4461
  for (const client of this.eventClients) {