Skip to content

Commit d6b84c7

Browse files
fix: remove dependency on export tool input for generating export name MCP-189 (#543)
1 parent ed8c19f commit d6b84c7

File tree

5 files changed

+18
-44
lines changed

5 files changed

+18
-44
lines changed

src/common/exportsManager.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
127127
public async readExport(exportName: string): Promise<string> {
128128
try {
129129
this.assertIsNotShuttingDown();
130-
exportName = decodeURIComponent(exportName);
130+
exportName = decodeAndNormalize(exportName);
131131
const exportHandle = this.storedExports[exportName];
132132
if (!exportHandle) {
133133
throw new Error("Requested export has either expired or does not exist.");
@@ -163,7 +163,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
163163
}): Promise<AvailableExport> {
164164
try {
165165
this.assertIsNotShuttingDown();
166-
const exportNameWithExtension = validateExportName(ensureExtension(exportName, "json"));
166+
const exportNameWithExtension = decodeAndNormalize(ensureExtension(exportName, "json"));
167167
if (this.storedExports[exportNameWithExtension]) {
168168
return Promise.reject(
169169
new Error("Export with same name is either already available or being generated.")
@@ -363,6 +363,10 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
363363
}
364364
}
365365

366+
export function decodeAndNormalize(text: string): string {
367+
return decodeURIComponent(text).normalize("NFKC");
368+
}
369+
366370
/**
367371
* Ensures the path ends with the provided extension */
368372
export function ensureExtension(pathOrName: string, extension: string): string {
@@ -373,22 +377,6 @@ export function ensureExtension(pathOrName: string, extension: string): string {
373377
return `${pathOrName}${extWithDot}`;
374378
}
375379

376-
/**
377-
* Small utility to decoding and validating provided export name for path
378-
* traversal or no extension */
379-
export function validateExportName(nameWithExtension: string): string {
380-
const decodedName = decodeURIComponent(nameWithExtension);
381-
if (!path.extname(decodedName)) {
382-
throw new Error("Provided export name has no extension");
383-
}
384-
385-
if (decodedName.includes("..") || decodedName.includes("/") || decodedName.includes("\\")) {
386-
throw new Error("Invalid export name: path traversal hinted");
387-
}
388-
389-
return decodedName;
390-
}
391-
392380
export function isExportExpired(createdAt: number, exportTimeoutMs: number): boolean {
393381
return Date.now() - createdAt > exportTimeoutMs;
394382
}

src/resources/common/exportedData.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ export class ExportedData {
7272
private autoCompleteExportName: CompleteResourceTemplateCallback = (value) => {
7373
try {
7474
return this.session.exportsManager.availableExports
75-
.filter(({ exportName }) => exportName.startsWith(value))
75+
.filter(({ exportName, exportTitle }) => {
76+
const lcExportName = exportName.toLowerCase();
77+
const lcExportTitle = exportTitle.toLowerCase();
78+
const lcValue = value.toLowerCase();
79+
return lcExportName.startsWith(lcValue) || lcExportTitle.includes(lcValue);
80+
})
7681
.map(({ exportName }) => exportName);
7782
} catch (error) {
7883
this.session.logger.error({

src/tools/mongodb/read/export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class ExportTool extends MongoDBToolBase {
8181
});
8282
}
8383

84-
const exportName = `${database}.${collection}.${new ObjectId().toString()}.json`;
84+
const exportName = `${new ObjectId().toString()}.json`;
8585

8686
const { exportURI, exportPath } = await this.session.exportsManager.createJSONExport({
8787
input: cursor,

tests/integration/resources/exportedData.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ describeWithMongoDB(
155155
},
156156
argument: {
157157
name: "exportName",
158-
value: "b",
158+
value: "big",
159159
},
160160
});
161-
expect(completeResponse.completion.total).toEqual(1);
161+
expect(completeResponse.completion.total).toBeGreaterThanOrEqual(1);
162162
});
163163
});
164164
},

tests/unit/common/exportsManager.test.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import type { FindCursor } from "mongodb";
55
import { Long } from "mongodb";
66
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
77
import type { ExportsManagerConfig } from "../../../src/common/exportsManager.js";
8-
import {
9-
ensureExtension,
10-
isExportExpired,
11-
ExportsManager,
12-
validateExportName,
13-
} from "../../../src/common/exportsManager.js";
8+
import { ensureExtension, isExportExpired, ExportsManager } from "../../../src/common/exportsManager.js";
149
import type { AvailableExport } from "../../../src/common/exportsManager.js";
1510
import { config } from "../../../src/common/config.js";
1611
import { ROOT_DIR } from "../../accuracy/sdk/constants.js";
@@ -30,14 +25,10 @@ const exportsManagerConfig: ExportsManagerConfig = {
3025
function getExportNameAndPath({
3126
uniqueExportsId = new ObjectId().toString(),
3227
uniqueFileId = new ObjectId().toString(),
33-
database = "foo",
34-
collection = "bar",
3528
}:
3629
| {
3730
uniqueExportsId?: string;
3831
uniqueFileId?: string;
39-
database?: string;
40-
collection?: string;
4132
}
4233
| undefined = {}): {
4334
sessionExportsPath: string;
@@ -46,7 +37,7 @@ function getExportNameAndPath({
4637
exportURI: string;
4738
uniqueExportsId: string;
4839
} {
49-
const exportName = `${database}.${collection}.${uniqueFileId}.json`;
40+
const exportName = `${uniqueFileId}.json`;
5041
// This is the exports directory for a session.
5142
const sessionExportsPath = path.join(exportsPath, uniqueExportsId);
5243
const exportPath = path.join(sessionExportsPath, exportName);
@@ -248,7 +239,7 @@ describe("ExportsManager unit test", () => {
248239
});
249240

250241
it("should handle encoded name", async () => {
251-
const { exportName, exportURI } = getExportNameAndPath({ database: "some database", collection: "coll" });
242+
const { exportName, exportURI } = getExportNameAndPath({ uniqueFileId: "1FOO 2BAR" });
252243
const { cursor } = createDummyFindCursor([]);
253244
const exportAvailableNotifier = getExportAvailableNotifier(encodeURI(exportURI), manager);
254245
await manager.createJSONExport({
@@ -611,16 +602,6 @@ describe("#ensureExtension", () => {
611602
});
612603
});
613604

614-
describe("#validateExportName", () => {
615-
it("should return decoded name when name is valid", () => {
616-
expect(validateExportName(encodeURIComponent("Test Name.json"))).toEqual("Test Name.json");
617-
});
618-
it("should throw when name is invalid", () => {
619-
expect(() => validateExportName("NoExtension")).toThrow("Provided export name has no extension");
620-
expect(() => validateExportName("../something.json")).toThrow("Invalid export name: path traversal hinted");
621-
});
622-
});
623-
624605
describe("#isExportExpired", () => {
625606
it("should return true if export is expired", () => {
626607
const createdAt = Date.now() - 1000;

0 commit comments

Comments
 (0)