import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { logger } from './logger.js';
export interface HttpServerConfig {
port: number;
host?: string;
apiKey?: string;
enableCors?: boolean;
}
export class HttpMCPServer {
private app: express.Application;
private mcpServer: Server;
private config: HttpServerConfig;
private transports: Map<string, SSEServerTransport>;
constructor(mcpServer: Server, config: HttpServerConfig) {
this.mcpServer = mcpServer;
this.config = {
host: '0.0.0.0',
enableCors: true,
...config
};
this.transports = new Map();
this.app = express();
this.setupMiddleware();
this.setupRoutes();
}
private setupMiddleware() {
// CORS設定
if (this.config.enableCors) {
this.app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
}
this.app.use(express.json());
// 認証ミドルウェア(APIキーが設定されている場合)
if (this.config.apiKey) {
this.app.use((req: Request, res: Response, next: NextFunction) => {
// ヘルスチェックは認証不要
if (req.path === '/health') {
return next();
}
const authHeader = req.headers.authorization;
const apiKey = authHeader?.replace('Bearer ', '');
if (!apiKey || apiKey !== this.config.apiKey) {
logger.warn('Unauthorized access attempt', {
ip: req.ip,
path: req.path
});
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or missing API key'
});
}
next();
});
}
// リクエストログ
this.app.use((req: Request, res: Response, next: NextFunction) => {
logger.info('HTTP request received', {
method: req.method,
path: req.path,
ip: req.ip
});
next();
});
}
private setupRoutes() {
// ヘルスチェック
this.app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'ok',
server: 'universal-mcp-server',
version: '1.0.0',
transport: 'http-sse',
timestamp: new Date().toISOString()
});
});
// サーバー情報
this.app.get('/info', (req: Request, res: Response) => {
res.json({
name: 'universal-mcp-server',
version: '1.0.0',
capabilities: {
resources: true,
tools: true
},
transport: 'http-sse',
authentication: !!this.config.apiKey
});
});
// SSE エンドポイント
this.app.get('/sse', async (req: Request, res: Response) => {
const clientId = req.query.clientId as string || `client-${Date.now()}`;
logger.info('SSE connection established', { clientId });
// SSE ヘッダー設定
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
try {
// SSE トランスポート作成
const transport = new SSEServerTransport('/message', res);
this.transports.set(clientId, transport);
// MCP サーバーと接続
await this.mcpServer.connect(transport);
logger.info('SSE transport connected', { clientId });
// クライアント切断時のクリーンアップ
req.on('close', () => {
logger.info('SSE connection closed', { clientId });
this.transports.delete(clientId);
});
} catch (error) {
logger.error('SSE connection failed', error as Error, { clientId });
res.end();
}
});
// メッセージ受信エンドポイント
this.app.post('/message', async (req: Request, res: Response) => {
const clientId = req.query.clientId as string;
if (!clientId || !this.transports.has(clientId)) {
return res.status(400).json({
error: 'Invalid client',
message: 'Client ID not found or SSE connection not established'
});
}
try {
const transport = this.transports.get(clientId)!;
await transport.handlePostMessage(req, res);
logger.debug('Message processed', { clientId });
} catch (error) {
logger.error('Message processing failed', error as Error, { clientId });
res.status(500).json({
error: 'Internal error',
message: (error as Error).message
});
}
});
// 404ハンドラー
this.app.use((req: Request, res: Response) => {
res.status(404).json({
error: 'Not found',
message: `Endpoint ${req.path} not found`
});
});
// エラーハンドラー
this.app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error('Express error', err);
res.status(500).json({
error: 'Internal server error',
message: err.message
});
});
}
async start(): Promise<void> {
return new Promise((resolve) => {
this.app.listen(this.config.port, this.config.host!, () => {
logger.info('HTTP MCP Server started', {
host: this.config.host,
port: this.config.port,
authentication: !!this.config.apiKey,
endpoints: {
health: `http://${this.config.host}:${this.config.port}/health`,
info: `http://${this.config.host}:${this.config.port}/info`,
sse: `http://${this.config.host}:${this.config.port}/sse`,
message: `http://${this.config.host}:${this.config.port}/message`
}
});
resolve();
});
});
}
}