inspector/core/mcp/messageTrackingTransport.ts
Cliff Hall 82269e78cb
refactor(core): apply v1.5-port cleanups deferred from #1306 (#1316)
* refactor(core): apply v1.5-port cleanups deferred from #1306

Addresses issue #1309 — five small, mechanical cleanups deferred from the
v1.5 runtime port:

1. Collapse BrowserOAuthStorage / NodeOAuthStorage / RemoteOAuthStorage
   onto a shared OAuthStorageBase. Three ~130-line duplicates become
   thin adapter wrappers; behavior + public API unchanged.
2. Re-enable erasableSyntaxOnly in tsconfig.app.json by rewriting the
   seven (plus two the issue missed in sampling/elicitation create-
   message) parameter-property constructors as explicit field + assign.
3. Route the lone console.error in inspectorClient.ts through this.logger.
4. Replace `${Date.now()}-${Math.random()}` with crypto.randomUUID() at
   the five local-only ID sites (message tracking + sampling/elicit).
5. Pin the remote-server hono app generic at new Hono<Env>() and drop
   the streamSSE cast. Root cause of the cast was an unresolved hono
   subpath: the tsconfig "hono/*" wildcard didn't honor the exports
   map, so hono/streaming leaked to the root-hoisted hono@4.12.12 while
   hono itself resolved to clients/web/hono@4.12.18. Replaced the
   wildcard with an explicit hono/streaming entry in both tsconfig.app
   and tsconfig.test, removing the package-identity mismatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(core): address #1316 PR review feedback

- Drop the stranded no-op getServerState() read at the top of
  clearClientInformation in OAuthStorageBase (carried over verbatim
  from the three originals).
- Drop the crypto.randomUUID() guard at the task-ID site so all
  six in-scope sites are consistent; the fallback branch was dead
  code on every supported runtime.
- Remove the residual `c as Context` cast at the streamSSE call site
  in core/mcp/remote/node/server.ts — once both hono imports
  resolve to the same package the route-typed Context flows through
  to streamSSE with no cast at all.
- Move the new `options` / `config` field declarations in
  RemoteClientTransport up with the rest of the field block, matching
  the layout used by the other five rewritten classes in this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:15:48 -04:00

126 lines
3.5 KiB
TypeScript

import type {
Transport,
TransportSendOptions,
} from "@modelcontextprotocol/sdk/shared/transport.js";
import type {
JSONRPCMessage,
MessageExtraInfo,
} from "@modelcontextprotocol/sdk/types.js";
import type {
JSONRPCRequest,
JSONRPCNotification,
JSONRPCResultResponse,
JSONRPCErrorResponse,
} from "@modelcontextprotocol/sdk/types.js";
export interface MessageTrackingCallbacks {
trackRequest?: (message: JSONRPCRequest) => void;
trackResponse?: (
message: JSONRPCResultResponse | JSONRPCErrorResponse,
) => void;
trackNotification?: (message: JSONRPCNotification) => void;
}
// Transport wrapper that intercepts all messages for tracking
export class MessageTrackingTransport implements Transport {
private baseTransport: Transport;
private callbacks: MessageTrackingCallbacks;
constructor(
baseTransport: Transport,
callbacks: MessageTrackingCallbacks,
) {
this.baseTransport = baseTransport;
this.callbacks = callbacks;
}
async start(): Promise<void> {
return this.baseTransport.start();
}
async send(
message: JSONRPCMessage,
options?: TransportSendOptions,
): Promise<void> {
// Track outgoing requests (only requests have a method and are sent by the client)
if ("method" in message && "id" in message) {
this.callbacks.trackRequest?.(message as JSONRPCRequest);
}
return this.baseTransport.send(message, options);
}
async close(): Promise<void> {
return this.baseTransport.close();
}
get onclose(): (() => void) | undefined {
return this.baseTransport.onclose;
}
set onclose(handler: (() => void) | undefined) {
this.baseTransport.onclose = handler;
}
get onerror(): ((error: Error) => void) | undefined {
return this.baseTransport.onerror;
}
set onerror(handler: ((error: Error) => void) | undefined) {
this.baseTransport.onerror = handler;
}
get onmessage():
| (<T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void)
| undefined {
return this.baseTransport.onmessage;
}
set onmessage(
handler:
| (<T extends JSONRPCMessage>(
message: T,
extra?: MessageExtraInfo,
) => void)
| undefined,
) {
if (handler) {
// Wrap the handler to track incoming messages
this.baseTransport.onmessage = <T extends JSONRPCMessage>(
message: T,
extra?: MessageExtraInfo,
) => {
// Track incoming messages
if (
"id" in message &&
message.id !== null &&
message.id !== undefined
) {
// Check if it's a response (has 'result' or 'error' property)
if ("result" in message || "error" in message) {
this.callbacks.trackResponse?.(
message as JSONRPCResultResponse | JSONRPCErrorResponse,
);
} else if ("method" in message) {
// This is a request coming from the server
this.callbacks.trackRequest?.(message as JSONRPCRequest);
}
} else if ("method" in message) {
// Notification (no ID, has method)
this.callbacks.trackNotification?.(message as JSONRPCNotification);
}
// Call the original handler
handler(message, extra);
};
} else {
this.baseTransport.onmessage = undefined;
}
}
get sessionId(): string | undefined {
return this.baseTransport.sessionId;
}
get setProtocolVersion(): ((version: string) => void) | undefined {
return this.baseTransport.setProtocolVersion;
}
}