Skip to content

Commit cd45930

Browse files
chore: sessionId is initialized on Session instantiation
1 parent e8845bc commit cd45930

File tree

5 files changed

+47
-81
lines changed

5 files changed

+47
-81
lines changed

src/common/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface UserConfig {
1919
apiClientSecret?: string;
2020
telemetry: "enabled" | "disabled";
2121
logPath: string;
22-
exportPath: string;
22+
exportsPath: string;
2323
exportTimeoutMs: number;
2424
exportCleanupIntervalMs: number;
2525
connectionString?: string;
@@ -38,7 +38,7 @@ export interface UserConfig {
3838
const defaults: UserConfig = {
3939
apiBaseUrl: "https://cloud.mongodb.com/",
4040
logPath: getLogPath(),
41-
exportPath: getExportPath(),
41+
exportsPath: getExportsPath(),
4242
exportTimeoutMs: 300000, // 5 minutes
4343
exportCleanupIntervalMs: 120000, // 2 minutes
4444
connectOptions: {
@@ -76,7 +76,7 @@ function getLogPath(): string {
7676
return logPath;
7777
}
7878

79-
function getExportPath(): string {
79+
function getExportsPath(): string {
8080
return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
8181
}
8282

src/common/session.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./connectionManager.js";
1111
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1212
import { ErrorCodes, MongoDBError } from "./errors.js";
13+
import { ObjectId } from "bson";
1314

1415
export interface SessionOptions {
1516
apiBaseUrl: string;
@@ -28,7 +29,7 @@ export type SessionEvents = {
2829
};
2930

3031
export class Session extends EventEmitter<SessionEvents> {
31-
sessionId?: string;
32+
readonly sessionId = new ObjectId().toString();
3233
connectionManager: ConnectionManager;
3334
apiClient: ApiClient;
3435
agentRunner?: {

src/common/sessionExportsManager.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type Export = {
2323

2424
export type SessionExportsManagerConfig = Pick<
2525
UserConfig,
26-
"exportPath" | "exportTimeoutMs" | "exportCleanupIntervalMs"
26+
"exportsPath" | "exportTimeoutMs" | "exportCleanupIntervalMs"
2727
>;
2828

2929
const MAX_LOCK_RETRIES = 10;
@@ -63,13 +63,7 @@ export class SessionExportsManager {
6363
}
6464

6565
public exportsDirectoryPath(): string {
66-
// If the session is not connected, we can't cannot work with exports
67-
// for that session.
68-
if (!this.session.sessionId) {
69-
throw new Error("Cannot retrieve exports directory, no active session. Try to reconnect to the MCP server");
70-
}
71-
72-
return path.join(this.config.exportPath, this.session.sessionId);
66+
return path.join(this.config.exportsPath, this.session.sessionId);
7367
}
7468

7569
public exportFilePath(exportsDirectoryPath: string, exportNameWithExtension: string): string {

src/server.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { AtlasTools } from "./tools/atlas/tools.js";
55
import { MongoDbTools } from "./tools/mongodb/tools.js";
66
import { Resources } from "./resources/resources.js";
77
import logger, { LogId, LoggerBase, McpLogger, DiskLogger, ConsoleLogger } from "./common/logger.js";
8-
import { ObjectId } from "mongodb";
98
import { Telemetry } from "./telemetry/telemetry.js";
109
import { UserConfig } from "./common/config.js";
1110
import { type ServerEvent } from "./telemetry/types.js";
@@ -89,7 +88,6 @@ export class Server {
8988

9089
this.mcpServer.server.oninitialized = () => {
9190
this.session.setAgentRunner(this.mcpServer.server.getClientVersion());
92-
this.session.sessionId = new ObjectId().toString();
9391

9492
logger.info(
9593
LogId.serverInitialized,

tests/unit/common/sessionExportsManager.test.ts

Lines changed: 40 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,33 @@ import { ROOT_DIR } from "../../accuracy/sdk/constants.js";
1111
import { timeout } from "../../integration/helpers.js";
1212
import { EJSON, EJSONOptions } from "bson";
1313

14-
const dummySessionId = "1FOO";
15-
const dummyExportsPath = path.join(ROOT_DIR, "tests", "tmp", "exports");
16-
const dummySessionExportPath = path.join(dummyExportsPath, dummySessionId);
14+
const exportsPath = path.join(ROOT_DIR, "tests", "tmp", "exports");
1715
const exportsManagerConfig: SessionExportsManagerConfig = {
18-
exportPath: dummyExportsPath,
16+
exportsPath,
1917
exportTimeoutMs: config.exportTimeoutMs,
2018
exportCleanupIntervalMs: config.exportCleanupIntervalMs,
2119
} as const;
22-
function getDummyExportName(timestamp: number) {
23-
return `foo.bar.${timestamp}.json`;
24-
}
25-
function getDummyExportPath(timestamp: number) {
26-
return path.join(dummySessionExportPath, getDummyExportName(timestamp));
20+
21+
function getExportNameAndPath(sessionId: string, timestamp: number) {
22+
const exportName = `foo.bar.${timestamp}.json`;
23+
const sessionExportsPath = path.join(exportsPath, sessionId);
24+
const exportPath = path.join(sessionExportsPath, exportName);
25+
return {
26+
sessionExportsPath,
27+
exportName,
28+
exportPath,
29+
exportURI: `exported-data://${exportName}`,
30+
};
2731
}
2832

29-
async function createDummyExport(timestamp: number) {
33+
async function createDummyExport(sessionId: string, timestamp: number) {
3034
const content = "[]";
31-
await fs.mkdir(dummySessionExportPath, { recursive: true });
32-
await fs.writeFile(getDummyExportPath(timestamp), content);
35+
const { exportName, exportPath, sessionExportsPath } = getExportNameAndPath(sessionId, timestamp);
36+
await fs.mkdir(sessionExportsPath, { recursive: true });
37+
await fs.writeFile(exportPath, content);
3338
return {
34-
name: getDummyExportName(timestamp),
35-
path: getDummyExportPath(timestamp),
39+
exportName,
40+
exportPath,
3641
content,
3742
};
3843
}
@@ -75,8 +80,8 @@ describe("SessionExportsManager integration test", () => {
7580

7681
beforeEach(async () => {
7782
await manager?.close();
78-
await fs.rm(exportsManagerConfig.exportPath, { recursive: true, force: true });
79-
await fs.mkdir(exportsManagerConfig.exportPath, { recursive: true });
83+
await fs.rm(exportsManagerConfig.exportsPath, { recursive: true, force: true });
84+
await fs.mkdir(exportsManagerConfig.exportsPath, { recursive: true });
8085
session = new Session({ apiBaseUrl: "" });
8186
manager = new SessionExportsManager(session, exportsManagerConfig);
8287
});
@@ -92,25 +97,22 @@ describe("SessionExportsManager integration test", () => {
9297
});
9398

9499
describe("#exportsDirectoryPath", () => {
95-
it("should throw when session is not initialized", () => {
96-
expect(() => manager.exportsDirectoryPath()).toThrow();
97-
});
98-
99100
it("should return a session path when session is initialized", () => {
100-
session.sessionId = dummySessionId;
101101
manager = new SessionExportsManager(session, exportsManagerConfig);
102-
expect(manager.exportsDirectoryPath()).toEqual(path.join(exportsManagerConfig.exportPath, dummySessionId));
102+
expect(manager.exportsDirectoryPath()).toEqual(
103+
path.join(exportsManagerConfig.exportsPath, session.sessionId)
104+
);
103105
});
104106
});
105107

106108
describe("#exportFilePath", () => {
107109
it("should throw when export name has no extension", () => {
108-
expect(() => manager.exportFilePath(dummySessionExportPath, "name")).toThrow();
110+
expect(() => manager.exportFilePath("session-path", "name")).toThrow();
109111
});
110112

111113
it("should return path to provided export file", () => {
112-
expect(manager.exportFilePath(dummySessionExportPath, "mflix.movies.json")).toEqual(
113-
path.join(dummySessionExportPath, "mflix.movies.json")
114+
expect(manager.exportFilePath("session-path", "mflix.movies.json")).toEqual(
115+
path.join("session-path", "mflix.movies.json")
114116
);
115117
});
116118
});
@@ -121,15 +123,16 @@ describe("SessionExportsManager integration test", () => {
121123
});
122124

123125
it("should return the resource content", async () => {
124-
const { name, content } = await createDummyExport(Date.now());
125-
session.sessionId = dummySessionId;
126-
manager = new SessionExportsManager(session, exportsManagerConfig);
127-
expect(await manager.readExport(name)).toEqual(content);
126+
const { exportName, content } = await createDummyExport(session.sessionId, Date.now());
127+
expect(await manager.readExport(exportName)).toEqual(content);
128128
});
129129
});
130130

131131
describe("#createJSONExport", () => {
132132
let inputCursor: FindCursor;
133+
let exportName: string;
134+
let exportPath: string;
135+
let exportURI: string;
133136
beforeEach(() => {
134137
void inputCursor?.close();
135138
inputCursor = createDummyFindCursor([
@@ -142,19 +145,17 @@ describe("SessionExportsManager integration test", () => {
142145
longNumber: Long.fromNumber(123456),
143146
},
144147
]);
148+
({ exportName, exportPath, exportURI } = getExportNameAndPath(session.sessionId, Date.now()));
145149
});
146150

147151
describe("when cursor is empty", () => {
148152
it("should create an empty export", async () => {
149153
inputCursor = createDummyFindCursor([]);
150154

151155
const emitSpy = vi.spyOn(session, "emit");
152-
session.sessionId = dummySessionId;
153-
manager = new SessionExportsManager(session, exportsManagerConfig);
154-
const timestamp = Date.now();
155156
await manager.createJSONExport({
156157
input: inputCursor,
157-
exportName: getDummyExportName(timestamp),
158+
exportName,
158159
jsonExportFormat: "relaxed",
159160
});
160161

@@ -163,19 +164,16 @@ describe("SessionExportsManager integration test", () => {
163164
expect(availableExports).toHaveLength(1);
164165
expect(availableExports).toContainEqual(
165166
expect.objectContaining({
166-
name: getDummyExportName(timestamp),
167-
uri: `exported-data://${getDummyExportName(timestamp)}`,
167+
name: exportName,
168+
uri: exportURI,
168169
})
169170
);
170171

171172
// Emit event
172-
expect(emitSpy).toHaveBeenCalledWith(
173-
"export-available",
174-
`exported-data://${getDummyExportName(timestamp)}`
175-
);
173+
expect(emitSpy).toHaveBeenCalledWith("export-available", exportURI);
176174

177175
// Exports relaxed json
178-
const jsonData = JSON.parse(await manager.readExport(getDummyExportName(timestamp))) as unknown[];
176+
const jsonData = JSON.parse(await manager.readExport(exportName)) as unknown[];
179177
expect(jsonData).toEqual([]);
180178
});
181179
});
@@ -186,8 +184,6 @@ describe("SessionExportsManager integration test", () => {
186184
])("$cond", ({ exportName }) => {
187185
it("should export relaxed json, update available exports and emit export-available event", async () => {
188186
const emitSpy = vi.spyOn(session, "emit");
189-
session.sessionId = dummySessionId;
190-
manager = new SessionExportsManager(session, exportsManagerConfig);
191187
await manager.createJSONExport({
192188
input: inputCursor,
193189
exportName,
@@ -221,8 +217,6 @@ describe("SessionExportsManager integration test", () => {
221217
])("$cond", ({ exportName }) => {
222218
it("should export canonical json, update available exports and emit export-available event", async () => {
223219
const emitSpy = vi.spyOn(session, "emit");
224-
session.sessionId = dummySessionId;
225-
manager = new SessionExportsManager(session, exportsManagerConfig);
226220
await manager.createJSONExport({
227221
input: inputCursor,
228222
exportName,
@@ -257,8 +251,6 @@ describe("SessionExportsManager integration test", () => {
257251
describe("when transform stream throws an error", () => {
258252
it("should remove the partial export and never make it available", async () => {
259253
const emitSpy = vi.spyOn(session, "emit");
260-
session.sessionId = dummySessionId;
261-
manager = new SessionExportsManager(session, exportsManagerConfig);
262254
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
263255
(manager as any).docToEJSONStream = function (ejsonOptions: EJSONOptions | undefined) {
264256
let docsTransformed = 0;
@@ -285,9 +277,6 @@ describe("SessionExportsManager integration test", () => {
285277
});
286278
};
287279

288-
const timestamp = Date.now();
289-
const exportName = getDummyExportName(timestamp);
290-
const exportPath = getDummyExportPath(timestamp);
291280
await expect(() =>
292281
manager.createJSONExport({
293282
input: inputCursor,
@@ -319,24 +308,8 @@ describe("SessionExportsManager integration test", () => {
319308
]);
320309
});
321310

322-
it("should do nothing if session is not initialized", async () => {
323-
const { path } = await createDummyExport(Date.now());
324-
new SessionExportsManager(session, {
325-
...exportsManagerConfig,
326-
exportTimeoutMs: 100,
327-
exportCleanupIntervalMs: 50,
328-
});
329-
330-
expect(await fileExists(path)).toEqual(true);
331-
await timeout(200);
332-
expect(await fileExists(path)).toEqual(true);
333-
});
334-
335311
it("should cleanup expired exports if session is initialized", async () => {
336-
session.sessionId = dummySessionId;
337-
const timestamp = Date.now();
338-
const exportName = getDummyExportName(timestamp);
339-
const exportPath = getDummyExportPath(timestamp);
312+
const { exportName, exportPath, exportURI } = getExportNameAndPath(session.sessionId, Date.now());
340313
const manager = new SessionExportsManager(session, {
341314
...exportsManagerConfig,
342315
exportTimeoutMs: 100,
@@ -351,7 +324,7 @@ describe("SessionExportsManager integration test", () => {
351324
expect(manager.listAvailableExports()).toContainEqual(
352325
expect.objectContaining({
353326
name: exportName,
354-
uri: `exported-data://${exportName}`,
327+
uri: exportURI,
355328
})
356329
);
357330
expect(await fileExists(exportPath)).toEqual(true);

0 commit comments

Comments
 (0)