Skip to content

Commit 34ee348

Browse files
committed
test: add streamable http tests [MCP-60]
1 parent f0a83b0 commit 34ee348

File tree

7 files changed

+116
-28
lines changed

7 files changed

+116
-28
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ RUN addgroup -S mcp && adduser -S mcp -G mcp
44
RUN npm install -g mongodb-mcp-server@${VERSION}
55
USER mcp
66
WORKDIR /home/mcp
7-
ENV MDB_MCP_LOGGERS=stderr,mcp
7+
ENV MDB_MCP_LOGGERS="stderr mcp"
88
ENTRYPOINT ["mongodb-mcp-server"]
99
LABEL maintainer="MongoDB Inc <info@mongodb.com>"
1010
LABEL description="MongoDB MCP Server"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ The `loggers` configuration option controls where logs are sent. You can specify
324324

325325
**Default:** `disk,mcp` (logs are written to disk and sent to the MCP client).
326326

327-
You can combine multiple loggers, e.g. `--loggers disk,stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`.
327+
You can combine multiple loggers, e.g. `--loggers disk stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`.
328328

329329
##### Example: Set logger via environment variable
330330

@@ -335,7 +335,7 @@ export MDB_MCP_LOGGERS="disk,stderr"
335335
##### Example: Set logger via command-line argument
336336

337337
```shell
338-
npx -y mongodb-mcp-server --loggers mcp,stderr
338+
npx -y mongodb-mcp-server --loggers mcp stderr
339339
```
340340

341341
##### Log File Location

