Skip to content

Commit af947d5

Browse files
chore: more reliable export tool and exported-data resource tests
1 parent bfa1dee commit af947d5

File tree

3 files changed

+71
-40
lines changed

3 files changed

+71
-40
lines changed

tests/integration/helpers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
import { InMemoryTransport } from "./inMemoryTransport.js";
33
import { Server } from "../../src/server.js";
44
import { UserConfig } from "../../src/common/config.js";
5-
import { McpError } from "@modelcontextprotocol/sdk/types.js";
5+
import { McpError, ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
66
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Session } from "../../src/common/session.js";
88
import { Telemetry } from "../../src/telemetry/telemetry.js";
@@ -274,3 +274,16 @@ function validateToolAnnotations(tool: ToolInfo, name: string, description: stri
274274
export function timeout(ms: number) {
275275
return new Promise((resolve) => setTimeout(resolve, ms));
276276
}
277+
278+
/**
279+
* Subscribes to the resources changed notification for the provided URI
280+
*/
281+
export function resourceChangedNotification(client: Client, uri: string): Promise<void> {
282+
return new Promise<void>((resolve) => {
283+
client.setNotificationHandler(ResourceUpdatedNotificationSchema, (notification) => {
284+
if (notification.params.uri === uri) {
285+
resolve();
286+
}
287+
});
288+
});
289+
}

tests/integration/resources/exportedData.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Long } from "bson";
22
import { describe, expect, it, beforeEach } from "vitest";
33
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4-
import { defaultTestConfig, timeout } from "../helpers.js";
4+
import { defaultTestConfig, resourceChangedNotification, timeout } from "../helpers.js";
55
import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js";
6+
import { contentWithResourceURILink } from "../tools/mongodb/read/export.test.js";
67

78
describeWithMongoDB(
89
"exported-data resource",
@@ -76,8 +77,9 @@ describeWithMongoDB(
7677
name: "export",
7778
arguments: { database: "db", collection: "coll" },
7879
});
79-
// Small timeout to let export finish
80-
await timeout(50);
80+
const content = exportResponse.content as CallToolResult["content"];
81+
const exportURI = contentWithResourceURILink(content)?.uri as string;
82+
await resourceChangedNotification(integration.mcpClient(), exportURI);
8183

8284
const exportedResourceURI = (exportResponse as CallToolResult).content.find(
8385
(part) => part.type === "resource_link"
@@ -98,8 +100,9 @@ describeWithMongoDB(
98100
name: "export",
99101
arguments: { database: "big", collection: "coll" },
100102
});
101-
// Small timeout to let export finish
102-
await timeout(50);
103+
const content = exportResponse.content as CallToolResult["content"];
104+
const exportURI = contentWithResourceURILink(content)?.uri as string;
105+
await resourceChangedNotification(integration.mcpClient(), exportURI);
103106

