diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 71a5b657..7304823f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,4 +55,4 @@ jobs: rm -rf node_modules npm pkg set scripts.prepare="exit 0" npm install --omit=dev - - run: npx -y @modelcontextprotocol/inspector --cli --method tools/list -- node dist/index.js --connectionString "mongodb://localhost" + - run: npx -y @modelcontextprotocol/inspector --cli --method tools/list -- node dist/index.js diff --git a/Dockerfile b/Dockerfile index 05da379f..d842f633 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ RUN addgroup -S mcp && adduser -S mcp -G mcp RUN npm install -g mongodb-mcp-server@${VERSION} USER mcp WORKDIR /home/mcp +ENV MDB_MCP_LOGGERS=stderr,mcp ENTRYPOINT ["mongodb-mcp-server"] LABEL maintainer="MongoDB Inc " LABEL description="MongoDB MCP Server" diff --git a/README.md b/README.md index d7537387..006fbef4 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,27 @@ With Atlas API credentials: } ``` +#### Option 6: Running as an HTTP Server + +You can run the MongoDB MCP Server as an HTTP server instead of the default stdio transport. This is useful if you want to interact with the server over HTTP, for example from a web client or to expose the server on a specific port. + +To start the server with HTTP transport, use the `--transport http` option: + +```shell +npx -y mongodb-mcp-server --transport http +``` + +By default, the server will listen on `http://127.0.0.1:3000`. You can customize the host and port using the `--httpHost` and `--httpPort` options: + +```shell +npx -y mongodb-mcp-server --transport http --httpHost=0.0.0.0 --httpPort=8080 +``` + +- `--httpHost` (default: 127.0.0.1): The host to bind the HTTP server. +- `--httpPort` (default: 3000): The port number for the HTTP server. + +> **Note:** The default transport is `stdio`, which is suitable for integration with most MCP clients. Use `http` transport if you need to interact with the server over HTTP. + ## 🛠️ Supported Tools ### Tool List @@ -278,23 +299,53 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow ### Configuration Options -| Option | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apiClientId` | Atlas API client ID for authentication. Required for running Atlas tools. | -| `apiClientSecret` | Atlas API client secret for authentication. Required for running Atlas tools. | -| `connectionString` | MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the `connect` tool before interacting with MongoDB data. | -| `logPath` | Folder to store logs. | -| `disabledTools` | An array of tool names, operation types, and/or categories of tools that will be disabled. | -| `readOnly` | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. | -| `indexCheck` | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. | -| `telemetry` | When set to disabled, disables telemetry collection. | +| Option | Default | Description | +| ------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apiClientId` | | Atlas API client ID for authentication. Required for running Atlas tools. | +| `apiClientSecret` | | Atlas API client secret for authentication. Required for running Atlas tools. | +| `connectionString` | | MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the `connect` tool before interacting with MongoDB data. | +| `loggers` | disk,mcp | Comma separated values, possible values are `mcp`, `disk` and `stderr`. See [Logger Options](#logger-options) for details. | +| `logPath` | see note\* | Folder to store logs. | +| `disabledTools` | | An array of tool names, operation types, and/or categories of tools that will be disabled. | +| `readOnly` | false | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. | +| `indexCheck` | false | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. | +| `telemetry` | enabled | When set to disabled, disables telemetry collection. | +| `transport` | stdio | Either 'stdio' or 'http'. | +| `httpPort` | 3000 | Port number. | +| `httpHost` | 127.0.0.1 | Host to bind the http server. | + +#### Logger Options + +The `loggers` configuration option controls where logs are sent. You can specify one or more logger types as a comma-separated list. The available options are: + +- `mcp`: Sends logs to the MCP client (if supported by the client/transport). +- `disk`: Writes logs to disk files. Log files are stored in the log path (see `logPath` above). +- `stderr`: Outputs logs to standard error (stderr), useful for debugging or when running in containers. + +**Default:** `disk,mcp` (logs are written to disk and sent to the MCP client). + +You can combine multiple loggers, e.g. `--loggers disk,stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`. + +##### Example: Set logger via environment variable + +```shell +export MDB_MCP_LOGGERS="disk,stderr" +``` + +##### Example: Set logger via command-line argument + +```shell +npx -y mongodb-mcp-server --loggers mcp,stderr +``` + +##### Log File Location -#### Log Path +When using the `disk` logger, log files are stored in: -Default log location is as follows: +- **Windows:** `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs` +- **macOS/Linux:** `~/.mongodb/mongodb-mcp/.app-logs` -- Windows: `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs` -- macOS/Linux: `~/.mongodb/mongodb-mcp/.app-logs` +You can override the log directory with the `logPath` option. #### Disabled Tools diff --git a/package-lock.json b/package-lock.json index 29132ba3..3919b575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", "bson": "^6.10.4", + "express": "^5.1.0", "lru-cache": "^11.1.0", "mongodb": "^6.17.0", "mongodb-connection-string-url": "^3.0.2", @@ -34,6 +35,7 @@ "@jest/globals": "^30.0.4", "@modelcontextprotocol/inspector": "^0.16.0", "@redocly/cli": "^1.34.4", + "@types/express": "^5.0.1", "@types/jest": "^30.0.0", "@types/node": "^24.0.12", "@types/simple-oauth2": "^5.0.7", @@ -5906,6 +5908,27 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -5913,6 +5936,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6006,6 +6061,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz", @@ -6016,6 +6078,43 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/simple-oauth2": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-5.0.7.tgz", diff --git a/package.json b/package.json index 53d6d2c6..4cee9b92 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@jest/globals": "^30.0.4", "@modelcontextprotocol/inspector": "^0.16.0", "@redocly/cli": "^1.34.4", + "@types/express": "^5.0.1", "@types/jest": "^30.0.0", "@types/node": "^24.0.12", "@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.4", + "express": "^5.1.0", "lru-cache": "^11.1.0", "mongodb": "^6.17.0", "mongodb-connection-string-url": "^3.0.2", diff --git a/src/common/config.ts b/src/common/config.ts index d9aa0bbc..8eda2fba 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -17,13 +17,17 @@ export interface UserConfig { apiBaseUrl: string; apiClientId?: string; apiClientSecret?: string; - telemetry?: "enabled" | "disabled"; + telemetry: "enabled" | "disabled"; logPath: string; connectionString?: string; connectOptions: ConnectOptions; disabledTools: Array; readOnly?: boolean; indexCheck?: boolean; + transport: "stdio" | "http"; + httpPort: number; + httpHost: string; + loggers: Array<"stderr" | "disk" | "mcp">; } const defaults: UserConfig = { @@ -39,6 +43,10 @@ const defaults: UserConfig = { telemetry: "enabled", readOnly: false, indexCheck: false, + transport: "stdio", + httpPort: 3000, + httpHost: "127.0.0.1", + loggers: ["disk", "mcp"], }; export const config = { diff --git a/src/common/logger.ts b/src/common/logger.ts index b1fb78a9..0e9186d8 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -12,6 +12,7 @@ export const LogId = { serverCloseRequested: mongoLogId(1_000_003), serverClosed: mongoLogId(1_000_004), serverCloseFailure: mongoLogId(1_000_005), + serverDuplicateLoggers: mongoLogId(1_000_006), atlasCheckCredentials: mongoLogId(1_001_001), atlasDeleteDatabaseUserFailure: mongoLogId(1_001_002), @@ -37,9 +38,16 @@ export const LogId = { mongodbDisconnectFailure: mongoLogId(1_004_002), toolUpdateFailure: mongoLogId(1_005_001), + + streamableHttpTransportStarted: mongoLogId(1_006_001), + streamableHttpTransportSessionInitialized: mongoLogId(1_006_002), + streamableHttpTransportRequestFailure: mongoLogId(1_006_003), + streamableHttpTransportCloseRequested: mongoLogId(1_006_004), + streamableHttpTransportCloseSuccess: mongoLogId(1_006_005), + streamableHttpTransportCloseFailure: mongoLogId(1_006_006), } as const; -abstract class LoggerBase { +export abstract class LoggerBase { abstract log(level: LogLevel, id: MongoLogId, context: string, message: string): void; info(id: MongoLogId, context: string, message: string): void { @@ -74,14 +82,14 @@ abstract class LoggerBase { } } -class ConsoleLogger extends LoggerBase { +export class ConsoleLogger extends LoggerBase { log(level: LogLevel, id: MongoLogId, context: string, message: string): void { message = redact(message); - console.error(`[${level.toUpperCase()}] ${id.__value} - ${context}: ${message}`); + console.error(`[${level.toUpperCase()}] ${id.__value} - ${context}: ${message} (${process.pid})`); } } -class DiskLogger extends LoggerBase { +export class DiskLogger extends LoggerBase { private constructor(private logWriter: MongoLogWriter) { super(); } @@ -133,7 +141,7 @@ class DiskLogger extends LoggerBase { } } -class McpLogger extends LoggerBase { +export class McpLogger extends LoggerBase { constructor(private server: McpServer) { super(); } @@ -152,18 +160,12 @@ class McpLogger extends LoggerBase { } class CompositeLogger extends LoggerBase { - private loggers: LoggerBase[]; + private loggers: LoggerBase[] = []; constructor(...loggers: LoggerBase[]) { super(); - if (loggers.length === 0) { - // default to ConsoleLogger - this.loggers = [new ConsoleLogger()]; - return; - } - - this.loggers = [...loggers]; + this.setLoggers(...loggers); } setLoggers(...loggers: LoggerBase[]): void { @@ -180,19 +182,5 @@ class CompositeLogger extends LoggerBase { } } -const logger = new CompositeLogger(); +const logger = new CompositeLogger(new ConsoleLogger()); export default logger; - -export async function setStdioPreset(server: McpServer, logPath: string): Promise { - const diskLogger = await DiskLogger.fromPath(logPath); - const mcpLogger = new McpLogger(server); - - logger.setLoggers(mcpLogger, diskLogger); -} - -export function setContainerPreset(server: McpServer): void { - const mcpLogger = new McpLogger(server); - const consoleLogger = new ConsoleLogger(); - - logger.setLoggers(mcpLogger, consoleLogger); -} diff --git a/src/index.ts b/src/index.ts index f94c4371..f09ed604 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,8 @@ import { Session } from "./common/session.js"; import { Server } from "./server.js"; import { packageInfo } from "./common/packageInfo.js"; import { Telemetry } from "./telemetry/telemetry.js"; -import { createEJsonTransport } from "./helpers/EJsonTransport.js"; +import { createStdioTransport } from "./transports/stdio.js"; +import { createHttpTransport } from "./transports/streamableHttp.js"; try { const session = new Session({ @@ -15,13 +16,16 @@ try { apiClientId: config.apiClientId, apiClientSecret: config.apiClientSecret, }); + + const transport = config.transport === "stdio" ? createStdioTransport() : createHttpTransport(); + + const telemetry = Telemetry.create(session, config); + const mcpServer = new McpServer({ name: packageInfo.mcpServerName, version: packageInfo.version, }); - const telemetry = Telemetry.create(session, config); - const server = new Server({ mcpServer, session, @@ -29,8 +33,6 @@ try { userConfig: config, }); - const transport = createEJsonTransport(); - const shutdown = () => { logger.info(LogId.serverCloseRequested, "server", `Server close requested`); @@ -48,6 +50,7 @@ try { }; process.once("SIGINT", shutdown); + process.once("SIGABRT", shutdown); process.once("SIGTERM", shutdown); process.once("SIGQUIT", shutdown); diff --git a/src/server.ts b/src/server.ts index 3c65d2e3..d58cca52 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,7 +3,7 @@ import { Session } from "./common/session.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; -import logger, { setStdioPreset, setContainerPreset, LogId } from "./common/logger.js"; +import logger, { LogId, LoggerBase, McpLogger, DiskLogger, ConsoleLogger } from "./common/logger.js"; import { ObjectId } from "mongodb"; import { Telemetry } from "./telemetry/telemetry.js"; import { UserConfig } from "./common/config.js"; @@ -11,7 +11,6 @@ import { type ServerEvent } from "./telemetry/types.js"; import { type ServerCommand } from "./telemetry/types.js"; import { CallToolRequestSchema, CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import assert from "assert"; -import { detectContainerEnv } from "./helpers/container.js"; import { ToolBase } from "./tools/tool.js"; export interface ServerOptions { @@ -38,6 +37,8 @@ export class Server { } async connect(transport: Transport): Promise { + await this.validateConfig(); + this.mcpServer.server.registerCapabilities({ logging: {} }); this.registerTools(); @@ -66,15 +67,17 @@ export class Server { return existingHandler(request, extra); }); - const containerEnv = await detectContainerEnv(); - - if (containerEnv) { - setContainerPreset(this.mcpServer); - } else { - await setStdioPreset(this.mcpServer, this.userConfig.logPath); + const loggers: LoggerBase[] = []; + if (this.userConfig.loggers.includes("mcp")) { + loggers.push(new McpLogger(this.mcpServer)); } - - await this.mcpServer.connect(transport); + if (this.userConfig.loggers.includes("disk")) { + loggers.push(await DiskLogger.fromPath(this.userConfig.logPath)); + } + if (this.userConfig.loggers.includes("stderr")) { + loggers.push(new ConsoleLogger()); + } + logger.setLoggers(...loggers); this.mcpServer.server.oninitialized = () => { this.session.setAgentRunner(this.mcpServer.server.getClientVersion()); @@ -99,7 +102,7 @@ export class Server { this.emitServerEvent("stop", Date.now() - closeTime, error); }; - await this.validateConfig(); + await this.mcpServer.connect(transport); } async close(): Promise { @@ -186,6 +189,35 @@ export class Server { } private async validateConfig(): Promise { + const transport = this.userConfig.transport as string; + if (transport !== "http" && transport !== "stdio") { + throw new Error(`Invalid transport: ${transport}`); + } + + const telemetry = this.userConfig.telemetry as string; + if (telemetry !== "enabled" && telemetry !== "disabled") { + throw new Error(`Invalid telemetry: ${telemetry}`); + } + + if (this.userConfig.httpPort < 1 || this.userConfig.httpPort > 65535) { + throw new Error(`Invalid httpPort: ${this.userConfig.httpPort}`); + } + + if (this.userConfig.loggers.length === 0) { + throw new Error("No loggers found in config"); + } + + const loggerTypes = new Set(this.userConfig.loggers); + if (loggerTypes.size !== this.userConfig.loggers.length) { + throw new Error("Duplicate loggers found in config"); + } + + for (const loggerType of this.userConfig.loggers as string[]) { + if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") { + throw new Error(`Invalid logger: ${loggerType}`); + } + } + if (this.userConfig.connectionString) { try { await this.session.connectToMongoDB(this.userConfig.connectionString, this.userConfig.connectOptions); diff --git a/src/helpers/EJsonTransport.ts b/src/transports/stdio.ts similarity index 96% rename from src/helpers/EJsonTransport.ts rename to src/transports/stdio.ts index 307e90bd..0f9f4c0c 100644 --- a/src/helpers/EJsonTransport.ts +++ b/src/transports/stdio.ts @@ -39,7 +39,7 @@ export class EJsonReadBuffer { // // This function creates a StdioServerTransport and replaces the internal readBuffer with EJsonReadBuffer // that uses EJson.parse instead. -export function createEJsonTransport(): StdioServerTransport { +export function createStdioTransport(): StdioServerTransport { const server = new StdioServerTransport(); server["_readBuffer"] = new EJsonReadBuffer(); diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts new file mode 100644 index 00000000..f613422f --- /dev/null +++ b/src/transports/streamableHttp.ts @@ -0,0 +1,103 @@ +import express from "express"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; + +import { config } from "../common/config.js"; +import logger, { LogId } from "../common/logger.js"; + +const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000; + +export function createHttpTransport(): StreamableHTTPServerTransport { + const app = express(); + app.enable("trust proxy"); // needed for reverse proxy support + app.use(express.urlencoded({ extended: true })); + app.use(express.json()); + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + app.post("/mcp", async (req: express.Request, res: express.Response) => { + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + logger.error( + LogId.streamableHttpTransportRequestFailure, + "streamableHttpTransport", + `Error handling request: ${error instanceof Error ? error.message : String(error)}` + ); + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED, + message: `failed to handle request`, + data: error instanceof Error ? error.message : String(error), + }, + }); + } + }); + + app.get("/mcp", async (req: express.Request, res: express.Response) => { + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + logger.error( + LogId.streamableHttpTransportRequestFailure, + "streamableHttpTransport", + `Error handling request: ${error instanceof Error ? error.message : String(error)}` + ); + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED, + message: `failed to handle request`, + data: error instanceof Error ? error.message : String(error), + }, + }); + } + }); + + app.delete("/mcp", async (req: express.Request, res: express.Response) => { + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + logger.error( + LogId.streamableHttpTransportRequestFailure, + "streamableHttpTransport", + `Error handling request: ${error instanceof Error ? error.message : String(error)}` + ); + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED, + message: `failed to handle request`, + data: error instanceof Error ? error.message : String(error), + }, + }); + } + }); + + const server = app.listen(config.httpPort, config.httpHost, () => { + logger.info( + LogId.streamableHttpTransportStarted, + "streamableHttpTransport", + `Server started on http://${config.httpHost}:${config.httpPort}` + ); + }); + + transport.onclose = () => { + logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`); + server.close((err?: Error) => { + if (err) { + logger.error( + LogId.streamableHttpTransportCloseFailure, + "streamableHttpTransport", + `Error closing server: ${err.message}` + ); + return; + } + logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`); + }); + }; + + return transport; +} diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 8f4e0539..84eecf14 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -25,6 +25,7 @@ export interface IntegrationTest { export const defaultTestConfig: UserConfig = { ...config, telemetry: "disabled", + loggers: ["stderr"], }; export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest { diff --git a/tests/unit/apiClient.test.ts b/tests/unit/common/apiClient.test.ts similarity index 98% rename from tests/unit/apiClient.test.ts rename to tests/unit/common/apiClient.test.ts index 6b9fd427..00d26e9f 100644 --- a/tests/unit/apiClient.test.ts +++ b/tests/unit/common/apiClient.test.ts @@ -1,6 +1,6 @@ import { jest } from "@jest/globals"; -import { ApiClient } from "../../src/common/atlas/apiClient.js"; -import { CommonProperties, TelemetryEvent, TelemetryResult } from "../../src/telemetry/types.js"; +import { ApiClient } from "../../../src/common/atlas/apiClient.js"; +import { CommonProperties, TelemetryEvent, TelemetryResult } from "../../../src/telemetry/types.js"; describe("ApiClient", () => { let apiClient: ApiClient; diff --git a/tests/unit/session.test.ts b/tests/unit/common/session.test.ts similarity index 95% rename from tests/unit/session.test.ts rename to tests/unit/common/session.test.ts index fdd4296b..bb43a4a0 100644 --- a/tests/unit/session.test.ts +++ b/tests/unit/common/session.test.ts @@ -1,7 +1,7 @@ import { jest } from "@jest/globals"; import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; -import { Session } from "../../src/common/session.js"; -import { config } from "../../src/common/config.js"; +import { Session } from "../../../src/common/session.js"; +import { config } from "../../../src/common/config.js"; jest.mock("@mongosh/service-provider-node-driver"); const MockNodeDriverServiceProvider = NodeDriverServiceProvider as jest.MockedClass; diff --git a/tests/unit/indexCheck.test.ts b/tests/unit/helpers/indexCheck.test.ts similarity index 98% rename from tests/unit/indexCheck.test.ts rename to tests/unit/helpers/indexCheck.test.ts index 82b67e68..aedac1cf 100644 --- a/tests/unit/indexCheck.test.ts +++ b/tests/unit/helpers/indexCheck.test.ts @@ -1,4 +1,4 @@ -import { usesIndex, getIndexCheckErrorMessage } from "../../src/helpers/indexCheck.js"; +import { usesIndex, getIndexCheckErrorMessage } from "../../../src/helpers/indexCheck.js"; import { Document } from "mongodb"; describe("indexCheck", () => { diff --git a/tests/unit/EJsonTransport.test.ts b/tests/unit/transports/stdio.test.ts similarity index 93% rename from tests/unit/EJsonTransport.test.ts rename to tests/unit/transports/stdio.test.ts index 6bbb7999..0e00968b 100644 --- a/tests/unit/EJsonTransport.test.ts +++ b/tests/unit/transports/stdio.test.ts @@ -1,15 +1,15 @@ import { Decimal128, MaxKey, MinKey, ObjectId, Timestamp, UUID } from "bson"; -import { createEJsonTransport, EJsonReadBuffer } from "../../src/helpers/EJsonTransport.js"; +import { createStdioTransport, EJsonReadBuffer } from "../../../src/transports/stdio.js"; import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Readable } from "stream"; import { ReadBuffer } from "@modelcontextprotocol/sdk/shared/stdio.js"; -describe("EJsonTransport", () => { +describe("stdioTransport", () => { let transport: StdioServerTransport; beforeEach(async () => { - transport = createEJsonTransport(); + transport = createStdioTransport(); await transport.start(); });