@trendmoon/mcp-server
Version:
TrendMoon MCP Server - Library and Standalone Server for Cryptocurrency and Social Data
246 lines β’ 10.6 kB
JavaScript
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