Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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: 2 additions & 1 deletion src/tools/atlas/listClusters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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>;
Expand All @@ -29,7 +30,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
23 changes: 23 additions & 0 deletions src/tools/mongodb/collectionIndexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";

export class CollectionIndexesTool extends MongoDBToolBase<typeof DbOperationArgs> {
protected name = "collection-indexes";
protected description = "Describe the indexes for a collection";
protected argsShape = DbOperationArgs;

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",
};
}),
};
}
}
38 changes: 38 additions & 0 deletions src/tools/mongodb/collectionSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";
import { parseSchema, SchemaField } from "mongodb-schema";

export class CollectionSchemaTool extends MongoDBToolBase<typeof DbOperationArgs> {
protected name = "collection-schema";
protected description = "Describe the schema for a collection";
protected argsShape = DbOperationArgs;

protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const documents = await provider.find(database, collection, {}, { limit: 5 }).toArray();
const schema = await parseSchema(documents);

return {
content: [
{
text: `Found ${schema.fields.length} fields in the schema for \`${database}.${collection}\``,
type: "text",
},
{
text: this.formatFieldOutput(schema.fields),
type: "text",
},
],
};
}

private formatFieldOutput(fields: SchemaField[]): string {
let result = "| Field | Type | Confidence |\n";
result += "|-------|------|-------------|\n";
for (const field of fields) {
result += `| ${field.name} | \`${field.type}\` | ${(field.probability * 100).toFixed(0)}% |\n`;
}
return result;
}
}
73 changes: 73 additions & 0 deletions src/tools/mongodb/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { z } from "zod";
import { CallToolResult, McpError } from "@modelcontextprotocol/sdk/types.js";
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool";
import { ErrorCodes } from "../../errors.js";

const argsShape = {
connectionStringOrClusterName: z
.string()
.optional()
.describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name"),
};

export class ConnectTool extends MongoDBToolBase<typeof argsShape> {
protected name = "connect";
protected description = "Connect to a MongoDB instance";
protected argsShape = argsShape;

protected async execute({ connectionStringOrClusterName }: ToolArgs<typeof 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;
}
}
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, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";
import { IndexDirection } from "mongodb";

const argsShape = {
...DbOperationArgs,
keys: z.record(z.string(), z.custom<IndexDirection>()).describe("The index definition"),
};

export class CreateIndexTool extends MongoDBToolBase<typeof argsShape> {
protected name = "create-index";
protected description = "Create an index for a collection";
protected argsShape = argsShape;

protected async execute({ database, collection, keys }: ToolArgs<typeof 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",
},
],
};
}
}
54 changes: 54 additions & 0 deletions src/tools/mongodb/find/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } from "../../tool.js";

const argsShape = {
collection: z.string().describe("Collection name"),
database: z.string().describe("Database name"),
filter: z
.object({})
.passthrough()
.optional()
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
projection: z
.object({})
.passthrough()
.optional()
.describe("The projection, matching the syntax of the projection argument of db.collection.find()"),
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
};

export class FindTool extends MongoDBToolBase<typeof argsShape> {
protected name = "find";
protected description = "Run a find query against a MongoDB collection";
protected argsShape = argsShape;

protected async execute({
database,
collection,
filter,
projection,
limit,
}: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const documents = await provider.find(database, collection, filter, { projection, limit }).toArray();

const content: Array<{ text: string; type: "text" }> = [
{
text: `Found ${documents.length} documents in the collection \`${collection}\`:`,
type: "text",
},
...documents.map((doc) => {
return {
text: JSON.stringify(doc),
type: "text",
} as { text: string; type: "text" };
}),
];

return {
content,
};
}
}
30 changes: 28 additions & 2 deletions src/tools/mongodb/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { State } from "../../state.js";
import { ConnectTool } from "./connect.js";
import { ListCollectionsTool } from "./listCollections.js";
import { CollectionIndexesTool } from "./collectionIndexes.js";
import { ListDatabasesTool } from "./listDatabases.js";
import { MongoDBToolState } from "./mongodbTool.js";
import { CreateIndexTool } from "./createIndex.js";
import { CollectionSchemaTool } from "./collectionSchema.js";
import { InsertOneTool } from "./insert/insertOne.js";
import { FindTool } from "./find/find.js";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function registerMongoDBTools(server: McpServer, state: State) {}
export function registerMongoDBTools(server: McpServer, state: State) {
const mongodbToolState: MongoDBToolState = {};

const tools = [
ConnectTool,
ListCollectionsTool,
ListDatabasesTool,
CollectionIndexesTool,
CreateIndexTool,
CollectionSchemaTool,
InsertOneTool,
FindTool,
];

for (const tool of tools) {
const instance = new tool(state, mongodbToolState);
instance.register(server);
}
}
33 changes: 33 additions & 0 deletions src/tools/mongodb/insert/insertOne.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { MongoDBToolBase } from "../mongodbTool.js";
import { ToolArgs } from "../../tool.js";

const argsShape = {
collection: z.string().describe("Collection name"),
database: z.string().describe("Database name"),
document: z
.object({})
.passthrough()
.describe("The document to insert, matching the syntax of the document argument of db.collection.insertOne()"),
};

export class InsertOneTool extends MongoDBToolBase<typeof argsShape> {
protected name = "insert-one";
protected description = "Insert a document into a MongoDB collection";
protected argsShape = argsShape;

protected async execute({ database, collection, document }: ToolArgs<typeof 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",
},
],
};
}
}
27 changes: 27 additions & 0 deletions src/tools/mongodb/listCollections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { DbOperationArgs, MongoDBToolBase } from "./mongodbTool.js";
import { ToolArgs } from "../tool.js";

const argsShape = {
database: DbOperationArgs.database,
};

export class ListCollectionsTool extends MongoDBToolBase<typeof argsShape> {
protected name = "list-collections";
protected description = "List all collections for a given database";
protected argsShape = argsShape;

protected async execute({ database }: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
const provider = this.ensureConnected();
const collections = await provider.listCollections(database);

return {
content: collections.map((collection) => {
return {
text: `Name: ${collection.name}`,
type: "text",
};
}),
};
}
}
Loading