Skip to content
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