Files
agenticSystem/mcp-server/requestMonitor.js
2026-04-01 23:16:45 +01:00

162 lines
5.3 KiB
JavaScript

import { EventEmitter } from "events";
function safeClone(value) {
try {
return JSON.parse(
JSON.stringify(value, (_, val) => (typeof val === "bigint" ? val.toString() : val))
);
} catch (error) {
return { error: `Serialization failed: ${error.message}` };
}
}
function formatError(error) {
if (!error) {
return null;
}
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
};
}
return {
name: "Error",
message: typeof error === "string" ? error : JSON.stringify(error),
};
}
export class McpRequestMonitor extends EventEmitter {
constructor(options = {}) {
super();
const { maxEntries = 300 } = options;
this.maxEntries = maxEntries;
this.entries = [];
this.nextId = 1;
}
start(method, request, extra = {}) {
const clonedRequest = safeClone(request);
let requestChars = 0;
try { requestChars = JSON.stringify(clonedRequest).length; } catch {}
const entry = {
id: this.nextId++,
method,
timestamp: new Date().toISOString(),
status: "pending",
durationMs: null,
sessionId: extra.sessionId || extra.requestInfo?.headers?.["mcp-session-id"] || null,
requestHeaders: extra.requestInfo?.headers ? { ...extra.requestInfo.headers } : undefined,
request: clonedRequest,
response: null,
error: null,
toolName: method === "tools/call" ? request?.params?.name || null : null,
requestChars,
responseChars: 0,
_startedAt: Date.now(),
};
this.entries.push(entry);
this.trimEntries();
this.emit("summary", this.toSummary(entry));
return entry;
}
finish(entry, response) {
if (!entry) return;
entry.status = "success";
entry.durationMs = Date.now() - entry._startedAt;
entry.response = safeClone(response);
try { entry.responseChars = JSON.stringify(entry.response).length; } catch {}
this.emit("summary", this.toSummary(entry));
}
fail(entry, error) {
if (!entry) return;
entry.status = "error";
entry.durationMs = Date.now() - entry._startedAt;
entry.error = formatError(error);
try { entry.responseChars = JSON.stringify(entry.error).length; } catch {}
this.emit("summary", this.toSummary(entry));
}
getSummaries() {
return this.entries.map((entry) => this.toSummary(entry));
}
getEntryById(id) {
const numericId = typeof id === "number" ? id : Number(id);
const entry = this.entries.find((item) => item.id === numericId);
if (!entry) return null;
const { _startedAt, ...rest } = entry;
return rest;
}
toSummary(entry) {
return {
id: entry.id,
method: entry.method,
timestamp: entry.timestamp,
status: entry.status,
durationMs: entry.durationMs,
sessionId: entry.sessionId,
errorMessage: entry.error?.message || null,
toolName: entry.toolName || null,
requestChars: entry.requestChars || 0,
responseChars: entry.responseChars || 0,
};
}
trimEntries() {
if (this.entries.length <= this.maxEntries) {
return;
}
this.entries.splice(0, this.entries.length - this.maxEntries);
}
getSessionStats() {
const statsMap = {};
for (const entry of this.entries) {
const sid = entry.sessionId;
if (!sid) continue;
if (!statsMap[sid]) {
statsMap[sid] = {
sessionId: sid,
totalRequests: 0,
totalToolCalls: 0,
totalRequestChars: 0,
totalResponseChars: 0,
toolBreakdown: {},
errorCount: 0,
};
}
const stats = statsMap[sid];
stats.totalRequests++;
stats.totalRequestChars += entry.requestChars || 0;
stats.totalResponseChars += entry.responseChars || 0;
if (entry.status === "error") stats.errorCount++;
if (entry.toolName) {
stats.totalToolCalls++;
if (!stats.toolBreakdown[entry.toolName]) {
stats.toolBreakdown[entry.toolName] = { count: 0, requestChars: 0, responseChars: 0, errors: 0, totalDurationMs: 0 };
}
const tb = stats.toolBreakdown[entry.toolName];
tb.count++;
tb.requestChars += entry.requestChars || 0;
tb.responseChars += entry.responseChars || 0;
if (entry.status === "error") tb.errors++;
if (entry.durationMs != null) tb.totalDurationMs += entry.durationMs;
}
}
for (const stats of Object.values(statsMap)) {
for (const tb of Object.values(stats.toolBreakdown)) {
tb.avgDurationMs = tb.count > 0 ? Math.round(tb.totalDurationMs / tb.count) : 0;
delete tb.totalDurationMs;
}
}
return Object.values(statsMap);
}
}