From f2bdfb75c5945243bf3caf92d8a7a9991882b933 Mon Sep 17 00:00:00 2001 From: Filipe Constantinov Menezes Date: Mon, 14 Jul 2025 18:49:57 +0100 Subject: [PATCH 1/3] test: add streamable http tests [MCP-60] --- Dockerfile | 2 +- README.md | 4 +- src/common/config.ts | 2 +- src/common/logger.ts | 11 ++-- src/index.ts | 2 +- src/transports/streamableHttp.ts | 56 ++++++++++------ tests/unit/transports/streamableHttp.test.ts | 67 ++++++++++++++++++++ 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 tests/unit/transports/streamableHttp.test.ts diff --git a/Dockerfile b/Dockerfile index d842f633..f74adf24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +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 +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 006fbef4..3d729bc3 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ The `loggers` configuration option controls where logs are sent. You can specify **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"`. +You can combine multiple loggers, e.g. `--loggers disk stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`. ##### Example: Set logger via environment variable @@ -335,7 +335,7 @@ export MDB_MCP_LOGGERS="disk,stderr" ##### Example: Set logger via command-line argument ```shell -npx -y mongodb-mcp-server --loggers mcp,stderr +npx -y mongodb-mcp-server --loggers mcp stderr ``` ##### Log File Location diff --git a/src/common/config.ts b/src/common/config.ts index 8eda2fba..98c13cfc 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -128,6 +128,6 @@ function SNAKE_CASE_toCamelCase(str: string): string { // Reads the cli args and parses them into a UserConfig object. function getCliConfig() { return argv(process.argv.slice(2), { - array: ["disabledTools"], + array: ["disabledTools", "loggers"], }) as unknown as Partial; } diff --git a/src/common/logger.ts b/src/common/logger.ts index 0e9186d8..8f6069a0 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -40,11 +40,12 @@ export const LogId = { 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), + streamableHttpTransportStartFailure: mongoLogId(1_006_002), + streamableHttpTransportSessionInitialized: mongoLogId(1_006_003), + streamableHttpTransportRequestFailure: mongoLogId(1_006_004), + streamableHttpTransportCloseRequested: mongoLogId(1_006_005), + streamableHttpTransportCloseSuccess: mongoLogId(1_006_006), + streamableHttpTransportCloseFailure: mongoLogId(1_006_007), } as const; export abstract class LoggerBase { diff --git a/src/index.ts b/src/index.ts index f09ed604..c5f4ddee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ try { apiClientSecret: config.apiClientSecret, }); - const transport = config.transport === "stdio" ? createStdioTransport() : createHttpTransport(); + const transport = config.transport === "stdio" ? createStdioTransport() : await createHttpTransport(); const telemetry = Telemetry.create(session, config); diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index f613422f..bb4d0f06 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -1,4 +1,5 @@ import express from "express"; +import http from "http"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { config } from "../common/config.js"; @@ -6,7 +7,7 @@ import logger, { LogId } from "../common/logger.js"; const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000; -export function createHttpTransport(): StreamableHTTPServerTransport { +export async function createHttpTransport(): Promise { const app = express(); app.enable("trust proxy"); // needed for reverse proxy support app.use(express.urlencoded({ extended: true })); @@ -76,28 +77,47 @@ export function createHttpTransport(): StreamableHTTPServerTransport { } }); - const server = app.listen(config.httpPort, config.httpHost, () => { + try { + const server = await new Promise((resolve, reject) => { + const result = app.listen(config.httpPort, config.httpHost, (err?: Error) => { + if (err) { + reject(err); + return; + } + resolve(result); + }); + }); + 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`); - }); - }; + 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; + } catch (error: unknown) { + const err = error instanceof Error ? error : new Error(String(error)); + logger.info( + LogId.streamableHttpTransportStartFailure, + "streamableHttpTransport", + `Error starting server: ${err.message}` + ); - return transport; + throw err; + } } diff --git a/tests/unit/transports/streamableHttp.test.ts b/tests/unit/transports/streamableHttp.test.ts new file mode 100644 index 00000000..efddc017 --- /dev/null +++ b/tests/unit/transports/streamableHttp.test.ts @@ -0,0 +1,67 @@ +import { createHttpTransport } from "../../../src/transports/streamableHttp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import { config } from "../../../src/common/config.js"; +import { z } from "zod"; +describe("streamableHttpTransport", () => { + let transport: StreamableHTTPServerTransport; + const mcpServer = new McpServer({ + name: "test", + version: "1.0.0", + }); + beforeAll(async () => { + transport = await createHttpTransport(); + mcpServer.registerTool( + "hello", + { + title: "Hello Tool", + description: "Say hello", + inputSchema: { name: z.string() }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `Hello, ${name}!` }], + }) + ); + await mcpServer.connect(transport); + }); + + afterAll(async () => { + await mcpServer.close(); + }); + + describe("client connects successfully", () => { + let client: StreamableHTTPClientTransport; + beforeAll(async () => { + client = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:3000/mcp")); + await client.start(); + }); + + afterAll(async () => { + await client.close(); + }); + + it("handles requests and sends responses", async () => { + client.onmessage = (message: JSONRPCMessage) => { + expect(message.jsonrpc).toBe("2.0"); + expect(message.result).toBeDefined(); + expect(message.result.tools).toBeDefined(); + expect(message.result.tools.length).toBe(1); + expect(message.result.tools[0].name).toBe("hello"); + expect(message.result.tools[0].description).toBe("Say hello"); + }; + + await client.send({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: { + _meta: { + progressToken: 1, + }, + }, + }); + }); + }); +}); From 70944573c4afefd51f596570e1365d276d1df486 Mon Sep 17 00:00:00 2001 From: Filipe Constantinov Menezes Date: Mon, 14 Jul 2025 18:53:06 +0100 Subject: [PATCH 2/3] fix: dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f74adf24..d842f633 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +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" +ENV MDB_MCP_LOGGERS=stderr,mcp ENTRYPOINT ["mongodb-mcp-server"] LABEL maintainer="MongoDB Inc " LABEL description="MongoDB MCP Server" From a6d4bdc5399908094c6b9f43a6f3906f879fcc48 Mon Sep 17 00:00:00 2001 From: Filipe Constantinov Menezes Date: Mon, 14 Jul 2025 19:04:23 +0100 Subject: [PATCH 3/3] fix: styles --- tests/unit/transports/streamableHttp.test.ts | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/unit/transports/streamableHttp.test.ts b/tests/unit/transports/streamableHttp.test.ts index efddc017..1150052b 100644 --- a/tests/unit/transports/streamableHttp.test.ts +++ b/tests/unit/transports/streamableHttp.test.ts @@ -3,7 +3,6 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; -import { config } from "../../../src/common/config.js"; import { z } from "zod"; describe("streamableHttpTransport", () => { let transport: StreamableHTTPServerTransport; @@ -20,7 +19,7 @@ describe("streamableHttpTransport", () => { description: "Say hello", inputSchema: { name: z.string() }, }, - async ({ name }) => ({ + ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }], }) ); @@ -44,12 +43,23 @@ describe("streamableHttpTransport", () => { it("handles requests and sends responses", async () => { client.onmessage = (message: JSONRPCMessage) => { + const messageResult = message as + | { + result?: { + tools: { + name: string; + description: string; + }[]; + }; + } + | undefined; + expect(message.jsonrpc).toBe("2.0"); - expect(message.result).toBeDefined(); - expect(message.result.tools).toBeDefined(); - expect(message.result.tools.length).toBe(1); - expect(message.result.tools[0].name).toBe("hello"); - expect(message.result.tools[0].description).toBe("Say hello"); + expect(messageResult).toBeDefined(); + expect(messageResult?.result?.tools).toBeDefined(); + expect(messageResult?.result?.tools.length).toBe(1); + expect(messageResult?.result?.tools[0]?.name).toBe("hello"); + expect(messageResult?.result?.tools[0]?.description).toBe("Say hello"); }; await client.send({