Skip to content

chore: Reactive resource support for debugging connectivity MCP-80 #413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 31, 2025
Merged
9 changes: 7 additions & 2 deletions src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SessionEvents> {
sessionId?: string;
serviceProvider?: NodeDriverServiceProvider;
apiClient: ApiClient;
Expand Down Expand Up @@ -116,5 +119,7 @@ export class Session extends EventEmitter<{
proxy: { useEnvironmentVariableProxies: true },
applyProxyToOIDC: true,
});

this.emit("connected");
}
}
40 changes: 40 additions & 0 deletions src/resources/common/config.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
50 changes: 50 additions & 0 deletions src/resources/common/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ReactiveResource } from "../resource.js";
import { config } from "../../common/config.js";

Check failure on line 2 in src/resources/common/debug.ts

View workflow job for this annotation

GitHub Actions / check-style

'config' is defined but never used
import type { UserConfig } from "../../common/config.js";

Check failure on line 3 in src/resources/common/debug.ts

View workflow job for this annotation

GitHub Actions / check-style

'UserConfig' is defined but never used

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary anymore, now that you're using event in connection-error case?


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);
}
}
73 changes: 73 additions & 0 deletions src/resources/resource.ts
Original file line number Diff line number Diff line change
@@ -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<K extends keyof SessionEvents> = SessionEvents[K][0];

type ResourceConfiguration = { name: string; uri: string; config: ResourceMetadata };

export function ReactiveResource<V, KE extends readonly (keyof SessionEvents)[]>(
{ 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<typeof event>);
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<E>[]): V;
abstract toOutput(state: V | undefined): string;
}

return NewReactiveResource;
}
4 changes: 4 additions & 0 deletions src/resources/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ConfigResource } from "./common/config.js";
import { DebugResource } from "./common/debug.js";

export const Resources = [ConfigResource, DebugResource] as const;
36 changes: 5 additions & 31 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<void> {
Expand Down
Loading