Skip to content

Commit 2c21af3

Browse files
authored
test: add streamable http tests [MCP-60] (#362)
1 parent ff982e0 commit 2c21af3

File tree

6 files changed

+125
-27
lines changed

6 files changed

+125
-27
lines changed

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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 { z } from "zod";
7+
describe("streamableHttpTransport", () => {
8+
let transport: StreamableHTTPServerTransport;
9+
const mcpServer = new McpServer({
10+
name: "test",
11+
version: "1.0.0",
12+
});
13+
beforeAll(async () => {
14+
transport = await createHttpTransport();
15+
mcpServer.registerTool(
16+
"hello",
17+
{
18+
title: "Hello Tool",
19+
description: "Say hello",
20+
inputSchema: { name: z.string() },
21+
},
22+
({ name }) => ({
23+
content: [{ type: "text", text: `Hello, ${name}!` }],
24+
})
25+
);
26+
await mcpServer.connect(transport);
27+
});
28+
29+
afterAll(async () => {
30+
await mcpServer.close();
31+
});
32+
33+
describe("client connects successfully", () => {
34+
let client: StreamableHTTPClientTransport;
35+
beforeAll(async () => {
36+
client = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:3000/mcp"));
37+
await client.start();
38+
});
39+
40+
afterAll(async () => {
41+
await client.close();
42+
});
43+
44+
it("handles requests and sends responses", async () => {
45+
client.onmessage = (message: JSONRPCMessage) => {
46+
const messageResult = message as
47+
| {
48+
result?: {
49+
tools: {
50+
name: string;
51+
description: string;
52+
}[];
53+
};
54+
}
55+
| undefined;
56+
57+
expect(message.jsonrpc).toBe("2.0");
58+
expect(messageResult).toBeDefined();
59+
expect(messageResult?.result?.tools).toBeDefined();
60+
expect(messageResult?.result?.tools.length).toBe(1);
61+
expect(messageResult?.result?.tools[0]?.name).toBe("hello");
62+
expect(messageResult?.result?.tools[0]?.description).toBe("Say hello");
63+
};
64+
65+
await client.send({
66+
jsonrpc: "2.0",
67+
id: 1,
68+
method: "tools/list",
69+
params: {
70+
_meta: {
71+
progressToken: 1,
72+
},
73+
},
74+
});
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)