diff --git a/README.md b/README.md index 006fbef48..3d729bc3f 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 8eda2fba3..98c13cfcb 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 0e9186d8e..8f6069a07 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 f09ed6047..c5f4ddee8 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 f613422fd..bb4d0f06a 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 000000000..1150052b5 --- /dev/null +++ b/tests/unit/transports/streamableHttp.test.ts @@ -0,0 +1,77 @@ +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 { 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() }, + }, + ({ 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) => { + const messageResult = message as + | { + result?: { + tools: { + name: string; + description: string; + }[]; + }; + } + | undefined; + + expect(message.jsonrpc).toBe("2.0"); + 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({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: { + _meta: { + progressToken: 1, + }, + }, + }); + }); + }); +});