104107
const exportedResourceURI = (exportResponse as CallToolResult).content.find(
105108
(part) => part.type === "resource_link"

tests/integration/tools/mongodb/read/export.test.ts

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,33 @@ import fs from "fs/promises";
22
import { beforeEach, describe, expect, it } from "vitest";
33
import {
44
databaseCollectionParameters,
5-
timeout,
5+
resourceChangedNotification,
66
validateThrowsForInvalidArguments,
77
validateToolMetadata,
88
} from "../../../helpers.js";
99
import { describeWithMongoDB } from "../mongodbHelpers.js";
1010
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
1111
import { Long } from "bson";
1212

13-
function contentWithTextResourceURI(content: CallToolResult["content"], namespace: string) {
13+
export function contentWithTextResourceURI(
14+
content: CallToolResult["content"]
15+
): CallToolResult["content"][number] | undefined {
1416
return content.find((part) => {
15-
return part.type === "text" && part.text.startsWith(`Data for namespace ${namespace}`);
17+
return part.type === "text" && part.text.startsWith(`Data for namespace`);
1618
});
1719
}
1820

19-
function contentWithResourceURILink(content: CallToolResult["content"], namespace: string) {
21+
export function contentWithResourceURILink(
22+
content: CallToolResult["content"]
23+
): CallToolResult["content"][number] | undefined {
2024
return content.find((part) => {
21-
return part.type === "resource_link" && part.uri.startsWith(`exported-data://${namespace}`);
25+
return part.type === "resource_link";
2226
});
2327
}
2428

25-
function contentWithExportPath(content: CallToolResult["content"]) {
29+
export function contentWithExportPath(
30+
content: CallToolResult["content"]
31+
): CallToolResult["content"][number] | undefined {
2632
return content.find((part) => {
2733
return (
2834
part.type === "text" &&
@@ -89,20 +95,22 @@ describeWithMongoDB("export tool", (integration) => {
8995
{ database: "test", collection: "bar", sort: [], limit: 10 },
9096
]);
9197

92-
it("when provided with incorrect namespace, export should have empty data", async function () {
98+
beforeEach(async () => {
9399
await integration.connectMcpClient();
100+
});
101+
102+
it("when provided with incorrect namespace, export should have empty data", async function () {
94103
const response = await integration.mcpClient().callTool({
95104
name: "export",
96105
arguments: { database: "non-existent", collection: "foos" },
97106
});
98-
// Small timeout to let export finish
99-
await timeout(10);
100-
101107
const content = response.content as CallToolResult["content"];
102-
const namespace = "non-existent.foos";
108+
const exportURI = contentWithResourceURILink(content)?.uri as string;
109+
await resourceChangedNotification(integration.mcpClient(), exportURI);
110+
103111
expect(content).toHaveLength(3);
104-
expect(contentWithTextResourceURI(content, namespace)).toBeDefined();
105-
expect(contentWithResourceURILink(content, namespace)).toBeDefined();
112+
expect(contentWithTextResourceURI(content)).toBeDefined();
113+
expect(contentWithResourceURILink(content)).toBeDefined();
106114

107115
const localPathPart = contentWithExportPath(content);
108116
expect(localPathPart).toBeDefined();
@@ -131,10 +139,11 @@ describeWithMongoDB("export tool", (integration) => {
131139
name: "export",
132140
arguments: { database: integration.randomDbName(), collection: "foo" },
133141
});
134-
// Small timeout to let export finish
135-
await timeout(10);
142+
const content = response.content as CallToolResult["content"];
143+
const exportURI = contentWithResourceURILink(content)?.uri as string;
144+
await resourceChangedNotification(integration.mcpClient(), exportURI);
136145

137-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
146+
const localPathPart = contentWithExportPath(content);
138147
expect(localPathPart).toBeDefined();
139148
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
140149
expect(localPath).toBeDefined();
@@ -154,10 +163,11 @@ describeWithMongoDB("export tool", (integration) => {
154163
name: "export",
155164
arguments: { database: integration.randomDbName(), collection: "foo", filter: { name: "foo" } },
156165
});
157-
// Small timeout to let export finish
158-
await timeout(10);
166+
const content = response.content as CallToolResult["content"];
167+
const exportURI = contentWithResourceURILink(content)?.uri as string;
168+
await resourceChangedNotification(integration.mcpClient(), exportURI);
159169

160-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
170+
const localPathPart = contentWithExportPath(content);
161171
expect(localPathPart).toBeDefined();
162172
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
163173
expect(localPath).toBeDefined();
@@ -176,10 +186,11 @@ describeWithMongoDB("export tool", (integration) => {
176186
name: "export",
177187
arguments: { database: integration.randomDbName(), collection: "foo", limit: 1 },
178188
});
179-
// Small timeout to let export finish
180-
await timeout(10);
189+
const content = response.content as CallToolResult["content"];
190+
const exportURI = contentWithResourceURILink(content)?.uri as string;
191+
await resourceChangedNotification(integration.mcpClient(), exportURI);
181192

182-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
193+
const localPathPart = contentWithExportPath(content);
183194
expect(localPathPart).toBeDefined();
184195
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
185196
expect(localPath).toBeDefined();
@@ -203,10 +214,11 @@ describeWithMongoDB("export tool", (integration) => {
203214
sort: { longNumber: 1 },
204215
},
205216
});
206-
// Small timeout to let export finish
207-
await timeout(10);
217+
const content = response.content as CallToolResult["content"];
218+
const exportURI = contentWithResourceURILink(content)?.uri as string;
219+
await resourceChangedNotification(integration.mcpClient(), exportURI);
208220

209-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
221+
const localPathPart = contentWithExportPath(content);
210222
expect(localPathPart).toBeDefined();
211223
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
212224
expect(localPath).toBeDefined();
@@ -230,10 +242,11 @@ describeWithMongoDB("export tool", (integration) => {
230242
projection: { _id: 0, name: 1 },
231243
},
232244
});
233-
// Small timeout to let export finish
234-
await timeout(10);
245+
const content = response.content as CallToolResult["content"];
246+
const exportURI = contentWithResourceURILink(content)?.uri as string;
247+
await resourceChangedNotification(integration.mcpClient(), exportURI);
235248

236-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
249+
const localPathPart = contentWithExportPath(content);
237250
expect(localPathPart).toBeDefined();
238251
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
239252
expect(localPath).toBeDefined();
@@ -261,10 +274,11 @@ describeWithMongoDB("export tool", (integration) => {
261274
jsonExportFormat: "relaxed",
262275
},
263276
});
264-
// Small timeout to let export finish
265-
await timeout(10);
277+
const content = response.content as CallToolResult["content"];
278+
const exportURI = contentWithResourceURILink(content)?.uri as string;
279+
await resourceChangedNotification(integration.mcpClient(), exportURI);
266280

267-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
281+
const localPathPart = contentWithExportPath(content);
268282
expect(localPathPart).toBeDefined();
269283
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
270284
expect(localPath).toBeDefined();
@@ -293,10 +307,11 @@ describeWithMongoDB("export tool", (integration) => {
293307
jsonExportFormat: "canonical",
294308
},
295309
});
296-
// Small timeout to let export finish
297-
await timeout(10);
310+
const content = response.content as CallToolResult["content"];
311+
const exportURI = contentWithResourceURILink(content)?.uri as string;
312+
await resourceChangedNotification(integration.mcpClient(), exportURI);
298313

299-
const localPathPart = contentWithExportPath(response.content as CallToolResult["content"]);
314+
const localPathPart = contentWithExportPath(content);
300315
expect(localPathPart).toBeDefined();
301316
const [, localPath] = /"(.*)"/.exec(String(localPathPart?.text)) ?? [];
302317
expect(localPath).toBeDefined();

0 commit comments

Comments
 (0)