Skip to content

Commit 73a09e6

Browse files
authored
Merge branch 'main' into chore/mcp-92
2 parents 03798dd + cd5cf37 commit 73a09e6

File tree

17 files changed

+362
-140
lines changed

17 files changed

+362
-140
lines changed

.github/workflows/check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ jobs:
5555
rm -rf node_modules
5656
npm pkg set scripts.prepare="exit 0"
5757
npm install --omit=dev
58-
- run: npx -y @modelcontextprotocol/inspector@0.16.2 --cli --method tools/list -- node dist/esm/index.js
58+
- run: npx -y @modelcontextprotocol/inspector@0.16.5 --cli --method tools/list -- node dist/esm/index.js

.github/workflows/prepare_release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
node-version-file: package.json
2929
registry-url: "https://registry.npmjs.org"
3030
cache: "npm"
31+
- name: Install dependencies
32+
run: npm ci
3133
- name: Bump version
3234
id: bump-version
3335
run: |

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@ai-sdk/google": "^1.2.22",
6363
"@ai-sdk/openai": "^1.3.23",
6464
"@eslint/js": "^9.30.1",
65-
"@modelcontextprotocol/inspector": "^0.16.0",
65+
"@modelcontextprotocol/inspector": "^0.16.5",
6666
"@mongodb-js/oidc-mock-provider": "^0.11.3",
6767
"@redocly/cli": "^2.0.7",
6868
"@types/express": "^5.0.1",

