@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.
- package/README.md +58 -107
- package/package.json +46 -28
- package/packages/core/dist/config/defaults.d.ts +1 -1
- package/packages/core/dist/config/defaults.d.ts.map +1 -1
- package/packages/core/dist/config/defaults.js +86 -48
- package/packages/core/dist/config/defaults.js.map +1 -1
- package/packages/core/dist/config/loader.d.ts +1 -1
- package/packages/core/dist/config/loader.d.ts.map +1 -1
- package/packages/core/dist/config/loader.js +151 -131
- package/packages/core/dist/config/loader.js.map +1 -1
- package/packages/core/dist/config/schema.d.ts +94 -2
- package/packages/core/dist/config/schema.d.ts.map +1 -1
- package/packages/core/dist/config/schema.js +124 -52
- package/packages/core/dist/config/schema.js.map +1 -1
- package/packages/core/dist/server/http.d.ts +7 -2
- package/packages/core/dist/server/http.d.ts.map +1 -1
- package/packages/core/dist/server/http.js +941 -717
- package/packages/core/dist/server/http.js.map +1 -1
- package/packages/core/dist/utils/toon-formatter.d.ts +88 -0
- package/packages/core/dist/utils/toon-formatter.d.ts.map +1 -0
- package/packages/core/dist/utils/toon-formatter.js +239 -0
- package/packages/core/dist/utils/toon-formatter.js.map +1 -0
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP Server - Express-based API for MetaLink
|
|
3
3
|
*/
|
|
4
|
-
import express from
|
|
5
|
-
import rateLimit from
|
|
6
|
-
import path from
|
|
7
|
-
import { fileURLToPath } from
|
|
8
|
-
import fs from
|
|
9
|
-
import { randomUUID, createHmac } from
|
|
10
|
-
import { ServerManager } from
|
|
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
|
|
13
|
-
import { globalMetrics, MetricsPersistence, MetricsAggregator } from
|
|
14
|
-
import { logger, generateRequestId, getOrCreateRequestId } from
|
|
15
|
-
import { getPromptsList, getPrompt } from
|
|
16
|
-
import { getResourcesList, getResourceTemplatesList, readResource } from
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
99
|
-
this.sessionPersistPath = path.join(configDir,
|
|
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(
|
|
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,
|
|
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) +
|
|
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:
|
|
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:
|
|
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[
|
|
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(
|
|
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[
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 = [
|
|
298
|
+
const allowedHosts = ["localhost", "127.0.0.1", "[::1]", "::1"];
|
|
288
299
|
if (!allowedHosts.includes(hostname)) {
|
|
289
|
-
logger.warn(
|
|
300
|
+
logger.warn("Rejected request from unauthorized origin", {
|
|
290
301
|
requestId,
|
|
291
302
|
origin,
|
|
292
303
|
hostname,
|
|
293
|
-
security:
|
|
304
|
+
security: "dns_rebinding_protection",
|
|
294
305
|
});
|
|
295
306
|
res.status(403).json({
|
|
296
|
-
jsonrpc:
|
|
307
|
+
jsonrpc: "2.0",
|
|
297
308
|
error: {
|
|
298
309
|
code: -32600,
|
|
299
|
-
message:
|
|
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(
|
|
318
|
+
logger.warn("Rejected request with malformed origin", {
|
|
308
319
|
requestId,
|
|
309
320
|
origin,
|
|
310
|
-
error: err instanceof Error ? err.message :
|
|
311
|
-
security:
|
|
321
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
322
|
+
security: "dns_rebinding_protection",
|
|
312
323
|
});
|
|
313
324
|
res.status(403).json({
|
|
314
|
-
jsonrpc:
|
|
325
|
+
jsonrpc: "2.0",
|
|
315
326
|
error: {
|
|
316
327
|
code: -32600,
|
|
317
|
-
message:
|
|
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(
|
|
327
|
-
this.app.use(
|
|
328
|
-
this.app.use(
|
|
329
|
-
this.app.use(
|
|
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 &&
|
|
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 &&
|
|
340
|
-
|
|
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(
|
|
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:
|
|
381
|
+
res.status(401).json({ error: "Missing Authorization header" });
|
|
368
382
|
return;
|
|
369
383
|
}
|
|
370
|
-
const [scheme] = authHeader.split(
|
|
371
|
-
if (scheme !==
|
|
372
|
-
res.status(401).json({ error:
|
|
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(
|
|
379
|
-
res.status(500).json({ error:
|
|
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(
|
|
383
|
-
.update(
|
|
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:
|
|
400
|
+
res.status(401).json({ error: "Invalid authentication token" });
|
|
386
401
|
return;
|
|
387
402
|
}
|
|
388
403
|
req.authenticated = true;
|
|
389
|
-
req.userId =
|
|
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(
|
|
399
|
-
api.get(
|
|
400
|
-
api.post(
|
|
401
|
-
api.post(
|
|
402
|
-
api.post(
|
|
403
|
-
api.post(
|
|
404
|
-
api.post(
|
|
405
|
-
api.get(
|
|
406
|
-
api.get(
|
|
407
|
-
api.post(
|
|
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(
|
|
410
|
-
api.post(
|
|
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(
|
|
413
|
-
api.put(
|
|
427
|
+
api.get("/config", this.getConfig.bind(this));
|
|
428
|
+
api.put("/config", this.updateConfig.bind(this));
|
|
414
429
|
// Registry management endpoints
|
|
415
|
-
api.post(
|
|
416
|
-
api.delete(
|
|
417
|
-
api.post(
|
|
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(
|
|
434
|
+
api.get("/health", this.healthCheck.bind(this));
|
|
420
435
|
// Version endpoint
|
|
421
|
-
api.get(
|
|
436
|
+
api.get("/version", this.getVersion.bind(this));
|
|
422
437
|
// Metrics endpoints (Phase 4 - v1.4.0)
|
|
423
|
-
api.get(
|
|
424
|
-
api.get(
|
|
425
|
-
api.get(
|
|
426
|
-
api.get(
|
|
427
|
-
api.get(
|
|
428
|
-
api.get(
|
|
429
|
-
api.get(
|
|
430
|
-
api.get(
|
|
431
|
-
api.get(
|
|
432
|
-
api.get(
|
|
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(
|
|
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(
|
|
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(
|
|
463
|
+
this.app.get("/mcp", this.handleMcpRequest.bind(this));
|
|
449
464
|
// POST endpoints for standard JSON-RPC
|
|
450
|
-
this.app.post(
|
|
465
|
+
this.app.post("/mcp", this.handleMcpRequest.bind(this));
|
|
451
466
|
// DELETE /mcp for session cleanup (per MCP spec)
|
|
452
|
-
this.app.delete(
|
|
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(
|
|
455
|
-
this.app.post(
|
|
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(
|
|
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(
|
|
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(
|
|
477
|
+
this.app.post("/rpc/rpc", this.handleMcpRequest.bind(this));
|
|
463
478
|
// Prometheus metrics endpoint (standard /metrics path for scraping)
|
|
464
|
-
this.app.get(
|
|
479
|
+
this.app.get("/metrics", this.getPrometheusMetrics.bind(this));
|
|
465
480
|
// Serve dashboard static files (if available)
|
|
466
|
-
const dashboardPath = path.join(__dirname,
|
|
481
|
+
const dashboardPath = path.join(__dirname, "../../../dashboard/dist");
|
|
467
482
|
const dashboardExists = fs.existsSync(dashboardPath);
|
|
468
|
-
const indexPath = path.join(dashboardPath,
|
|
483
|
+
const indexPath = path.join(dashboardPath, "index.html");
|
|
469
484
|
const indexExists = dashboardExists && fs.existsSync(indexPath);
|
|
470
|
-
console.log(
|
|
471
|
-
console.log(
|
|
472
|
-
console.log(
|
|
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(
|
|
476
|
-
this.app.get(
|
|
477
|
-
console.log(
|
|
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(
|
|
495
|
+
console.log("[HTTP] GET / route registered successfully");
|
|
481
496
|
}
|
|
482
497
|
else {
|
|
483
|
-
console.log(
|
|
498
|
+
console.log("[HTTP] Skipping GET / - index.html not found");
|
|
484
499
|
}
|
|
485
500
|
if (dashboardExists) {
|
|
486
|
-
console.log(
|
|
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(
|
|
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(
|
|
496
|
-
this.app.get(
|
|
497
|
-
console.log(
|
|
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(
|
|
515
|
+
console.log("[HTTP] Fallback route registered");
|
|
501
516
|
}
|
|
502
517
|
else {
|
|
503
|
-
console.log(
|
|
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(
|
|
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:
|
|
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
|
|
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,
|
|
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) +
|
|
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(
|
|
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:
|
|
589
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
575
590
|
return;
|
|
576
591
|
}
|
|
577
|
-
res.setHeader(
|
|
578
|
-
res.setHeader(
|
|
579
|
-
res.setHeader(
|
|
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(
|
|
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(
|
|
604
|
+
this.serverManager.on("server:started", (data) => {
|
|
590
605
|
this.broadcastEvent({
|
|
591
|
-
type:
|
|
606
|
+
type: "server:started",
|
|
592
607
|
data,
|
|
593
608
|
});
|
|
594
609
|
});
|
|
595
|
-
this.serverManager.on(
|
|
610
|
+
this.serverManager.on("server:stopped", (data) => {
|
|
596
611
|
this.broadcastEvent({
|
|
597
|
-
type:
|
|
612
|
+
type: "server:stopped",
|
|
598
613
|
data,
|
|
599
614
|
});
|
|
600
615
|
});
|
|
601
|
-
this.serverManager.on(
|
|
616
|
+
this.serverManager.on("server:error", (data) => {
|
|
602
617
|
this.broadcastEvent({
|
|
603
|
-
type:
|
|
618
|
+
type: "server:error",
|
|
604
619
|
data,
|
|
605
620
|
});
|
|
606
621
|
});
|
|
607
|
-
this.serverManager.on(
|
|
622
|
+
this.serverManager.on("health:check", (data) => {
|
|
608
623
|
this.broadcastEvent({
|
|
609
|
-
type:
|
|
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(
|
|
629
|
+
this.serverManager.on("server:removed", (data) => {
|
|
615
630
|
this.broadcastEvent({
|
|
616
|
-
type:
|
|
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 ===
|
|
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 ||
|
|
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 :
|
|
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 ===
|
|
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 ?
|
|
697
|
+
type: isStdio ? "stdio" : "http",
|
|
682
698
|
env: config.env || {},
|
|
683
|
-
status: (this.serverManager.getServerStatus(config.name)?.status ||
|
|
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(
|
|
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
|
|
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(
|
|
724
|
-
:
|
|
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 :
|
|
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(
|
|
747
|
-
:
|
|
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 :
|
|
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
|
|
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 :
|
|
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 :
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 ===
|
|
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 ===
|
|
955
|
+
const isStdio = serverConfig.transport === "stdio" ||
|
|
956
|
+
serverConfig.transport === undefined;
|
|
927
957
|
const baseResponse = {
|
|
928
958
|
name: serverConfig.name,
|
|
929
|
-
status: runtimeStatus?.status ||
|
|
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 :
|
|
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 !==
|
|
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 :
|
|
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 !==
|
|
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 :
|
|
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
|
|
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 !==
|
|
1031
|
-
res
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
1076
|
-
error:
|
|
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:
|
|
1084
|
-
error: error instanceof Error ? error.message :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
1129
|
-
error: error instanceof Error ? error.message :
|
|
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 :
|
|
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 :
|
|
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(
|
|
1209
|
+
res.set("Content-Type", "text/plain");
|
|
1174
1210
|
res.send(globalMetrics.exportPrometheus());
|
|
1175
1211
|
}
|
|
1176
1212
|
catch (error) {
|
|
1177
|
-
res
|
|
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 :
|
|
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({
|
|
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
|
|
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 ||
|
|
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:
|
|
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
|
|
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 ||
|
|
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:
|
|
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
|
|
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 ||
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1320
|
-
|
|
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 :
|
|
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(
|
|
1409
|
+
console.log("[MetricsPersistence] Successfully restored metrics from disk");
|
|
1354
1410
|
}
|
|
1355
1411
|
else {
|
|
1356
|
-
console.log(
|
|
1412
|
+
console.log("[MetricsPersistence] No persisted metrics found, starting fresh");
|
|
1357
1413
|
}
|
|
1358
1414
|
}
|
|
1359
1415
|
catch (error) {
|
|
1360
|
-
console.error(
|
|
1361
|
-
console.log(
|
|
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[
|
|
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(
|
|
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:
|
|
1476
|
+
protocolVersion: "2025-06-18",
|
|
1421
1477
|
});
|
|
1422
|
-
const clientName = clientInfo?.name ||
|
|
1423
|
-
const clientVersion = clientInfo?.version ||
|
|
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),
|
|
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(
|
|
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(
|
|
1576
|
+
console.log("[SessionPersistence] No persisted sessions found, starting fresh");
|
|
1521
1577
|
return;
|
|
1522
1578
|
}
|
|
1523
|
-
const content = fs.readFileSync(this.sessionPersistPath,
|
|
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 ===
|
|
1552
|
-
console.log(
|
|
1607
|
+
if (errCode === "ENOENT") {
|
|
1608
|
+
console.log("[SessionPersistence] No persisted sessions found, starting fresh");
|
|
1553
1609
|
}
|
|
1554
1610
|
else {
|
|
1555
|
-
console.error(
|
|
1556
|
-
console.log(
|
|
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[
|
|
1622
|
+
const sessionId = req?.headers["mcp-session-id"];
|
|
1567
1623
|
// Validate JSON-RPC 2.0 structure
|
|
1568
|
-
if (!request.jsonrpc || request.jsonrpc !==
|
|
1569
|
-
return [
|
|
1570
|
-
|
|
1624
|
+
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
|
|
1625
|
+
return [
|
|
1626
|
+
{
|
|
1627
|
+
jsonrpc: "2.0",
|
|
1571
1628
|
id,
|
|
1572
|
-
error: {
|
|
1573
|
-
|
|
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 ===
|
|
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 = [
|
|
1585
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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:
|
|
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?.[
|
|
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[
|
|
1647
|
-
const callbackPort = req.headers[
|
|
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
|
|
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:
|
|
1665
|
-
version
|
|
1666
|
-
}
|
|
1736
|
+
name: "metalink",
|
|
1737
|
+
version,
|
|
1738
|
+
},
|
|
1667
1739
|
};
|
|
1668
|
-
return [
|
|
1669
|
-
|
|
1740
|
+
return [
|
|
1741
|
+
{
|
|
1742
|
+
jsonrpc: "2.0",
|
|
1670
1743
|
id,
|
|
1671
|
-
result
|
|
1672
|
-
},
|
|
1744
|
+
result,
|
|
1745
|
+
},
|
|
1746
|
+
responseSessionId,
|
|
1747
|
+
];
|
|
1673
1748
|
}
|
|
1674
|
-
else if (method ===
|
|
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
|
-
|
|
1763
|
+
return [
|
|
1764
|
+
{
|
|
1765
|
+
jsonrpc: "2.0",
|
|
1690
1766
|
id,
|
|
1691
|
-
error: { code: -32603, message:
|
|
1692
|
-
},
|
|
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 ===
|
|
1774
|
+
if (method === "ping") {
|
|
1697
1775
|
// MCP spec: ping method for keepalive/health checks
|
|
1698
1776
|
result = {};
|
|
1699
1777
|
}
|
|
1700
|
-
else if (method ===
|
|
1778
|
+
else if (method === "roots/list") {
|
|
1701
1779
|
result = { roots: [] };
|
|
1702
1780
|
}
|
|
1703
|
-
else if (method ===
|
|
1781
|
+
else if (method === "prompts/list") {
|
|
1704
1782
|
result = { prompts: getPromptsList() };
|
|
1705
1783
|
}
|
|
1706
|
-
else if (method ===
|
|
1784
|
+
else if (method === "resources/list") {
|
|
1707
1785
|
result = { resources: getResourcesList() };
|
|
1708
1786
|
}
|
|
1709
|
-
else if (method ===
|
|
1787
|
+
else if (method === "resources/templates/list") {
|
|
1710
1788
|
result = { resourceTemplates: getResourceTemplatesList() };
|
|
1711
1789
|
}
|
|
1712
|
-
else if (method ===
|
|
1790
|
+
else if (method === "prompts/get") {
|
|
1713
1791
|
const promptParams = params;
|
|
1714
1792
|
if (!promptParams.name) {
|
|
1715
|
-
throw new InvalidParamsError(
|
|
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 :
|
|
1799
|
+
throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown prompt error");
|
|
1722
1800
|
}
|
|
1723
1801
|
}
|
|
1724
|
-
else if (method ===
|
|
1802
|
+
else if (method === "resources/read") {
|
|
1725
1803
|
const resourceParams = params;
|
|
1726
1804
|
if (!resourceParams.uri) {
|
|
1727
|
-
throw new InvalidParamsError(
|
|
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 :
|
|
1811
|
+
throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown resource error");
|
|
1734
1812
|
}
|
|
1735
1813
|
}
|
|
1736
|
-
else if (method ===
|
|
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({
|
|
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 =
|
|
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 ===
|
|
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({
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1853
|
+
return [
|
|
1854
|
+
{
|
|
1855
|
+
jsonrpc: "2.0",
|
|
1771
1856
|
id,
|
|
1772
|
-
error: { code: -32601, message: `Unknown method: ${method}` }
|
|
1773
|
-
},
|
|
1857
|
+
error: { code: -32601, message: `Unknown method: ${method}` },
|
|
1858
|
+
},
|
|
1859
|
+
sessionId,
|
|
1860
|
+
];
|
|
1774
1861
|
}
|
|
1775
1862
|
}
|
|
1776
|
-
return [
|
|
1777
|
-
|
|
1863
|
+
return [
|
|
1864
|
+
{
|
|
1865
|
+
jsonrpc: "2.0",
|
|
1778
1866
|
id,
|
|
1779
|
-
result
|
|
1780
|
-
},
|
|
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
|
-
|
|
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 :
|
|
1796
|
-
}
|
|
1797
|
-
},
|
|
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[
|
|
1806
|
-
const sessionId = req.headers[
|
|
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(
|
|
1909
|
+
reqLogger.info("MCP request received", {
|
|
1817
1910
|
path: req.path,
|
|
1818
|
-
protocol: req.headers[
|
|
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 ===
|
|
1823
|
-
const sessionId = req.headers[
|
|
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(
|
|
1920
|
+
reqLogger.info("Health check request", {
|
|
1828
1921
|
accept: req.headers.accept,
|
|
1829
|
-
protocolVersion: req.headers[
|
|
1922
|
+
protocolVersion: req.headers["mcp-protocol-version"],
|
|
1830
1923
|
});
|
|
1831
1924
|
// Health check - always return JSON regardless of Accept header
|
|
1832
|
-
res.setHeader(
|
|
1833
|
-
res.setHeader(
|
|
1834
|
-
res.setHeader(
|
|
1835
|
-
res
|
|
1836
|
-
|
|
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(
|
|
1844
|
-
res.setHeader(
|
|
1845
|
-
res.setHeader(
|
|
1846
|
-
res.setHeader(
|
|
1847
|
-
res.setHeader(
|
|
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
|
|
1945
|
+
res
|
|
1946
|
+
.status(404)
|
|
1947
|
+
.json({ error: "Session not found or not initialized" });
|
|
1851
1948
|
return;
|
|
1852
1949
|
}
|
|
1853
|
-
res.setHeader(
|
|
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[
|
|
1856
|
-
const lastEventId = lastEventIdHeader
|
|
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(
|
|
1957
|
+
reqLogger.info("SSE stream resumption requested", {
|
|
1859
1958
|
lastEventId,
|
|
1860
|
-
transport:
|
|
1959
|
+
transport: "sse",
|
|
1861
1960
|
});
|
|
1862
1961
|
}
|
|
1863
1962
|
else {
|
|
1864
|
-
reqLogger.info(
|
|
1865
|
-
transport:
|
|
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(
|
|
1973
|
+
reqLogger.debug("Sending buffered events for resumption", {
|
|
1875
1974
|
eventCount: bufferedEvents.length,
|
|
1876
|
-
transport:
|
|
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, {
|
|
1887
|
-
|
|
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(
|
|
1999
|
+
res.write(":keepalive\n\n");
|
|
1899
2000
|
}
|
|
1900
2001
|
catch (error) {
|
|
1901
|
-
reqLogger.debug(
|
|
1902
|
-
error: error instanceof Error ? error.message :
|
|
1903
|
-
transport:
|
|
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(
|
|
2010
|
+
req.on("close", () => {
|
|
1910
2011
|
clearInterval(keepaliveInterval);
|
|
1911
2012
|
this.sseConnections.delete(sessionId);
|
|
1912
|
-
reqLogger.info(
|
|
1913
|
-
transport:
|
|
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 ||
|
|
1926
|
-
const hasJSON = acceptHeader.includes(
|
|
1927
|
-
const hasSSE = acceptHeader.includes(
|
|
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(
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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 ===
|
|
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(
|
|
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(
|
|
2060
|
+
console.error("[MCP] Body parsing error:", parseError);
|
|
1960
2061
|
res.status(400).json({
|
|
1961
|
-
jsonrpc:
|
|
2062
|
+
jsonrpc: "2.0",
|
|
1962
2063
|
error: {
|
|
1963
2064
|
code: -32700,
|
|
1964
|
-
message:
|
|
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(
|
|
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(
|
|
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:
|
|
2005
|
-
error: { code: -32700, message:
|
|
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 ===
|
|
2023
|
-
const protocolVersion = request.params
|
|
2024
|
-
|
|
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[
|
|
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({
|
|
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 =
|
|
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[
|
|
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:
|
|
2075
|
-
error: { code: -32700, message:
|
|
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
|
|
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(
|
|
2098
|
-
res.setHeader(
|
|
2099
|
-
res.setHeader(
|
|
2100
|
-
res.setHeader(
|
|
2101
|
-
res.setHeader(
|
|
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(
|
|
2225
|
+
reqLogger.debug("Sending JSON response", {
|
|
2119
2226
|
responseCount: responses.length,
|
|
2120
2227
|
batch: isBatchRequest,
|
|
2121
|
-
transport:
|
|
2228
|
+
transport: "json",
|
|
2122
2229
|
});
|
|
2123
|
-
res.setHeader(
|
|
2124
|
-
res.setHeader(
|
|
2125
|
-
res.setHeader(
|
|
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(
|
|
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(
|
|
2244
|
+
responseText = responses.map((r) => JSON.stringify(r)).join("\n");
|
|
2138
2245
|
}
|
|
2139
2246
|
res.write(responseText);
|
|
2140
2247
|
res.end();
|
|
2141
|
-
reqLogger.info(
|
|
2248
|
+
reqLogger.info("MCP response sent", {
|
|
2142
2249
|
responseCount: responses.length,
|
|
2143
2250
|
duration_ms: responseTime,
|
|
2144
|
-
transport:
|
|
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(
|
|
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(
|
|
2159
|
-
res.setHeader(
|
|
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(
|
|
2164
|
-
error: error instanceof Error ? error.message :
|
|
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(
|
|
2169
|
-
res.setHeader(
|
|
2170
|
-
res.status(500).json({
|
|
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 !==
|
|
2288
|
+
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
|
|
2180
2289
|
return {
|
|
2181
|
-
jsonrpc:
|
|
2290
|
+
jsonrpc: "2.0",
|
|
2182
2291
|
id,
|
|
2183
|
-
error: {
|
|
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 ===
|
|
2301
|
+
if (method === "notifications/initialized") {
|
|
2190
2302
|
return null; // No response for notifications
|
|
2191
2303
|
}
|
|
2192
|
-
if (method ===
|
|
2304
|
+
if (method === "initialize") {
|
|
2193
2305
|
result = {
|
|
2194
|
-
protocolVersion:
|
|
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:
|
|
2313
|
+
name: "metalink",
|
|
2202
2314
|
version,
|
|
2203
2315
|
},
|
|
2204
2316
|
};
|
|
2205
2317
|
}
|
|
2206
|
-
else if (method ===
|
|
2318
|
+
else if (method === "ping") {
|
|
2207
2319
|
// MCP spec: ping method for keepalive/health checks
|
|
2208
2320
|
result = {};
|
|
2209
2321
|
}
|
|
2210
|
-
else if (method ===
|
|
2322
|
+
else if (method === "roots/list") {
|
|
2211
2323
|
result = { roots: [] };
|
|
2212
2324
|
}
|
|
2213
|
-
else if (method ===
|
|
2325
|
+
else if (method === "prompts/list") {
|
|
2214
2326
|
result = { prompts: getPromptsList() };
|
|
2215
2327
|
}
|
|
2216
|
-
else if (method ===
|
|
2328
|
+
else if (method === "resources/list") {
|
|
2217
2329
|
result = { resources: getResourcesList() };
|
|
2218
2330
|
}
|
|
2219
|
-
else if (method ===
|
|
2331
|
+
else if (method === "resources/templates/list") {
|
|
2220
2332
|
result = { resourceTemplates: getResourceTemplatesList() };
|
|
2221
2333
|
}
|
|
2222
|
-
else if (method ===
|
|
2334
|
+
else if (method === "prompts/get") {
|
|
2223
2335
|
const promptParams = params;
|
|
2224
2336
|
if (!promptParams.name) {
|
|
2225
|
-
throw new InvalidParamsError(
|
|
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 :
|
|
2343
|
+
throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown prompt error");
|
|
2232
2344
|
}
|
|
2233
2345
|
}
|
|
2234
|
-
else if (method ===
|
|
2346
|
+
else if (method === "resources/read") {
|
|
2235
2347
|
const resourceParams = params;
|
|
2236
2348
|
if (!resourceParams.uri) {
|
|
2237
|
-
throw new InvalidParamsError(
|
|
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 :
|
|
2355
|
+
throw new InvalidParamsError(err instanceof Error ? err.message : "Unknown resource error");
|
|
2244
2356
|
}
|
|
2245
2357
|
}
|
|
2246
|
-
else if (method ===
|
|
2358
|
+
else if (method === "tools/list") {
|
|
2247
2359
|
result = await this.mcpListTools();
|
|
2248
2360
|
}
|
|
2249
|
-
else if (method ===
|
|
2361
|
+
else if (method === "tools/call") {
|
|
2250
2362
|
const callParams = params;
|
|
2251
2363
|
if (!callParams.name) {
|
|
2252
|
-
throw new InvalidParamsError(
|
|
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:
|
|
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:
|
|
2391
|
+
jsonrpc: "2.0",
|
|
2280
2392
|
id,
|
|
2281
2393
|
error: {
|
|
2282
2394
|
code: errorCode,
|
|
2283
|
-
message: error instanceof Error ? error.message :
|
|
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:
|
|
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:
|
|
2452
|
+
type: "object",
|
|
2341
2453
|
properties: {
|
|
2342
2454
|
query: {
|
|
2343
|
-
type:
|
|
2344
|
-
description:
|
|
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:
|
|
2348
|
-
description:
|
|
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:
|
|
2355
|
-
description:
|
|
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:
|
|
2469
|
+
type: "object",
|
|
2358
2470
|
properties: {
|
|
2359
2471
|
server_name: {
|
|
2360
|
-
type:
|
|
2472
|
+
type: "string",
|
|
2361
2473
|
description: 'Server name (e.g., "memory", "jira-basic-auth")',
|
|
2362
2474
|
},
|
|
2363
2475
|
tool_name: {
|
|
2364
|
-
type:
|
|
2476
|
+
type: "string",
|
|
2365
2477
|
description: 'Tool name (e.g., "create_entities", "search_issues")',
|
|
2366
2478
|
},
|
|
2367
2479
|
},
|
|
2368
|
-
required: [
|
|
2480
|
+
required: ["server_name", "tool_name"],
|
|
2369
2481
|
},
|
|
2370
2482
|
}, {
|
|
2371
|
-
name:
|
|
2483
|
+
name: "execute_tool",
|
|
2372
2484
|
description: this.getExecuteToolDescription(),
|
|
2373
2485
|
inputSchema: {
|
|
2374
|
-
type:
|
|
2486
|
+
type: "object",
|
|
2375
2487
|
properties: {
|
|
2376
2488
|
server_name: {
|
|
2377
|
-
type:
|
|
2489
|
+
type: "string",
|
|
2378
2490
|
description: 'Server name (e.g., "jira-basic-auth", "memory")',
|
|
2379
|
-
example:
|
|
2491
|
+
example: "jira-basic-auth",
|
|
2380
2492
|
},
|
|
2381
2493
|
tool_name: {
|
|
2382
|
-
type:
|
|
2494
|
+
type: "string",
|
|
2383
2495
|
description: 'Tool name (e.g., "confluence_search", "create_entities")',
|
|
2384
|
-
example:
|
|
2496
|
+
example: "confluence_search",
|
|
2385
2497
|
},
|
|
2386
2498
|
arguments: {
|
|
2387
|
-
type:
|
|
2388
|
-
description:
|
|
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:
|
|
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:
|
|
2397
|
-
description:
|
|
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:
|
|
2402
|
-
description:
|
|
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:
|
|
2407
|
-
description:
|
|
2408
|
-
}
|
|
2518
|
+
type: "string",
|
|
2519
|
+
description: "Optional: Opaque cursor from previous response to continue pagination",
|
|
2520
|
+
},
|
|
2409
2521
|
},
|
|
2410
|
-
required: [
|
|
2411
|
-
additionalProperties: false
|
|
2412
|
-
}
|
|
2522
|
+
required: ["server_name", "tool_name", "arguments"],
|
|
2523
|
+
additionalProperties: false,
|
|
2524
|
+
},
|
|
2413
2525
|
}, {
|
|
2414
|
-
name:
|
|
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:
|
|
2529
|
+
type: "object",
|
|
2418
2530
|
properties: {
|
|
2419
2531
|
server_name: {
|
|
2420
|
-
type:
|
|
2532
|
+
type: "string",
|
|
2421
2533
|
description: 'Server name (e.g., "jira-basic-auth", "memory")',
|
|
2422
|
-
example:
|
|
2534
|
+
example: "memory",
|
|
2423
2535
|
},
|
|
2424
2536
|
tool_name: {
|
|
2425
|
-
type:
|
|
2537
|
+
type: "string",
|
|
2426
2538
|
description: 'Tool name (e.g., "create_issue", "delete_entities")',
|
|
2427
|
-
example:
|
|
2539
|
+
example: "create_entities",
|
|
2428
2540
|
},
|
|
2429
2541
|
arguments: {
|
|
2430
|
-
type:
|
|
2431
|
-
description:
|
|
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: [
|
|
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:
|
|
2439
|
-
description:
|
|
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:
|
|
2444
|
-
description:
|
|
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:
|
|
2449
|
-
description:
|
|
2450
|
-
}
|
|
2566
|
+
type: "string",
|
|
2567
|
+
description: "Optional: Opaque cursor from previous response to continue pagination",
|
|
2568
|
+
},
|
|
2451
2569
|
},
|
|
2452
|
-
required: [
|
|
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 || {
|
|
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}`,
|
|
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(
|
|
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 ===
|
|
2523
|
-
const rateLimitKey = sessionId ||
|
|
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,
|
|
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}-`,
|
|
2654
|
+
globalMetrics.recordToolExecution(serverName, name.replace(`${serverName}-`, ""), latency);
|
|
2534
2655
|
}
|
|
2535
2656
|
// Log successful tool execution
|
|
2536
|
-
logger.info(
|
|
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}`,
|
|
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}-`,
|
|
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(
|
|
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 ===
|
|
2573
|
-
name ===
|
|
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 ===
|
|
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}`,
|
|
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(
|
|
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
|
|
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:
|
|
2791
|
+
type: "text",
|
|
2667
2792
|
text: JSON.stringify({
|
|
2668
|
-
servers: allServers.map(s => {
|
|
2669
|
-
const isStdio = s.transport ===
|
|
2793
|
+
servers: allServers.map((s) => {
|
|
2794
|
+
const isStdio = s.transport === "stdio" || s.transport === undefined;
|
|
2670
2795
|
return {
|
|
2671
2796
|
name: s.name,
|
|
2672
|
-
...(isStdio
|
|
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
|
|
2808
|
+
case "list_servers": {
|
|
2682
2809
|
const servers = this.configLoader.getServers();
|
|
2683
2810
|
return {
|
|
2684
2811
|
content: [
|
|
2685
2812
|
{
|
|
2686
|
-
type:
|
|
2813
|
+
type: "text",
|
|
2687
2814
|
text: JSON.stringify({
|
|
2688
|
-
servers: servers.map(s => {
|
|
2689
|
-
const isStdio = s.transport ===
|
|
2815
|
+
servers: servers.map((s) => {
|
|
2816
|
+
const isStdio = s.transport === "stdio" || s.transport === undefined;
|
|
2690
2817
|
return {
|
|
2691
2818
|
name: s.name,
|
|
2692
|
-
...(isStdio
|
|
2693
|
-
|
|
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
|
|
2832
|
+
case "list_tools": {
|
|
2703
2833
|
const serverName = callArgs?.server_name;
|
|
2704
2834
|
if (!serverName)
|
|
2705
|
-
throw new InvalidParamsError(
|
|
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:
|
|
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
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
2905
|
+
else if (propSchema?.type === "string")
|
|
2776
2906
|
exampleArgs[param] = `<${param}>`;
|
|
2777
|
-
else if (propSchema?.type ===
|
|
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(
|
|
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 ||
|
|
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 ===
|
|
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(
|
|
2836
|
-
errorStr.includes(
|
|
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(
|
|
2842
|
-
const optionalList = optionalParams.length > 0 ? optionalParams.join(
|
|
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
|
|
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(
|
|
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 ||
|
|
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 ===
|
|
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(
|
|
2931
|
-
errorStr.includes(
|
|
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(
|
|
2937
|
-
const optionalList = optionalParams.length > 0 ? optionalParams.join(
|
|
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
|
|
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(
|
|
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
|
|
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) ||
|
|
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 ||
|
|
3142
|
+
const toolDesc = (tool.description || "").toLowerCase();
|
|
2997
3143
|
// Determine match type
|
|
2998
|
-
let matchType =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3161
|
+
matchType = "all";
|
|
3016
3162
|
shouldInclude = true;
|
|
3017
3163
|
}
|
|
3018
3164
|
else if (nameMatches) {
|
|
3019
|
-
matchType =
|
|
3165
|
+
matchType = "name";
|
|
3020
3166
|
shouldInclude = true;
|
|
3021
3167
|
}
|
|
3022
3168
|
else if (descMatches) {
|
|
3023
|
-
matchType =
|
|
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 =
|
|
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 ===
|
|
3199
|
+
else if (prop.type === "string") {
|
|
3054
3200
|
// Generate meaningful examples for common param names
|
|
3055
|
-
if (key ===
|
|
3056
|
-
exampleArgs[key] =
|
|
3057
|
-
else if (key ===
|
|
3058
|
-
exampleArgs[key] =
|
|
3059
|
-
else if (key ===
|
|
3060
|
-
exampleArgs[key] =
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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 ===
|
|
3069
|
-
|
|
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 ===
|
|
3224
|
+
else if (prop.type === "boolean") {
|
|
3075
3225
|
exampleArgs[key] = true;
|
|
3076
3226
|
}
|
|
3077
|
-
else if (prop.type ===
|
|
3227
|
+
else if (prop.type === "array") {
|
|
3078
3228
|
exampleArgs[key] = [];
|
|
3079
3229
|
}
|
|
3080
|
-
else if (prop.type ===
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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
|
|
3155
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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 ===
|
|
3239
|
-
exampleArgs[key] = prop.description ? `<${key}>` :
|
|
3394
|
+
else if (prop.type === "string") {
|
|
3395
|
+
exampleArgs[key] = prop.description ? `<${key}>` : "example";
|
|
3240
3396
|
}
|
|
3241
|
-
else if (prop.type ===
|
|
3397
|
+
else if (prop.type === "number") {
|
|
3242
3398
|
exampleArgs[key] = 10;
|
|
3243
3399
|
}
|
|
3244
|
-
else if (prop.type ===
|
|
3400
|
+
else if (prop.type === "boolean") {
|
|
3245
3401
|
exampleArgs[key] = true;
|
|
3246
3402
|
}
|
|
3247
|
-
else if (prop.type ===
|
|
3403
|
+
else if (prop.type === "array") {
|
|
3248
3404
|
exampleArgs[key] = [];
|
|
3249
3405
|
}
|
|
3250
|
-
else if (prop.type ===
|
|
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 &&
|
|
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:
|
|
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:
|
|
3295
|
-
text:
|
|
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 ===
|
|
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(
|
|
3576
|
-
const query = (args2.query ||
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
3616
|
-
|
|
3617
|
-
|
|
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
|
|
3625
|
-
|
|
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:
|
|
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 :
|
|
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 ===
|
|
3892
|
+
if (status?.status === "running") {
|
|
3704
3893
|
// Check for force flag in query params
|
|
3705
|
-
const force = req.query.force ===
|
|
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:
|
|
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 :
|
|
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(
|
|
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:
|
|
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 :
|
|
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(
|
|
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 <
|
|
3812
|
-
|
|
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:
|
|
3818
|
-
method:
|
|
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:
|
|
4039
|
+
method: "POST",
|
|
3850
4040
|
headers: {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
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 ===
|
|
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 :
|
|
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:
|
|
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 ===
|
|
4105
|
+
requiresConfirmation: result.safety === "risky",
|
|
3916
4106
|
});
|
|
3917
4107
|
}
|
|
3918
4108
|
catch (error) {
|
|
3919
4109
|
res.status(500).json({
|
|
3920
|
-
error: error instanceof Error
|
|
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 !==
|
|
4122
|
+
if (!tool || typeof tool !== "string") {
|
|
3931
4123
|
res.status(400).json({
|
|
3932
|
-
error:
|
|
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:
|
|
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
|
|
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 !==
|
|
4158
|
+
if (!tool || typeof tool !== "string") {
|
|
3965
4159
|
res.status(400).json({
|
|
3966
|
-
error:
|
|
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:
|
|
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
|
|
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 !==
|
|
4194
|
+
if (!pattern || typeof pattern !== "string") {
|
|
3999
4195
|
res.status(400).json({
|
|
4000
|
-
error:
|
|
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 :
|
|
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 !==
|
|
4230
|
+
if (!pattern || typeof pattern !== "string") {
|
|
4035
4231
|
res.status(400).json({
|
|
4036
|
-
error:
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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:
|
|
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:
|
|
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
|
|
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 !==
|
|
4351
|
+
if (!rules || typeof rules !== "object") {
|
|
4152
4352
|
res.status(400).json({
|
|
4153
|
-
error:
|
|
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)
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
const
|
|
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
|
-
:
|
|
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 = [
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
4455
|
+
console.log("[MetricsPersistence] Saved final metrics on shutdown");
|
|
4232
4456
|
}
|
|
4233
4457
|
catch (error) {
|
|
4234
|
-
console.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) {
|