Skip to content

Commit ce6d2e9

Browse files
committed
chore: Add debug resource relevant events
Also add unit tests to show how to test resources.
1 parent 84aa8ba commit ce6d2e9

File tree

6 files changed

+111
-43
lines changed

6 files changed

+111
-43
lines changed

src/common/session.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type SessionEvents = {
1717
connected: [];
1818
close: [];
1919
disconnect: [];
20+
"connection-error": [string];
2021
};
2122

2223
export class Session extends EventEmitter<SessionEvents> {
@@ -105,20 +106,27 @@ export class Session extends EventEmitter<SessionEvents> {
105106
connectionString,
106107
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
107108
});
108-
this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, {
109-
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
110-
productName: "MongoDB MCP",
111-
readConcern: {
112-
level: connectOptions.readConcern,
113-
},
114-
readPreference: connectOptions.readPreference,
115-
writeConcern: {
116-
w: connectOptions.writeConcern,
117-
},
118-
timeoutMS: connectOptions.timeoutMS,
119-
proxy: { useEnvironmentVariableProxies: true },
120-
applyProxyToOIDC: true,
121-
});
109+
110+
try {
111+
this.serviceProvider = await NodeDriverServiceProvider.connect(connectionString, {
112+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
113+
productName: "MongoDB MCP",
114+
readConcern: {
115+
level: connectOptions.readConcern,
116+
},
117+
readPreference: connectOptions.readPreference,
118+
writeConcern: {
119+
w: connectOptions.writeConcern,
120+
},
121+
timeoutMS: connectOptions.timeoutMS,
122+
proxy: { useEnvironmentVariableProxies: true },
123+
applyProxyToOIDC: true,
124+
});
125+
} catch (error: unknown) {
126+
const message = error instanceof Error ? error.message : `${error}`;
127+
this.emit("connection-error", message);
128+
throw error;
129+
}
122130

123131
this.emit("connected");
124132
}