src/common/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export interface UserConfig extends CliOptions {
116116
transport: "stdio" | "http";
117117
httpPort: number;
118118
httpHost: string;
119+
httpHeaders: Record<string, string>;
119120
loggers: Array<"stderr" | "disk" | "mcp">;
120121
idleTimeoutMs: number;
121122
notificationTimeoutMs: number;
@@ -137,6 +138,7 @@ export const defaultUserConfig: UserConfig = {
137138
loggers: ["disk", "mcp"],
138139
idleTimeoutMs: 600000, // 10 minutes
139140
notificationTimeoutMs: 540000, // 9 minutes
141+
httpHeaders: {},
140142
};
141143

142144
export const config = setupUserConfig({

src/common/logger.ts

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import fs from "fs/promises";
22
import type { MongoLogId, MongoLogWriter } from "mongodb-log-writer";
33
import { mongoLogId, MongoLogManager } from "mongodb-log-writer";
44
import redact from "mongodb-redact";
5-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
65
import type { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
76
import { EventEmitter } from "events";
7+
import type { Server } from "../lib.js";
88

99
export type LogLevel = LoggingMessageNotification["params"]["level"];
1010

@@ -51,6 +51,8 @@ export const LogId = {
5151
streamableHttpTransportSessionCloseNotificationFailure: mongoLogId(1_006_004),
5252
streamableHttpTransportRequestFailure: mongoLogId(1_006_005),
5353
streamableHttpTransportCloseFailure: mongoLogId(1_006_006),
54+
streamableHttpTransportKeepAliveFailure: mongoLogId(1_006_007),
55+
streamableHttpTransportKeepAlive: mongoLogId(1_006_008),
5456

5557
exportCleanupError: mongoLogId(1_007_001),
5658
exportCreationError: mongoLogId(1_007_002),
@@ -64,7 +66,7 @@ export const LogId = {
6466
oidcFlow: mongoLogId(1_008_001),
6567
} as const;
6668

67-
interface LogPayload {
69+
export interface LogPayload {
6870
id: MongoLogId;
6971
context: string;
7072
message: string;
@@ -152,6 +154,26 @@ export abstract class LoggerBase<T extends EventMap<T> = DefaultEventMap> extend
152154
public emergency(payload: LogPayload): void {
153155
this.log("emergency", payload);
154156
}
157+
158+
protected mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
159+
switch (level) {
160+
case "info":
161+
return "info";
162+
case "warning":
163+
return "warn";
164+
case "error":
165+
return "error";
166+
case "notice":
167+
case "debug":
168+
return "debug";
169+
case "critical":
170+
case "alert":
171+
case "emergency":
172+
return "fatal";
173+
default:
174+
return "info";
175+
}
176+
}
155177
}
156178

157179
export class ConsoleLogger extends LoggerBase {
@@ -225,42 +247,40 @@ export class DiskLogger extends LoggerBase<{ initialized: [] }> {
225247

226248
this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message, payload.attributes);
227249
}
228-
229-
private mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
230-
switch (level) {
231-
case "info":
232-
return "info";
233-
case "warning":
234-
return "warn";
235-
case "error":
236-
return "error";
237-
case "notice":
238-
case "debug":
239-
return "debug";
240-
case "critical":
241-
case "alert":
242-
case "emergency":
243-
return "fatal";
244-
default:
245-
return "info";
246-
}
247-
}
248250
}
249251

250252
export class McpLogger extends LoggerBase {
251-
public constructor(private readonly server: McpServer) {
253+
private static readonly LOG_LEVELS: LogLevel[] = [
254+
"debug",
255+
"info",
256+
"notice",
257+
"warning",
258+
"error",
259+
"critical",
260+
"alert",
261+
"emergency",
262+
];
263+
264+
public constructor(private readonly server: Server) {
252265
super();
253266
}
254267

255268
protected readonly type: LoggerType = "mcp";
256269

257270
protected logCore(level: LogLevel, payload: LogPayload): void {
258271
// Only log if the server is connected
259-
if (!this.server?.isConnected()) {
272+
if (!this.server.mcpServer.isConnected()) {
273+
return;
274+
}
275+
276+
const minimumLevel = McpLogger.LOG_LEVELS.indexOf(this.server.mcpLogLevel);
277+
const currentLevel = McpLogger.LOG_LEVELS.indexOf(level);
278+
if (minimumLevel > currentLevel) {
279+
// Don't log if the requested level is lower than the minimum level
260280
return;
261281
}
262282

263-
void this.server.server.sendLoggingMessage({
283+
void this.server.mcpServer.server.sendLoggingMessage({
264284
level,
265285
data: `[${payload.context}]: ${payload.message}`,
266286
});
@@ -286,7 +306,11 @@ export class CompositeLogger extends LoggerBase {
286306
public log(level: LogLevel, payload: LogPayload): void {
287307
// Override the public method to avoid the base logger redacting the message payload
288308
for (const logger of this.loggers) {
289-
logger.log(level, { ...payload, attributes: { ...this.attributes, ...payload.attributes } });
309+
const attributes =
310+
Object.keys(this.attributes).length > 0 || payload.attributes
311+
? { ...this.attributes, ...payload.attributes }
312+
: undefined;
313+
logger.log(level, { ...payload, attributes });
290314
}
291315
}
292316

src/helpers/deviceId.ts

Lines changed: 22 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,63 @@
11
import { getDeviceId } from "@mongodb-js/device-id";
2-
import nodeMachineId from "node-machine-id";
2+
import * as nodeMachineId from "node-machine-id";
33
import type { LoggerBase } from "../common/logger.js";
44
import { LogId } from "../common/logger.js";
55

66
export const DEVICE_ID_TIMEOUT = 3000;
77

88
export class DeviceId {
9-
private deviceId: string | undefined = undefined;
10-
private deviceIdPromise: Promise<string> | undefined = undefined;
11-
private abortController: AbortController | undefined = undefined;
9+
private static readonly UnknownDeviceId = Promise.resolve("unknown");
10+
11+
private deviceIdPromise: Promise<string>;
12+
private abortController: AbortController;
1213
private logger: LoggerBase;
1314
private readonly getMachineId: () => Promise<string>;
1415
private timeout: number;
15-
private static instance: DeviceId | undefined = undefined;
1616

1717
private constructor(logger: LoggerBase, timeout: number = DEVICE_ID_TIMEOUT) {
1818
this.logger = logger;
1919
this.timeout = timeout;
2020
this.getMachineId = (): Promise<string> => nodeMachineId.machineId(true);
21+
this.abortController = new AbortController();
22+
23+
this.deviceIdPromise = DeviceId.UnknownDeviceId;
2124
}
2225

23-
public static create(logger: LoggerBase, timeout?: number): DeviceId {
24-
if (this.instance) {
25-
throw new Error("DeviceId instance already exists, use get() to retrieve the device ID");
26-
}
26+
private initialize(): void {
27+
this.deviceIdPromise = getDeviceId({
28+
getMachineId: this.getMachineId,
29+
onError: (reason, error) => {
30+
this.handleDeviceIdError(reason, String(error));
31+
},
32+
timeout: this.timeout,
33+
abortSignal: this.abortController.signal,
34+
});
35+
}
2736

37+
public static create(logger: LoggerBase, timeout?: number): DeviceId {
2838
const instance = new DeviceId(logger, timeout ?? DEVICE_ID_TIMEOUT);
29-
instance.setup();
30-
31-
this.instance = instance;
39+
instance.initialize();
3240

3341
return instance;
3442
}
3543

36-
private setup(): void {
37-
this.deviceIdPromise = this.calculateDeviceId();
38-
}
39-
4044
/**
4145
* Closes the device ID calculation promise and abort controller.
4246
*/
4347
public close(): void {
44-
if (this.abortController) {
45-
this.abortController.abort();
46-
this.abortController = undefined;
47-
}
48-
49-
this.deviceId = undefined;
50-
this.deviceIdPromise = undefined;
51-
DeviceId.instance = undefined;
48+
this.abortController.abort();
5249
}
5350

5451
/**
5552
* Gets the device ID, waiting for the calculation to complete if necessary.
5653
* @returns Promise that resolves to the device ID string
5754
*/
5855
public get(): Promise<string> {
59-
if (this.deviceId) {
60-
return Promise.resolve(this.deviceId);
61-
}
62-
63-
if (this.deviceIdPromise) {
64-
return this.deviceIdPromise;
65-
}
66-
67-
return this.calculateDeviceId();
68-
}
69-
70-
/**
71-
* Internal method that performs the actual device ID calculation.
72-
*/
73-
private async calculateDeviceId(): Promise<string> {
74-
if (!this.abortController) {
75-
this.abortController = new AbortController();
76-
}
77-
78-
this.deviceIdPromise = getDeviceId({
79-
getMachineId: this.getMachineId,
80-
onError: (reason, error) => {
81-
this.handleDeviceIdError(reason, String(error));
82-
},
83-
timeout: this.timeout,
84-
abortSignal: this.abortController.signal,
85-
});
86-
8756
return this.deviceIdPromise;
8857
}
8958

9059
private handleDeviceIdError(reason: string, error: string): void {
91-
this.deviceIdPromise = Promise.resolve("unknown");
60+
this.deviceIdPromise = DeviceId.UnknownDeviceId;
9261

9362
switch (reason) {
9463
case "resolutionError":

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ async function main(): Promise<void> {
9191
transportRunner.logger.info({
9292
id: LogId.serverCloseRequested,
9393
context: "server",
94-
message: "Closing server",
94+
message: `Closing server due to error: ${error as string}`,
95+
noRedaction: true,
9596
});
97+
9698
try {
9799
await transportRunner.close();
98100
transportRunner.logger.info({

src/lib.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export { Server, type ServerOptions } from "./server.js";
22
export { Telemetry } from "./telemetry/telemetry.js";
33
export { Session, type SessionOptions } from "./common/session.js";
4-
export type { UserConfig } from "./common/config.js";
4+
export { type UserConfig, defaultUserConfig } from "./common/config.js";
5+
export { StreamableHttpRunner } from "./transports/streamableHttp.js";
6+
export { LoggerBase } from "./common/logger.js";
7+
export type { LogPayload, LoggerType, LogLevel } from "./common/logger.js";

src/server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
44
import { AtlasTools } from "./tools/atlas/tools.js";
55
import { MongoDbTools } from "./tools/mongodb/tools.js";
66
import { Resources } from "./resources/resources.js";
7+
import type { LogLevel } from "./common/logger.js";
78
import { LogId } from "./common/logger.js";
89
import type { Telemetry } from "./telemetry/telemetry.js";
910
import type { UserConfig } from "./common/config.js";
@@ -12,6 +13,7 @@ import { type ServerCommand } from "./telemetry/types.js";
1213
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
1314
import {
1415
CallToolRequestSchema,
16+
SetLevelRequestSchema,
1517
SubscribeRequestSchema,
1618
UnsubscribeRequestSchema,
1719
} from "@modelcontextprotocol/sdk/types.js";
@@ -32,6 +34,13 @@ export class Server {
3234
private readonly telemetry: Telemetry;
3335
public readonly userConfig: UserConfig;
3436
public readonly tools: ToolBase[] = [];
37+
38+
private _mcpLogLevel: LogLevel = "debug";
39+
40+
public get mcpLogLevel(): LogLevel {
41+
return this._mcpLogLevel;
42+
}
43+
3544
private readonly startTime: number;
3645
private readonly subscriptions = new Set<string>();
3746

@@ -97,6 +106,11 @@ export class Server {
97106
return {};
98107
});
99108

109+
this.mcpServer.server.setRequestHandler(SetLevelRequestSchema, ({ params }) => {
110+
this._mcpLogLevel = params.level;
111+
return {};
112+
});
113+
100114
this.mcpServer.server.oninitialized = (): void => {
101115
this.session.setMcpClient(this.mcpServer.server.getClientVersion());
102116
// Placed here to start the connection to the config connection string as soon as the server is initialized.

0 commit comments

Comments
 (0)