src/common/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,6 @@ function SNAKE_CASE_toCamelCase(str: string): string {
128128
// Reads the cli args and parses them into a UserConfig object.
129129
function getCliConfig() {
130130
return argv(process.argv.slice(2), {
131-
array: ["disabledTools"],
131+
array: ["disabledTools", "loggers"],
132132
}) as unknown as Partial<UserConfig>;
133133
}

src/common/logger.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ export const LogId = {
4040
toolUpdateFailure: mongoLogId(1_005_001),
4141

4242
streamableHttpTransportStarted: mongoLogId(1_006_001),
43-
streamableHttpTransportSessionInitialized: mongoLogId(1_006_002),
44-
streamableHttpTransportRequestFailure: mongoLogId(1_006_003),
45-
streamableHttpTransportCloseRequested: mongoLogId(1_006_004),
46-
streamableHttpTransportCloseSuccess: mongoLogId(1_006_005),
47-
streamableHttpTransportCloseFailure: mongoLogId(1_006_006),
43+
streamableHttpTransportStartFailure: mongoLogId(1_006_002),
44+
streamableHttpTransportSessionInitialized: mongoLogId(1_006_003),
45+
streamableHttpTransportRequestFailure: mongoLogId(1_006_004),
46+
streamableHttpTransportCloseRequested: mongoLogId(1_006_005),
47+
streamableHttpTransportCloseSuccess: mongoLogId(1_006_006),
48+
streamableHttpTransportCloseFailure: mongoLogId(1_006_007),
4849
} as const;
4950

5051
export abstract class LoggerBase {

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ try {
1717
apiClientSecret: config.apiClientSecret,
1818
});
1919

20-
const transport = config.transport === "stdio" ? createStdioTransport() : createHttpTransport();
20+
const transport = config.transport === "stdio" ? createStdioTransport() : await createHttpTransport();
2121

2222
const telemetry = Telemetry.create(session, config);
2323

src/transports/streamableHttp.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import express from "express";
2+
import http from "http";
23
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
34

45
import { config } from "../common/config.js";
56
import logger, { LogId } from "../common/logger.js";
67

78
const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000;
89

9-
export function createHttpTransport(): StreamableHTTPServerTransport {
10+
export async function createHttpTransport(): Promise<StreamableHTTPServerTransport> {
1011
const app = express();
1112
app.enable("trust proxy"); // needed for reverse proxy support
1213
app.use(express.urlencoded({ extended: true }));
@@ -76,28 +77,47 @@ export function createHttpTransport(): StreamableHTTPServerTransport {
7677
}
7778
});
7879

79-
const server = app.listen(config.httpPort, config.httpHost, () => {
80+
try {
81+
const server = await new Promise<http.Server>((resolve, reject) => {
82+
const result = app.listen(config.httpPort, config.httpHost, (err?: Error) => {
83+
if (err) {
84+
reject(err);
85+
return;
86+
}
87+
resolve(result);
88+
});
89+
});
90+
8091
logger.info(
8192
LogId.streamableHttpTransportStarted,
8293
"streamableHttpTransport",
8394
`Server started on http://${config.httpHost}:${config.httpPort}`
8495
);
85-
});
8696

87-
transport.onclose = () => {
88-
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
89-
server.close((err?: Error) => {
90-
if (err) {
91-
logger.error(
92-
LogId.streamableHttpTransportCloseFailure,
93-
"streamableHttpTransport",
94-
`Error closing server: ${err.message}`
95-
);
96-
return;
97-
}
98-
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
99-
});
100-
};
97+
transport.onclose = () => {
98+
logger.info(LogId.streamableHttpTransportCloseRequested, "streamableHttpTransport", `Closing server`);
99+
server.close((err?: Error) => {
100+
if (err) {
101+
logger.error(
102+
LogId.streamableHttpTransportCloseFailure,
103+
"streamableHttpTransport",
104+
`Error closing server: ${err.message}`
105+
);
106+
return;
107+
}
108+
logger.info(LogId.streamableHttpTransportCloseSuccess, "streamableHttpTransport", `Server closed`);
109+
});
110+
};
111+
112+
return transport;
113+
} catch (error: unknown) {
114+
const err = error instanceof Error ? error : new Error(String(error));
115+
logger.info(
116+
LogId.streamableHttpTransportStartFailure,
117+
"streamableHttpTransport",
118+
`Error starting server: ${err.message}`
119+
);
101120

102-
return transport;
121+
throw err;
122+
}
103123
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createHttpTransport } from "../../../src/transports/streamableHttp.js";
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
6+
import { config } from "../../../src/common/config.js";
7+
import { z } from "zod";
8+
describe("streamableHttpTransport", () => {
9+
let transport: StreamableHTTPServerTransport;
10+
const mcpServer = new McpServer({
11+
name: "test",
12+
version: "1.0.0",
13+
});
14+
beforeAll(async () => {
15+
transport = await createHttpTransport();
16+
mcpServer.registerTool(
17+
"hello",
18+
{
19+
title: "Hello Tool",
20+
description: "Say hello",
21+
inputSchema: { name: z.string() },
22+
},
23+
async ({ name }) => ({
24+
content: [{ type: "text", text: `Hello, ${name}!` }],
25+
})
26+
);
27+
await mcpServer.connect(transport);
28+
});
29+
30+
afterAll(async () => {
31+
await mcpServer.close();
32+
});
33+
34+
describe("client connects successfully", () => {
35+
let client: StreamableHTTPClientTransport;
36+
beforeAll(async () => {
37+
client = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:3000/mcp"));
38+
await client.start();
39+
});
40+
41+
afterAll(async () => {
42+
await client.close();
43+
});
44+
45+
it("handles requests and sends responses", async () => {
46+
client.onmessage = (message: JSONRPCMessage) => {
47+
expect(message.jsonrpc).toBe("2.0");
48+
expect(message.result).toBeDefined();
49+
expect(message.result.tools).toBeDefined();
50+
expect(message.result.tools.length).toBe(1);
51+
expect(message.result.tools[0].name).toBe("hello");
52+
expect(message.result.tools[0].description).toBe("Say hello");
53+
};
54+
55+
await client.send({
56+
jsonrpc: "2.0",
57+
id: 1,
58+
method: "tools/list",
59+
params: {
60+
_meta: {
61+
progressToken: 1,
62+
},
63+
},
64+
});
65+
});
66+
});
67+
});

0 commit comments

Comments
 (0)