Skip to content

Commit cf3cb48

Browse files
chore: model Resources the same way as Tools
1 parent dc457ae commit cf3cb48

File tree

9 files changed

+89
-66
lines changed

9 files changed

+89
-66
lines changed

src/common/sessionExportsManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,11 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
241241
: undefined;
242242
}
243243

244-
private docToEJSONStream(ejsonOptions: EJSONOptions | undefined) {
244+
private docToEJSONStream(ejsonOptions: EJSONOptions | undefined): Transform {
245245
let docsTransformed = 0;
246246
return new Transform({
247247
objectMode: true,
248-
transform: function (chunk: unknown, encoding, callback) {
248+
transform: function (chunk: unknown, encoding, callback): void {
249249
++docsTransformed;
250250
try {
251251
const doc: string = EJSON.stringify(chunk, undefined, undefined, ejsonOptions);
@@ -256,7 +256,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
256256
callback(err as Error);
257257
}
258258
},
259-
final: function (callback) {
259+
final: function (callback): void {
260260
this.push("]");
261261
callback(null);
262262
},
@@ -291,7 +291,7 @@ export class SessionExportsManager extends EventEmitter<SessionExportsManagerEve
291291
}
292292
}
293293

294-
private async silentlyRemoveExport(exportPath: string) {
294+
private async silentlyRemoveExport(exportPath: string): Promise<void> {
295295
try {
296296
await fs.unlink(exportPath);
297297
} catch (error) {
@@ -335,6 +335,6 @@ export function validateExportName(nameWithExtension: string): string {
335335
return decodedName;
336336
}
337337

338-
export function isExportExpired(createdAt: number, exportTimeoutMs: number) {
338+
export function isExportExpired(createdAt: number, exportTimeoutMs: number): boolean {
339339
return Date.now() - createdAt > exportTimeoutMs;
340340
}

src/resources/common/config.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
import { ReactiveResource } from "../resource.js";
2-
import { config } from "../../common/config.js";
32
import type { UserConfig } from "../../common/config.js";
4-
import type { Server } from "../../server.js";
53
import type { Telemetry } from "../../telemetry/telemetry.js";
4+
import type { Session } from "../../lib.js";
65

76
export class ConfigResource extends ReactiveResource<UserConfig, readonly []> {
8-
constructor(server: Server, telemetry: Telemetry) {
9-
super(
10-
{
7+
constructor(session: Session, config: UserConfig, telemetry: Telemetry) {
8+
super({
9+
resourceConfiguration: {
1110
name: "config",
1211
uri: "config://config",
1312
config: {
1413
description:
1514
"Server configuration, supplied by the user either as environment variables or as startup arguments",
1615
},
1716
},
18-
{
17+
options: {
1918
initial: { ...config },
2019
events: [],
2120
},
22-
server,
23-
telemetry
24-
);
21+
session,
22+
config,
23+
telemetry,
24+
});
2525
}
2626
reduce(eventName: undefined, event: undefined): UserConfig {
2727
void eventName;

src/resources/common/debug.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ReactiveResource } from "../resource.js";
2-
import type { Server } from "../../server.js";
32
import type { Telemetry } from "../../telemetry/telemetry.js";
3+
import { Session, UserConfig } from "../../lib.js";
44

55
type ConnectionStateDebuggingInformation = {
66
readonly tag: "connected" | "connecting" | "disconnected" | "errored";
@@ -14,23 +14,24 @@ export class DebugResource extends ReactiveResource<
1414
ConnectionStateDebuggingInformation,
1515
readonly ["connect", "disconnect", "close", "connection-error"]
1616
> {
17-
constructor(server: Server, telemetry: Telemetry) {
18-
super(
19-
{
17+
constructor(session: Session, config: UserConfig, telemetry: Telemetry) {
18+
super({
19+
resourceConfiguration: {
2020
name: "debug-mongodb",
2121
uri: "debug://mongodb",
2222
config: {
2323
description:
2424
"Debugging information for MongoDB connectivity issues. Tracks the last connectivity error and attempt information.",
2525
},
2626
},
27-
{
27+
options: {
2828
initial: { tag: "disconnected" },
2929
events: ["connect", "disconnect", "close", "connection-error"],
3030
},
31-
server,
32-
telemetry
33-
);
31+
session,
32+
config,
33+
telemetry,
34+
});
3435
}
3536
reduce(
3637
eventName: "connect" | "disconnect" | "close" | "connection-error",

src/resources/common/exportedData.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,28 @@ import {
66
} from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Server } from "../../server.js";
88
import { LogId } from "../../common/logger.js";
9+
import { Session } from "../../common/session.js";
910

1011
export class ExportedData {
1112
private readonly name = "exported-data";
1213
private readonly description = "Data files exported in the current session.";
1314
private readonly uri = "exported-data://{exportName}";
15+
private server?: Server;
1416

15-
constructor(private readonly server: Server) {
16-
this.server.session.exportsManager.on("export-available", (uri) => {
17-
this.server.mcpServer.sendResourceListChanged();
18-
void this.server.mcpServer.server.sendResourceUpdated({
17+
constructor(private readonly session: Session) {
18+
this.session.exportsManager.on("export-available", (uri) => {
19+
this.server?.mcpServer.sendResourceListChanged();
20+
void this.server?.mcpServer.server.sendResourceUpdated({
1921
uri,
2022
});
2123
});
22-
this.server.session.exportsManager.on("export-expired", () => {
23-
this.server.mcpServer.sendResourceListChanged();
24+
this.session.exportsManager.on("export-expired", () => {
25+
this.server?.mcpServer.sendResourceListChanged();
2426
});
2527
}
2628

27-
public register(): void {
29+
public register(server: Server): void {
30+
this.server = server;
2831
this.server.mcpServer.registerResource(
2932
this.name,
3033
new ResourceTemplate(this.uri, {
@@ -48,15 +51,15 @@ export class ExportedData {
4851
private listResourcesCallback: ListResourcesCallback = () => {
4952
try {
5053
return {
51-
resources: this.server.session.exportsManager.availableExports.map(({ exportName, exportURI }) => ({
54+
resources: this.session.exportsManager.availableExports.map(({ exportName, exportURI }) => ({
5255
name: exportName,
5356
description: this.exportNameToDescription(exportName),
5457
uri: exportURI,
5558
mimeType: "application/json",
5659
})),
5760
};
5861
} catch (error) {
59-
this.server.session.logger.error({
62+
this.session.logger.error({
6063
id: LogId.exportedDataListError,
6164
context: "Error when listing exported data resources",
6265
message: error instanceof Error ? error.message : String(error),
@@ -69,11 +72,11 @@ export class ExportedData {
6972

7073
private autoCompleteExportName: CompleteResourceTemplateCallback = (value) => {
7174
try {
72-
return this.server.session.exportsManager.availableExports
75+
return this.session.exportsManager.availableExports
7376
.filter(({ exportName }) => exportName.startsWith(value))
7477
.map(({ exportName }) => exportName);
7578
} catch (error) {
76-
this.server.session.logger.error({
79+
this.session.logger.error({
7780
id: LogId.exportedDataAutoCompleteError,
7881
context: "Error when autocompleting exported data",
7982
message: error instanceof Error ? error.message : String(error),
@@ -88,7 +91,7 @@ export class ExportedData {
8891
throw new Error("Cannot retrieve exported data, exportName not provided.");
8992
}
9093

91-
const content = await this.server.session.exportsManager.readExport(exportName);
94+
const content = await this.session.exportsManager.readExport(exportName);
9295

9396
return {
9497
contents: [
@@ -112,7 +115,7 @@ export class ExportedData {
112115
}
113116
};
114117

115-
private exportNameToDescription(exportName: string) {
118+
private exportNameToDescription(exportName: string): string {
116119
const match = exportName.match(/^(.+)\.(\d+)\.json$/);
117120
if (!match) return "Exported data for an unknown namespace.";
118121

src/resources/resource.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,41 @@ export type ReactiveResourceOptions<Value, RelevantEvents extends readonly (keyo
2020
};
2121

2222
export abstract class ReactiveResource<Value, RelevantEvents extends readonly (keyof SessionEvents)[]> {
23-
protected readonly session: Session;
24-
protected readonly config: UserConfig;
23+
protected server?: Server;
24+
protected session: Session;
25+
protected config: UserConfig;
26+
protected telemetry: Telemetry;
27+
2528
protected current: Value;
2629
protected readonly name: string;
2730
protected readonly uri: string;
2831
protected readonly resourceConfig: ResourceMetadata;
2932
protected readonly events: RelevantEvents;
3033

31-
constructor(
32-
resourceConfiguration: ResourceConfiguration,
33-
options: ReactiveResourceOptions<Value, RelevantEvents>,
34-
protected readonly server: Server,
35-
protected readonly telemetry: Telemetry,
36-
current?: Value
37-
) {
34+
constructor({
35+
resourceConfiguration,
36+
options,
37+
session,
38+
config,
39+
telemetry,
40+
current,
41+
}: {
42+
resourceConfiguration: ResourceConfiguration;
43+
options: ReactiveResourceOptions<Value, RelevantEvents>;
44+
session: Session;
45+
config: UserConfig;
46+
telemetry: Telemetry;
47+
current?: Value;
48+
}) {
49+
this.session = session;
50+
this.config = config;
51+
this.telemetry = telemetry;
52+
3853
this.name = resourceConfiguration.name;
3954
this.uri = resourceConfiguration.uri;
4055
this.resourceConfig = resourceConfiguration.config;
4156
this.events = options.events;
4257
this.current = current ?? options.initial;
43-
this.session = server.session;
44-
this.config = server.userConfig;
4558

4659
this.setupEventListeners();
4760
}
@@ -55,7 +68,8 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
5568
}
5669
}
5770

58-
public register(): void {
71+
public register(server: Server): void {
72+
this.server = server;
5973
this.server.mcpServer.registerResource(this.name, this.uri, this.resourceConfig, this.resourceCallback);
6074
}
6175

@@ -71,8 +85,8 @@ export abstract class ReactiveResource<Value, RelevantEvents extends readonly (k
7185

7286
private async triggerUpdate(): Promise<void> {
7387
try {
74-
await this.server.mcpServer.server.sendResourceUpdated({ uri: this.uri });
75-
this.server.mcpServer.sendResourceListChanged();
88+
await this.server?.mcpServer.server.sendResourceUpdated({ uri: this.uri });
89+
this.server?.mcpServer.sendResourceListChanged();
7690
} catch (error: unknown) {
7791
this.session.logger.warning({
7892
id: LogId.resourceUpdateFailure,

src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ export class Server {
146146

147147
private registerResources(): void {
148148
for (const resourceConstructor of Resources) {
149-
const resource = new resourceConstructor(this, this.telemetry);
150-
resource.register();
149+
const resource = new resourceConstructor(this.session, this.userConfig, this.telemetry);
150+
resource.register(this);
151151
}
152152
}
153153

tests/integration/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ function validateToolAnnotations(tool: ToolInfo, name: string, description: stri
271271
}
272272
}
273273

274-
export function timeout(ms: number) {
274+
export function timeout(ms: number): Promise<void> {
275275
return new Promise((resolve) => setTimeout(resolve, ms));
276276
}
277277

tests/unit/common/sessionExportsManager.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ const exportsManagerConfig: SessionExportsManagerConfig = {
2525
exportCleanupIntervalMs: config.exportCleanupIntervalMs,
2626
} as const;
2727

28-
function getExportNameAndPath(sessionId: string, timestamp: number) {
28+
function getExportNameAndPath(
29+
sessionId: string,
30+
timestamp: number
31+
): {
32+
sessionExportsPath: string;
33+
exportName: string;
34+
exportPath: string;
35+
exportURI: string;
36+
} {
2937
const exportName = `foo.bar.${timestamp}.json`;
3038
const sessionExportsPath = path.join(exportsPath, sessionId);
3139
const exportPath = path.join(sessionExportsPath, exportName);
@@ -44,7 +52,7 @@ function createDummyFindCursor(
4452
let index = 0;
4553
const readable = new Readable({
4654
objectMode: true,
47-
async read() {
55+
async read(): Promise<void> {
4856
if (index < dataArray.length) {
4957
if (chunkPushTimeoutMs) {
5058
await timeout(chunkPushTimeoutMs);
@@ -61,7 +69,7 @@ function createDummyFindCursor(
6169

6270
let notifyClose: () => Promise<void>;
6371
const cursorCloseNotification = new Promise<void>((resolve) => {
64-
notifyClose = async () => {
72+
notifyClose = async (): Promise<void> => {
6573
await timeout(10);
6674
resolve();
6775
};
@@ -81,7 +89,7 @@ function createDummyFindCursor(
8189
};
8290
}
8391

84-
async function fileExists(filePath: string) {
92+
async function fileExists(filePath: string): Promise<boolean> {
8593
try {
8694
await fs.access(filePath);
8795
return true;
@@ -288,11 +296,11 @@ describe("SessionExportsManager unit test", () => {
288296
it("should remove the partial export and never make it available", async () => {
289297
const emitSpy = vi.spyOn(manager, "emit");
290298
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
291-
(manager as any).docToEJSONStream = function (ejsonOptions: EJSONOptions | undefined) {
299+
(manager as any).docToEJSONStream = function (ejsonOptions: EJSONOptions | undefined): Transform {
292300
let docsTransformed = 0;
293301
return new Transform({
294302
objectMode: true,
295-
transform: function (chunk: unknown, encoding, callback) {
303+
transform: function (chunk: unknown, encoding, callback): void {
296304
++docsTransformed;
297305
try {
298306
if (docsTransformed === 1) {
@@ -306,7 +314,7 @@ describe("SessionExportsManager unit test", () => {
306314
callback(err as Error);
307315
}
308316
},
309-
final: function (callback) {
317+
final: function (callback): void {
310318
this.push("]");
311319
callback(null);
312320
},

tests/unit/resources/common/debug.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { beforeEach, describe, expect, it } from "vitest";
22
import { DebugResource } from "../../../../src/resources/common/debug.js";
33
import { Session } from "../../../../src/common/session.js";
4-
import { Server } from "../../../../src/server.js";
54
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
65
import { config } from "../../../../src/common/config.js";
6+
import { CompositeLogger } from "../../../../src/common/logger.js";
77

88
describe("debug resource", () => {
9-
// eslint-disable-next-line
10-
const session = new Session({} as any);
11-
// eslint-disable-next-line
12-
const server = new Server({ session } as any);
9+
const session = new Session({ apiBaseUrl: "", logger: new CompositeLogger() });
1310
const telemetry = Telemetry.create(session, { ...config, telemetry: "disabled" });
1411

15-
let debugResource: DebugResource = new DebugResource(server, telemetry);
12+
let debugResource: DebugResource = new DebugResource(session, config, telemetry);
1613

1714
beforeEach(() => {
18-
debugResource = new DebugResource(server, telemetry);
15+
debugResource = new DebugResource(session, config, telemetry);
1916
});
2017

2118
it("should be connected when a connected event happens", () => {

0 commit comments

Comments
 (0)