/**
* HTTP transport layer for MCP server
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import multer from 'multer';
import { createServer, Server as HttpServer } from 'http';
import { WebSocket, WebSocketServer } from 'ws';
import { MCPServer } from '../core/server.js';
import { MCPRequest, MCPResponse } from '../types/core.js';
export interface HttpTransportConfig {
enableWebSocket?: boolean;
enableFileUpload?: boolean;
uploadLimitMb?: number;
corsOrigins?: string[];
}
export class HttpTransport {
private app: express.Application;
private httpServer: HttpServer;
private wsServer?: WebSocketServer;
private mcpServer: MCPServer;
private config: HttpTransportConfig;
constructor(mcpServer: MCPServer, config: HttpTransportConfig = {}) {
this.mcpServer = mcpServer;
this.config = {
enableWebSocket: true,
enableFileUpload: true,
uploadLimitMb: 50,
corsOrigins: ['http://localhost:3001'], // Dashboard default port
...config
};
this.app = express();
this.httpServer = createServer(this.app);
this.setupMiddleware();
this.setupRoutes();
if (this.config.enableWebSocket) {
this.setupWebSocket();
}
}
private setupMiddleware(): void {
// Security
this.app.use(helmet({
crossOriginEmbedderPolicy: false, // Allow image attachments
crossOriginResourcePolicy: { policy: 'cross-origin' }
}));
// CORS
this.app.use(cors({
origin: this.config.corsOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-request-id']
}));
// Logging
this.app.use(morgan('combined'));
// Body parsing with increased limit
this.app.use(express.json({ limit: `${this.config.uploadLimitMb}mb` }));
this.app.use(express.urlencoded({ extended: true, limit: `${this.config.uploadLimitMb}mb` }));
// File upload support
if (this.config.enableFileUpload) {
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: this.config.uploadLimitMb! * 1024 * 1024
}
});
this.app.use('/upload', upload.any());
}
}
private setupRoutes(): void {
// Health check
this.app.get('/health', (req, res) => {
const stats = this.mcpServer.getRegistry().getStats();
res.json({
status: 'ok',
server: {
running: this.mcpServer.isRunning(),
uptime: process.uptime()
},
registry: stats
});
});
// MCP JSON-RPC endpoint
this.app.post('/mcp', async (req, res) => {
try {
const mcpRequest: MCPRequest = {
jsonrpc: '2.0',
id: req.body.id || Date.now(),
method: req.body.method,
params: req.body.params,
x_request_id: req.headers['x-request-id'] as string
};
const response = await this.mcpServer.handleRequest(mcpRequest);
// Handle image attachments
if (this.hasImageAttachments(response)) {
await this.handleImageResponse(response, res);
} else {
res.json(response);
}
} catch (error) {
console.error('HTTP request error:', error);
res.status(500).json({
jsonrpc: '2.0',
id: req.body.id || null,
error: {
code: 'UPSTREAM_ERROR',
message: error instanceof Error ? error.message : 'Unknown error'
}
});
}
});
// Tools listing endpoint
this.app.get('/tools', (req, res) => {
const tools = this.mcpServer.getRegistry().getAllTools();
res.json({ tools });
});
// Sessions endpoint
this.app.get('/sessions', (req, res) => {
const sessions = this.mcpServer.getRegistry().getAllSessions();
const stats = this.mcpServer.getRegistry().getStats();
res.json({ sessions, stats });
});
// Recent logs endpoint
this.app.get('/logs', (req, res) => {
const limit = parseInt(req.query.limit as string) || 100;
const logs = this.mcpServer.getRecentLogs(limit);
res.json({ logs });
});
// Static file serving for dashboard assets
this.app.use('/assets', express.static('dashboard/dist/assets'));
// Serve dashboard SPA
this.app.get('*', (req, res) => {
// For API routes, return 404
if (req.path.startsWith('/api') || req.path.startsWith('/mcp')) {
res.status(404).json({ error: 'Not found' });
return;
}
// Serve dashboard index.html for all other routes
res.sendFile('dashboard/dist/index.html', { root: process.cwd() });
});
// Error handling
this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Express error:', err);
res.status(500).json({
error: {
code: 'UPSTREAM_ERROR',
message: err.message || 'Internal server error'
}
});
});
}
private hasImageAttachments(response: MCPResponse): boolean {
if (!response.result) return false;
const result = response.result;
if (typeof result !== 'object') return false;
// Check for image_name fields which indicate attachments
return Object.keys(result).some(key => key === 'image_name' || key.endsWith('_image_name'));
}
private async handleImageResponse(response: MCPResponse, res: express.Response): Promise<void> {
// For image responses, we need to return a multipart response
// Image attachment handling via JSON
const result = response.result;
const images: Array<{ name: string; data: Buffer; mimeType: string }> = [];
// Extract image data from result
this.extractImageData(result, images);
if (images.length === 1) {
// Single image - return as binary with JSON metadata in headers
const image = images[0];
res.set({
'Content-Type': image.mimeType,
'X-MCP-Response': JSON.stringify({
...response,
result: {
...result,
// Remove binary data from JSON
...this.stripImageData(result)
}
})
});
res.send(image.data);
} else if (images.length > 1) {
// Multiple images - return JSON with base64 encoded images
const resultWithBase64 = { ...result };
for (const image of images) {
const baseName = image.name.replace(/\.(png|jpg|jpeg|gif|webp)$/i, '');
resultWithBase64[`${baseName}_image_base64`] = image.data.toString('base64');
resultWithBase64[`${baseName}_mime_type`] = image.mimeType;
}
res.json({
...response,
result: this.stripImageData(resultWithBase64)
});
} else {
// No images found, return normal JSON
res.json(response);
}
}
private extractImageData(obj: any, images: Array<{ name: string; data: Buffer; mimeType: string }>): void {
if (!obj || typeof obj !== 'object') return;
for (const [key, value] of Object.entries(obj)) {
if (key.endsWith('_image_data') && Buffer.isBuffer(value)) {
const baseName = key.replace('_image_data', '');
const name = obj[`${baseName}_image_name`] || obj['image_name'] || `${baseName}.png`;
const mimeType = obj[`${baseName}_mime_type`] || 'image/png';
images.push({ name, data: value, mimeType });
}
}
}
private stripImageData(obj: any): any {
if (!obj || typeof obj !== 'object') return obj;
const result = { ...obj };
for (const key of Object.keys(result)) {
if (key.endsWith('_image_data')) {
delete result[key];
}
}
return result;
}
private setupWebSocket(): void {
this.wsServer = new WebSocketServer({
server: this.httpServer,
path: '/ws'
});
this.wsServer.on('connection', (ws: WebSocket, req) => {
console.log('WebSocket connection established');
ws.on('message', async (data) => {
try {
const mcpRequest = JSON.parse(data.toString());
const response = await this.mcpServer.handleRequest(mcpRequest);
ws.send(JSON.stringify(response));
} catch (error) {
const errorResponse = {
jsonrpc: '2.0',
id: null,
error: {
code: 'UPSTREAM_ERROR',
message: error instanceof Error ? error.message : 'Unknown error'
}
};
ws.send(JSON.stringify(errorResponse));
}
});
ws.on('close', () => {
console.log('WebSocket connection closed');
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
// Broadcast tool calls and session events to connected clients
this.mcpServer.on('tool_call', (log) => {
this.broadcast('tool_call', log);
});
this.mcpServer.getRegistry().on('session_created', (sessionId, type) => {
this.broadcast('session_created', { sessionId, type });
});
this.mcpServer.getRegistry().on('session_destroyed', (sessionId, type) => {
this.broadcast('session_destroyed', { sessionId, type });
});
}
private broadcast(event: string, data: any): void {
if (!this.wsServer) return;
const message = JSON.stringify({ event, data });
this.wsServer.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
async start(): Promise<void> {
return new Promise((resolve, reject) => {
const serverConfig = this.mcpServer.getConfig();
this.httpServer.listen(serverConfig.port, serverConfig.host, () => {
console.log(`MCP Server listening on http://${serverConfig.host}:${serverConfig.port}`);
if (this.config.enableWebSocket) {
console.log(`WebSocket server available at ws://${serverConfig.host}:${serverConfig.port}/ws`);
}
this.mcpServer.setRunning(true);
resolve();
});
this.httpServer.on('error', (error) => {
console.error('HTTP server error:', error);
reject(error);
});
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
this.mcpServer.setRunning(false);
if (this.wsServer) {
this.wsServer.close();
}
this.httpServer.close(() => {
console.log('MCP Server stopped');
resolve();
});
});
}
}