@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
332 lines (331 loc) • 13 kB
JavaScript
"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;