UNPKG

@trendmoon/mcp-server

Version:

TrendMoon MCP Server - Library and Standalone Server for Cryptocurrency and Social Data

246 lines β€’ 10.6 kB
import express from "express"; import { randomUUID } from "node:crypto"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; export class HttpTransport { app; config; server; isStarted = false; transports = {}; mcpServer; constructor(config = {}) { this.config = { port: 3000, host: '0.0.0.0', enableCors: true, ...config }; this.app = express(); this.setupMiddleware(); this.setupRoutes(); console.log('πŸ”§ HTTP Transport configured with Streamable HTTP...'); } setupMiddleware() { this.app.use(express.json()); if (this.config.enableCors) { this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, mcp-session-id'); if (req.method === 'OPTIONS') { res.sendStatus(200); return; } next(); }); } } setupRoutes() { // Handle POST requests for client-to-server communication this.app.post('/mcp', async (req, res) => { if (!this.mcpServer) { res.status(503).json({ jsonrpc: '2.0', error: { code: -32000, message: 'MCP Server not connected', }, id: null, }); return; } try { // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && this.transports[sessionId]) { // Reuse existing transport transport = this.transports[sessionId]; console.log('πŸ”„ Reusing existing transport for session:', sessionId); } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request console.log('πŸ†• Creating new MCP session...'); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID this.transports[sessionId] = transport; console.log('βœ… New MCP session initialized:', sessionId); } }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { console.log('πŸ”Œ Transport closed for session:', transport.sessionId); delete this.transports[transport.sessionId]; } }; // Connect to the MCP server console.log('πŸ”— Connecting MCP server to new transport...'); await this.mcpServer.connect(transport); console.log('βœ… MCP server connected to transport'); } else { // Invalid request console.error('❌ Invalid request - no session ID and not an initialize request'); res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided and not an initialize request', }, id: null, }); return; } // Handle the request console.log('πŸ“¨ Handling MCP request...'); await transport.handleRequest(req, res, req.body); } catch (error) { console.error('❌ Error handling MCP POST request:', error); res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: `Internal server error: ${error.message}`, }, id: null, }); } }); // Reusable handler for GET and DELETE requests const handleSessionRequest = async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !this.transports[sessionId]) { console.error('❌ Invalid or missing session ID:', sessionId); res.status(400).send('Invalid or missing session ID'); return; } const transport = this.transports[sessionId]; console.log(`πŸ“‘ Handling ${req.method} request for session:`, sessionId); await transport.handleRequest(req, res); }; // Handle GET requests for server-to-client notifications via SSE this.app.get('/mcp', handleSessionRequest); // Handle DELETE requests for session termination this.app.delete('/mcp', handleSessionRequest); // Endpoint racine pour les requΓͺtes directes (compatibilitΓ©) this.app.post('/', async (req, res) => { console.log('πŸ“¨ Direct MCP request received, redirecting to /mcp endpoint...'); // Ajouter le header pour traiter comme une nouvelle session si c'est un initialize if (isInitializeRequest(req.body)) { console.log('πŸ†• Initialize request detected, forwarding to /mcp'); // Forward to /mcp endpoint req.url = '/mcp'; return this.app._router.handle(req, res, () => { }); } else { res.status(400).json({ error: 'Direct MCP requests require session management', message: 'Please send initialize request to /mcp endpoint first' }); } }); // Health check this.app.get('/health', (req, res) => { res.json({ status: this.mcpServer ? 'ready' : 'not_connected', timestamp: new Date().toISOString(), server: 'TrendMoon MCP Server', transport: 'Streamable HTTP', sessions: Object.keys(this.transports).length, mcpConnected: !!this.mcpServer }); }); // Info endpoint this.app.get('/info', (req, res) => { res.json({ name: 'TrendMoon MCP Server', version: '1.0.0', transport: 'Streamable HTTP', endpoints: { mcp: '/mcp (POST, GET, DELETE)', health: '/health', info: '/info' }, activeSessions: Object.keys(this.transports).length, protocol: 'MCP Streamable HTTP Transport', mcpConnected: !!this.mcpServer, usage: { initialize: 'POST /mcp with initialize request', requests: 'POST /mcp with mcp-session-id header', notifications: 'GET /mcp with mcp-session-id header (SSE)', terminate: 'DELETE /mcp with mcp-session-id header' } }); }); } async start() { if (this.isStarted) { console.log("ℹ️ HTTP Transport already started, skipping..."); return; } return new Promise((resolve, reject) => { try { const port = this.config.port ?? 3000; const host = this.config.host ?? '0.0.0.0'; console.log(`πŸš€ Starting Streamable HTTP server on ${host}:${port}...`); this.server = this.app.listen(port, host, () => { this.isStarted = true; console.log(`πŸš€ MCP Streamable HTTP Server running on http://${host}:${port}`); console.log(`πŸ“‘ MCP endpoint: http://${host}:${port}/mcp`); console.log(`πŸ’“ Health check: http://${host}:${port}/health`); console.log(`ℹ️ For MCP Inspector, use: http://localhost:${port}/mcp`); console.log(`πŸ“˜ Protocol: Streamable HTTP (modern MCP transport)`); resolve(); }); this.server.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.error(`❌ Port ${port} is already in use. Please try a different port.`); reject(new Error(`Port ${port} is already in use`)); } else { console.error('❌ Server error:', error); reject(error); } }); } catch (error) { console.error('❌ Failed to start HTTP server:', error); reject(error); } }); } setMcpServer(mcpServer) { this.mcpServer = mcpServer; console.log('πŸ”— MCP Server connected to Streamable HTTP transport'); } close() { console.log('πŸ”Œ Closing Streamable HTTP transport...'); // Fermer toutes les sessions Object.entries(this.transports).forEach(([sessionId, transport]) => { try { console.log('πŸ”Œ Closing session:', sessionId); transport.close?.(); } catch (error) { console.error('❌ Error closing transport:', error); } }); this.transports = {}; if (this.server) { this.isStarted = false; return new Promise((resolve) => { this.server.close(() => { console.log('βœ… Streamable HTTP transport closed'); resolve(); }); }); } return Promise.resolve(); } } //# sourceMappingURL=HttpTransport.js.map