src/resources/common/config.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@ export class ConfigResource extends ReactiveResource(
1616
events: [],
1717
}
1818
) {
19-
reduce(previous: UserConfig, eventName: undefined, event: undefined): UserConfig {
19+
reduce(eventName: undefined, event: undefined): UserConfig {
20+
void eventName;
2021
void event;
21-
return previous;
22+
23+
return this.current;
2224
}
2325

24-
toOutput(state: UserConfig): string {
26+
toOutput(): string {
2527
const result = {
26-
telemetry: state.telemetry,
27-
logPath: state.logPath,
28-
connectionString: state.connectionString
28+
telemetry: this.current.telemetry,
29+
logPath: this.current.logPath,
30+
connectionString: this.current.connectionString
2931
? "set; access to MongoDB tools are currently available to use"
3032
: "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,
33+
connectOptions: this.current.connectOptions,
3234
atlas:
33-
state.apiClientId && state.apiClientSecret
35+
this.current.apiClientId && this.current.apiClientSecret
3436
? "set; MongoDB Atlas tools are currently available to use"
3537
: "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'.",
3638
};

src/resources/common/debug.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { ReactiveResource } from "../resource.js";
2-
import { config } from "../../common/config.js";
3-
import type { UserConfig } from "../../common/config.js";
42

53
type ConnectionStateDebuggingInformation = {
64
readonly tag: "connected" | "connecting" | "disconnected" | "errored";
@@ -19,32 +17,43 @@ export class DebugResource extends ReactiveResource(
1917
},
2018
},
2119
{
22-
initial: { tag: "disconnected" },
23-
events: ["connected", "disconnect", "close"],
20+
initial: { tag: "disconnected" } as ConnectionStateDebuggingInformation,
21+
events: ["connected", "disconnect", "close", "connection-error"],
2422
}
2523
) {
2624
reduce(
27-
previous: ConnectionStateDebuggingInformation,
28-
eventName: "connected" | "disconnect" | "close",
29-
event: undefined
25+
eventName: "connected" | "disconnect" | "close" | "connection-error",
26+
event: string | undefined
3027
): ConnectionStateDebuggingInformation {
3128
void event;
3229

3330
switch (eventName) {
3431
case "connected":
3532
return { tag: "connected" };
33+
case "connection-error":
34+
return { tag: "errored", errorReason: event };
3635
case "disconnect":
37-
return { tag: "disconnected" };
3836
case "close":
3937
return { tag: "disconnected" };
4038
}
4139
}
4240

43-
toOutput(state: ConnectionStateDebuggingInformation): string {
44-
const result = {
45-
connectionStatus: state.tag,
46-
};
41+
toOutput(): string {
42+
let result = "";
43+
44+
switch (this.current.tag) {
45+
case "connected":
46+
result += "The user is connected to the MongoDB cluster.";
47+
break;
48+
case "errored":
49+
result += `The user is not connected to a MongoDB cluster because of an error.\n`;
50+
result += `<error>${this.current.errorReason}</error>`;
51+
break;
52+
case "disconnected":
53+
result += "The user is not connected to a MongoDB cluster.";
54+
break;
55+
}
4756

48-
return JSON.stringify(result);
57+
return result;
4958
}
5059
}

src/resources/resource.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ export function ReactiveResource<V, KE extends readonly (keyof SessionEvents)[]>
2323

2424
abstract class NewReactiveResource {
2525
private registeredResource?: RegisteredResource;
26+
protected readonly session: Session;
27+
protected readonly config: UserConfig;
2628

2729
constructor(
2830
protected readonly server: Server,
29-
protected readonly session: Session,
30-
protected readonly config: UserConfig,
3131
protected readonly telemetry: Telemetry,
32-
private current?: V
32+
protected current: V
3333
) {
3434
this.current = initial;
35+
this.session = server.session;
36+
this.config = server.userConfig;
3537

3638
for (const event of events) {
3739
this.session.on(event, (...args: SessionEvents[typeof event]) => {
38-
this.current = this.reduce(this.current, event, (args as unknown[])[0] as PayloadOf<typeof event>);
40+
this.reduceApply(event, (args as unknown[])[0] as PayloadOf<typeof event>);
3941
this.triggerUpdate();
4042
});
4143
}
@@ -53,7 +55,7 @@ export function ReactiveResource<V, KE extends readonly (keyof SessionEvents)[]>
5355
private resourceCallback: ReadResourceCallback = (uri) => ({
5456
contents: [
5557
{
56-
text: this.toOutput(this.current),
58+
text: this.toOutput(),
5759
mimeType: "application/json",
5860
uri: uri.href,
5961
},
@@ -65,8 +67,12 @@ export function ReactiveResource<V, KE extends readonly (keyof SessionEvents)[]>
6567
this.server.mcpServer.sendResourceListChanged();
6668
}
6769

68-
abstract reduce(previous: V | undefined, eventName: E, ...event: PayloadOf<E>[]): V;
69-
abstract toOutput(state: V | undefined): string;
70+
reduceApply(eventName: E, ...event: PayloadOf<E>[]): void {
71+
this.current = this.reduce(eventName, ...event);
72+
}
73+
74+
protected abstract reduce(eventName: E, ...event: PayloadOf<E>[]): V;
75+
abstract toOutput(): string;
7076
}
7177

7278
return NewReactiveResource;

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class Server {
157157

158158
private registerResources() {
159159
for (const resourceConstructor of Resources) {
160-
const resource = new resourceConstructor(this, this.session, this.userConfig, this.telemetry);
160+
const resource = new resourceConstructor(this, this.telemetry);
161161
resource.register();
162162
}
163163
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { DebugResource } from "../../../../src/resources/common/debug.js";
3+
import { Session } from "../../../../src/common/session.js";
4+
import { Server } from "../../../../src/server.js";
5+
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
6+
import { config } from "../../../../src/common/config.js";
7+
8+
describe("debug resource", () => {
9+
let session = new Session({} as any);
10+
let server = new Server({ session } as any);
11+
let telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" });
12+
13+
let debugResource: DebugResource = new DebugResource(server, telemetry, { tag: "disconnected" });
14+
15+
it("should be connected when a connected event happens", () => {
16+
debugResource.reduceApply("connected", undefined);
17+
const output = debugResource.toOutput();
18+
19+
expect(output).toContain(`The user is connected to the MongoDB cluster.`);
20+
});
21+
22+
it("should be disconnected when a disconnect event happens", () => {
23+
debugResource.reduceApply("disconnect", undefined);
24+
const output = debugResource.toOutput();
25+
26+
expect(output).toContain(`The user is not connected to a MongoDB cluster.`);
27+
});
28+
29+
it("should be disconnected when a close event happens", () => {
30+
debugResource.reduceApply("close", undefined);
31+
const output = debugResource.toOutput();
32+
33+
expect(output).toContain(`The user is not connected to a MongoDB cluster.`);
34+
});
35+
36+
it("should be disconnected and contain an error when an error event occurred", () => {
37+
debugResource.reduceApply("connection-error", "Error message from the server");
38+
const output = debugResource.toOutput();
39+
40+
expect(output).toContain(`The user is not connected to a MongoDB cluster because of an error.`);
41+
expect(output).toContain(`<error>Error message from the server</error>`);
42+
});
43+
});

0 commit comments

Comments
 (0)