Skip to content

Commit e8ad418

Browse files
authored
Merge pull request #8 from vercel/log_v1
feat: new onEvent callback for user land logging of server calls
2 parents 4710161 + 264af6b commit e8ad418

File tree

3 files changed

+371
-93
lines changed

3 files changed

+371
-93
lines changed

src/lib/event-emitter.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { ServerResponse, type IncomingMessage } from "node:http";
2+
import {
3+
type McpErrorEvent,
4+
type McpEvent,
5+
type McpRequestEvent,
6+
type McpSessionEvent,
7+
createEvent,
8+
} from "./log-helper";
9+
10+
export class EventEmittingResponse extends ServerResponse {
11+
private onEvent?: (event: McpEvent) => void;
12+
private sessionId?: string;
13+
private requestId: string;
14+
private startTime: number;
15+
16+
constructor(
17+
req: IncomingMessage,
18+
onEvent?: (event: McpEvent) => void,
19+
sessionId?: string
20+
) {
21+
super(req);
22+
this.onEvent = onEvent;
23+
this.sessionId = sessionId;
24+
this.requestId = crypto.randomUUID();
25+
this.startTime = Date.now();
26+
}
27+
28+
emitEvent(event: Omit<McpEvent, "timestamp" | "sessionId" | "requestId">) {
29+
if (this.onEvent) {
30+
this.onEvent(
31+
createEvent({
32+
...event,
33+
sessionId: this.sessionId,
34+
requestId: this.requestId,
35+
} as Omit<McpEvent, "timestamp">)
36+
);
37+
}
38+
}
39+
40+
startSession(
41+
transport: "SSE" | "HTTP",
42+
clientInfo?: { userAgent?: string; ip?: string }
43+
) {
44+
this.emitEvent({
45+
type: "SESSION_STARTED",
46+
transport,
47+
clientInfo,
48+
} as Omit<McpSessionEvent, "timestamp" | "sessionId" | "requestId">);
49+
}
50+
51+
endSession(transport: "SSE" | "HTTP") {
52+
this.emitEvent({
53+
type: "SESSION_ENDED",
54+
transport,
55+
} as Omit<McpSessionEvent, "timestamp" | "sessionId" | "requestId">);
56+
}
57+
58+
requestReceived(method: string, parameters?: unknown) {
59+
this.emitEvent({
60+
type: "REQUEST_RECEIVED",
61+
method,
62+
parameters,
63+
status: "success",
64+
} as Omit<McpRequestEvent, "timestamp" | "sessionId" | "requestId">);
65+
}
66+
67+
requestCompleted(method: string, result?: unknown, error?: Error | string) {
68+
this.emitEvent({
69+
type: "REQUEST_COMPLETED",
70+
method,
71+
result,
72+
duration: Date.now() - this.startTime,
73+
status: error ? "error" : "success",
74+
} as Omit<McpRequestEvent, "timestamp" | "sessionId" | "requestId">);
75+
76+
if (error) {
77+
this.error(error, `Error executing request ${method}`, "request");
78+
}
79+
}
80+
81+
error(
82+
error: Error | string,
83+
context?: string,
84+
source: McpErrorEvent["source"] = "system",
85+
severity: McpErrorEvent["severity"] = "error"
86+
) {
87+
this.emitEvent({
88+
type: "ERROR",
89+
error,
90+
context,
91+
source,
92+
severity,
93+
} as Omit<McpErrorEvent, "timestamp" | "sessionId" | "requestId">);
94+
}
95+
96+
end(
97+
chunk?: unknown,
98+
encoding?: BufferEncoding | (() => void),
99+
cb?: () => void
100+
): this {
101+
let finalChunk = chunk;
102+
let finalEncoding = encoding;
103+
let finalCallback = cb;
104+
105+
if (typeof chunk === "function") {
106+
finalCallback = chunk as () => void;
107+
finalChunk = undefined;
108+
finalEncoding = undefined;
109+
} else if (typeof encoding === "function") {
110+
finalCallback = encoding as () => void;
111+
finalEncoding = undefined;
112+
}
113+
114+
return super.end(
115+
finalChunk as string | Buffer,
116+
finalEncoding as BufferEncoding,
117+
finalCallback
118+
);
119+
}
120+
}

src/lib/log-helper.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export type McpEventType =
2+
| "SESSION_STARTED" // When a new client session begins (either HTTP or SSE)
3+
| "SESSION_ENDED" // When a client session ends (SSE disconnection)
4+
| "REQUEST_RECEIVED" // When a request is received from the client
5+
| "REQUEST_COMPLETED" // When a request completes
6+
| "ERROR"; // When an error occurs during any operation
7+
8+
export interface McpEventBase {
9+
type: McpEventType;
10+
timestamp: number;
11+
sessionId?: string;
12+
requestId?: string; // To track individual requests within a session
13+
}
14+
15+
export interface McpSessionEvent extends McpEventBase {
16+
type: "SESSION_STARTED" | "SESSION_ENDED";
17+
transport: "SSE" | "HTTP";
18+
clientInfo?: {
19+
userAgent?: string;
20+
ip?: string;
21+
};
22+
}
23+
24+
export interface McpRequestEvent extends McpEventBase {
25+
type: "REQUEST_RECEIVED" | "REQUEST_COMPLETED";
26+
method: string;
27+
parameters?: unknown;
28+
result?: unknown;
29+
duration?: number; // For REQUEST_COMPLETED events
30+
status: "success" | "error";
31+
}
32+
33+
export interface McpErrorEvent extends McpEventBase {
34+
type: "ERROR";
35+
error: Error | string;
36+
context?: string;
37+
source: "request" | "session" | "system";
38+
severity: "warning" | "error" | "fatal";
39+
}
40+
41+
export type McpEvent = McpSessionEvent | McpRequestEvent | McpErrorEvent;
42+
43+
export function createEvent<T extends McpEvent>(
44+
event: Omit<T, "timestamp">
45+
): T {
46+
return {
47+
...event,
48+
timestamp: Date.now(),
49+
} as T;
50+
}

0 commit comments

Comments
 (0)