| #!/usr/bin/env node |
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| import { setupTools } from './tools/setupTools.js'; |
| import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; |
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; |
| import express from 'express'; |
| import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js" |
| import { randomUUID } from "node:crypto"; |
| import cors from 'cors'; |
| import {config} from "./config.js"; |
|
|
| type StreamableSession = { |
| server: McpServer; |
| transport: StreamableHTTPServerTransport; |
| closed: boolean; |
| }; |
|
|
| type SseSession = { |
| server: McpServer; |
| transport: SSEServerTransport; |
| closed: boolean; |
| }; |
|
|
| function createServer(): McpServer { |
| const server = new McpServer({ |
| name: 'web-search', |
| version: '1.2.0' |
| }); |
|
|
| setupTools(server); |
| return server; |
| } |
|
|
| async function main() { |
| |
| if (process.env.MODE === undefined || process.env.MODE === 'both' || process.env.MODE === 'stdio') { |
| console.error('🔌 Starting STDIO transport...'); |
| const server = createServer(); |
| const stdioTransport = new StdioServerTransport(); |
| await server.connect(stdioTransport).then(() => { |
| console.error('✅ STDIO transport enabled'); |
| }).catch(error => { |
| console.error('❌ Failed to initialize STDIO transport:', error); |
| }); |
| } |
|
|
| |
| if (config.enableHttpServer) { |
| console.error('🔌 Starting HTTP server...'); |
| |
| const app = express(); |
| app.use(express.json()); |
|
|
| |
| if (config.enableCors) { |
| app.use(cors({ |
| origin: config.corsOrigin || '*', |
| methods: ['GET', 'POST', 'DELETE'], |
| })); |
| app.options('*', cors()); |
| } |
|
|
| app.get('/', (_req, res) => { |
| res.type('html').send(`<!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Open-WebSearch MCP</title> |
| <style> |
| :root { color-scheme: light dark; } |
| body { |
| margin: 0; |
| font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| background: #0b1020; |
| color: #e8ecf3; |
| } |
| .wrap { |
| max-width: 860px; |
| margin: 0 auto; |
| padding: 48px 24px; |
| } |
| .card { |
| background: rgba(255, 255, 255, 0.06); |
| border: 1px solid rgba(255, 255, 255, 0.12); |
| border-radius: 18px; |
| padding: 24px; |
| backdrop-filter: blur(8px); |
| } |
| code { |
| background: rgba(255, 255, 255, 0.08); |
| padding: 2px 8px; |
| border-radius: 8px; |
| } |
| ul { line-height: 1.8; } |
| a { color: #9ecbff; } |
| .muted { opacity: 0.82; } |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
| <div class="card"> |
| <h1>Open-WebSearch MCP is running</h1> |
| <p class="muted">This Hugging Face Space exposes the MCP server over HTTP.</p> |
| <ul> |
| <li>MCP endpoint: <code>/mcp</code></li> |
| <li>Legacy SSE endpoint: <code>/sse</code></li> |
| <li>Health check: <code>/healthz</code></li> |
| <li>Configured port: <code>${process.env.PORT ?? '3000'}</code></li> |
| <li>Default search engine: <code>${config.defaultSearchEngine}</code></li> |
| </ul> |
| </div> |
| </div> |
| </body> |
| </html>`); |
| }); |
|
|
| app.get('/healthz', (_req, res) => { |
| res.json({ |
| ok: true, |
| mode: process.env.MODE ?? 'both', |
| port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, |
| defaultSearchEngine: config.defaultSearchEngine, |
| }); |
| }); |
|
|
| |
| const transports = { |
| streamable: {} as Record<string, StreamableSession>, |
| sse: {} as Record<string, SseSession> |
| }; |
|
|
| |
| app.post('/mcp', async (req, res) => { |
| |
| const sessionId = req.headers['mcp-session-id'] as string | undefined; |
| let transport: StreamableHTTPServerTransport; |
|
|
| if (sessionId && transports.streamable[sessionId]) { |
| |
| transport = transports.streamable[sessionId].transport; |
| } else if (!sessionId && isInitializeRequest(req.body)) { |
| |
| const server = createServer(); |
| const session = {} as StreamableSession; |
|
|
| transport = new StreamableHTTPServerTransport({ |
| sessionIdGenerator: () => randomUUID(), |
| onsessioninitialized: (sessionId) => { |
| |
| transports.streamable[sessionId] = session; |
| }, |
| |
| |
| |
| |
| }); |
|
|
| session.server = server; |
| session.transport = transport; |
| session.closed = false; |
|
|
| |
| transport.onclose = () => { |
| if (transport.sessionId && transports.streamable[transport.sessionId] === session) { |
| delete transports.streamable[transport.sessionId]; |
| } |
|
|
| if (session.closed) { |
| return; |
| } |
|
|
| session.closed = true; |
| void server.close().catch(error => { |
| console.error('❌ Failed to close streamable MCP server:', error); |
| }); |
| }; |
|
|
| |
| try { |
| await server.connect(transport); |
| } catch (error) { |
| session.closed = true; |
| void server.close().catch(closeError => { |
| console.error('❌ Failed to close streamable MCP server after connect error:', closeError); |
| }); |
| throw error; |
| } |
| } else { |
| |
| res.status(400).json({ |
| jsonrpc: '2.0', |
| error: { |
| code: -32000, |
| message: 'Bad Request: No valid session ID provided', |
| }, |
| id: null, |
| }); |
| return; |
| } |
|
|
| |
| await transport.handleRequest(req, res, req.body); |
| }); |
|
|
| |
| const handleSessionRequest = async (req: express.Request, res: express.Response) => { |
| const sessionId = req.headers['mcp-session-id'] as string | undefined; |
| if (!sessionId || !transports.streamable[sessionId]) { |
| res.status(400).send('Invalid or missing session ID'); |
| return; |
| } |
|
|
| const transport = transports.streamable[sessionId]; |
| await transport.transport.handleRequest(req, res); |
| }; |
|
|
| |
| app.get('/mcp', handleSessionRequest); |
|
|
| |
| app.delete('/mcp', handleSessionRequest); |
|
|
| |
| app.get('/sse', async (req, res) => { |
| |
| const transport = new SSEServerTransport('/messages', res); |
| const server = createServer(); |
| const session: SseSession = { |
| server, |
| transport, |
| closed: false |
| }; |
|
|
| transports.sse[transport.sessionId] = session; |
|
|
| transport.onclose = () => { |
| if (transports.sse[transport.sessionId] === session) { |
| delete transports.sse[transport.sessionId]; |
| } |
|
|
| if (session.closed) { |
| return; |
| } |
|
|
| session.closed = true; |
| void server.close().catch(error => { |
| console.error('❌ Failed to close SSE MCP server:', error); |
| }); |
| }; |
|
|
| try { |
| await server.connect(transport); |
| } catch (error) { |
| delete transports.sse[transport.sessionId]; |
| session.closed = true; |
| void server.close().catch(closeError => { |
| console.error('❌ Failed to close SSE MCP server after connect error:', closeError); |
| }); |
| throw error; |
| } |
| }); |
|
|
| |
| app.post('/messages', async (req, res) => { |
| const sessionId = req.query.sessionId as string; |
| const session = transports.sse[sessionId]; |
| if (session) { |
| await session.transport.handlePostMessage(req, res, req.body); |
| } else { |
| res.status(400).send('No transport found for sessionId'); |
| } |
| }); |
|
|
| |
| const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; |
|
|
| app.listen(PORT, '0.0.0.0', () => { |
| console.error(`✅ HTTP server running on port ${PORT}`) |
| }); |
| } else { |
| console.error('ℹ️ HTTP server disabled, running in STDIO mode only') |
| } |
| } |
|
|
| main().catch(console.error); |
|
|