Initial commit
This commit is contained in:
161
mcp-server/requestMonitor.js
Normal file
161
mcp-server/requestMonitor.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user