Skip to content

Commit ab7d1c9

Browse files
authored
Merge branch 'main' into issue216
2 parents 1f92854 + d54c921 commit ab7d1c9

File tree

18 files changed

+559
-198
lines changed

18 files changed

+559
-198
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ body:
77
- type: markdown
88
attributes:
99
value: "Please fill out the following details to help us address the issue."
10+
- type: textarea
11+
id: version
12+
attributes:
13+
label: "Version"
14+
description: "Please provide the version of the MCP Server where the bug occurred. (e.g., 0.1.0, main branch sha, etc.)"
15+
placeholder: "e.g., 0.1.0"
16+
validations:
17+
required: true
1018
- type: checkboxes
1119
id: app
1220
attributes:

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,24 @@ Use your Atlas API Service Accounts credentials. Must follow all the steps in [A
8787
}
8888
```
8989

90-
#### Other options
90+
### Option 3: Standalone Service using command arguments
9191

92-
Alternatively you can use environment variables in the config file or set them and run the server via npx.
92+
Start Server using npx command:
93+
94+
```shell
95+
npx -y mongodb-mcp-server --apiClientId="your-atlas-service-accounts-client-id" --apiClientSecret="your-atlas-service-accounts-client-secret"
96+
```
97+
98+
- For a complete list of arguments see [Configuration Options](#configuration-options)
99+
- To configure your Atlas Service Accounts credentials please refer to [Atlas API Access](#atlas-api-access)
100+
101+
#### Option 4: Standalone Service using environment variables
102+
103+
```shell
104+
npx -y mongodb-mcp-server
105+
```
106+
107+
You can use environment variables in the config file or set them and run the server via npx.
93108

94109
- Connection String via environment variables in the MCP file [example](#connection-string-with-environment-variables)
95110
- Atlas API credentials via environment variables in the MCP file [example](#atlas-api-credentials-with-environment-variables)

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default defineConfig([
4848
"coverage",
4949
"global.d.ts",
5050
"eslint.config.js",
51-
"jest.config.ts",
51+
"jest.config.cjs",
5252
"src/types/*.d.ts",
5353
]),
5454
eslintPluginPrettierRecommended,

jest.config.ts renamed to jest.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @type {import('ts-jest').JestConfigWithTsJest} **/
2-
export default {
2+
module.exports = {
33
preset: "ts-jest/presets/default-esm",
44
testEnvironment: "node",
55
extensionsToTreatAsEsm: [".ts"],

package-lock.json

Lines changed: 41 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mongodb-mcp-server",
33
"description": "MongoDB Model Context Protocol Server",
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"main": "dist/index.js",
66
"author": "MongoDB <[email protected]>",
77
"homepage": "https://github.com/mongodb-js/mongodb-mcp-server",
@@ -61,6 +61,7 @@
6161
},
6262
"dependencies": {
6363
"@modelcontextprotocol/sdk": "^1.8.0",
64+
"@mongodb-js/device-id": "^0.2.1",
6465
"@mongodb-js/devtools-connect": "^3.7.2",
6566
"@mongosh/service-provider-node-driver": "^3.6.0",
6667
"bson": "^6.10.3",

src/common/atlas/apiClient.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export class ApiClient {
8989
return !!(this.oauth2Client && this.accessToken);
9090
}
9191

92+
public async validateAccessToken(): Promise<void> {
93+
await this.getAccessToken();
94+
}
95+
9296
public async getIpInfo(): Promise<{
9397
currentIpv4Address: string;
9498
}> {
@@ -114,22 +118,59 @@ export class ApiClient {
114118
}>;
115119
}
116120

117-
async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
118-
let endpoint = "api/private/unauth/telemetry/events";
121+
public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
122+
if (!this.options.credentials) {
123+
await this.sendUnauthEvents(events);
124+
return;
125+
}
126+
127+
try {
128+
await this.sendAuthEvents(events);
129+
} catch (error) {
130+
if (error instanceof ApiClientError) {
131+
if (error.response.status !== 401) {
132+
throw error;
133+
}
134+
}
135+
136+
// send unauth events if any of the following are true:
137+
// 1: the token is not valid (not ApiClientError)
138+
// 2: if the api responded with 401 (ApiClientError with status 401)
139+
await this.sendUnauthEvents(events);
140+
}
141+
}
142+
143+
private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
144+
const accessToken = await this.getAccessToken();
145+
if (!accessToken) {
146+
throw new Error("No access token available");
147+
}
148+
const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
149+
const response = await fetch(authUrl, {
150+
method: "POST",
151+
headers: {
152+
Accept: "application/json",
153+
"Content-Type": "application/json",
154+
"User-Agent": this.options.userAgent,
155+
Authorization: `Bearer ${accessToken}`,
156+
},
157+
body: JSON.stringify(events),
158+
});
159+
160+
if (!response.ok) {
161+
throw await ApiClientError.fromResponse(response);
162+
}
163+
}
164+
165+
private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
119166
const headers: Record<string, string> = {
120167
Accept: "application/json",
121168
"Content-Type": "application/json",
122169
"User-Agent": this.options.userAgent,
123170
};
124171

125-
const accessToken = await this.getAccessToken();
126-
if (accessToken) {
127-
endpoint = "api/private/v1.0/telemetry/events";
128-
headers["Authorization"] = `Bearer ${accessToken}`;
129-
}
130-
131-
const url = new URL(endpoint, this.options.baseUrl);
132-
const response = await fetch(url, {
172+
const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
173+
const response = await fetch(unauthUrl, {
133174
method: "POST",
134175
headers,
135176
body: JSON.stringify(events),
@@ -245,6 +286,7 @@ export class ApiClient {
245286
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
246287
options
247288
);
289+
248290
if (error) {
249291
throw ApiClientError.fromError(response, error);
250292
}

src/helpers/EJsonTransport.ts

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

src/helpers/deferred-promise.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

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) {

0 commit comments

Comments
 (0)