diff --git a/README.md b/README.md index 562e977..ac1215f 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,28 @@ You can use environment variables in the config file or set them and run the ser - Connection String via environment variables in the MCP file [example](#connection-string-with-environment-variables) - Atlas API credentials via environment variables in the MCP file [example](#atlas-api-credentials-with-environment-variables) +#### Option 5: HTTP/SSE Transport (Custom Port) + +You can run the MCP server using HTTP or SSE transport by specifying the `--transportType` and (optionally) `--port` arguments. The default port is `5700` if not specified. + +**Example (HTTP):** + +```shell +npx -y mongodb-mcp-server --transportType=http --port=3000 --connectionString="mongodb://localhost:27017" +``` + +**Example (SSE):** + +```shell +npx -y mongodb-mcp-server --transportType=sse --port=3000 --connectionString="mongodb://localhost:27017" +``` + +- `--transportType` can be `http` or `sse` +- `--port` sets the port (default: 5700) +- All other configuration options (like `--connectionString`, `--apiClientId`, etc.) are supported + +This allows you to expose the MCP server over HTTP/SSE for integration with clients that require a network endpoint. + ## 🛠️ Supported Tools ### Tool List diff --git a/package.json b/package.json index 9ca3489..adeca00 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@jest/globals": "^29.7.0", "@modelcontextprotocol/inspector": "^0.10.2", "@redocly/cli": "^1.34.2", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", @@ -65,6 +66,7 @@ "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.3", + "express": "^5.1.0", "lru-cache": "^11.1.0", "mongodb": "^6.15.0", "mongodb-connection-string-url": "^3.0.2", diff --git a/src/config.ts b/src/config.ts index 9be5445..ee58964 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,8 @@ export interface UserConfig { connectOptions: ConnectOptions; disabledTools: Array; readOnly?: boolean; + transportType?: "stdio" | "sse" | "http"; + port: string; } const defaults: UserConfig = { @@ -37,6 +39,8 @@ const defaults: UserConfig = { disabledTools: [], telemetry: "enabled", readOnly: false, + transportType: "stdio", + port: "5700", }; export const config = { diff --git a/src/index.ts b/src/index.ts index ee33207..d4e56b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,10 @@ import logger, { LogId } from "./logger.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import express from "express"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { config } from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; @@ -29,9 +33,81 @@ try { userConfig: config, }); - const transport = createEJsonTransport(); + if (config.transportType === "http" || config.transportType === "sse") { + const app = express(); + app.use(express.json()); - await server.connect(transport); + // Map to store transports by session ID + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; + + // 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[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId: string) => { + // Store the transport by session ID + transports[sessionId] = transport; + }, + }); + + // Clean up transport when closed + transport.onclose = () => { + if (transport.sessionId) { + delete transports[transport.sessionId]; + } + }; + + // Connect to the MCP server + await server.connect(transport); + } 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[sessionId]) { + res.status(400).send("Invalid or missing session ID"); + return; + } + + const transport = transports[sessionId]; + await 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); + const PORT = config.port; + app.listen(PORT); + } else { + const transport = createEJsonTransport(); + await server.connect(transport); + } } catch (error: unknown) { logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`); process.exit(1);