#!/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() {
// Enable STDIO mode if MODE is 'both' or 'stdio' or not specified
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);
});
}
// Only set up HTTP server if enabled
if (config.enableHttpServer) {
console.error('π Starting HTTP server...');
// εε»Ί Express εΊη¨
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(`
Open-WebSearch MCP
Open-WebSearch MCP is running
This Hugging Face Space exposes the MCP server over HTTP.
- MCP endpoint:
/mcp
- Legacy SSE endpoint:
/sse
- Health check:
/healthz
- Configured port:
${process.env.PORT ?? '3000'}
- Default search engine:
${config.defaultSearchEngine}
`);
});
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,
});
});
// Store transports for each session type
const transports = {
streamable: {} as Record,
sse: {} as Record
};
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.streamable[sessionId]) {
// Reuse existing transport
transport = transports.streamable[sessionId].transport;
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
const server = createServer();
const session = {} as StreamableSession;
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports.streamable[sessionId] = session;
},
// DNS rebinding protection is disabled by default for backwards compatibility. If you are running this server
// locally, make sure to set:
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1'],
});
session.server = server;
session.transport = transport;
session.closed = false;
// Clean up transport when closed
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);
});
};
// Connect to the MCP server
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 {
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
// Reusable handler for GET and DELETE requests
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);
};
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);
// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);
// Legacy SSE endpoint for older clients
app.get('/sse', async (req, res) => {
// Create SSE transport for legacy clients
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;
}
});
// Legacy message endpoint for older clients
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');
}
});
// Read the port number from the environment variable; use the default port 3000 if it is not set.
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);