From 84aa8bac17b539b6894cbb8be6d9ce2a86a74e95 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 30 Jul 2025 19:27:21 +0200 Subject: [PATCH 01/11] chore: Probe of concept for reactive resources based on the session It can listen to events on the session and their arguments, and reduces the current state with the argument to provide a new state. --- src/common/session.ts | 9 ++++- src/resources/common/config.ts | 40 +++++++++++++++++++ src/resources/common/debug.ts | 50 +++++++++++++++++++++++ src/resources/resource.ts | 73 ++++++++++++++++++++++++++++++++++ src/resources/resources.ts | 4 ++ src/server.ts | 36 +++-------------- 6 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 src/resources/common/config.ts create mode 100644 src/resources/common/debug.ts create mode 100644 src/resources/resource.ts create mode 100644 src/resources/resources.ts diff --git a/src/common/session.ts b/src/common/session.ts index 689a25d8e..7752f7b60 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -13,10 +13,13 @@ export interface SessionOptions { apiClientSecret?: string; } -export class Session extends EventEmitter<{ +export type SessionEvents = { + connected: []; close: []; disconnect: []; -}> { +}; + +export class Session extends EventEmitter { sessionId?: string; serviceProvider?: NodeDriverServiceProvider; apiClient: ApiClient; @@ -116,5 +119,7 @@ export class Session extends EventEmitter<{ proxy: { useEnvironmentVariableProxies: true }, applyProxyToOIDC: true, }); + + this.emit("connected"); } } diff --git a/src/resources/common/config.ts b/src/resources/common/config.ts new file mode 100644 index 000000000..a4e158f86 --- /dev/null +++ b/src/resources/common/config.ts @@ -0,0 +1,40 @@ +import { ReactiveResource } from "../resource.js"; +import { config } from "../../common/config.js"; +import type { UserConfig } from "../../common/config.js"; + +export class ConfigResource extends ReactiveResource( + { + name: "config", + uri: "config://config", + config: { + description: + "Server configuration, supplied by the user either as environment variables or as startup arguments", + }, + }, + { + initial: { ...config }, + events: [], + } +) { + reduce(previous: UserConfig, eventName: undefined, event: undefined): UserConfig { + void event; + return previous; + } + + toOutput(state: UserConfig): string { + const result = { + telemetry: state.telemetry, + logPath: state.logPath, + connectionString: state.connectionString + ? "set; access to MongoDB tools are currently available to use" + : "not set; before using any MongoDB tool, you need to configure a connection string, alternatively you can setup MongoDB Atlas access, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", + connectOptions: state.connectOptions, + atlas: + state.apiClientId && state.apiClientSecret + ? "set; MongoDB Atlas tools are currently available to use" + : "not set; MongoDB Atlas tools are currently unavailable, to have access to MongoDB Atlas tools like creating clusters or connecting to clusters make sure to setup credentials, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", + }; + + return JSON.stringify(result); + } +} diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts new file mode 100644 index 000000000..6c3ed325f --- /dev/null +++ b/src/resources/common/debug.ts @@ -0,0 +1,50 @@ +import { ReactiveResource } from "../resource.js"; +import { config } from "../../common/config.js"; +import type { UserConfig } from "../../common/config.js"; + +type ConnectionStateDebuggingInformation = { + readonly tag: "connected" | "connecting" | "disconnected" | "errored"; + readonly connectionStringAuthType?: "scram" | "ldap" | "kerberos" | "oidc-auth-flow" | "oidc-device-flow" | "x.509"; + readonly oidcLoginUrl?: string; + readonly oidcUserCode?: string; + readonly errorReason?: string; +}; + +export class DebugResource extends ReactiveResource( + { + name: "debug", + uri: "config://debug", + config: { + description: "Debugging information for connectivity issues.", + }, + }, + { + initial: { tag: "disconnected" }, + events: ["connected", "disconnect", "close"], + } +) { + reduce( + previous: ConnectionStateDebuggingInformation, + eventName: "connected" | "disconnect" | "close", + event: undefined + ): ConnectionStateDebuggingInformation { + void event; + + switch (eventName) { + case "connected": + return { tag: "connected" }; + case "disconnect": + return { tag: "disconnected" }; + case "close": + return { tag: "disconnected" }; + } + } + + toOutput(state: ConnectionStateDebuggingInformation): string { + const result = { + connectionStatus: state.tag, + }; + + return JSON.stringify(result); + } +} diff --git a/src/resources/resource.ts b/src/resources/resource.ts new file mode 100644 index 000000000..fdb15dad3 --- /dev/null +++ b/src/resources/resource.ts @@ -0,0 +1,73 @@ +import { Server } from "../server.js"; +import { Session } from "../common/session.js"; +import { UserConfig } from "../common/config.js"; +import { Telemetry } from "../telemetry/telemetry.js"; +import type { SessionEvents } from "../common/session.js"; +import { ReadResourceCallback, RegisteredResource, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js"; + +type PayloadOf = SessionEvents[K][0]; + +type ResourceConfiguration = { name: string; uri: string; config: ResourceMetadata }; + +export function ReactiveResource( + { name, uri, config: resourceConfig }: ResourceConfiguration, + { + initial, + events, + }: { + initial: V; + events: KE; + } +) { + type E = KE[number]; + + abstract class NewReactiveResource { + private registeredResource?: RegisteredResource; + + constructor( + protected readonly server: Server, + protected readonly session: Session, + protected readonly config: UserConfig, + protected readonly telemetry: Telemetry, + private current?: V + ) { + this.current = initial; + + for (const event of events) { + this.session.on(event, (...args: SessionEvents[typeof event]) => { + this.current = this.reduce(this.current, event, (args as unknown[])[0] as PayloadOf); + this.triggerUpdate(); + }); + } + } + + public register(): void { + this.registeredResource = this.server.mcpServer.registerResource( + name, + uri, + resourceConfig, + this.resourceCallback + ); + } + + private resourceCallback: ReadResourceCallback = (uri) => ({ + contents: [ + { + text: this.toOutput(this.current), + mimeType: "application/json", + uri: uri.href, + }, + ], + }); + + private triggerUpdate() { + this.registeredResource?.update({}); + this.server.mcpServer.sendResourceListChanged(); + } + + abstract reduce(previous: V | undefined, eventName: E, ...event: PayloadOf[]): V; + abstract toOutput(state: V | undefined): string; + } + + return NewReactiveResource; +} diff --git a/src/resources/resources.ts b/src/resources/resources.ts new file mode 100644 index 000000000..40a177023 --- /dev/null +++ b/src/resources/resources.ts @@ -0,0 +1,4 @@ +import { ConfigResource } from "./common/config.js"; +import { DebugResource } from "./common/debug.js"; + +export const Resources = [ConfigResource, DebugResource] as const; diff --git a/src/server.ts b/src/server.ts index d58cca520..952a621c6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,7 @@ import { Session } from "./common/session.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; import { MongoDbTools } from "./tools/mongodb/tools.js"; +import { Resources } from "./resources/resources.js"; import logger, { LogId, LoggerBase, McpLogger, DiskLogger, ConsoleLogger } from "./common/logger.js"; import { ObjectId } from "mongodb"; import { Telemetry } from "./telemetry/telemetry.js"; @@ -155,37 +156,10 @@ export class Server { } private registerResources() { - this.mcpServer.resource( - "config", - "config://config", - { - description: - "Server configuration, supplied by the user either as environment variables or as startup arguments", - }, - (uri) => { - const result = { - telemetry: this.userConfig.telemetry, - logPath: this.userConfig.logPath, - connectionString: this.userConfig.connectionString - ? "set; access to MongoDB tools are currently available to use" - : "not set; before using any MongoDB tool, you need to configure a connection string, alternatively you can setup MongoDB Atlas access, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", - connectOptions: this.userConfig.connectOptions, - atlas: - this.userConfig.apiClientId && this.userConfig.apiClientSecret - ? "set; MongoDB Atlas tools are currently available to use" - : "not set; MongoDB Atlas tools are currently unavailable, to have access to MongoDB Atlas tools like creating clusters or connecting to clusters make sure to setup credentials, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", - }; - return { - contents: [ - { - text: JSON.stringify(result), - mimeType: "application/json", - uri: uri.href, - }, - ], - }; - } - ); + for (const resourceConstructor of Resources) { + const resource = new resourceConstructor(this, this.session, this.userConfig, this.telemetry); + resource.register(); + } } private async validateConfig(): Promise { From ce6d2e9cb8aebe57845a0dd081b22ee779be41f4 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 10:26:21 +0200 Subject: [PATCH 02/11] chore: Add debug resource relevant events Also add unit tests to show how to test resources. --- src/common/session.ts | 36 +++++++++++-------- src/resources/common/config.ts | 18 +++++----- src/resources/common/debug.ts | 35 +++++++++++------- src/resources/resource.ts | 20 +++++++---- src/server.ts | 2 +- tests/unit/resources/common/debug.test.ts | 43 +++++++++++++++++++++++ 6 files changed, 111 insertions(+), 43 deletions(-) create mode 100644 tests/unit/resources/common/debug.test.ts diff --git a/src/common/session.ts b/src/common/session.ts index 7752f7b60..fb3e4a9b9 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -17,6 +17,7 @@ export type SessionEvents = { connected: []; close: []; disconnect: []; + "connection-error": [string]; }; export class Session extends EventEmitter { @@ -105,20 +106,27 @@ export class Session extends EventEmitter { connectionString, defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`, }); - this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, { - productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/", - productName: "MongoDB MCP", - readConcern: { - level: connectOptions.readConcern, - }, - readPreference: connectOptions.readPreference, - writeConcern: { - w: connectOptions.writeConcern, - }, - timeoutMS: connectOptions.timeoutMS, - proxy: { useEnvironmentVariableProxies: true }, - applyProxyToOIDC: true, - }); + + try { + this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, { + productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/", + productName: "MongoDB MCP", + readConcern: { + level: connectOptions.readConcern, + }, + readPreference: connectOptions.readPreference, + writeConcern: { + w: connectOptions.writeConcern, + }, + timeoutMS: connectOptions.timeoutMS, + proxy: { useEnvironmentVariableProxies: true }, + applyProxyToOIDC: true, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : `${error}`; + this.emit("connection-error", message); + throw error; + } this.emit("connected"); } diff --git a/src/resources/common/config.ts b/src/resources/common/config.ts index a4e158f86..8d2e80897 100644 --- a/src/resources/common/config.ts +++ b/src/resources/common/config.ts @@ -16,21 +16,23 @@ export class ConfigResource extends ReactiveResource( events: [], } ) { - reduce(previous: UserConfig, eventName: undefined, event: undefined): UserConfig { + reduce(eventName: undefined, event: undefined): UserConfig { + void eventName; void event; - return previous; + + return this.current; } - toOutput(state: UserConfig): string { + toOutput(): string { const result = { - telemetry: state.telemetry, - logPath: state.logPath, - connectionString: state.connectionString + telemetry: this.current.telemetry, + logPath: this.current.logPath, + connectionString: this.current.connectionString ? "set; access to MongoDB tools are currently available to use" : "not set; before using any MongoDB tool, you need to configure a connection string, alternatively you can setup MongoDB Atlas access, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", - connectOptions: state.connectOptions, + connectOptions: this.current.connectOptions, atlas: - state.apiClientId && state.apiClientSecret + this.current.apiClientId && this.current.apiClientSecret ? "set; MongoDB Atlas tools are currently available to use" : "not set; MongoDB Atlas tools are currently unavailable, to have access to MongoDB Atlas tools like creating clusters or connecting to clusters make sure to setup credentials, more info at 'https://github.com/mongodb-js/mongodb-mcp-server'.", }; diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index 6c3ed325f..2b3c49388 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -1,6 +1,4 @@ import { ReactiveResource } from "../resource.js"; -import { config } from "../../common/config.js"; -import type { UserConfig } from "../../common/config.js"; type ConnectionStateDebuggingInformation = { readonly tag: "connected" | "connecting" | "disconnected" | "errored"; @@ -19,32 +17,43 @@ export class DebugResource extends ReactiveResource( }, }, { - initial: { tag: "disconnected" }, - events: ["connected", "disconnect", "close"], + initial: { tag: "disconnected" } as ConnectionStateDebuggingInformation, + events: ["connected", "disconnect", "close", "connection-error"], } ) { reduce( - previous: ConnectionStateDebuggingInformation, - eventName: "connected" | "disconnect" | "close", - event: undefined + eventName: "connected" | "disconnect" | "close" | "connection-error", + event: string | undefined ): ConnectionStateDebuggingInformation { void event; switch (eventName) { case "connected": return { tag: "connected" }; + case "connection-error": + return { tag: "errored", errorReason: event }; case "disconnect": - return { tag: "disconnected" }; case "close": return { tag: "disconnected" }; } } - toOutput(state: ConnectionStateDebuggingInformation): string { - const result = { - connectionStatus: state.tag, - }; + toOutput(): string { + let result = ""; + + switch (this.current.tag) { + case "connected": + result += "The user is connected to the MongoDB cluster."; + break; + case "errored": + result += `The user is not connected to a MongoDB cluster because of an error.\n`; + result += `${this.current.errorReason}`; + break; + case "disconnected": + result += "The user is not connected to a MongoDB cluster."; + break; + } - return JSON.stringify(result); + return result; } } diff --git a/src/resources/resource.ts b/src/resources/resource.ts index fdb15dad3..80250b0b1 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -23,19 +23,21 @@ export function ReactiveResource abstract class NewReactiveResource { private registeredResource?: RegisteredResource; + protected readonly session: Session; + protected readonly config: UserConfig; constructor( protected readonly server: Server, - protected readonly session: Session, - protected readonly config: UserConfig, protected readonly telemetry: Telemetry, - private current?: V + protected current: V ) { this.current = initial; + this.session = server.session; + this.config = server.userConfig; for (const event of events) { this.session.on(event, (...args: SessionEvents[typeof event]) => { - this.current = this.reduce(this.current, event, (args as unknown[])[0] as PayloadOf); + this.reduceApply(event, (args as unknown[])[0] as PayloadOf); this.triggerUpdate(); }); } @@ -53,7 +55,7 @@ export function ReactiveResource private resourceCallback: ReadResourceCallback = (uri) => ({ contents: [ { - text: this.toOutput(this.current), + text: this.toOutput(), mimeType: "application/json", uri: uri.href, }, @@ -65,8 +67,12 @@ export function ReactiveResource this.server.mcpServer.sendResourceListChanged(); } - abstract reduce(previous: V | undefined, eventName: E, ...event: PayloadOf[]): V; - abstract toOutput(state: V | undefined): string; + reduceApply(eventName: E, ...event: PayloadOf[]): void { + this.current = this.reduce(eventName, ...event); + } + + protected abstract reduce(eventName: E, ...event: PayloadOf[]): V; + abstract toOutput(): string; } return NewReactiveResource; diff --git a/src/server.ts b/src/server.ts index 952a621c6..1eccbdcdc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -157,7 +157,7 @@ export class Server { private registerResources() { for (const resourceConstructor of Resources) { - const resource = new resourceConstructor(this, this.session, this.userConfig, this.telemetry); + const resource = new resourceConstructor(this, this.telemetry); resource.register(); } } diff --git a/tests/unit/resources/common/debug.test.ts b/tests/unit/resources/common/debug.test.ts new file mode 100644 index 000000000..296682630 --- /dev/null +++ b/tests/unit/resources/common/debug.test.ts @@ -0,0 +1,43 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DebugResource } from "../../../../src/resources/common/debug.js"; +import { Session } from "../../../../src/common/session.js"; +import { Server } from "../../../../src/server.js"; +import { Telemetry } from "../../../../src/telemetry/telemetry.js"; +import { config } from "../../../../src/common/config.js"; + +describe("debug resource", () => { + let session = new Session({} as any); + let server = new Server({ session } as any); + let telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" }); + + let debugResource: DebugResource = new DebugResource(server, telemetry, { tag: "disconnected" }); + + it("should be connected when a connected event happens", () => { + debugResource.reduceApply("connected", undefined); + const output = debugResource.toOutput(); + + expect(output).toContain(`The user is connected to the MongoDB cluster.`); + }); + + it("should be disconnected when a disconnect event happens", () => { + debugResource.reduceApply("disconnect", undefined); + const output = debugResource.toOutput(); + + expect(output).toContain(`The user is not connected to a MongoDB cluster.`); + }); + + it("should be disconnected when a close event happens", () => { + debugResource.reduceApply("close", undefined); + const output = debugResource.toOutput(); + + expect(output).toContain(`The user is not connected to a MongoDB cluster.`); + }); + + it("should be disconnected and contain an error when an error event occurred", () => { + debugResource.reduceApply("connection-error", "Error message from the server"); + const output = debugResource.toOutput(); + + expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`); + expect(output).toContain(`Error message from the server`); + }); +}); From 26e1407070e3ca06e34e4cb80b3719d6551bb278 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 11:22:20 +0200 Subject: [PATCH 03/11] chore: Fix compilation error --- src/resources/resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/resource.ts b/src/resources/resource.ts index 80250b0b1..b4f27dde9 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -25,11 +25,11 @@ export function ReactiveResource private registeredResource?: RegisteredResource; protected readonly session: Session; protected readonly config: UserConfig; + protected current: V; constructor( protected readonly server: Server, - protected readonly telemetry: Telemetry, - protected current: V + protected readonly telemetry: Telemetry ) { this.current = initial; this.session = server.session; From c042cffc716db5f8df378e98504fe332f3344063 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 11:36:12 +0200 Subject: [PATCH 04/11] chore: Fix more compilation and linting issues --- src/resources/resource.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/resources/resource.ts b/src/resources/resource.ts index b4f27dde9..9a2875369 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -29,9 +29,10 @@ export function ReactiveResource constructor( protected readonly server: Server, - protected readonly telemetry: Telemetry + protected readonly telemetry: Telemetry, + current?: V ) { - this.current = initial; + this.current = current ?? initial; this.session = server.session; this.config = server.userConfig; From 76ddf156847ba462247261b72a7a2bd4068e179b Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 11:43:20 +0200 Subject: [PATCH 05/11] chore: force pinging the database, as the provider does not test it We might be telling the user that they successfully connected to a database and we haven't tested it, failing afterwards. By running the hello command, we verify that the user does indeed connected. --- src/common/session.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/session.ts b/src/common/session.ts index fb3e4a9b9..42b736bed 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -122,6 +122,8 @@ export class Session extends EventEmitter { proxy: { useEnvironmentVariableProxies: true }, applyProxyToOIDC: true, }); + + await this.serviceProvider?.runCommand?.("admin", { hello: 1 }); } catch (error: unknown) { const message = error instanceof Error ? error.message : `${error}`; this.emit("connection-error", message); From c5b696b6c62ffdc3e2adee19f646f475647468d8 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 11:55:02 +0200 Subject: [PATCH 06/11] chore: Fix linting issues --- src/common/session.ts | 2 +- src/resources/common/debug.ts | 1 + tests/unit/resources/common/debug.test.ts | 16 +++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index 42b736bed..84acb16b4 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -125,7 +125,7 @@ export class Session extends EventEmitter { await this.serviceProvider?.runCommand?.("admin", { hello: 1 }); } catch (error: unknown) { - const message = error instanceof Error ? error.message : `${error}`; + const message = error instanceof Error ? error.message : `${error as string}`; this.emit("connection-error", message); throw error; } diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index 2b3c49388..b5034a37f 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -49,6 +49,7 @@ export class DebugResource extends ReactiveResource( result += `The user is not connected to a MongoDB cluster because of an error.\n`; result += `${this.current.errorReason}`; break; + case "connecting": case "disconnected": result += "The user is not connected to a MongoDB cluster."; break; diff --git a/tests/unit/resources/common/debug.test.ts b/tests/unit/resources/common/debug.test.ts index 296682630..78f057143 100644 --- a/tests/unit/resources/common/debug.test.ts +++ b/tests/unit/resources/common/debug.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { DebugResource } from "../../../../src/resources/common/debug.js"; import { Session } from "../../../../src/common/session.js"; import { Server } from "../../../../src/server.js"; @@ -6,11 +6,17 @@ import { Telemetry } from "../../../../src/telemetry/telemetry.js"; import { config } from "../../../../src/common/config.js"; describe("debug resource", () => { - let session = new Session({} as any); - let server = new Server({ session } as any); - let telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" }); + // eslint-disable-next-line + const session = new Session({} as any); + // eslint-disable-next-line + const server = new Server({ session } as any); + const telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" }); - let debugResource: DebugResource = new DebugResource(server, telemetry, { tag: "disconnected" }); + let debugResource: DebugResource = new DebugResource(server, telemetry); + + beforeEach(() => { + debugResource = new DebugResource(server, telemetry); + }); it("should be connected when a connected event happens", () => { debugResource.reduceApply("connected", undefined); From 235ffcf80a905178e079bdba708de29795e50dc5 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 12:39:28 +0200 Subject: [PATCH 07/11] chore: PR improvements and use notify one single change --- src/resources/resource.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/resources/resource.ts b/src/resources/resource.ts index 9a2875369..6672a2b0d 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -3,34 +3,33 @@ import { Session } from "../common/session.js"; import { UserConfig } from "../common/config.js"; import { Telemetry } from "../telemetry/telemetry.js"; import type { SessionEvents } from "../common/session.js"; -import { ReadResourceCallback, RegisteredResource, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ReadResourceCallback, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js"; type PayloadOf = SessionEvents[K][0]; type ResourceConfiguration = { name: string; uri: string; config: ResourceMetadata }; -export function ReactiveResource( +export function ReactiveResource( { name, uri, config: resourceConfig }: ResourceConfiguration, { initial, events, }: { - initial: V; - events: KE; + initial: Value; + events: RelevantEvents; } ) { - type E = KE[number]; + type SomeEvent = RelevantEvents[number]; abstract class NewReactiveResource { - private registeredResource?: RegisteredResource; protected readonly session: Session; protected readonly config: UserConfig; - protected current: V; + protected current: Value; constructor( protected readonly server: Server, protected readonly telemetry: Telemetry, - current?: V + current?: Value ) { this.current = current ?? initial; this.session = server.session; @@ -45,12 +44,7 @@ export function ReactiveResource } public register(): void { - this.registeredResource = this.server.mcpServer.registerResource( - name, - uri, - resourceConfig, - this.resourceCallback - ); + this.server.mcpServer.registerResource(name, uri, resourceConfig, this.resourceCallback); } private resourceCallback: ReadResourceCallback = (uri) => ({ @@ -64,15 +58,14 @@ export function ReactiveResource }); private triggerUpdate() { - this.registeredResource?.update({}); - this.server.mcpServer.sendResourceListChanged(); + void this.server.mcpServer.server.sendResourceUpdated({ uri }); } - reduceApply(eventName: E, ...event: PayloadOf[]): void { + reduceApply(eventName: SomeEvent, ...event: PayloadOf[]): void { this.current = this.reduce(eventName, ...event); } - protected abstract reduce(eventName: E, ...event: PayloadOf[]): V; + protected abstract reduce(eventName: SomeEvent, ...event: PayloadOf[]): Value; abstract toOutput(): string; } From 7dc6746518a088eb849f573eda373b02b850aabf Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 12:40:41 +0200 Subject: [PATCH 08/11] chore: Change name and uri of the debug resource --- src/resources/common/debug.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index b5034a37f..0d0389ace 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -10,8 +10,8 @@ type ConnectionStateDebuggingInformation = { export class DebugResource extends ReactiveResource( { - name: "debug", - uri: "config://debug", + name: "debug-mongodb-connectivity", + uri: "debug://mongodb-connectivity", config: { description: "Debugging information for connectivity issues.", }, From 550a2846e61ba503da71330e9d6e8c12413cf888 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 13:31:40 +0200 Subject: [PATCH 09/11] chore: it seems that resourceListChanged is mandatory it seems that some agents won't detect changes in the resources until we tell them that the list also changed --- src/resources/resource.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/resources/resource.ts b/src/resources/resource.ts index 6672a2b0d..4b7812b7c 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -38,7 +38,7 @@ export function ReactiveResource { this.reduceApply(event, (args as unknown[])[0] as PayloadOf); - this.triggerUpdate(); + void this.triggerUpdate(); }); } } @@ -57,8 +57,9 @@ export function ReactiveResource[]): void { From e50d706847d051cdac2eafe3f656acb7139ac5d1 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 13:40:31 +0200 Subject: [PATCH 10/11] chore: Gracefully fail when the server is down The server might be closed because the connection made the server die or because it's a unit test. In that case, do not fail, just log the error and continue. This is not a critical feature, in case that we couldn't sync once, we will try again at some point or just let it be. --- src/resources/resource.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/resources/resource.ts b/src/resources/resource.ts index 4b7812b7c..271d3d3e1 100644 --- a/src/resources/resource.ts +++ b/src/resources/resource.ts @@ -4,6 +4,7 @@ import { UserConfig } from "../common/config.js"; import { Telemetry } from "../telemetry/telemetry.js"; import type { SessionEvents } from "../common/session.js"; import { ReadResourceCallback, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js"; +import logger, { LogId } from "../common/logger.js"; type PayloadOf = SessionEvents[K][0]; @@ -58,8 +59,16 @@ export function ReactiveResource[]): void { From d0790b5acfef9b81efe254e7a81b26f8f14690ae Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 31 Jul 2025 14:09:23 +0200 Subject: [PATCH 11/11] chore: Rename to connect for consistency --- src/common/session.ts | 4 ++-- src/resources/common/debug.ts | 6 +++--- tests/unit/resources/common/debug.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/session.ts b/src/common/session.ts index 84acb16b4..2a75af337 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -14,7 +14,7 @@ export interface SessionOptions { } export type SessionEvents = { - connected: []; + connect: []; close: []; disconnect: []; "connection-error": [string]; @@ -130,6 +130,6 @@ export class Session extends EventEmitter { throw error; } - this.emit("connected"); + this.emit("connect"); } } diff --git a/src/resources/common/debug.ts b/src/resources/common/debug.ts index 0d0389ace..c8de2dd05 100644 --- a/src/resources/common/debug.ts +++ b/src/resources/common/debug.ts @@ -18,17 +18,17 @@ export class DebugResource extends ReactiveResource( }, { initial: { tag: "disconnected" } as ConnectionStateDebuggingInformation, - events: ["connected", "disconnect", "close", "connection-error"], + events: ["connect", "disconnect", "close", "connection-error"], } ) { reduce( - eventName: "connected" | "disconnect" | "close" | "connection-error", + eventName: "connect" | "disconnect" | "close" | "connection-error", event: string | undefined ): ConnectionStateDebuggingInformation { void event; switch (eventName) { - case "connected": + case "connect": return { tag: "connected" }; case "connection-error": return { tag: "errored", errorReason: event }; diff --git a/tests/unit/resources/common/debug.test.ts b/tests/unit/resources/common/debug.test.ts index 78f057143..4a2f704b5 100644 --- a/tests/unit/resources/common/debug.test.ts +++ b/tests/unit/resources/common/debug.test.ts @@ -19,7 +19,7 @@ describe("debug resource", () => { }); it("should be connected when a connected event happens", () => { - debugResource.reduceApply("connected", undefined); + debugResource.reduceApply("connect", undefined); const output = debugResource.toOutput(); expect(output).toContain(`The user is connected to the MongoDB cluster.`);