mcp-framework
Version:
Framework for building Model Context Protocol (MCP) servers in Typescript
250 lines (249 loc) • 9.97 kB
JavaScript
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';
import { AbstractTransport } from '../base.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { logger } from '../../core/Logger.js';
import { handleAuthentication } from '../utils/auth-handler.js';
import { initializeOAuthMetadata } from '../utils/oauth-metadata.js';
export class HttpStreamTransport extends AbstractTransport {
type = 'http-stream';
_isRunning = false;
_port;
_server;
_endpoint;
_enableJsonResponse = false;
_config;
_oauthMetadata;
_transports = {};
constructor(config = {}) {
super();
this._config = config;
this._port = config.port || 8080;
this._endpoint = config.endpoint || '/mcp';
this._enableJsonResponse = config.responseMode === 'batch';
// Initialize OAuth metadata if OAuth provider is configured
this._oauthMetadata = initializeOAuthMetadata(this._config.auth, 'HTTP Stream');
logger.debug(`HttpStreamTransport configured with: ${JSON.stringify({
port: this._port,
endpoint: this._endpoint,
responseMode: config.responseMode,
batchTimeout: config.batchTimeout,
maxMessageSize: config.maxMessageSize,
auth: config.auth ? {
provider: config.auth.provider.constructor.name,
endpoints: config.auth.endpoints
} : undefined,
cors: config.cors ? true : false,
})}`);
}
async start() {
if (this._isRunning) {
throw new Error('HttpStreamTransport already started');
}
return new Promise((resolve, reject) => {
this._server = createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'OPTIONS') {
this.setCorsHeaders(res, true);
res.writeHead(204).end();
return;
}
this.setCorsHeaders(res);
if (req.method === 'GET' && url.pathname === '/.well-known/oauth-protected-resource') {
if (this._oauthMetadata) {
this._oauthMetadata.serve(res);
}
else {
res.writeHead(404).end('Not Found');
}
return;
}
if (url.pathname === this._endpoint) {
await this.handleMcpRequest(req, res);
}
else {
res.writeHead(404).end('Not Found');
}
}
catch (error) {
logger.error(`Error handling request: ${error}`);
if (!res.headersSent) {
res.writeHead(500).end('Internal Server Error');
}
}
});
this._server.on('error', (error) => {
logger.error(`HTTP server error: ${error}`);
this._onerror?.(error);
if (!this._isRunning) {
reject(error);
}
});
this._server.on('close', () => {
logger.info('HTTP server closed');
this._isRunning = false;
this._onclose?.();
});
this._server.listen(this._port, () => {
logger.info(`HTTP server listening on port ${this._port}, endpoint ${this._endpoint}`);
this._isRunning = true;
resolve();
});
});
}
async handleMcpRequest(req, res) {
const sessionId = req.headers['mcp-session-id'];
let transport;
// Determine if this is an initialize request (needs body parsing)
const body = req.method === 'POST' ? await this.readRequestBody(req) : null;
const isInitialize = !sessionId && body && isInitializeRequest(body);
// Perform authentication check once at the beginning
const authEndpoint = isInitialize ? 'sse' : 'messages';
if (this._config.auth?.endpoints?.[authEndpoint] !== false) {
const isAuthenticated = await handleAuthentication(req, res, this._config.auth, isInitialize ? 'initialize' : 'message');
if (!isAuthenticated)
return;
}
// Handle different request scenarios
if (sessionId && this._transports[sessionId]) {
// Existing session
transport = this._transports[sessionId];
logger.debug(`Reusing existing session: ${sessionId}`);
}
else if (isInitialize) {
// New session initialization
logger.info('Creating new session for initialization request');
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
logger.info(`Session initialized: ${sessionId}`);
this._transports[sessionId] = transport;
},
enableJsonResponse: this._enableJsonResponse,
});
transport.onclose = () => {
if (transport.sessionId) {
logger.info(`Transport closed for session: ${transport.sessionId}`);
delete this._transports[transport.sessionId];
}
};
transport.onerror = (error) => {
logger.error(`Transport error for session: ${error}`);
if (transport.sessionId) {
delete this._transports[transport.sessionId];
}
};
transport.onmessage = async (message) => {
if (this._onmessage) {
await this._onmessage(message);
}
};
await transport.handleRequest(req, res, body);
return;
}
else if (!sessionId) {
// No session ID and not an initialize request
this.sendError(res, 400, -32000, 'Bad Request: No valid session ID provided');
return;
}
else {
// Session ID provided but not found
this.sendError(res, 404, -32001, 'Session not found');
return;
}
// Existing session - handle request
await transport.handleRequest(req, res, body);
}
async readRequestBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
try {
const parsed = body ? JSON.parse(body) : null;
resolve(parsed);
}
catch (error) {
reject(error);
}
});
req.on('error', reject);
});
}
setCorsHeaders(res, includeMaxAge = false) {
if (!this._config.cors)
return;
const cors = this._config.cors;
res.setHeader('Access-Control-Allow-Origin', cors.allowOrigin || '*');
res.setHeader('Access-Control-Allow-Methods', cors.allowMethods || 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', cors.allowHeaders || 'Content-Type, Authorization, Mcp-Session-Id');
res.setHeader('Access-Control-Expose-Headers', cors.exposeHeaders || 'Content-Type, Authorization, Mcp-Session-Id');
if (includeMaxAge) {
res.setHeader('Access-Control-Max-Age', cors.maxAge || '86400');
}
}
sendError(res, status, code, message) {
if (res.headersSent)
return;
res.writeHead(status).end(JSON.stringify({
jsonrpc: '2.0',
error: {
code,
message,
},
id: null,
}));
}
async send(message) {
if (!this._isRunning) {
logger.warn('Attempted to send message, but HTTP transport is not running');
return;
}
const activeSessions = Object.entries(this._transports);
if (activeSessions.length === 0) {
logger.warn('No active sessions to send message to');
return;
}
logger.debug(`Broadcasting message to ${activeSessions.length} sessions: ${JSON.stringify(message)}`);
const failedSessions = [];
for (const [sessionId, transport] of activeSessions) {
try {
await transport.send(message);
}
catch (error) {
logger.error(`Error sending message to session ${sessionId}: ${error}`);
failedSessions.push(sessionId);
}
}
if (failedSessions.length > 0) {
failedSessions.forEach((sessionId) => delete this._transports[sessionId]);
logger.warn(`Failed to send message to ${failedSessions.length} sessions.`);
}
}
async close() {
if (!this._isRunning) {
return;
}
for (const transport of Object.values(this._transports)) {
try {
await transport.close();
}
catch (error) {
logger.error(`Error closing transport: ${error}`);
}
}
this._transports = {};
if (this._server) {
this._server.close();
this._server = undefined;
}
this._isRunning = false;
}
isRunning() {
return this._isRunning;
}
}