Skip to content

Commit ba54952

Browse files
committed
chore: add tests for collection-schema
1 parent 795858a commit ba54952

File tree

13 files changed

+414
-306
lines changed

13 files changed

+414
-306
lines changed
Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
33
import { ToolArgs, OperationType } from "../../tool.js";
4-
import { parseSchema, SchemaField } from "mongodb-schema";
4+
import { getSimplifiedSchema, parseSchema, SchemaField, SchemaType } from "mongodb-schema";
55

66
export class CollectionSchemaTool extends MongoDBToolBase {
77
protected name = "collection-schema";
@@ -13,29 +13,31 @@ export class CollectionSchemaTool extends MongoDBToolBase {
1313
protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
1414
const provider = await this.ensureConnected();
1515
const documents = await provider.find(database, collection, {}, { limit: 5 }).toArray();
16-
const schema = await parseSchema(documents);
16+
const schema = await getSimplifiedSchema(documents);
17+
18+
const fieldsCount = Object.entries(schema).length;
19+
if (fieldsCount === 0) {
20+
return {
21+
content: [
22+
{
23+
text: `Could not deduce the schema for "${database}.${collection}". This may be because it doesn't exist or is empty.`,
24+
type: "text",
25+
},
26+
],
27+
};
28+
}
1729

1830
return {
1931
content: [
2032
{
21-
text: `Found ${schema.fields.length} fields in the schema for \`${database}.${collection}\``,
33+
text: `Found ${fieldsCount} fields in the schema for "${database}.${collection}"`,
2234
type: "text",
2335
},
2436
{
25-
text: this.formatFieldOutput(schema.fields),
37+
text: JSON.stringify(schema),
2638
type: "text",
2739
},
2840
],
2941
};
3042
}
31-
32-
private formatFieldOutput(fields: SchemaField[]): string {
33-
let result = "| Field | Type | Confidence |\n";
34-
result += "|-------|------|-------------|\n";
35-
for (const field of fields) {
36-
const fieldType = Array.isArray(field.type) ? field.type.join(", ") : field.type;
37-
result += `| ${field.name} | \`${fieldType}\` | ${(field.probability * 100).toFixed(0)}% |\n`;
38-
}
39-
return result;
40-
}
4143
}

