MongoDB MCP Server

Official
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { MongoClient, Db, Collection, Document, AggregateOptions } from "mongodb"; //import dotenv from "dotenv"; import * as dotenv from 'dotenv'; dotenv.config(); const MONGODB_URI = process.env.MONGODB_URI; if (!MONGODB_URI) { throw new Error("MONGODB_URI environment variable is required"); } interface AggregateToolArguments { collection: string; pipeline: Document[]; options?: AggregateOptions & { allowDiskUse?: boolean; maxTimeMS?: number; comment?: string; }; } interface ExplainToolArguments { collection: string; pipeline: Document[]; verbosity?: "queryPlanner" | "executionStats" | "allPlansExecution"; } interface SampleDocumentsArguments { collection: string; count?: number; } class MongoDBServer { private server: Server; private client!: MongoClient; private db!: Db; constructor() { this.server = new Server( { name: "example-servers/mongodb", version: "0.1.0", description: "MongoDB MCP server providing secure access to MongoDB databases", }, { capabilities: { resources: { description: "MongoDB collections and their schemas", mimeTypes: ["application/json"], }, tools: { description: "MongoDB aggregation and analysis tools", }, }, } ); this.setupHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; // Handle both SIGINT (Ctrl+C) and SIGTERM (process termination) const cleanup = async () => { console.log("Shutting down MongoDB MCP server..."); try { await this.close(); process.exit(0); } catch (error) { console.error("Error during cleanup:", error); process.exit(1); } }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); // Handle uncaught exceptions and unhandled rejections process.on('uncaughtException', async (error) => { console.error('Uncaught Exception:', error); await cleanup(); }); process.on('unhandledRejection', async (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); await cleanup(); }); } private setupHandlers(): void { this.setupResourceHandlers(); this.setupToolHandlers(); } private setupResourceHandlers(): void { this.server.setRequestHandler(ListResourcesRequestSchema, async () => { const collections = await this.db.listCollections().toArray(); return { resources: collections.map((collection: Document) => ({ uri: `mcp-mongodb://${collection.name}/schema`, mimeType: "application/json", name: `"${collection.name}" collection schema`, description: `Schema information for the ${collection.name} collection`, })), }; }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const match = uri.match(/^mcp-mongodb:\/\/([^/]+)\/schema$/); if (!match) { throw new McpError( ErrorCode.InvalidRequest, "Invalid resource URI" ); } const collectionName = match[1]; try { const sampleDoc = await this.db.collection(collectionName).findOne(); if (!sampleDoc) { return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify({ message: "Collection is empty" }, null, 2), }, ], }; } const documentSchema = Object.entries(sampleDoc).map(([key, value]) => ({ field_name: key, field_type: typeof value, description: `Field ${key} of type ${typeof value}`, })); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(documentSchema, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `MongoDB error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "aggregate", description: "Run a MongoDB aggregation pipeline", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to query", }, pipeline: { type: "array", items: { type: "object" }, description: "MongoDB aggregation pipeline stages (e.g., $match, $group, $sort)", }, options: { type: "object", description: "Optional aggregation options", properties: { allowDiskUse: { type: "boolean", description: "Allow writing to temporary files", }, maxTimeMS: { type: "number", description: "Maximum execution time in milliseconds", }, comment: { type: "string", description: "Optional comment to help trace operations", } } } }, required: ["collection", "pipeline"], }, examples: [ { name: "Count documents by status", arguments: { collection: "orders", pipeline: [ { $group: { _id: "$status", count: { $sum: 1 } } }, { $sort: { count: -1 } } ] } } ] }, { name: "explain", description: "Get the execution plan for an aggregation pipeline", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to analyze", }, pipeline: { type: "array", items: { type: "object" }, description: "MongoDB aggregation pipeline stages to analyze", }, verbosity: { type: "string", enum: ["queryPlanner", "executionStats", "allPlansExecution"], default: "queryPlanner", description: "Level of detail in the execution plan", } }, required: ["collection", "pipeline"], }, examples: [ { name: "Analyze index usage", arguments: { collection: "users", pipeline: [ { $match: { status: "active" } }, { $sort: { lastLogin: -1 } } ], verbosity: "executionStats" } } ] }, { name: "sample", description: "Get random sample documents from a collection", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to sample from", }, count: { type: "number", description: "Number of documents to sample (default: 5, max: 10)", minimum: 1, maximum: 10, default: 5, } }, required: ["collection"], }, examples: [ { name: "Get 5 random documents", arguments: { collection: "listings", count: 5 } } ] } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "aggregate": { if (!this.isAggregateToolArguments(request.params.arguments)) { return { content: [{ type: "text", text: "Invalid arguments: expected collection and pipeline parameters" }], isError: true, }; } const { collection, pipeline, options = {} } = request.params.arguments; try { const hasLimit = pipeline.some(stage => "$limit" in stage); const safePipeline = hasLimit ? pipeline : [...pipeline, { $limit: 1000 }]; const result = await this.db .collection(collection) .aggregate(safePipeline, { ...options, maxTimeMS: options.maxTimeMS || 30000 }) .toArray(); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: `Aggregation error: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true, }; } } case "explain": { if (!this.isExplainToolArguments(request.params.arguments)) { return { content: [{ type: "text", text: "Invalid arguments: expected collection and pipeline parameters" }], isError: true, }; } const { collection, pipeline } = request.params.arguments; try { const result = await this.db .collection(collection) .aggregate(pipeline, { explain: true }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: `Explain error: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true, }; } } case "sample": { if (!this.isSampleDocumentsArguments(request.params.arguments)) { return { content: [{ type: "text", text: "Invalid arguments: expected collection name" }], isError: true, }; } const { collection, count = 5 } = request.params.arguments; const safeCount = Math.min(Math.max(1, count), 10); try { const result = await this.db .collection(collection) .aggregate([ { $sample: { size: safeCount } } ]) .toArray(); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } catch (error) { return { content: [{ type: "text", text: `Sample error: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true, }; } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } private isAggregateToolArguments(value: unknown): value is AggregateToolArguments { if (!value || typeof value !== 'object') return false; const obj = value as Record<string, unknown>; return ( typeof obj.collection === 'string' && Array.isArray(obj.pipeline) && (!obj.options || typeof obj.options === 'object') ); } private isExplainToolArguments(value: unknown): value is ExplainToolArguments { if (!value || typeof value !== 'object') return false; const obj = value as Record<string, unknown>; return ( typeof obj.collection === 'string' && Array.isArray(obj.pipeline) && (!obj.verbosity || ["queryPlanner", "executionStats", "allPlansExecution"].includes(obj.verbosity as string)) ); } private isSampleDocumentsArguments(value: unknown): value is SampleDocumentsArguments { if (!value || typeof value !== 'object') return false; const obj = value as Record<string, unknown>; return ( typeof obj.collection === 'string' && (!obj.count || (typeof obj.count === 'number' && obj.count > 0 && obj.count <= 10)) ); } async connect(): Promise<void> { try { this.client = new MongoClient(MONGODB_URI!); await this.client.connect(); this.db = this.client.db(); } catch (error) { console.error("Failed to connect to MongoDB:", error); throw error; } } async close(): Promise<void> { if (this.client) { await this.client.close(); } } async run(): Promise<void> { await this.connect(); const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new MongoDBServer(); server.run().catch((error) => { console.error(error); server.close().catch(console.error); process.exit(1); });