Skip to main content
Glama
http.ts10.7 kB
/** * 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(); }); }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JacobFV/mcp-fullstack'

If you have feedback or need assistance with the MCP directory API, please join our Discord server