@cyanheads/jinaai-mcp-server
Version:
A Model Context Protocol (MCP) server that provides intelligent web reading capabilities using the Jina AI Reader API. It extracts clean, LLM-ready content from any URL.
153 lines • 6.76 kB
JavaScript
/**
* @fileoverview Implements a stateless transport manager for the MCP SDK.
*
* This manager handles single, ephemeral MCP operations. For each incoming request,
* it dynamically creates a temporary McpServer and transport instance, processes the
* request, and then immediately schedules the resources for cleanup. This approach
* is ideal for simple, one-off tool calls that do not require persistent session state.
*
* The key challenge addressed here is bridging the Node.js-centric MCP SDK with
* modern, Web Standards-based frameworks like Hono. This is achieved by deferring
* resource cleanup until the response stream has been fully consumed by the web
* framework, preventing premature closure and truncated responses.
*
* @module src/mcp-server/transports/core/statelessTransportManager
*/
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { Readable } from "stream";
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
import { BaseTransportManager } from "./baseTransportManager.js";
import { HonoStreamResponse } from "./honoNodeBridge.js";
import { convertNodeHeadersToWebHeaders } from "./headerUtils.js";
/**
* Manages ephemeral, single-request MCP operations.
*/
export class StatelessTransportManager extends BaseTransportManager {
/**
* Handles a single, stateless MCP request.
*
* This method orchestrates the creation of temporary server and transport instances,
* handles the request, and ensures resources are cleaned up only after the
* response stream is closed.
*
* @param headers - The incoming request headers.
* @param body - The parsed body of the request.
* @param context - The request context for logging and tracing.
* @returns A promise resolving to a streaming TransportResponse.
*/
async handleRequest(headers, body, context) {
const opContext = {
...context,
operation: "StatelessTransportManager.handleRequest",
};
logger.debug("Creating ephemeral server instance for stateless request.", opContext);
let server;
let transport;
try {
// 1. Create ephemeral instances for this request.
server = await this.createServerInstanceFn();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
onsessioninitialized: undefined,
});
await server.connect(transport);
logger.debug("Ephemeral server connected to transport.", opContext);
// 2. Set up the Node.js-to-Web stream bridge.
const mockReq = {
headers,
method: "POST",
};
const mockResBridge = new HonoStreamResponse();
// 3. Defer cleanup until the stream is fully processed.
// This is the critical fix to prevent premature resource release.
this.setupDeferredCleanup(mockResBridge, server, transport, opContext);
// 4. Process the request using the MCP transport.
const mockRes = mockResBridge;
await transport.handleRequest(mockReq, mockRes, body);
logger.info("Stateless request handled successfully.", opContext);
// 5. Convert headers and create the final streaming response.
const responseHeaders = convertNodeHeadersToWebHeaders(mockRes.getHeaders());
const webStream = Readable.toWeb(mockResBridge);
return {
type: "stream",
headers: responseHeaders,
statusCode: mockRes.statusCode,
stream: webStream,
};
}
catch (error) {
// If an error occurs before the stream is returned, we must clean up immediately.
if (server || transport) {
this.cleanup(server, transport, opContext);
}
throw ErrorHandler.handleError(error, {
operation: "StatelessTransportManager.handleRequest",
context: opContext,
rethrow: true,
});
}
}
/**
* Attaches listeners to the response stream to trigger resource cleanup
* only after the stream has been fully consumed or has errored.
*
* @param stream - The response stream bridge.
* @param server - The ephemeral McpServer instance.
* @param transport - The ephemeral transport instance.
* @param context - The request context for logging.
*/
setupDeferredCleanup(stream, server, transport, context) {
let cleanedUp = false;
const cleanupFn = (error) => {
if (cleanedUp)
return;
cleanedUp = true;
if (error) {
logger.warning("Stream ended with an error, proceeding to cleanup.", {
...context,
error: error.message,
});
}
// Cleanup is fire-and-forget.
this.cleanup(server, transport, context);
};
// 'close' is the most reliable event, firing on both normal completion and abrupt termination.
stream.on("close", () => cleanupFn());
stream.on("error", (err) => cleanupFn(err));
}
/**
* Performs the actual cleanup of ephemeral resources.
* This method is designed to be "fire-and-forget".
*/
cleanup(server, transport, context) {
const opContext = {
...context,
operation: "StatelessTransportManager.cleanup",
};
logger.debug("Scheduling cleanup for ephemeral resources.", opContext);
Promise.all([transport?.close(), server?.close()])
.then(() => {
logger.debug("Ephemeral resources cleaned up successfully.", opContext);
})
.catch((cleanupError) => {
logger.warning("Error during stateless resource cleanup.", {
...opContext,
error: cleanupError instanceof Error
? cleanupError.message
: String(cleanupError),
});
});
}
/**
* Shuts down the manager. For the stateless manager, this is a no-op
* as there are no persistent resources to manage.
*/
async shutdown() {
const context = requestContextService.createRequestContext({
operation: "StatelessTransportManager.shutdown",
});
logger.info("Stateless transport manager shutdown - no persistent resources to clean up.", context);
return Promise.resolve();
}
}
//# sourceMappingURL=statelessTransportManager.js.map