diff --git a/src/tools/mongodb/create/insertMany.ts b/src/tools/mongodb/create/insertMany.ts index a09de18d..eb624275 100644 --- a/src/tools/mongodb/create/insertMany.ts +++ b/src/tools/mongodb/create/insertMany.ts @@ -27,7 +27,7 @@ export class InsertManyTool extends MongoDBToolBase { return { content: [ { - text: `Inserted \`${result.insertedCount}\` documents into collection \`${collection}\``, + text: `Inserted \`${result.insertedCount}\` document(s) into collection "${collection}"`, type: "text", }, { diff --git a/src/tools/mongodb/create/insertOne.ts b/src/tools/mongodb/create/insertOne.ts deleted file mode 100644 index b10c891d..00000000 --- a/src/tools/mongodb/create/insertOne.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from "zod"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; -import { ToolArgs, OperationType } from "../../tool.js"; - -export class InsertOneTool extends MongoDBToolBase { - protected name = "insert-one"; - protected description = "Insert a document into a MongoDB collection"; - protected argsShape = { - ...DbOperationArgs, - document: z - .object({}) - .passthrough() - .describe( - "The document to insert, matching the syntax of the document argument of db.collection.insertOne()" - ), - }; - - protected operationType: OperationType = "create"; - - protected async execute({ - database, - collection, - document, - }: ToolArgs): Promise { - const provider = await this.ensureConnected(); - const result = await provider.insertOne(database, collection, document); - - return { - content: [ - { - text: `Inserted document with ID \`${result.insertedId.toString()}\` into collection \`${collection}\``, - type: "text", - }, - ], - }; - } -} diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index ed250832..1c59889a 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -4,7 +4,6 @@ import { CollectionIndexesTool } from "./read/collectionIndexes.js"; import { ListDatabasesTool } from "./metadata/listDatabases.js"; import { CreateIndexTool } from "./create/createIndex.js"; import { CollectionSchemaTool } from "./metadata/collectionSchema.js"; -import { InsertOneTool } from "./create/insertOne.js"; import { FindTool } from "./read/find.js"; import { InsertManyTool } from "./create/insertMany.js"; import { DeleteManyTool } from "./delete/deleteMany.js"; @@ -28,7 +27,6 @@ export const MongoDbTools = [ CollectionIndexesTool, CreateIndexTool, CollectionSchemaTool, - InsertOneTool, FindTool, InsertManyTool, DeleteManyTool, diff --git a/tests/integration/tools/mongodb/create/insertMany.test.ts b/tests/integration/tools/mongodb/create/insertMany.test.ts new file mode 100644 index 00000000..d89e4893 --- /dev/null +++ b/tests/integration/tools/mongodb/create/insertMany.test.ts @@ -0,0 +1,141 @@ +import { + getResponseContent, + validateParameters, + dbOperationParameters, + setupIntegrationTest, +} from "../../../helpers.js"; +import { McpError } from "@modelcontextprotocol/sdk/types.js"; +import config from "../../../../../src/config.js"; + +describe("insertMany tool", () => { + const integration = setupIntegrationTest(); + + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const insertMany = tools.find((tool) => tool.name === "insert-many")!; + expect(insertMany).toBeDefined(); + expect(insertMany.description).toBe("Insert an array of documents into a MongoDB collection"); + + validateParameters(insertMany, [ + ...dbOperationParameters, + { + name: "documents", + type: "array", + description: + "The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()", + required: true, + }, + ]); + }); + + describe("with invalid arguments", () => { + const args = [ + {}, + { collection: "bar", database: 123, documents: [] }, + { collection: [], database: "test", documents: [] }, + { collection: "bar", database: "test", documents: "my-document" }, + { collection: "bar", database: "test", documents: { name: "Peter" } }, + ]; + for (const arg of args) { + it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => { + await integration.connectMcpClient(); + try { + await integration.mcpClient().callTool({ name: "insert-many", arguments: arg }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + const mcpError = error as McpError; + expect(mcpError.code).toEqual(-32602); + expect(mcpError.message).toContain("Invalid arguments for tool insert-many"); + } + }); + } + }); + + const validateDocuments = async (collection: string, expectedDocuments: object[]) => { + const collections = await integration.mongoClient().db(integration.randomDbName()).listCollections().toArray(); + expect(collections.find((c) => c.name === collection)).toBeDefined(); + + const docs = await integration + .mongoClient() + .db(integration.randomDbName()) + .collection(collection) + .find() + .toArray(); + + expect(docs).toHaveLength(expectedDocuments.length); + for (const expectedDocument of expectedDocuments) { + expect(docs).toContainEqual(expect.objectContaining(expectedDocument)); + } + }; + + it("creates the namespace if necessary", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "insert-many", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + documents: [{ prop1: "value1" }], + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain('Inserted `1` document(s) into collection "coll1"'); + + await validateDocuments("coll1", [{ prop1: "value1" }]); + }); + + it("returns an error when inserting duplicates", async () => { + const { insertedIds } = await integration + .mongoClient() + .db(integration.randomDbName()) + .collection("coll1") + .insertMany([{ prop1: "value1" }]); + + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "insert-many", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + documents: [{ prop1: "value1", _id: insertedIds[0] }], + }, + }); + + const content = getResponseContent(response.content); + expect(content).toContain("Error running insert-many"); + expect(content).toContain("duplicate key error"); + expect(content).toContain(insertedIds[0].toString()); + }); + + describe("when not connected", () => { + it("connects automatically if connection string is configured", async () => { + config.connectionString = integration.connectionString(); + + const response = await integration.mcpClient().callTool({ + name: "insert-many", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + documents: [{ prop1: "value1" }], + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain('Inserted `1` document(s) into collection "coll1"'); + }); + + it("throw an error if connection string is not configured", async () => { + const response = await integration.mcpClient().callTool({ + name: "insert-many", + arguments: { + database: integration.randomDbName(), + collection: "coll1", + documents: [{ prop1: "value1" }], + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("You need to connect to a MongoDB instance before you can access its data."); + }); + }); +});