Skip to content

Commit 71fa493

Browse files
committed
Use ejson parsing for stdio messages
1 parent ec590e8 commit 71fa493

File tree

5 files changed

+134
-3
lines changed

5 files changed

+134
-3
lines changed

src/helpers/EJsonTransport.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { JSONRPCMessage, JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
2+
import { EJSON } from "bson";
3+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4+
5+
class EJsonReadBuffer {
6+
private _buffer?: Buffer;
7+
8+
append(chunk: Buffer): void {
9+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
10+
}
11+
12+
readMessage(): JSONRPCMessage | null {
13+
if (!this._buffer) {
14+
return null;
15+
}
16+
17+
const index = this._buffer.indexOf("\n");
18+
if (index === -1) {
19+
return null;
20+
}
21+
22+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
23+
this._buffer = this._buffer.subarray(index + 1);
24+
25+
// This is using EJSON.parse instead of JSON.parse to handle BSON types
26+
return JSONRPCMessageSchema.parse(EJSON.parse(line));
27+
}
28+
29+
clear(): void {
30+
this._buffer = undefined;
31+
}
32+
}
33+
34+
// This is a hacky workaround for https://github.com/mongodb-js/mongodb-mcp-server/issues/211
35+
// The underlying issue is that StdioServerTransport uses JSON.parse to deserialize
36+
// messages, but that doesn't handle bson types, such as ObjectId when serialized as EJSON.
37+
//
38+
// This function creates a StdioServerTransport and replaces the internal readBuffer with EJsonReadBuffer
39+
// that uses EJson.parse instead.
40+
export function createEJsonTransport(): StdioServerTransport {
41+
const server = new StdioServerTransport();
42+
server["_readBuffer"] = new EJsonReadBuffer();
43+
44+
return server;
45+
}

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env node
22

3-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
43
import logger, { LogId } from "./logger.js";
54
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
65
import { config } from "./config.js";
76
import { Session } from "./session.js";
87
import { Server } from "./server.js";
98
import { packageInfo } from "./helpers/packageInfo.js";
109
import { Telemetry } from "./telemetry/telemetry.js";
10+
import { createEJsonTransport } from "./helpers/EJsonTransport.js";
1111

1212
try {
1313
const session = new Session({
@@ -29,7 +29,7 @@ try {
2929
userConfig: config,
3030
});
3131

32-
const transport = new StdioServerTransport();
32+
const transport = createEJsonTransport();
3333

3434
await server.connect(transport);
3535
} catch (error: unknown) {

tests/integration/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export function validateThrowsForInvalidArguments(
227227
}
228228

229229
/** Expects the argument being defined and asserts it */
230-
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
230+
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined | null> {
231231
expect(arg).toBeDefined();
232+
expect(arg).not.toBeNull();
232233
}

tests/integration/tools/mongodb/read/find.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
validateToolMetadata,
55
validateThrowsForInvalidArguments,
66
getResponseElements,
7+
expectDefined,
78
} from "../../../helpers.js";
89
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
910

@@ -171,6 +172,33 @@ describeWithMongoDB("find tool", (integration) => {
171172
expect(JSON.parse(elements[i + 1].text).value).toEqual(i);
172173
}
173174
});
175+
176+
it("can find objects by $oid", async () => {
177+
await integration.connectMcpClient();
178+
179+
const fooObject = await integration
180+
.mongoClient()
181+
.db(integration.randomDbName())
182+
.collection("foo")
183+
.findOne();
184+
expectDefined(fooObject);
185+
186+
const response = await integration.mcpClient().callTool({
187+
name: "find",
188+
arguments: {
189+
database: integration.randomDbName(),
190+
collection: "foo",
191+
filter: { _id: fooObject._id },
192+
},
193+
});
194+
195+
const elements = getResponseElements(response.content);
196+
expect(elements).toHaveLength(2);
197+
expect(elements[0].text).toEqual('Found 1 documents in the collection "foo":');
198+
199+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
200+
expect(JSON.parse(elements[1].text).value).toEqual(fooObject.value);
201+
});
174202
});
175203

176204
validateAutoConnectBehavior(integration, "find", () => {

tests/unit/EJsonTransport.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Decimal128, MaxKey, MinKey, ObjectId, Timestamp, UUID } from "bson";
2+
import { createEJsonTransport } from "../../src/helpers/EJsonTransport.js";
3+
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
4+
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
5+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6+
import { Readable } from "stream";
7+
8+
describe("EJsonTransport", () => {
9+
let transport: StdioServerTransport;
10+
beforeEach(async () => {
11+
transport = createEJsonTransport();
12+
await transport.start();
13+
});
14+
15+
afterEach(async () => {
16+
await transport.close();
17+
});
18+
19+
it("ejson deserializes messages", () => {
20+
const messages: { message: JSONRPCMessage; extra?: { authInfo?: AuthInfo } }[] = [];
21+
transport.onmessage = (
22+
message,
23+
extra?: {
24+
authInfo?: AuthInfo;
25+
}
26+
) => {
27+
messages.push({ message, extra });
28+
};
29+
30+
(transport["_stdin"] as Readable).emit(
31+
"data",
32+
Buffer.from(
33+
'{"jsonrpc":"2.0","id":1,"method":"testMethod","params":{"oid":{"$oid":"681b741f13aa74a0687b5110"},"uuid":{"$uuid":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"},"date":{"$date":"2025-05-07T14:54:23.973Z"},"decimal":{"$numberDecimal":"1234567890987654321"},"int32":123,"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"timestamp":{"$timestamp":{"t":123,"i":456}}}}\n',
34+
"utf-8"
35+
)
36+
);
37+
38+
expect(messages.length).toBe(1);
39+
const message = messages[0].message;
40+
41+
expect(message).toEqual({
42+
jsonrpc: "2.0",
43+
id: 1,
44+
method: "testMethod",
45+
params: {
46+
oid: new ObjectId("681b741f13aa74a0687b5110"),
47+
uuid: new UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"),
48+
date: new Date(Date.parse("2025-05-07T14:54:23.973Z")),
49+
decimal: new Decimal128("1234567890987654321"),
50+
int32: 123,
51+
maxKey: new MaxKey(),
52+
minKey: new MinKey(),
53+
timestamp: new Timestamp({ t: 123, i: 456 }),
54+
},
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)