@sei-js/mcp-server
Version:
Model Context Protocol (MCP) server for interacting with EVM-compatible networks
181 lines (180 loc) • 7.3 kB
JavaScript
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import cors from 'cors';
import express from 'express';
import startServer from './server.js';
// Environment variables - hardcoded values
const PORT = 3001;
const HOST = '0.0.0.0';
console.error(`Configured to listen on ${HOST}:${PORT}`);
// Setup Express
const app = express();
app.use(express.json());
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
exposedHeaders: ['Content-Type', 'Access-Control-Allow-Origin']
}));
// Add OPTIONS handling for preflight requests
app.options('*', cors());
// Keep track of active connections with session IDs
const connections = new Map();
// Initialize the server
let server = null;
startServer()
.then((s) => {
server = s;
console.error('MCP Server initialized successfully');
})
.catch((error) => {
console.error('Failed to initialize server:', error);
process.exit(1);
});
// Define routes
// @ts-ignore
app.get('/sse', (req, res) => {
console.error(`Received SSE connection request from ${req.ip}`);
console.error(`Query parameters: ${JSON.stringify(req.query)}`);
// Set CORS headers explicitly
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (!server) {
console.error('Server not initialized yet, rejecting SSE connection');
return res.status(503).send('Server not initialized');
}
// Generate a unique session ID if one is not provided
// The sessionId is crucial for mapping SSE connections to message handlers
const sessionId = generateSessionId();
console.error(`Creating SSE session with ID: ${sessionId}`);
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
// Create transport - handle before writing to response
try {
console.error(`Creating SSE transport for session: ${sessionId}`);
// Create and store the transport keyed by session ID
// Note: The path must match what the client expects (typically "/messages")
const transport = new SSEServerTransport('/messages', res);
connections.set(sessionId, transport);
// Handle connection close
req.on('close', () => {
console.error(`SSE connection closed for session: ${sessionId}`);
connections.delete(sessionId);
});
// Connect transport to server - this must happen before sending any data
server
.connect(transport)
.then(() => {
// Send an initial event with the session ID for the client to use in messages
// Only send this after the connection is established
console.error(`SSE connection established for session: ${sessionId}`);
// Send the session ID to the client
res.write(`data: ${JSON.stringify({ type: 'session_init', sessionId })}\n\n`);
})
.catch((error) => {
console.error(`Error connecting transport to server: ${error}`);
connections.delete(sessionId);
});
}
catch (error) {
console.error(`Error creating SSE transport: ${error}`);
connections.delete(sessionId);
res.status(500).send(`Internal server error: ${error}`);
}
});
// @ts-ignore
app.post('/messages', (req, res) => {
// Extract the session ID from the URL query parameters
let sessionId = req.query.sessionId?.toString();
// If no sessionId is provided and there's only one connection, use that
if (!sessionId && connections.size === 1) {
sessionId = Array.from(connections.keys())[0];
console.error(`No sessionId provided, using the only active session: ${sessionId}`);
}
console.error(`Received message for sessionId ${sessionId}`);
console.error(`Message body: ${JSON.stringify(req.body)}`);
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (!server) {
console.error('Server not initialized yet');
return res.status(503).json({ error: 'Server not initialized' });
}
if (!sessionId) {
console.error('No session ID provided and multiple connections exist');
return res.status(400).json({
error: 'No session ID provided. Please provide a sessionId query parameter or connect to /sse first.',
activeConnections: connections.size
});
}
const transport = connections.get(sessionId);
if (!transport) {
console.error(`Session not found: ${sessionId}`);
return res.status(404).json({ error: 'Session not found' });
}
console.error(`Handling message for session: ${sessionId}`);
try {
transport.handlePostMessage(req, res).catch((error) => {
console.error(`Error handling post message: ${error}`);
res.status(500).json({ error: `Internal server error: ${error.message}` });
});
}
catch (error) {
console.error(`Exception handling post message: ${error}`);
res.status(500).json({ error: `Internal server error: ${error}` });
}
});
// Add a simple health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
server: server ? 'initialized' : 'initializing',
activeConnections: connections.size,
connectedSessionIds: Array.from(connections.keys())
});
});
// Add a root endpoint for basic info
app.get('/', (req, res) => {
res.status(200).json({
name: 'MCP Server',
version: '1.0.0',
endpoints: {
sse: '/sse',
messages: '/messages',
health: '/health'
},
status: server ? 'ready' : 'initializing',
activeConnections: connections.size
});
});
// Helper function to generate a UUID-like session ID
function generateSessionId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// Handle process termination gracefully
process.on('SIGINT', () => {
console.error('Shutting down server...');
connections.forEach((transport, sessionId) => {
console.error(`Closing connection for session: ${sessionId}`);
});
process.exit(0);
});
// Start the HTTP server on a different port (3001) to avoid conflicts
const httpServer = app
.listen(PORT, HOST, () => {
console.error(`Template MCP Server running at http://${HOST}:${PORT}`);
console.error(`SSE endpoint: http://${HOST}:${PORT}/sse`);
console.error(`Messages endpoint: http://${HOST}:${PORT}/messages (sessionId optional if only one connection)`);
console.error(`Health check: http://${HOST}:${PORT}/health`);
})
.on('error', (err) => {
console.error(`Server error: ${err}`);
});