Description
Describe the bug
When calling a tool the input parameters are not getting passed as arguments when using a zod schema for subclassed instances of MCP server.
To Reproduce
Steps to reproduce the behavior:
We have a fairly robust multi-module system where modules can be dynamically loaded. Hence, we abstract 3rd party libraries into our own package and provide a simple interface. So, in our case, we have defined a simple subclass that we expose to users of our platform. Then from there, the users/developers may also subclass that version or use an instance of that version. This eliminates the need for multiple plugins/extensions in our framework to load or add modelcontextprotocol as a dependency as our "host" application loads it and ensures only one version and one copy is loaded.
If we the mcp class directly in our code, everything works fine. If we subclass a subclass -- it will not work. We have tried about 100 different ways. The issue is how the modules are loaded in node -- and from extensive debugging seems to be how the types are defined for tools with overly complex unions - we are adding an enhancement request on that topic as it is way too complicated and difficult to consume.
The only way we can get this to work (outside of a direct subclass) -- is to embed a wrapped callback inside a specialty registration. But then, some architecture decisions of making member variables private vs protected (or not creating a getter) to variables like _registeredTools causes an additional headache.
Expected behavior
A clear and concise description of what you expected to happen.
The parameters should be passed as defined in the spec. This occurs regardless of the transport mechanism, have tried with sse, more recent http keep alive, and also stdio -- this seems to be a typescript to js issue
Logs
If applicable, add logs to help explain your problem.
Additional context
Add any other context about the problem here.
Using this version of streamableHttp.ts you can see in our logs how it isn't passed:
import
{
ServerResponse,
IncomingMessage
} from "node:http";
import getRawBody from "raw-body";
import contentType from "content-type";
import { randomUUID } from "node:crypto";
import
{
RequestId,
JSONRPCMessage,
isJSONRPCError,
isJSONRPCRequest,
isJSONRPCResponse,
isInitializeRequest,
JSONRPCMessageSchema
} from "../types.js";
import { Transport } from "../shared/transport.js";
const MAXIMUM_MESSAGE_SIZE = "4mb";
export type StreamId = string;
export type EventId = string;
/**
-
Interface for resumability support via event storage
/
export interface EventStore
{
/*- Stores an event for later retrieval
- @param streamId ID of the stream the event belongs to
- @param message The JSON-RPC message to store
- @returns The generated event ID for the stored event
*/
storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise;
replayEventsAfter(lastEventId: EventId, { send }: {send: (eventId: EventId, message: JSONRPCMessage) => Promise}): Promise;
}
/**
-
Configuration options for StreamableHTTPServerTransport
/
export interface StreamableHTTPServerTransportOptions
{
/*- Function that generates a session ID for the transport.
- The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash)
- Return undefined to disable session management.
*/
sessionIdGenerator: (() => string) | undefined;
/**
- A callback for session initialization events
- This is called when the server initializes a new session.
- Useful in cases when you need to register multiple mcp sessions
- and need to keep track of them.
- @param sessionId The generated session ID
*/
onsessioninitialized?: (sessionId: string) => void;
/**
- If true, the server will return JSON responses instead of starting an SSE stream.
- This can be useful for simple request/response scenarios without streaming.
- Default is false (SSE streams are preferred).
*/
enableJsonResponse?: boolean;
/**
- Event store for resumability support
- If provided, resumability will be enabled, allowing clients to reconnect and resume messages
*/
eventStore?: EventStore;
}
/**
-
Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
-
It supports both SSE streaming and direct HTTP responses.
-
Usage example:
-
-
// Stateful mode - server sets the session ID
-
const statefulTransport = new StreamableHTTPServerTransport({
-
sessionIdGenerator: () => randomUUID(),
-
});
-
// Stateless mode - explicitly set session ID to undefined
-
const statelessTransport = new StreamableHTTPServerTransport({
-
sessionIdGenerator: undefined,
-
});
-
// Using with pre-parsed request body
-
app.post('/mcp', (req, res) => {
-
transport.handleRequest(req, res, req.body);
-
});
-
-
In stateful mode:
-
- Session ID is generated and included in response headers
-
- Session ID is always included in initialization responses
-
- Requests with invalid session IDs are rejected with 404 Not Found
-
- Non-initialization requests without a session ID are rejected with 400 Bad Request
-
- State is maintained in-memory (connections, message history)
-
In stateless mode:
-
- No Session ID is included in any responses
-
- No session validation is performed
*/
export class StreamableHTTPServerTransport implements Transport
{
// when sessionId is not set (undefined), it means the transport is in stateless mode
private sessionIdGenerator: (() => string) | undefined;
private _started: boolean = false;
private _streamMapping: Map<string, ServerResponse> = new Map();
private _requestToStreamMapping: Map<RequestId, string> = new Map();
private _requestResponseMap: Map<RequestId, JSONRPCMessage> = new Map();
private _initialized: boolean = false;
private _enableJsonResponse: boolean = false;
private _standaloneSseStreamId: string = '_GET_stream';
private _eventStore?: EventStore;
private _onsessioninitialized?: (sessionId: string) => void;
sessionId?: string | undefined;
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(options: StreamableHTTPServerTransportOptions)
{
this.sessionIdGenerator = options.sessionIdGenerator;
this._enableJsonResponse = options.enableJsonResponse ?? false;
this._eventStore = options.eventStore;
this._onsessioninitialized = options.onsessioninitialized;
}/**
-
Starts the transport. This is required by the Transport interface but is a no-op
-
for the Streamable HTTP transport as connections are managed per-request.
*/
async start(): Promise
{
if(this._started)
{
throw new Error("Transport already started");
}this._started = true;
}
/**
- Handles an incoming HTTP request, whether GET or POST
*/
async handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise
{
console.log("handleRequest:StreamableHTTPServerTransport:Received request:", req.method, req.url, parsedBody);
if(req.method === "POST")
{
console.log("post");
await this.handlePostRequest(req, res, parsedBody);
}
else if(req.method === "GET")
{
console.log("get");
await this.handleGetRequest(req, res);
}
else if(req.method === "DELETE")
{
console.log("delete");
await this.handleDeleteRequest(req, res);
}
else
{
console.log("unsupported");
await this.handleUnsupportedRequest(res);
}
}
/**
-
Handles GET requests for SSE stream
*/
private async handleGetRequest(req: IncomingMessage, res: ServerResponse): Promise
{
// The client MUST include an Accept header, listing text/event-stream as a supported content type.
const acceptHeader = req.headers.accept;
if(!acceptHeader?.includes("text/event-stream"))
{
res.writeHead(406).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Not Acceptable: Client must accept text/event-stream"
},
id: null
}))return;
}
// If an Mcp-Session-Id is returned by the server during initialization,
// clients using the Streamable HTTP transport MUST include it
// in the Mcp-Session-Id header on all of their subsequent HTTP requests.
if(!this.validateSession(req, res))
{
return;
}// Handle resumability: check for Last-Event-ID header
if(this._eventStore)
{
const lastEventId = req.headers['last-event-id'] as string | undefined;
if(lastEventId)
{
await this.replayEvents(lastEventId, res);
return;
}
}// The server MUST either return Content-Type: text/event-stream in response to this HTTP GET,
// or else return HTTP 405 Method Not Allowed
const headers: Record<string, string> =
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
}// After initialization, always include the session ID if we have one
if(this.sessionId !== undefined)
{
headers["mcp-session-id"] = this.sessionId;
}// Check if there's already an active standalone SSE stream for this session
if(this._streamMapping.get(this._standaloneSseStreamId) !== undefined)
{
// Only one GET SSE stream is allowed per session
res.writeHead(409).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Conflict: Only one SSE stream is allowed per session"
},
id: null
}))return;
}
// We need to send headers immediately as messages will arrive much later,
// otherwise the client will just wait for the first message
res.writeHead(200, headers).flushHeaders();// Assign the response to the standalone SSE stream
this._streamMapping.set(this._standaloneSseStreamId, res);// Set up close handler for client disconnects
res.on("close", () =>
{
this._streamMapping.delete(this._standaloneSseStreamId);
})
}
/**
-
Replays events that would have been sent after the specified event ID
-
Only used when resumability is enabled
*/
private async replayEvents(lastEventId: string, res: ServerResponse): Promise
{
if(!this._eventStore)
{
return;
}try
{
const headers: Record<string, string> =
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive"
}if(this.sessionId !== undefined) { headers["mcp-session-id"] = this.sessionId; } res.writeHead(200, headers).flushHeaders(); const streamId = await this._eventStore?.replayEventsAfter(lastEventId, { send: async (eventId: string, message: JSONRPCMessage) => { if(!this.writeSSEEvent(res, message, eventId)) { this.onerror?.(new Error("Failed replay events")); res.end(); } } }) this._streamMapping.set(streamId, res);
}
catch(error)
{
this.onerror?.(error as Error);
}
}
/**
-
Writes an event to the SSE stream with proper formatting
*/
private writeSSEEvent(res: ServerResponse, message: JSONRPCMessage, eventId?: string): boolean
{
let eventData =event: message\n
;
// Include event ID if provided - this is important for resumability
if(eventId)
{
eventData +=id: ${eventId}\n
;
}eventData +=
data: ${JSON.stringify(message)}\n\n
;
return res.write(eventData);
}
/**
- Handles unsupported requests (PUT, PATCH, etc.)
*/
private async handleUnsupportedRequest(res: ServerResponse): Promise
{
res.writeHead(405,
{
"Allow": "GET, POST, DELETE"
})
.end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Method not allowed."
},
id: null
}))
}
/**
-
Handles POST requests containing JSON-RPC messages
*/
private async handlePostRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise
{
try
{
console.log("handlePostRequest:POST request received:", req.method, req.url, parsedBody);
// Validate the Accept header
const acceptHeader = req.headers.accept;
// The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
if(!acceptHeader?.includes("application/json") || !acceptHeader.includes("text/event-stream"))
{
console.log("handlePostRequest.1");
res.writeHead(406).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Not Acceptable: Client must accept both application/json and text/event-stream"
},
id: null
}))return; } console.log("handlePostRequest.2"); const ct = req.headers["content-type"]; if(!ct || !ct.includes("application/json")) { console.log("handlePostRequest.3"); res.writeHead(415).end(JSON.stringify( { jsonrpc: "2.0", error: { code: -32000, message: "Unsupported Media Type: Content-Type must be application/json" }, id: null })) return; } console.log("handlePostRequest.4"); let rawMessage; if(parsedBody !== undefined) { rawMessage = parsedBody; } else { console.log("handlePostRequest.5"); const parsedCt = contentType.parse(ct); const body = await getRawBody(req, { limit: MAXIMUM_MESSAGE_SIZE, encoding: parsedCt.parameters.charset ?? "utf-8", }) rawMessage = JSON.parse(body.toString()); } let messages: JSONRPCMessage[]; console.log("handlePostRequest.6:messages:", rawMessage); // handle batch and single messages if(Array.isArray(rawMessage)) { console.log("handlePostRequest.7"); messages = rawMessage.map(msg => JSONRPCMessageSchema.parse(msg)); } else { console.log("handlePostRequest.8"); messages = [JSONRPCMessageSchema.parse(rawMessage)]; console.log("handlePostRequest.8.1:messages", messages); } // Check if this is an initialization request // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/ console.log("handlePostRequest.9"); const isInitializationRequest = messages.some(isInitializeRequest); if(isInitializationRequest) { console.log("handlePostRequest.10"); // If it's a server with session management and the session ID is already set we should reject the request // to avoid re-initialization. if(this._initialized && this.sessionId !== undefined) { console.log("handlePostRequest.11"); res.writeHead(400).end(JSON.stringify( { jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request: Server already initialized" }, id: null })) return; } console.log("handlePostRequest.12"); if(messages.length > 1) { console.log("handlePostRequest.13"); res.writeHead(400).end(JSON.stringify( { jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request: Only one initialization request is allowed" }, id: null })) return; } console.log("handlePostRequest.14"); this.sessionId = this.sessionIdGenerator?.(); this._initialized = true; // If we have a session ID and an onsessioninitialized handler, call it immediately // This is needed in cases where the server needs to keep track of multiple sessions if(this.sessionId && this._onsessioninitialized) { this._onsessioninitialized(this.sessionId); } } // If an Mcp-Session-Id is returned by the server during initialization, // clients using the Streamable HTTP transport MUST include it // in the Mcp-Session-Id header on all of their subsequent HTTP requests. console.log("handlePostRequest.15"); if(!isInitializationRequest && !this.validateSession(req, res)) { console.log("handlePostRequest.16"); return; } // check if it contains requests const hasRequests = messages.some(isJSONRPCRequest); console.log("handlePostRequest.17"); if(!hasRequests) { console.log("handlePostRequest.18"); // if it only contains notifications or responses, return 202 res.writeHead(202).end(); // handle each message for(const message of messages) { this.onmessage?.(message); } } else if(hasRequests) { console.log("handlePostRequest.19"); // The default behavior is to use SSE streaming // but in some cases server will return JSON responses const streamId = randomUUID(); if(!this._enableJsonResponse) { const headers: Record<string, string> = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } // After initialization, always include the session ID if we have one if(this.sessionId !== undefined) { headers["mcp-session-id"] = this.sessionId; } res.writeHead(200, headers); } // Store the response for this request to send messages back through this connection // We need to track by request ID to maintain the connection console.log("handlePostRequest.20"); for(const message of messages) { console.log("handlePostRequest.20.1"); if(isJSONRPCRequest(message)) { console.log("handlePostRequest.20.2:isJSONRPCRequest"); this._streamMapping.set(streamId, res); this._requestToStreamMapping.set(message.id, streamId); } } // Set up close handler for client disconnects res.on("close", () => { this._streamMapping.delete(streamId); }) // handle each message // JUST BAD CODE (CHECK IF ONMESSAGE, THEN ITERATE) for(const message of messages) { console.log("handlePostRequest:21:message", message); console.log("handlePostRequest:22:onmessage", this.onmessage); this.onmessage?.(message); } // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses // This will be handled by the send() method when responses are ready }
}
catch(error)
{
console.error("Error handling request:", error);
// return JSON-RPC formatted error
res.writeHead(400).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32700,
message: "Parse error",
data: String(error)
},
id: null
}))this.onerror?.(error as Error);
}
}
/**
-
Handles DELETE requests to terminate sessions
*/
private async handleDeleteRequest(req: IncomingMessage, res: ServerResponse): Promise
{
if(!this.validateSession(req, res))
{
return;
}await this.close();
res.writeHead(200).end();
}
/**
-
Validates session ID for non-initialization requests
-
Returns true if the session is valid, false otherwise
*/
private validateSession(req: IncomingMessage, res: ServerResponse): boolean
{
if(this.sessionIdGenerator === undefined)
{
// If the sessionIdGenerator ID is not set, the session management is disabled
// and we don't need to validate the session ID
return true;
}if(!this._initialized)
{
// If the server has not been initialized yet, reject all requests
res.writeHead(400).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Bad Request: Server not initialized"
},
id: null
}))return false;
}
const sessionId = req.headers["mcp-session-id"];
if(!sessionId)
{
// Non-initialization requests without a session ID should return 400 Bad Request
res.writeHead(400).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Bad Request: Mcp-Session-Id header is required"
},
id: null
}))return false;
}
else if(Array.isArray(sessionId))
{
res.writeHead(400).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32000,
message: "Bad Request: Mcp-Session-Id header must be a single value"
},
id: null
}))return false;
}
else if(sessionId !== this.sessionId)
{
// Reject requests with invalid session ID with 404 Not Found
res.writeHead(404).end(JSON.stringify(
{
jsonrpc: "2.0",
error:
{
code: -32001,
message: "Session not found"
},
id: null
}))return false;
}
return true;
}
async close(): Promise
{
// Close all SSE connections
this._streamMapping.forEach((response) =>
{
response.end();
})this._streamMapping.clear(); // Clear any pending responses this._requestResponseMap.clear(); this.onclose?.();
}
async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise
{
let requestId = options?.relatedRequestId;
if(isJSONRPCResponse(message) || isJSONRPCError(message))
{
// If the message is a response, use the request ID from the message
requestId = message.id;
}// Check if this message should be sent on the standalone SSE stream (no request ID) // Ignore notifications from tools (which have relatedRequestId set) // Those will be sent via dedicated response SSE streams if(requestId === undefined) { // For standalone SSE streams, we can only send requests and notifications if(isJSONRPCResponse(message) || isJSONRPCError(message)) { throw (new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request")); } const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId) if(standaloneSse === undefined) { // The spec says the server MAY send messages on the stream, so it's ok to discard if no stream return; } // Generate and store event ID if event store is provided let eventId: string | undefined; if(this._eventStore) { // Stores the event and gets the generated event ID eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message); } // Send the message to the standalone SSE stream this.writeSSEEvent(standaloneSse, message, eventId); return; } // Get the response for this request const streamId = this._requestToStreamMapping.get(requestId); const response = this._streamMapping.get(streamId!); if(!streamId) { throw new Error(`No connection established for request ID: ${String(requestId)}`); } if(!this._enableJsonResponse) { // For SSE responses, generate event ID if event store is provided let eventId: string | undefined; if(this._eventStore) { eventId = await this._eventStore.storeEvent(streamId, message); } if(response) { // Write the event to the response stream this.writeSSEEvent(response, message, eventId); } } if(isJSONRPCResponse(message) || isJSONRPCError(message)) { this._requestResponseMap.set(requestId, message); const relatedIds = Array.from(this._requestToStreamMapping.entries()).filter(([_, streamId]) => this._streamMapping.get(streamId) === response).map(([id]) => id); // Check if we have responses for all requests using this connection const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id)); if(allResponsesReady) { if(!response) { throw new Error(`No connection established for request ID: ${String(requestId)}`); } if(this._enableJsonResponse) { // All responses ready, send as JSON const headers: Record<string, string> = { 'Content-Type': 'application/json', } if(this.sessionId !== undefined) { headers['mcp-session-id'] = this.sessionId; } const responses = relatedIds.map(id => this._requestResponseMap.get(id)!); response.writeHead(200, headers); if(responses.length === 1) { response.end(JSON.stringify(responses[0])); } else { response.end(JSON.stringify(responses)); } } else { // End the SSE stream response.end(); } // Clean up for(const id of relatedIds) { this._requestResponseMap.delete(id); this._requestToStreamMapping.delete(id); } } }
}
} - No session validation is performed
and this sub-class:
import { logger } from "@grunge-ai/lib-logger";
import
{
AiMcpServer,
AiToolCallback
} from "@grunge-ai/lib-server";
import { AiMcpResponseType } from "shared/mcp";
import { z } from "zod";
const AddNumbersSchema = z.object(
{
a: z.number(), // .min(0, "The first number must be greater than or equal to 0"),
b: z.number()
})
type AddNumbersProps = z.infer;
export class AiCliMcpServer extends AiMcpServer
{
// constructor(serverInfo: Implementation, options?: ServerOptions)
constructor(serverInfo: any, options?: any)
{
super(serverInfo, options);
// have tried this with and without binding here -- thinking it might have been a bug in the way the module loader loads the class across modules
this.tool.bind(this);
this.connect.bind(this);
this.close.bind(this);
this.resource.bind(this);
this.isConnected.bind(this);
this.prompt.bind(this);
this.sendResourceListChanged.bind(this);
this.sendToolListChanged.bind(this);
this.sendPromptListChanged.bind(this);
}
public async start(): Promise<void>
{
const addParamsSchema =
{
a: z.number().min(0, "The first number must be greater than or equal to 0"),
b: z.number()
};
// NOTE: have tried about 30 different ways by passing in schema, props as a type
// using zod, without zod, with and without the type, using any, unknown, etc
// the issue seems to be in multiple sub-classes with the complex types used in the McpServer
this.tool(
"add",
"Adds two numbers",
addParamsSchema,
async (props: any, _extra: any): Promise<AiMcpResponseType> =>
{
const { a, b } = props;
logger.info("Adding numbers", props);
const response: AiMcpResponseType =
{
content:
[{
type: "text",
text: String(a + b)
}]
};
logger.info(`Adding ${a} + ${b} with result ${response.content[0].text}`);
return response;
}
)
}
public async stop(): Promise<void>
{
logger.info("Stopping AiCliMcpServer");
}
public async connect(transport: any): Promise<void>
{
// Store the original onmessage handler
const originalOnmessage = transport.onmessage;
// Replace with our own that logs the message
transport.onmessage = (message: any, extra?: any) =>
{
logger.info("Intercepted message:", message);
logger.info("Extra info:", extra);
// If this is a tool call, log the arguments
if(message.method === "tools/call" && message.params?.arguments)
{
logger.info("Tool call arguments:", message.params.arguments);
}
// Call the original handler
if(originalOnmessage)
{
return originalOnmessage(message, extra);
}
}
return await super.connect(transport);
}
}
i added extensive logs statements at each step in the process to see where the handoff of the message (parameters) was failing -->
👀 INFO: MCP Server started with UUID: 34e4a497-ed87-429a-9e30-096004f59e7e
👀 INFO: MCP server created
🎉 SUCCESS: 'AiCliMcpProvider' started
👀 INFO: Serving extension service 'ai-cli-service' routes
👀 INFO: Serving cli services with 1 providers
👀 INFO: Starting cli mcp server
🦠 DEBUG: description: Adds two numbers
🦠 DEBUG: firstArg: {
a: ZodNumber {
spa: [Function: bound safeParseAsync] AsyncFunction,
_def: { checks: [Array], typeName: 'ZodNumber', coerce: false },
parse: [Function: bound parse],
safeParse: [Function: bound safeParse],
parseAsync: [Function: bound parseAsync] AsyncFunction,
safeParseAsync: [Function: bound safeParseAsync] AsyncFunction,
refine: [Function: bound refine],
refinement: [Function: bound refinement],
superRefine: [Function: bound superRefine],
optional: [Function: bound optional],
nullable: [Function: bound nullable],
nullish: [Function: bound nullish],
array: [Function: bound array],
promise: [Function: bound promise],
or: [Function: bound or],
and: [Function: bound and],
transform: [Function: bound transform],
brand: [Function: bound brand],
default: [Function: bound default],
catch: [Function: bound catch],
describe: [Function: bound describe],
pipe: [Function: bound pipe],
readonly: [Function: bound readonly],
isNullable: [Function: bound isNullable],
isOptional: [Function: bound isOptional],
'~standard': { version: 1, vendor: 'zod', validate: [Function: validate] },
min: [Function: gte],
max: [Function: lte],
step: [Function: multipleOf]
},
b: ZodNumber {
spa: [Function: bound safeParseAsync] AsyncFunction,
_def: { checks: [], typeName: 'ZodNumber', coerce: false },
parse: [Function: bound parse],
safeParse: [Function: bound safeParse],
parseAsync: [Function: bound parseAsync] AsyncFunction,
safeParseAsync: [Function: bound safeParseAsync] AsyncFunction,
refine: [Function: bound refine],
refinement: [Function: bound refinement],
superRefine: [Function: bound superRefine],
optional: [Function: bound optional],
nullable: [Function: bound nullable],
nullish: [Function: bound nullish],
array: [Function: bound array],
promise: [Function: bound promise],
or: [Function: bound or],
and: [Function: bound and],
transform: [Function: bound transform],
brand: [Function: bound brand],
default: [Function: bound default],
catch: [Function: bound catch],
describe: [Function: bound describe],
pipe: [Function: bound pipe],
readonly: [Function: bound readonly],
isNullable: [Function: bound isNullable],
isOptional: [Function: bound isOptional],
'~standard': { version: 1, vendor: 'zod', validate: [Function: validate] },
min: [Function: gte],
max: [Function: lte],
step: [Function: multipleOf]
}
}
💩 HUH: firstArg is ToolAnnotations
💩 HUH: cb: [Function (anonymous)]
👀 INFO: Serving cli mcp routes
👀 INFO: Server routes:
👀 INFO: []
👀 INFO:
🎉 SUCCESS: Server "server-ts-web-studio running on http://localhost:3001
🦠 DEBUG: Inbound 'POST' request to '/cli/sse?clientId=client-1746478698778' {
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
},
jsonrpc: '2.0',
id: 0
}
👀 INFO: POST request received: {
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
},
jsonrpc: '2.0',
id: 0
}
👀 INFO: Session ID: undefined
👀 INFO: b
👀 INFO: c
👀 INFO: Connecting transport with id: undefined
👀 INFO: e
handleRequest:StreamableHTTPServerTransport:Received request: POST /cli/sse?clientId=client-1746478698778 {
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
},
jsonrpc: '2.0',
id: 0
}
post
handlePostRequest:POST request received: POST /cli/sse?clientId=client-1746478698778 {
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
},
jsonrpc: '2.0',
id: 0
}
handlePostRequest.2
handlePostRequest.4
handlePostRequest.6:messages: {
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
},
jsonrpc: '2.0',
id: 0
}
handlePostRequest.8
handlePostRequest.8.1:messages [
{
jsonrpc: '2.0',
id: 0,
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: [Object]
}
}
]
handlePostRequest.9
handlePostRequest.10
handlePostRequest.12
handlePostRequest.14
handlePostRequest.15
handlePostRequest.17
handlePostRequest.19
handlePostRequest.20
handlePostRequest.20.1
handlePostRequest.20.2:isJSONRPCRequest
handlePostRequest:21:message {
jsonrpc: '2.0',
id: 0,
method: 'initialize',
params: {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: { name: 'demo-client', version: '1.0.0', capabilities: {} }
}
}
handlePostRequest:22:onmessage [Function (anonymous)]
🦠 DEBUG: Inbound 'POST' request to '/cli/sse?clientId=client-1746478698778' { method: 'notifications/initialized', jsonrpc: '2.0' }
👀 INFO: POST request received: { method: 'notifications/initialized', jsonrpc: '2.0' }
👀 INFO: Session ID: ecddcfd6-90f2-42ac-abaf-78fb058ecc35
👀 INFO: a
👀 INFO: e
handleRequest:StreamableHTTPServerTransport:Received request: POST /cli/sse?clientId=client-1746478698778 { method: 'notifications/initialized', jsonrpc: '2.0' }
post
handlePostRequest:POST request received: POST /cli/sse?clientId=client-1746478698778 { method: 'notifications/initialized', jsonrpc: '2.0' }
handlePostRequest.2
handlePostRequest.4
handlePostRequest.6:messages: { method: 'notifications/initialized', jsonrpc: '2.0' }
handlePostRequest.8
handlePostRequest.8.1:messages [ { jsonrpc: '2.0', method: 'notifications/initialized' } ]
handlePostRequest.9
handlePostRequest.15
handlePostRequest.17
handlePostRequest.18
🦠 DEBUG: Inbound 'GET' request to '/cli/sse?clientId=client-1746478698778'
👀 INFO: f
handleRequest:StreamableHTTPServerTransport:Received request: GET /cli/sse?clientId=client-1746478698778 undefined
get
🦠 DEBUG: Inbound 'POST' request to '/cli/sse?clientId=client-1746478698778' {
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } },
jsonrpc: '2.0',
id: 1
}
👀 INFO: POST request received: {
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } },
jsonrpc: '2.0',
id: 1
}
👀 INFO: Session ID: ecddcfd6-90f2-42ac-abaf-78fb058ecc35
👀 INFO: a
👀 INFO: e
handleRequest:StreamableHTTPServerTransport:Received request: POST /cli/sse?clientId=client-1746478698778 {
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } },
jsonrpc: '2.0',
id: 1
}
post
handlePostRequest:POST request received: POST /cli/sse?clientId=client-1746478698778 {
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } },
jsonrpc: '2.0',
id: 1
}
handlePostRequest.2
handlePostRequest.4
handlePostRequest.6:messages: {
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } },
jsonrpc: '2.0',
id: 1
}
handlePostRequest.8
handlePostRequest.8.1:messages [
{
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: { name: 'add', arguments: [Object] }
}
]
handlePostRequest.9
handlePostRequest.15
handlePostRequest.17
handlePostRequest.19
handlePostRequest.20
handlePostRequest.20.1
handlePostRequest.20.2:isJSONRPCRequest
handlePostRequest:21:message {
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: { name: 'add', arguments: { a: 42, b: 58 } }
}
handlePostRequest:22:onmessage [Function (anonymous)]
👀 INFO: Adding numbers {
signal: AbortSignal { aborted: false },
sessionId: 'ecddcfd6-90f2-42ac-abaf-78fb058ecc35',
_meta: undefined,
sendNotification: [Function: sendNotification],
sendRequest: [Function: sendRequest],
authInfo: undefined,
requestId: 1
}
👀 INFO: Adding undefined + undefined with result NaN
was able to get it to work by adding this method to the base class to be more explicit and wrap the callback
public registerTool(name: string, description: string, callback: (args: any, extra: any) => any): void
{
logger.info(Registering tool '${name}'
);
// Define a callback that matches exactly what's needed
const wrappedCallback = (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) =>
{
// This signature matches the first overload - it receives only 'extra'
// Inside here, we'll check for arguments in the 'extra' parameter
const requestArgs = extra.requestId ? (extra as any)?.request?.params?.arguments || {} : {};
// Call the user's callback with extracted args
return callback(requestArgs, extra);
}
// Create the registered tool with a simplified structure
const toolDef =
{
description,
inputSchema: z.object({
a: z.number(),
b: z.number()
}),
callback: wrappedCallback, // Use our wrapper that matches expected signature
enabled: true,
disable: () => { toolDef.enabled = false; },
enable: () => { toolDef.enabled = true; },
remove: () => { delete this._registeredTools[name]; },
update: () => { /* empty */ }
}
// Add it to the tools registry using "as any" to bypass type checking
this._registeredTools[name] = toolDef as any;
// Initialize the handlers
this.setToolRequestHandlers();
this.sendToolListChanged();
logger.info(`Tool '${name}' registered successfully`);
}