Skip to content

Commit 2c736b9

Browse files
committed
chore: add tests for collection-storage-size
1 parent 0636cc7 commit 2c736b9

File tree

14 files changed

+197
-165
lines changed

14 files changed

+197
-165
lines changed

src/tools/mongodb/metadata/collectionStorageSize.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ToolArgs, OperationType } from "../../tool.js";
44

55
export class CollectionStorageSizeTool extends MongoDBToolBase {
66
protected name = "collection-storage-size";
7-
protected description = "Gets the size of the collection in MB";
7+
protected description = "Gets the size of the collection";
88
protected argsShape = DbOperationArgs;
99

1010
protected operationType: OperationType = "metadata";
@@ -14,17 +14,55 @@ export class CollectionStorageSizeTool extends MongoDBToolBase {
1414
const [{ value }] = (await provider
1515
.aggregate(database, collection, [
1616
{ $collStats: { storageStats: {} } },
17-
{ $group: { _id: null, value: { $sum: "$storageStats.storageSize" } } },
17+
{ $group: { _id: null, value: { $sum: "$storageStats.size" } } },
1818
])
1919
.toArray()) as [{ value: number }];
2020

21+
const { units, value: scaledValue } = CollectionStorageSizeTool.getStats(value);
22+
2123
return {
2224
content: [
2325
{
24-
text: `The size of \`${database}.${collection}\` is \`${(value / 1024 / 1024).toFixed(2)} MB\``,
26+
text: `The size of "${database}.${collection}" is \`${scaledValue.toFixed(2)} ${units}\``,
2527
type: "text",
2628
},
2729
],
2830
};
2931
}
32+
33+
protected handleError(
34+
error: unknown,
35+
args: ToolArgs<typeof this.argsShape>
36+
): Promise<CallToolResult> | CallToolResult {
37+
if (error instanceof Error && "codeName" in error && error.codeName === "NamespaceNotFound") {
38+
return {
39+
content: [
40+
{
41+
text: `The size of "${args.database}.${args.collection}" cannot be determined because the collection does not exist.`,
42+
type: "text",
43+
},
44+
],
45+
};
46+
}
47+
48+
return super.handleError(error, args);
49+
}
50+
51+
private static getStats(value: number): { value: number; units: string } {
52+
const kb = 1024;
53+
const mb = kb * 1024;
54+
const gb = mb * 1024;
55+
56+
if (value > gb) {
57+
return { value: value / gb, units: "GB" };
58+
}
59+
60+
if (value > mb) {
61+
return { value: value / mb, units: "MB" };
62+
}
63+
if (value > kb) {
64+
return { value: value / kb, units: "KB" };
65+
}
66+
return { value, units: "bytes" };
67+
}
3068
}

src/tools/mongodb/mongodbTool.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import { ToolBase, ToolCategory } from "../tool.js";
2+
import { ToolArgs, ToolBase, ToolCategory } from "../tool.js";
33
import { Session } from "../../session.js";
44
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
55
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
@@ -30,7 +30,10 @@ export abstract class MongoDBToolBase extends ToolBase {
3030
return this.session.serviceProvider;
3131
}
3232

