Skip to content

feat: add data access tools #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4,799 changes: 4,120 additions & 679 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"publishConfig": {
"access": "public"
},
"type": "module",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to add this since I was seeing complaints that node was first trying to parse it as cjs, but was seeing an esm. I don't have a strong opinion on whether we should go with one or the other.

"scripts": {
"prepare": "npm run build",
"build:clean": "rm -rf dist",
Expand All @@ -40,11 +41,16 @@
"globals": "^16.0.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.1",
"zod": "^3.24.2"
"typescript-eslint": "^8.29.1"
},
"dependencies": {
"@types/express": "^5.0.1"
"@mongodb-js/devtools-connect": "^3.7.2",
"@mongosh/service-provider-node-driver": "^3.6.0",
"@types/express": "^5.0.1",
"bson": "^6.10.3",
"mongodb": "^6.15.0",
"mongodb-schema": "^12.6.2",
"zod": "^3.24.2"
},
"engines": {
"node": ">=23.0.0"
Expand Down
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";

const packageMetadata = fs.readFileSync(path.resolve("./package.json"), "utf8");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
Comment on lines -4 to +8
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may have regressed at some point, but path.resolve("./package.json") is looking at dist/config.js rather than in the parent directory. For some reason, Claude is not happy about relative paths, so "../package.json" didn't work either, so I needed the __dirname shenanigans above, since esm doesn't have __dirname built in.

const packageJson = JSON.parse(packageMetadata);

export const config = {
Expand Down
4 changes: 4 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ErrorCodes {
NotConnectedToMongoDB = 1_000_000,
InvalidParams = 1_000_001,
}
3 changes: 1 addition & 2 deletions src/tools/atlas/atlasTool.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ZodRawShape } from "zod";
import { ToolBase } from "../tool.js";
import { ApiClient } from "../../client.js";
import { State } from "../../state.js";

export abstract class AtlasToolBase<Args extends ZodRawShape = ZodRawShape> extends ToolBase<Args> {
export abstract class AtlasToolBase extends ToolBase {
constructor(
state: State,
protected apiClient: ApiClient
Expand Down
7 changes: 3 additions & 4 deletions src/tools/atlas/listClusters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { ensureAuthenticated } from "./auth.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "./atlasTool.js";
import { State } from "../../state.js";
import { ToolArgs } from "../tool.js";

export class ListClustersTool extends AtlasToolBase<{
projectId: ZodString | ZodOptional<ZodString>;
}> {
export class ListClustersTool extends AtlasToolBase {
protected name = "listClusters";
protected description = "List MongoDB Atlas clusters";
protected argsShape;
Expand All @@ -29,7 +28,7 @@ export class ListClustersTool extends AtlasToolBase<{
};
}

protected async execute({ projectId }: { projectId: string }): Promise<CallToolResult> {
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
await ensureAuthenticated(this.state, this.apiClient);

let clusters: AtlasCluster[] | undefined = undefined;
Expand Down
3 changes: 1 addition & 2 deletions src/tools/atlas/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ZodRawShape } from "zod";
import { ToolBase } from "../tool.js";
import { ApiClient } from "../../client.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
Expand All @@ -8,7 +7,7 @@ import { ListClustersTool } from "./listClusters.js";
import { ListProjectsTool } from "./listProjects.js";

export function registerAtlasTools(server: McpServer, state: State, apiClient: ApiClient) {
const tools: ToolBase<ZodRawShape>[] = [
const tools: ToolBase[] = [
new AuthTool(state, apiClient),
new ListClustersTool(state, apiClient),
new ListProjectsTool(state, apiClient),
Expand Down
24 changes: 24 additions & 0 deletions src/tools/mongodb/collectionIndexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";

export class CollectionIndexesTool extends MongoDBToolBase {
protected name = "collection-indexes";
protected description = "Describe the indexes for a collection";
protected argsShape = DbOperationArgs;
protected operationType: DbOperationType = "read";

protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const indexes = await provider.getIndexes(database, collection);

return {
content: indexes.map((indexDefinition) => {
return {
text: `Field: ${indexDefinition.name}: ${JSON.stringify(indexDefinition.key)}`,
type: "text",
};
}),
};
}
}
75 changes: 75 additions & 0 deletions src/tools/mongodb/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from "zod";
import { CallToolResult, McpError } from "@modelcontextprotocol/sdk/types.js";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool";
import { ErrorCodes } from "../../errors.js";

export class ConnectTool extends MongoDBToolBase {
protected name = "connect";
protected description = "Connect to a MongoDB instance";
protected argsShape = {
connectionStringOrClusterName: z
.string()
.optional()
.describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name"),
};

protected operationType: DbOperationType = "metadata";

protected async execute({
connectionStringOrClusterName,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
if (!connectionStringOrClusterName) {
// TODO: try reconnecting to the default connection
return {
content: [
{ type: "text", text: "No connection details provided." },
{ type: "text", text: "Please provide either a connection string or a cluster name" },
{
type: "text",
text: "Alternatively, you can use the default deployment at mongodb://localhost:27017",
},
],
};
}

let connectionString: string;

if (typeof connectionStringOrClusterName === "string") {
if (
connectionStringOrClusterName.startsWith("mongodb://") ||
connectionStringOrClusterName.startsWith("mongodb+srv://")
) {
connectionString = connectionStringOrClusterName;
} else {
// TODO:
return {
content: [
{
type: "text",
text: `Connecting via cluster name not supported yet. Please provide a connection string.`,
},
],
};
}
} else {
throw new McpError(ErrorCodes.InvalidParams, "Invalid connection options");
}

await this.connect(connectionString);

return {
content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }],
};
}

private async connect(connectionString: string): Promise<void> {
const provider = await NodeDriverServiceProvider.connect(connectionString, {
productDocsLink: "https://docs.mongodb.com/todo-mcp",
Copy link
Preview

Copilot AI Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The productDocsLink value appears to be a placeholder. It is recommended to update this URL to point to the correct documentation for MongoDB MCP.

Suggested change
productDocsLink: "https://docs.mongodb.com/todo-mcp",
productDocsLink: "https://docs.mongodb.com/manual/reference/mongodb-mcp/",

Copilot uses AI. Check for mistakes.

productName: "MongoDB MCP",
});

this.mongodbState.serviceProvider = provider;
}
}
40 changes: 40 additions & 0 deletions src/tools/mongodb/create/insertMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } from "../../tool.js";

export class InsertManyTool extends MongoDBToolBase {
protected name = "insert-many";
protected description = "Insert an array of documents into a MongoDB collection";
protected argsShape = {
...DbOperationArgs,
documents: z
.array(z.object({}).passthrough().describe("An individual MongoDB document"))
.describe(
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()"
),
};
protected operationType: DbOperationType = "create";

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

return {
content: [
{
text: `Inserted \`${result.insertedCount}\` documents into collection \`${collection}\``,
type: "text",
},
{
text: `Inserted IDs: ${Object.values(result.insertedIds).join(", ")}`,
type: "text",
},
],
};
}
}
38 changes: 38 additions & 0 deletions src/tools/mongodb/create/insertOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } 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: DbOperationType = "create";

protected async execute({
database,
collection,
document,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const result = await provider.insertOne(database, collection, document);

return {
content: [
{
text: `Inserted document with ID \`${result.insertedId}\` into collection \`${collection}\``,
type: "text",
},
],
};
}
}
34 changes: 34 additions & 0 deletions src/tools/mongodb/createIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";
import { IndexDirection } from "mongodb";

export class CreateIndexTool extends MongoDBToolBase {
protected name = "create-index";
protected description = "Create an index for a collection";
protected argsShape = {
...DbOperationArgs,
keys: z.record(z.string(), z.custom<IndexDirection>()).describe("The index definition"),
};

protected operationType: DbOperationType = "create";

protected async execute({ database, collection, keys }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const indexes = await provider.createIndexes(database, collection, [
{
key: keys,
},
]);

return {
content: [
{
text: `Created the index \`${indexes[0]}\``,
type: "text",
},
],
};
}
}
38 changes: 38 additions & 0 deletions src/tools/mongodb/delete/deleteMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } from "../../tool.js";

export class DeleteManyTool extends MongoDBToolBase {
protected name = "delete-many";
protected description = "Removes all documents that match the filter from a MongoDB collection";
protected argsShape = {
...DbOperationArgs,
filter: z
.object({})
.passthrough()
.optional()
.describe(
"The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()"
),
};
protected operationType: DbOperationType = "delete";

protected async execute({
database,
collection,
filter,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const result = await provider.deleteMany(database, collection, filter);

return {
content: [
{
text: `Deleted \`${result.deletedCount}\` documents from collection \`${collection}\``,
type: "text",
},
],
};
}
}
38 changes: 38 additions & 0 deletions src/tools/mongodb/delete/deleteOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } from "../../tool.js";

export class DeleteOneTool extends MongoDBToolBase {
protected name = "delete-one";
protected description = "Removes a single document that match the filter from a MongoDB collection";
protected argsShape = {
...DbOperationArgs,
filter: z
.object({})
.passthrough()
.optional()
.describe(
"The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()"
),
};
protected operationType: DbOperationType = "delete";

protected async execute({
database,
collection,
filter,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const result = await provider.deleteOne(database, collection, filter);

return {
content: [
{
text: `Deleted \`${result.deletedCount}\` documents from collection \`${collection}\``,
type: "text",
},
],
};
}
}
Loading