tests/integration/helpers.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ interface ParameterInfo {
1919

2020
type ToolInfo = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
2121

22-
export function setupIntegrationTest(): {
22+
interface IntegrationTestSetup {
2323
mcpClient: () => Client;
2424
mongoClient: () => MongoClient;
2525
connectionString: () => string;
2626
connectMcpClient: () => Promise<void>;
2727
randomDbName: () => string;
28-
} {
28+
}
29+
30+
export function setupIntegrationTest(): IntegrationTestSetup {
2931
let mongoCluster: runner.MongoCluster | undefined;
3032
let mongoClient: MongoClient | undefined;
3133

@@ -208,8 +210,57 @@ export const dbOperationParameters: ParameterInfo[] = [
208210
{ name: "collection", type: "string", description: "Collection name", required: true },
209211
];
210212

211-
export function validateParameters(tool: ToolInfo, parameters: ParameterInfo[]): void {
213+
export async function validateToolMetadata(
214+
mcpClient: Client,
215+
name: string,
216+
description: string,
217+
parameters: ParameterInfo[]
218+
): Promise<void> {
219+
const { tools } = await mcpClient.listTools();
220+
const tool = tools.find((tool) => tool.name === name)!;
221+
expect(tool).toBeDefined();
222+
expect(tool.description).toBe(description);
223+
212224
const toolParameters = getParameters(tool);
213225
expect(toolParameters).toHaveLength(parameters.length);
214226
expect(toolParameters).toIncludeAllMembers(parameters);
215227
}
228+
229+
export function validateAutoConnectBehavior(
230+
integration: IntegrationTestSetup,
231+
name: string,
232+
validation: () => {
233+
args: { [x: string]: unknown };
234+
expectedResponse?: string;
235+
validate?: (content: unknown) => void;
236+
}
237+
): void {
238+
it("connects automatically if connection string is configured", async () => {
239+
config.connectionString = integration.connectionString();
240+
241+
const validationInfo = validation();
242+
243+
const response = await integration.mcpClient().callTool({
244+
name,
245+
arguments: validationInfo.args,
246+
});
247+
248+
if (validationInfo.expectedResponse) {
249+
const content = getResponseContent(response.content);
250+
expect(content).toContain(validationInfo.expectedResponse);
251+
}
252+
253+
if (validationInfo.validate) {
254+
validationInfo.validate(response.content);
255+
}
256+
});
257+
258+
it("throws an error if connection string is not configured", async () => {
259+
const response = await integration.mcpClient().callTool({
260+
name,
261+
arguments: validation().args,
262+
});
263+
const content = getResponseContent(response.content);
264+
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
265+
});
266+
}

tests/integration/tools/mongodb/create/createCollection.test.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import {
22
getResponseContent,
3-
validateParameters,
43
dbOperationParameters,
54
setupIntegrationTest,
5+
validateToolMetadata,
6+
validateAutoConnectBehavior,
67
} from "../../../helpers.js";
78
import { toIncludeSameMembers } from "jest-extended";
89
import { McpError } from "@modelcontextprotocol/sdk/types.js";
9-
import { ObjectId } from "bson";
10-
import config from "../../../../../src/config.js";
1110

1211
describe("createCollection tool", () => {
1312
const integration = setupIntegrationTest();
1413

1514
it("should have correct metadata", async () => {
16-
const { tools } = await integration.mcpClient().listTools();
17-
const listCollections = tools.find((tool) => tool.name === "create-collection")!;
18-
expect(listCollections).toBeDefined();
19-
expect(listCollections.description).toBe(
20-
"Creates a new collection in a database. If the database doesn't exist, it will be created automatically."
15+
await validateToolMetadata(
16+
integration.mcpClient(),
17+
"create-collection",
18+
"Creates a new collection in a database. If the database doesn't exist, it will be created automatically.",
19+
dbOperationParameters
2120
);
22-
23-
validateParameters(listCollections, dbOperationParameters);
2421
});
2522

2623
describe("with invalid arguments", () => {
@@ -115,24 +112,11 @@ describe("createCollection tool", () => {
115112
});
116113

117114
describe("when not connected", () => {
118-
it("connects automatically if connection string is configured", async () => {
119-
config.connectionString = integration.connectionString();
120-
121-
const response = await integration.mcpClient().callTool({
122-
name: "create-collection",
123-
arguments: { database: integration.randomDbName(), collection: "new-collection" },
124-
});
125-
const content = getResponseContent(response.content);
126-
expect(content).toEqual(`Collection "new-collection" created in database "${integration.randomDbName()}".`);
127-
});
128-
129-
it("throws an error if connection string is not configured", async () => {
130-
const response = await integration.mcpClient().callTool({
131-
name: "create-collection",
132-
arguments: { database: integration.randomDbName(), collection: "new-collection" },
133-
});
134-
const content = getResponseContent(response.content);
135-
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
115+
validateAutoConnectBehavior(integration, "create-collection", () => {
116+
return {
117+
args: { database: integration.randomDbName(), collection: "new-collection" },
118+
expectedResponse: `Collection "new-collection" created in database "${integration.randomDbName()}".`,
119+
};
136120
});
137121
});
138122
});

tests/integration/tools/mongodb/create/createIndex.test.ts

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {
22
getResponseContent,
3-
validateParameters,
43
dbOperationParameters,
54
setupIntegrationTest,
5+
validateToolMetadata,
6+
validateAutoConnectBehavior,
67
} from "../../../helpers.js";
78
import { McpError } from "@modelcontextprotocol/sdk/types.js";
89
import { IndexDirection } from "mongodb";
@@ -12,12 +13,7 @@ describe("createIndex tool", () => {
1213
const integration = setupIntegrationTest();
1314

1415
it("should have correct metadata", async () => {
15-
const { tools } = await integration.mcpClient().listTools();
16-
const createIndex = tools.find((tool) => tool.name === "create-index")!;
17-
expect(createIndex).toBeDefined();
18-
expect(createIndex.description).toBe("Create an index for a collection");
19-
20-
validateParameters(createIndex, [
16+
await validateToolMetadata(integration.mcpClient(), "create-index", "Create an index for a collection", [
2117
...dbOperationParameters,
2218
{
2319
name: "keys",
@@ -216,34 +212,15 @@ describe("createIndex tool", () => {
216212
}
217213

218214
describe("when not connected", () => {
219-
it("connects automatically if connection string is configured", async () => {
220-
config.connectionString = integration.connectionString();
221-
222-
const response = await integration.mcpClient().callTool({
223-
name: "create-index",
224-
arguments: {
215+
validateAutoConnectBehavior(integration, "create-index", () => {
216+
return {
217+
args: {
225218
database: integration.randomDbName(),
226219
collection: "coll1",
227220
keys: { prop1: 1 },
228221
},
229-
});
230-
const content = getResponseContent(response.content);
231-
expect(content).toEqual(
232-
`Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"`
233-
);
234-
});
235-
236-
it("throws an error if connection string is not configured", async () => {
237-
const response = await integration.mcpClient().callTool({
238-
name: "create-index",
239-
arguments: {
240-
database: integration.randomDbName(),
241-
collection: "coll1",
242-
keys: { prop1: 1 },
243-
},
244-
});
245-
const content = getResponseContent(response.content);
246-
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
222+
expectedResponse: `Created the index "prop1_1" on collection "coll1" in database "${integration.randomDbName()}"`,
223+
};
247224
});
248225
});
249226
});

tests/integration/tools/mongodb/create/insertMany.test.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {
22
getResponseContent,
3-
validateParameters,
43
dbOperationParameters,
54
setupIntegrationTest,
5+
validateToolMetadata,
6+
validateAutoConnectBehavior,
67
} from "../../../helpers.js";
78
import { McpError } from "@modelcontextprotocol/sdk/types.js";
89
import config from "../../../../../src/config.js";
@@ -11,21 +12,21 @@ describe("insertMany tool", () => {
1112
const integration = setupIntegrationTest();
1213

1314
it("should have correct metadata", async () => {
14-
const { tools } = await integration.mcpClient().listTools();
15-
const insertMany = tools.find((tool) => tool.name === "insert-many")!;
16-
expect(insertMany).toBeDefined();
17-
expect(insertMany.description).toBe("Insert an array of documents into a MongoDB collection");
18-
19-
validateParameters(insertMany, [
20-
...dbOperationParameters,
21-
{
22-
name: "documents",
23-
type: "array",
24-
description:
25-
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()",
26-
required: true,
27-
},
28-
]);
15+
validateToolMetadata(
16+
integration.mcpClient(),
17+
"insert-many",
18+
"Insert an array of documents into a MongoDB collection",
19+
[
20+
...dbOperationParameters,
21+
{
22+
name: "documents",
23+
type: "array",
24+
description:
25+
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()",
26+
required: true,
27+
},
28+
]
29+
);
2930
});
3031

3132
describe("with invalid arguments", () => {
@@ -110,32 +111,15 @@ describe("insertMany tool", () => {
110111
});
111112

112113
describe("when not connected", () => {
113-
it("connects automatically if connection string is configured", async () => {
114-
config.connectionString = integration.connectionString();
115-
116-
const response = await integration.mcpClient().callTool({
117-
name: "insert-many",
118-
arguments: {
119-
database: integration.randomDbName(),
120-
collection: "coll1",
121-
documents: [{ prop1: "value1" }],
122-
},
123-
});
124-
const content = getResponseContent(response.content);
125-
expect(content).toContain('Inserted `1` document(s) into collection "coll1"');
126-
});
127-
128-
it("throws an error if connection string is not configured", async () => {
129-
const response = await integration.mcpClient().callTool({
130-
name: "insert-many",
131-
arguments: {
114+
validateAutoConnectBehavior(integration, "insert-many", () => {
115+
return {
116+
args: {
132117
database: integration.randomDbName(),
133118
collection: "coll1",
134119
documents: [{ prop1: "value1" }],
135120
},
136-
});
137-
const content = getResponseContent(response.content);
138-
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
121+
expectedResponse: 'Inserted `1` document(s) into collection "coll1"',
122+
};
139123
});
140124
});
141125
});

0 commit comments

Comments
 (0)