33-
protected handleError(error: unknown): Promise<CallToolResult> | CallToolResult {
33+
protected handleError(
34+
error: unknown,
35+
args: ToolArgs<typeof this.argsShape>
36+
): Promise<CallToolResult> | CallToolResult {
3437
if (error instanceof MongoDBError && error.code === ErrorCodes.NotConnectedToMongoDB) {
3538
return {
3639
content: [
@@ -47,7 +50,7 @@ export abstract class MongoDBToolBase extends ToolBase {
4750
};
4851
}
4952

50-
return super.handleError(error);
53+
return super.handleError(error, args);
5154
}
5255

5356
protected async connectToMongoDB(connectionString: string): Promise<void> {

src/tools/tool.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export abstract class ToolBase {
4444
} catch (error: unknown) {
4545
logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`);
4646

47-
return await this.handleError(error);
47+
return await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
4848
}
4949
};
5050

@@ -76,7 +76,11 @@ export abstract class ToolBase {
7676
}
7777

7878
// This method is intended to be overridden by subclasses to handle errors
79-
protected handleError(error: unknown): Promise<CallToolResult> | CallToolResult {
79+
protected handleError(
80+
error: unknown,
81+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
82+
args: ToolArgs<typeof this.argsShape>
83+
): Promise<CallToolResult> | CallToolResult {
8084
return {
8185
content: [
8286
{

tests/integration/helpers.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
99
import { MongoClient, ObjectId } from "mongodb";
1010
import { toIncludeAllMembers } from "jest-extended";
1111
import config from "../../src/config.js";
12+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
1213

1314
interface ParameterInfo {
1415
name: string;
@@ -210,6 +211,8 @@ export const dbOperationParameters: ParameterInfo[] = [
210211
{ name: "collection", type: "string", description: "Collection name", required: true },
211212
];
212213

214+
export const dbOperationInvalidArgTests = [{}, { database: 123 }, { foo: "bar", database: "test" }, { database: [] }];
215+
213216
export async function validateToolMetadata(
214217
mcpClient: Client,
215218
name: string,
@@ -264,3 +267,24 @@ export function validateAutoConnectBehavior(
264267
expect(content).toContain("You need to connect to a MongoDB instance before you can access its data.");
265268
});
266269
}
270+
271+
export function validateThrowsForInvalidArguments(
272+
integration: IntegrationTestSetup,
273+
name: string,
274+
args: { [x: string]: unknown }[]
275+
): void {
276+
for (const arg of args) {
277+
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
278+
await integration.connectMcpClient();
279+
try {
280+
await integration.mcpClient().callTool({ name, arguments: arg });
281+
expect.fail("Expected an error to be thrown");
282+
} catch (error) {
283+
expect(error).toBeInstanceOf(McpError);
284+
const mcpError = error as McpError;
285+
expect(mcpError.code).toEqual(-32602);
286+
expect(mcpError.message).toContain(`Invalid arguments for tool ${name}`);
287+
}
288+
});
289+
}
290+
}

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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
8+
dbOperationInvalidArgTests,
79
} from "../../../helpers.js";
810
import { toIncludeSameMembers } from "jest-extended";
911
import { McpError } from "@modelcontextprotocol/sdk/types.js";
@@ -21,26 +23,7 @@ describe("createCollection tool", () => {
2123
});
2224

2325
describe("with invalid arguments", () => {
24-
const args = [
25-
{},
26-
{ database: 123, collection: "bar" },
27-
{ foo: "bar", database: "test", collection: "bar" },
28-
{ collection: [], database: "test" },
29-
];
30-
for (const arg of args) {
31-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
32-
await integration.connectMcpClient();
33-
try {
34-
await integration.mcpClient().callTool({ name: "create-collection", arguments: arg });
35-
expect.fail("Expected an error to be thrown");
36-
} catch (error) {
37-
expect(error).toBeInstanceOf(McpError);
38-
const mcpError = error as McpError;
39-
expect(mcpError.code).toEqual(-32602);
40-
expect(mcpError.message).toContain("Invalid arguments for tool create-collection");
41-
}
42-
});
43-
}
26+
validateThrowsForInvalidArguments(integration, "create-collection", dbOperationInvalidArgTests);
4427
});
4528

4629
describe("with non-existent database", () => {

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

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
78
} from "../../../helpers.js";
89
import { McpError } from "@modelcontextprotocol/sdk/types.js";
910
import { IndexDirection } from "mongodb";
@@ -31,28 +32,14 @@ describe("createIndex tool", () => {
3132
});
3233

3334
describe("with invalid arguments", () => {
34-
const args = [
35+
validateThrowsForInvalidArguments(integration, "create-index", [
3536
{},
3637
{ collection: "bar", database: 123, keys: { foo: 1 } },
3738
{ collection: "bar", database: "test", keys: { foo: 5 } },
3839
{ collection: [], database: "test", keys: { foo: 1 } },
3940
{ collection: "bar", database: "test", keys: { foo: 1 }, name: 123 },
4041
{ collection: "bar", database: "test", keys: "foo", name: "my-index" },
41-
];
42-
for (const arg of args) {
43-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
44-
await integration.connectMcpClient();
45-
try {
46-
await integration.mcpClient().callTool({ name: "create-index", arguments: arg });
47-
expect.fail("Expected an error to be thrown");
48-
} catch (error) {
49-
expect(error).toBeInstanceOf(McpError);
50-
const mcpError = error as McpError;
51-
expect(mcpError.code).toEqual(-32602);
52-
expect(mcpError.message).toContain("Invalid arguments for tool create-index");
53-
}
54-
});
55-
}
42+
]);
5643
});
5744

5845
const validateIndex = async (collection: string, expected: { name: string; key: object }[]) => {

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

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
78
} from "../../../helpers.js";
89
import { McpError } from "@modelcontextprotocol/sdk/types.js";
910
import config from "../../../../../src/config.js";
@@ -30,27 +31,13 @@ describe("insertMany tool", () => {
3031
});
3132

3233
describe("with invalid arguments", () => {
33-
const args = [
34+
validateThrowsForInvalidArguments(integration, "insert-many", [
3435
{},
3536
{ collection: "bar", database: 123, documents: [] },
3637
{ collection: [], database: "test", documents: [] },
3738
{ collection: "bar", database: "test", documents: "my-document" },
3839
{ collection: "bar", database: "test", documents: { name: "Peter" } },
39-
];
40-
for (const arg of args) {
41-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
42-
await integration.connectMcpClient();
43-
try {
44-
await integration.mcpClient().callTool({ name: "insert-many", arguments: arg });
45-
expect.fail("Expected an error to be thrown");
46-
} catch (error) {
47-
expect(error).toBeInstanceOf(McpError);
48-
const mcpError = error as McpError;
49-
expect(mcpError.code).toEqual(-32602);
50-
expect(mcpError.message).toContain("Invalid arguments for tool insert-many");
51-
}
52-
});
53-
}
40+
]);
5441
});
5542

5643
const validateDocuments = async (collection: string, expectedDocuments: object[]) => {

tests/integration/tools/mongodb/delete/deleteMany.test.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
78
} from "../../../helpers.js";
8-
import { McpError } from "@modelcontextprotocol/sdk/types.js";
9-
import config from "../../../../../src/config.js";
109

1110
describe("deleteMany tool", () => {
1211
const integration = setupIntegrationTest();
@@ -30,27 +29,13 @@ describe("deleteMany tool", () => {
3029
});
3130

3231
describe("with invalid arguments", () => {
33-
const args = [
32+
validateThrowsForInvalidArguments(integration, "delete-many", [
3433
{},
3534
{ collection: "bar", database: 123, filter: {} },
3635
{ collection: [], database: "test", filter: {} },
3736
{ collection: "bar", database: "test", filter: "my-document" },
3837
{ collection: "bar", database: "test", filter: [{ name: "Peter" }] },
39-
];
40-
for (const arg of args) {
41-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
42-
await integration.connectMcpClient();
43-
try {
44-
await integration.mcpClient().callTool({ name: "delete-many", arguments: arg });
45-
expect.fail("Expected an error to be thrown");
46-
} catch (error) {
47-
expect(error).toBeInstanceOf(McpError);
48-
const mcpError = error as McpError;
49-
expect(mcpError.code).toEqual(-32602);
50-
expect(mcpError.message).toContain("Invalid arguments for tool delete-many");
51-
}
52-
});
53-
}
38+
]);
5439
});
5540

5641
it("doesn't create the collection if it doesn't exist", async () => {

tests/integration/tools/mongodb/delete/dropCollection.test.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
8+
dbOperationInvalidArgTests,
79
} from "../../../helpers.js";
8-
import { McpError } from "@modelcontextprotocol/sdk/types.js";
9-
import config from "../../../../../src/config.js";
1010

1111
describe("dropCollection tool", () => {
1212
const integration = setupIntegrationTest();
@@ -21,26 +21,7 @@ describe("dropCollection tool", () => {
2121
});
2222

2323
describe("with invalid arguments", () => {
24-
const args = [
25-
{},
26-
{ database: 123, collection: "bar" },
27-
{ foo: "bar", database: "test", collection: "bar" },
28-
{ collection: [], database: "test" },
29-
];
30-
for (const arg of args) {
31-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
32-
await integration.connectMcpClient();
33-
try {
34-
await integration.mcpClient().callTool({ name: "drop-collection", arguments: arg });
35-
expect.fail("Expected an error to be thrown");
36-
} catch (error) {
37-
expect(error).toBeInstanceOf(McpError);
38-
const mcpError = error as McpError;
39-
expect(mcpError.code).toEqual(-32602);
40-
expect(mcpError.message).toContain("Invalid arguments for tool drop-collection");
41-
}
42-
});
43-
}
24+
validateThrowsForInvalidArguments(integration, "drop-collection", dbOperationInvalidArgTests);
4425
});
4526

4627
it("can drop non-existing collection", async () => {

tests/integration/tools/mongodb/delete/dropDatabase.test.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
setupIntegrationTest,
55
validateToolMetadata,
66
validateAutoConnectBehavior,
7+
validateThrowsForInvalidArguments,
8+
dbOperationInvalidArgTests,
79
} from "../../../helpers.js";
8-
import { McpError } from "@modelcontextprotocol/sdk/types.js";
9-
import config from "../../../../../src/config.js";
1010

1111
describe("dropDatabase tool", () => {
1212
const integration = setupIntegrationTest();
@@ -21,21 +21,7 @@ describe("dropDatabase tool", () => {
2121
});
2222

2323
describe("with invalid arguments", () => {
24-
const args = [{}, { database: 123 }, { foo: "bar", database: "test" }];
25-
for (const arg of args) {
26-
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
27-
await integration.connectMcpClient();
28-
try {
29-
await integration.mcpClient().callTool({ name: "drop-database", arguments: arg });
30-
expect.fail("Expected an error to be thrown");
31-
} catch (error) {
32-
expect(error).toBeInstanceOf(McpError);
33-
const mcpError = error as McpError;
34-
expect(mcpError.code).toEqual(-32602);
35-
expect(mcpError.message).toContain("Invalid arguments for tool drop-database");
36-
}
37-
});
38-
}
24+
validateThrowsForInvalidArguments(integration, "drop-database", dbOperationInvalidArgTests);
3925
});
4026

4127
it("can drop non-existing database", async () => {

0 commit comments

Comments
 (0)