Skip to content

Commit 84aa8ba

Browse files
committed
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.
1 parent b24fe5e commit 84aa8ba

File tree

6 files changed

+179
-33
lines changed

6 files changed

+179
-33
lines changed

src/common/session.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ export interface SessionOptions {
1313
apiClientSecret?: string;
1414
}
1515

16-
export class Session extends EventEmitter<{
16+
export type SessionEvents = {
17+
connected: [];
1718
close: [];
1819
disconnect: [];
19-
}> {
20+
};
21+
22+
export class Session extends EventEmitter<SessionEvents> {
2023
sessionId?: string;
2124
serviceProvider?: NodeDriverServiceProvider;
2225
apiClient: ApiClient;
@@ -116,5 +119,7 @@ export class Session extends EventEmitter<{
116119
proxy: { useEnvironmentVariableProxies: true },
117120
applyProxyToOIDC: true,
118121
});
122+
123+
this.emit("connected");
119124
}
120125
}

src/resources/common/config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ReactiveResource } from "../resource.js";
2+
import { config } from "../../common/config.js";
3+
import type { UserConfig } from "../../common/config.js";
4+
5+
export class ConfigResource extends ReactiveResource(
6+
{
7+
name: "config",
8+
uri: "config://config",
9+
config: {
10+
description:
11+
"Server configuration, supplied by the user either as environment variables or as startup arguments",
12+
},
13+
},
14+
{
15+
initial: { ...config },
16+
events: [],
17+
}
18+
) {
19+
reduce(previous: UserConfig, eventName: undefined, event: undefined): UserConfig {
20+
void event;
21+
return previous;
22+
}
23+
24+
toOutput(state: UserConfig): string {
25+
const result = {
26+
telemetry: state.telemetry,
27+
logPath: state.logPath,
28+
connectionString: state.connectionString
29+
? "set; access to MongoDB tools are currently available to use"
30+
: "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'.",
31+
connectOptions: state.connectOptions,
32+
atlas:
33+
state.apiClientId && state.apiClientSecret
34+
? "set; MongoDB Atlas tools are currently available to use"
35+
: "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'.",
36+
};
37+
38+
return JSON.stringify(result);
39+
}
40+
}

src/resources/common/debug.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ReactiveResource } from "../resource.js";
2+
import { config } from "../../common/config.js";
3+
import type { UserConfig } from "../../common/config.js";
4+
5+
type ConnectionStateDebuggingInformation = {
6+
readonly tag: "connected" | "connecting" | "disconnected" | "errored";
7+
readonly connectionStringAuthType?: "scram" | "ldap" | "kerberos" | "oidc-auth-flow" | "oidc-device-flow" | "x.509";
8+
readonly oidcLoginUrl?: string;
9+
readonly oidcUserCode?: string;
10+
readonly errorReason?: string;
11+
};
12+
13+
export class DebugResource extends ReactiveResource(
14+
{
15+
name: "debug",
16+
uri: "config://debug",
17+
config: {
18+
description: "Debugging information for connectivity issues.",
19+
},
20+
},
21+
{
22+
initial: { tag: "disconnected" },
23+
events: ["connected", "disconnect", "close"],
24+
}
25+
) {
26+
reduce(
27+
previous: ConnectionStateDebuggingInformation,
28+
eventName: "connected" | "disconnect" | "close",
29+
event: undefined
30+
): ConnectionStateDebuggingInformation {
31+
void event;
32+
33+
switch (eventName) {
34+
case "connected":
35+
return { tag: "connected" };
36+
case "disconnect":
37+
return { tag: "disconnected" };
38+
case "close":
39+
return { tag: "disconnected" };
40+
}
41+
}
42+
43+
toOutput(state: ConnectionStateDebuggingInformation): string {
44+
const result = {
45+
connectionStatus: state.tag,
46+
};
47+
48+
return JSON.stringify(result);
49+
}
50+
}

