Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"dist"
],
"scripts": {
"start": "node dist/index.js --transport http --loggers stderr mcp --previewFeatures vectorSearch",
"start": "node dist/index.js --transport http --loggers stderr mcp --previewFeatures search",
"start:stdio": "node dist/index.js --transport stdio --loggers stderr mcp",
"prepare": "husky && pnpm run build",
"build:clean": "rm -rf dist",
Expand Down
17 changes: 15 additions & 2 deletions src/tools/mongodb/create/insertMany.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
import type { ToolResult } from "../../tool.js";
import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js";
import { zEJSON } from "../../args.js";
import { type Document } from "bson";
Expand Down Expand Up @@ -37,14 +37,21 @@ export class InsertManyTool extends MongoDBToolBase {
),
}
: commonArgs;

protected outputShape = {
success: z.boolean(),
insertedCount: z.number(),
insertedIds: z.array(z.any()),
};

static operationType: OperationType = "create";

protected async execute({
database,
collection,
documents,
embeddingParameters: providedEmbeddingParameters,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
}: ToolArgs<typeof this.argsShape>): Promise<ToolResult<typeof this.outputShape>> {
const provider = await this.ensureConnected();

const embeddingParameters = this.isFeatureEnabled("search")
Expand All @@ -70,8 +77,14 @@ export class InsertManyTool extends MongoDBToolBase {
`Inserted \`${result.insertedCount}\` document(s) into ${database}.${collection}.`,
`Inserted IDs: ${Object.values(result.insertedIds).join(", ")}`
);

return {
content,
structuredContent: {
success: true,
insertedCount: result.insertedCount,
insertedIds: Object.values(result.insertedIds),
},
};
}

Expand Down
24 changes: 18 additions & 6 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { z } from "zod";
import { type ZodRawShape, type ZodNever } from "zod";
import type { z, ZodTypeAny } from "zod";
import { type ZodRawShape } from "zod";
import type { RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
import type { Session } from "../common/session.js";
Expand All @@ -10,10 +10,17 @@
import type { Server } from "../server.js";
import type { Elicitation } from "../elicitation.js";
import type { PreviewFeature } from "../common/schemas.js";
import { type ZodNever } from "zod";

export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
export type ToolCallbackArgs<Args extends ZodRawShape> = Parameters<ToolCallback<Args>>;

export type ToolResult<OutputSchema extends ZodRawShape | undefined = undefined> = {
content: { type: "text"; text: string }[];
structuredContent: OutputSchema extends ZodRawShape ? z.objectOutputType<OutputSchema, ZodTypeAny> : never;
isError?: boolean;
};

export type ToolExecutionContext<Args extends ZodRawShape = ZodRawShape> = Parameters<ToolCallback<Args>>[1];

/**
Expand Down Expand Up @@ -274,6 +281,8 @@
*/
protected abstract argsShape: ZodRawShape;

protected outputShape?: ZodRawShape;

private registeredTool: RegisteredTool | undefined;

protected get annotations(): ToolAnnotations {
Expand Down Expand Up @@ -462,11 +471,14 @@
}
};

this.registeredTool = server.mcpServer.tool(
this.registeredTool = server.mcpServer.registerTool(

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should include custom telemetry metadata

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should return empty metadata by default

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should include custom telemetry metadata

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should return empty metadata by default

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should include custom telemetry metadata

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22

Check failure on line 474 in src/tools/tool.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/unit/toolBase.test.ts > ToolBase > resolveTelemetryMetadata > should return empty metadata by default

TypeError: server.mcpServer.registerTool is not a function ❯ TestTool.register src/tools/tool.ts:474:48 ❯ tests/unit/toolBase.test.ts:157:22
this.name,
this.description,
this.argsShape,
this.annotations,
{
description: this.description,
inputSchema: this.argsShape,
annotations: this.annotations,
outputSchema: this.outputShape,
},
callback
);

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function setupIntegrationTest(
}

// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export function getResponseContent(content: unknown | { content: unknown }): string {
export function getResponseContent(content: unknown | { content: unknown; structuredContent: unknown }): string {
return getResponseElements(content)
.map((item) => item.text)
.join("\n");
Expand Down
45 changes: 32 additions & 13 deletions tests/integration/tools/mongodb/create/insertMany.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ describeWithMongoDB("insertMany tool when search is disabled", (integration) =>
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain(`Inserted \`1\` document(s) into ${integration.randomDbName()}.coll1.`);

await validateDocuments("coll1", [{ prop1: "value1" }]);
validateStructuredContent(response.structuredContent, extractInsertedIds(content));
});

it("returns an error when inserting duplicates", async () => {
Expand All @@ -95,7 +96,7 @@ describeWithMongoDB("insertMany tool when search is disabled", (integration) =>
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Error running insert-many");
expect(content).toContain("duplicate key error");
expect(content).toContain(insertedIds[0]?.toString());
Expand Down Expand Up @@ -174,12 +175,14 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);

const docCount = await collection.countDocuments({ _id: insertedIds[0] });
expect(docCount).toBe(1);

validateStructuredContent(response.structuredContent, insertedIds);
});

it("returns an error when there is a search index and embeddings parameter are wrong", async () => {
Expand Down Expand Up @@ -214,7 +217,7 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Error running insert-many");
const untrustedContent = getDataFromUntrustedContent(content);
expect(untrustedContent).toContain(
Expand Down Expand Up @@ -263,10 +266,11 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(doc).toBeDefined();
Expand Down Expand Up @@ -316,10 +320,11 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(2);
validateStructuredContent(response.structuredContent, insertedIds);

const doc1 = await collection.findOne({ _id: insertedIds[0] });
expect(doc1?.title).toBe("The Matrix");
Expand Down Expand Up @@ -369,10 +374,11 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(doc?.info).toBeDefined();
Expand Down Expand Up @@ -417,10 +423,11 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(doc?.title).toBe("The Matrix");
Expand Down Expand Up @@ -452,10 +459,11 @@ describeWithMongoDB(
},
},
});
const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect((doc?.title as Record<string, unknown>)?.text).toBe("The Matrix");
Expand Down Expand Up @@ -495,7 +503,7 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Error running insert-many");
expect(content).toContain("Field 'nonExistentField' does not have a vector search index in collection");
expect(content).toContain("Only fields with vector search indexes can have embeddings generated");
Expand Down Expand Up @@ -529,10 +537,11 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
expect(insertedIds).toHaveLength(1);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(doc?.title).toBe("The Matrix");
Expand Down Expand Up @@ -564,9 +573,10 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(Array.isArray(doc?.titleEmbeddings)).toBe(true);
Expand Down Expand Up @@ -614,9 +624,10 @@ describeWithMongoDB(
},
});

const content = getResponseContent(response.content);
const content = getResponseContent(response);
expect(content).toContain("Documents were inserted successfully.");
const insertedIds = extractInsertedIds(content);
validateStructuredContent(response.structuredContent, insertedIds);

const doc = await collection.findOne({ _id: insertedIds[0] });
expect(doc?.title).toBe("The Matrix");
Expand Down Expand Up @@ -692,3 +703,11 @@ function extractInsertedIds(content: string): ObjectId[] {
.map((e) => ObjectId.createFromHexString(e)) ?? []
);
}

function validateStructuredContent(structuredContent: unknown, expectedIds: ObjectId[]): void {
expect(structuredContent).toEqual({
success: true,
insertedCount: expectedIds.length,
insertedIds: expectedIds,
});
}
Loading