UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

332 lines (331 loc) 13 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StatelessStreamableHTTPTransport = exports.StatelessHttpServerTransport = void 0; const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const node_crypto_1 = require("node:crypto"); const raw_body_1 = __importDefault(require("raw-body")); const content_type_1 = __importDefault(require("content-type")); const base_streamable_http_1 = require("./base-streamable-http"); const home_1 = __importDefault(require("../../templates/home")); const http_context_1 = require("./http-context"); const oauth_1 = require("../../../auth/oauth"); const cli_icons_1 = require("../../../utils/cli-icons"); const setup_cors_1 = require("./setup-cors"); // no session management, POST only class StatelessHttpServerTransport extends base_streamable_http_1.BaseHttpServerTransport { debug; bodySizeLimit; _started = false; _singleResponseCollectors = new Map(); _requestToCollectorMapping = new Map(); constructor(debug, bodySizeLimit) { super(); this.debug = debug; this.bodySizeLimit = bodySizeLimit; } // avoid restarting // sort of singleton async start() { if (this._started) { throw new Error("Transport already started"); } this._started = true; } async close() { this._singleResponseCollectors?.forEach((collector) => { if (!collector.res.headersSent) { collector.res.writeHead(503).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Service unavailable: Server shutting down", }, id: null, })); } }); this._singleResponseCollectors?.clear(); this._requestToCollectorMapping?.clear(); } async send(message) { const requestId = message.id; if (requestId === undefined || requestId === null) { // In stateless mode, we can't handle notifications without request IDs if (this.debug) { console.log("[StatelessHTTP] Dropping notification without request ID"); } return; } const collectorId = this._requestToCollectorMapping?.get(requestId); if (collectorId) { const collector = this._singleResponseCollectors?.get(collectorId); if (collector && (message.result !== undefined || message.error !== undefined)) { collector.responses.push(message); collector.requestIds.delete(requestId); if (collector.requestIds.size === 0) { const headers = { "Content-Type": "application/json", }; const responseBody = collector.responses.length === 1 ? collector.responses[0] : collector.responses; collector.res .writeHead(200, headers) .end(JSON.stringify(responseBody)); this._singleResponseCollectors?.delete(collectorId); for (const response of collector.responses) { if (response.id !== undefined && response.id !== null) { this._requestToCollectorMapping?.delete(response.id); } } } } } } async handleRequest(req, res, parsedBody) { // Only support POST in stateless mode if (req.method !== "POST") { res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed.", }, id: null, })); return; } await this.handlePOST(req, res, parsedBody); } async handlePOST(req, res, parsedBody) { try { const acceptHeader = req.headers.accept; const acceptsJson = acceptHeader?.includes("application/json"); if (!acceptsJson) { res.writeHead(406).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Not Acceptable: Client must accept application/json", }, id: null, })); return; } let rawMessage; if (parsedBody !== undefined) { rawMessage = parsedBody; } else { const ct = req.headers["content-type"]; if (!ct || !ct.includes("application/json")) { 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; } const parsedCt = content_type_1.default.parse(ct); const body = await (0, raw_body_1.default)(req, { limit: this.bodySizeLimit, encoding: parsedCt.parameters.charset ?? "utf-8", }); rawMessage = JSON.parse(body.toString()); } const messages = Array.isArray(rawMessage) ? rawMessage : [rawMessage]; const hasRequests = messages.some((msg) => msg.method && msg.id !== undefined); if (!hasRequests) { // Handle notifications (no response expected) res.writeHead(202).end(); return; } // Handle requests that expect responses const requestIds = messages .filter((msg) => msg.method && msg.id !== undefined) .map((msg) => msg.id); if (requestIds.length === 0) { res.writeHead(202).end(); return; } const responseCollector = []; const expectedResponses = requestIds.length; const collectorId = (0, node_crypto_1.randomUUID)(); this._singleResponseCollectors = this._singleResponseCollectors || new Map(); this._singleResponseCollectors.set(collectorId, { res, requestIds: new Set(requestIds), responses: responseCollector, expectedCount: expectedResponses, }); for (const requestId of requestIds) { this._requestToCollectorMapping = this._requestToCollectorMapping || new Map(); this._requestToCollectorMapping.set(requestId, collectorId); } // MCP SDK transport interface mandatory for (const message of messages) { if (this.onmessage) { this.onmessage(message); } } } catch (error) { res.writeHead(400).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32700, message: "Parse error", data: String(error), }, id: null, })); } } } exports.StatelessHttpServerTransport = StatelessHttpServerTransport; // Stateless HTTP Transport wrapper class StatelessStreamableHTTPTransport { app; server; port; endpoint; debug; options; createServerFn; corsConfig; oauthProxy; middlewares; constructor(createServerFn, options = {}, corsConfig = {}, oauthConfig, middlewares) { this.options = { ...options, }; this.app = (0, express_1.default)(); this.server = http_1.default.createServer(this.app); this.port = options.port ?? parseInt(process.env.PORT || "3001", 10); this.endpoint = options.endpoint ?? "/mcp"; this.debug = options.debug ?? false; this.createServerFn = createServerFn; this.corsConfig = corsConfig; this.middlewares = middlewares; // setup oauth proxy if configuration is provided if (oauthConfig) { this.oauthProxy = (0, oauth_1.createOAuthProxy)(oauthConfig); } this.setupMiddleware(options.bodySizeLimit || "10mb"); this.setupRoutes(); } log(message, ...args) { if (this.debug) { console.log(`[StatelessHTTP] ${message}`, ...args); } } setupMiddleware(bodySizeLimit) { this.app.use((req, res, next) => { const cors = this.corsConfig; // set cors headers dynamically (0, setup_cors_1.setResponseCorsHeaders)(cors, res); next(); }); this.app.use(express_1.default.json({ limit: bodySizeLimit })); this.app.use((req, _res, next) => { this.log(`${req.method} ${req.path}`); next(); }); } setupRoutes() { this.app.get("/health", (_req, res) => { res.status(200).json({ status: "ok", transport: "streamable-http", mode: "stateless", }); }); this.app.get("/", (_req, res) => { res.send((0, home_1.default)(this.endpoint)); }); if (this.oauthProxy) { this.app.use(this.oauthProxy.router); } // isolate requests context this.app.use((req, _res, next) => { const id = (0, node_crypto_1.randomUUID)(); (0, http_context_1.httpContextProvider)({ id, headers: req.headers }, () => { next(); }); }); // routes beyond this point get intercepted by the middleware if (this.middlewares && this.middlewares.length > 0) { this.app.use(this.middlewares); } if (this.oauthProxy) { this.app.use(this.oauthProxy.middleware); } this.app.use(this.endpoint, async (req, res) => { await this.handleStatelessRequest(req, res); }); } async handleStatelessRequest(req, res) { try { // Create new instances for complete isolation const server = await this.createServerFn(); const transport = new StatelessHttpServerTransport(this.debug, this.options.bodySizeLimit || "10mb"); // cleanup when request/connection closes res.on("close", () => { transport.close(); server.close(); }); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("[HTTP-server] Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } } start() { const host = this.options.host || "127.0.0.1"; this.server.listen(this.port, host, () => { console.log(`${cli_icons_1.greenCheck} MCP Server running on http://${host}:${this.port}${this.endpoint}`); if (this.oauthProxy && this.debug) { console.log(`🔐 OAuth endpoints available:`); console.log(` Discovery: http://${host}:${this.port}/.well-known/oauth-authorization-server`); console.log(` Authorize: http://${host}:${this.port}/oauth2/authorize`); console.log(` Token: http://${host}:${this.port}/oauth2/token`); console.log(` Revoke: http://${host}:${this.port}/oauth2/revoke`); console.log(` Introspect: http://${host}:${this.port}/oauth2/introspect`); } this.setupShutdownHandlers(); }); } setupShutdownHandlers() { process.on("SIGINT", this.shutdown.bind(this)); process.on("SIGTERM", this.shutdown.bind(this)); } shutdown() { this.log("Shutting down server"); this.server.close(); process.exit(0); } } exports.StatelessStreamableHTTPTransport = StatelessStreamableHTTPTransport;