import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
/**
* Defensive server resource with automatic cleanup.
* Ensures .close() is only called if the method exists and logs any errors.
*/
export interface ServerResource {
server: Server;
cleanup: () => Promise<void>;
}
/**
* Defensive HTTP bridge resource with automatic cleanup.
* Ensures .close() is only called if the method exists and logs any errors.
*/
export interface HttpBridgeResource {
port: number;
close: () => Promise<void>;
}
/**
* Helper to invoke the initialize handler on a server for testing.
* This is a test utility that works around the lack of a public API for initialization in tests.
* @param server - The MCP server instance
* @returns Promise resolving to the initialize response
*/
export async function invokeInitialize(server: Server): Promise<unknown> {
const handlers = (server as unknown as {
_requestHandlers?: Map<string, unknown>;
})._requestHandlers;
const handler = handlers?.get("initialize");
if (typeof handler !== "function") {
throw new Error("Initialize handler not registered");
}
const request = InitializeRequestSchema.parse({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-05-01",
capabilities: {},
clientInfo: { name: "test-suite", version: "1.0.0" }
}
});
const abortController = new AbortController();
return (handler as (
request: typeof request,
extra: {
signal: AbortSignal;
requestId: typeof request.id;
sendNotification: (notification: unknown) => Promise<void>;
sendRequest: (request: unknown) => Promise<unknown>;
}
) => Promise<unknown>)(request, {
signal: abortController.signal,
requestId: request.id,
sendNotification: async () => {},
sendRequest: async () => {
throw new Error("sendRequest is not supported in tests");
}
});
}
/**
* Defensively close a server instance.
* Only calls .close() if the method exists.
* Logs but does not throw on errors.
*/
export async function closeServerDefensively(server: unknown): Promise<void> {
if (!server) {
return;
}
if (typeof server === "object" && "close" in server && typeof server.close === "function") {
try {
await server.close();
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.warn("server_close_warning", {
message: "Error while closing server",
error: errorMsg
});
}
} else {
console.warn("server_close_skipped", {
message: "Server does not have a .close() method",
serverType: typeof server,
hasClose: server && typeof server === "object" ? "close" in server : false
});
}
}
/**
* Defensively close an HTTP bridge instance.
* Only calls .close() if the method exists.
* Logs but does not throw on errors.
*/
export async function closeHttpBridgeDefensively(httpBridge: unknown): Promise<void> {
if (!httpBridge) {
return;
}
if (typeof httpBridge === "object" && "close" in httpBridge && typeof httpBridge.close === "function") {
try {
await httpBridge.close();
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.warn("http_bridge_close_warning", {
message: "Error while closing HTTP bridge",
error: errorMsg
});
}
} else {
console.warn("http_bridge_close_skipped", {
message: "HTTP bridge does not have a .close() method",
bridgeType: typeof httpBridge,
hasClose: httpBridge && typeof httpBridge === "object" ? "close" in httpBridge : false
});
}
}
/**
* Get a dynamic port for testing.
* Always returns 0 to let the OS assign a random available port.
* This prevents port collisions when running multiple tests in parallel.
*/
export function getDynamicPort(): number {
return 0;
}
/**
* Create a cleanup handler that can be used in try-finally blocks.
* Ensures all resources are cleaned up even if some fail.
*/
export function createCleanupHandler(...cleanupFns: Array<() => Promise<void>>): () => Promise<void> {
return async () => {
const errors: Error[] = [];
for (const fn of cleanupFns) {
try {
await fn();
} catch (error) {
if (error instanceof Error) {
errors.push(error);
}
}
}
if (errors.length > 0) {
console.warn("cleanup_errors", {
message: `${errors.length} error(s) during cleanup`,
errors: errors.map(e => e.message)
});
}
};
}