src/resources/resource.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Server } from "../server.js";
2+
import { Session } from "../common/session.js";
3+
import { UserConfig } from "../common/config.js";
4+
import { Telemetry } from "../telemetry/telemetry.js";
5+
import type { SessionEvents } from "../common/session.js";
6+
import { ReadResourceCallback, RegisteredResource, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
8+
type PayloadOf<K extends keyof SessionEvents> = SessionEvents[K][0];
9+
10+
type ResourceConfiguration = { name: string; uri: string; config: ResourceMetadata };
11+
12+
export function ReactiveResource<V, KE extends readonly (keyof SessionEvents)[]>(
13+
{ name, uri, config: resourceConfig }: ResourceConfiguration,
14+
{
15+
initial,
16+
events,
17+
}: {
18+
initial: V;
19+
events: KE;
20+
}
21+
) {
22+
type E = KE[number];
23+
24+
abstract class NewReactiveResource {
25+
private registeredResource?: RegisteredResource;
26+
27+
constructor(
28+
protected readonly server: Server,
29+
protected readonly session: Session,
30+
protected readonly config: UserConfig,
31+
protected readonly telemetry: Telemetry,
32+
private current?: V
33+
) {
34+
this.current = initial;
35+
36+
for (const event of events) {
37+
this.session.on(event, (...args: SessionEvents[typeof event]) => {
38+
this.current = this.reduce(this.current, event, (args as unknown[])[0] as PayloadOf<typeof event>);
39+
this.triggerUpdate();
40+
});
41+
}
42+
}
43+
44+
public register(): void {
45+
this.registeredResource = this.server.mcpServer.registerResource(
46+
name,
47+
uri,
48+
resourceConfig,
49+
this.resourceCallback
50+
);
51+
}
52+
53+
private resourceCallback: ReadResourceCallback = (uri) => ({
54+
contents: [
55+
{
56+
text: this.toOutput(this.current),
57+
mimeType: "application/json",
58+
uri: uri.href,
59+
},
60+
],
61+
});
62+
63+
private triggerUpdate() {
64+
this.registeredResource?.update({});
65+
this.server.mcpServer.sendResourceListChanged();
66+
}
67+
68+
abstract reduce(previous: V | undefined, eventName: E, ...event: PayloadOf<E>[]): V;
69+
abstract toOutput(state: V | undefined): string;
70+
}
71+
72+
return NewReactiveResource;
73+
}

src/resources/resources.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { ConfigResource } from "./common/config.js";
2+
import { DebugResource } from "./common/debug.js";
3+
4+
export const Resources = [ConfigResource, DebugResource] as const;

src/server.ts

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Session } from "./common/session.js";
33
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
44
import { AtlasTools } from "./tools/atlas/tools.js";
55
import { MongoDbTools } from "./tools/mongodb/tools.js";
6+
import { Resources } from "./resources/resources.js";
67
import logger, { LogId, LoggerBase, McpLogger, DiskLogger, ConsoleLogger } from "./common/logger.js";
78
import { ObjectId } from "mongodb";
89
import { Telemetry } from "./telemetry/telemetry.js";
@@ -155,37 +156,10 @@ export class Server {
155156
}
156157

157158
private registerResources() {
158-
this.mcpServer.resource(
159-
"config",
160-
"config://config",
161-
{
162-
description:
163-
"Server configuration, supplied by the user either as environment variables or as startup arguments",
164-
},
165-
(uri) => {
166-
const result = {
167-
telemetry: this.userConfig.telemetry,
168-
logPath: this.userConfig.logPath,
169-
connectionString: this.userConfig.connectionString
170-
? "set; access to MongoDB tools are currently available to use"
171-
: "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'.",
172-
connectOptions: this.userConfig.connectOptions,
173-
atlas:
174-
this.userConfig.apiClientId && this.userConfig.apiClientSecret
175-
? "set; MongoDB Atlas tools are currently available to use"
176-
: "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'.",
177-
};
178-
return {
179-
contents: [
180-
{
181-
text: JSON.stringify(result),
182-
mimeType: "application/json",
183-
uri: uri.href,
184-
},
185-
],
186-
};
187-
}
188-
);
159+
for (const resourceConstructor of Resources) {
160+
const resource = new resourceConstructor(this, this.session, this.userConfig, this.telemetry);
161+
resource.register();
162+
}
189163
}
190164

191165
private async validateConfig(): Promise<void> {

0 commit comments

Comments
 (0)