UNPKG

mcp-mongo-server

Version:

A Model Context Protocol server for MongoDB connections

1,522 lines (1,500 loc) 47.4 kB
#!/usr/bin/env node // src/index.ts import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; // src/mongo.ts import { MongoClient, ReadPreference } from "mongodb"; async function connectToMongoDB(url, readOnly) { try { const options = readOnly ? { readPreference: ReadPreference.SECONDARY } : {}; const client = new MongoClient(url, options); await client.connect(); const db = client.db(); console.warn(`Connected to MongoDB database: ${db.databaseName}`); return { client, db, isConnected: true, isReadOnlyMode: readOnly }; } catch (error) { console.error("Failed to connect to MongoDB:", error); return { client: null, db: null, isConnected: false, isReadOnlyMode: readOnly }; } } // src/server.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, CompleteRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, PingRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // src/schemas/call.ts import { ObjectId } from "mongodb"; var COLLECTION_OPERATIONS = [ "query", "aggregate", "update", "insert", "createIndex", "count" ]; var WRITE_OPERATIONS = ["update", "insert", "createIndex"]; async function handleCallToolRequest({ request, client, db, isReadOnlyMode }) { const { name, arguments: args = {} } = request.params; const operation = name; const objectIdMode = args.objectIdMode || "auto"; const filteredArgs = {}; for (const [key, value] of Object.entries(args)) { if (key !== "objectIdMode") { filteredArgs[key] = value; } } if (args.sort) { args.sort = parseSort(args.sort); } Object.assign(args, filteredArgs); validateOperation(operation); checkReadOnlyMode(operation, isReadOnlyMode); let collection = null; if (COLLECTION_OPERATIONS.includes(operation)) { const collectionName = args.collection; if (!collectionName) { throw new Error( `Collection name is required for '${operation}' operation` ); } collection = db.collection(collectionName); validateCollection(collection); } switch (operation) { case "query": return handleQuery(collection, args, objectIdMode); case "aggregate": return handleAggregate(collection, args, objectIdMode); case "update": return handleUpdate(collection, args, objectIdMode); case "serverInfo": return handleServerInfo(db, isReadOnlyMode, args); case "insert": return handleInsert(collection, args, objectIdMode); case "createIndex": return handleCreateIndex(collection, args, objectIdMode); case "count": return handleCount(collection, args, objectIdMode); case "listCollections": return handleListCollections(db, args, objectIdMode); default: throw new Error(`Unknown operation: ${operation}`); } } function validateOperation(operation) { const validOperations = [ "query", "aggregate", "update", "serverInfo", "insert", "createIndex", "count", "listCollections" ]; if (!validOperations.includes(operation)) { throw new Error(`Unknown operation: ${operation}`); } } function validateCollection(collection) { if (!collection.collectionName) { throw new Error("Collection name cannot be empty"); } if (collection.collectionName.startsWith("system.")) { throw new Error("Access to system collections is not allowed"); } } function checkReadOnlyMode(operation, isReadOnlyMode) { if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) { throw new Error( `ReadonlyError: Operation '${operation}' is not allowed in read-only mode` ); } } function parseSort(sort) { if (!sort) return null; if (typeof sort !== "object" || sort === null || Array.isArray(sort)) { return null; } const validSort = {}; for (const [key, value] of Object.entries(sort)) { if (typeof value === "number" && (value === 1 || value === -1)) { validSort[key] = value; } } return Object.keys(validSort).length > 0 ? validSort : null; } function parseFilter(filter, objectIdMode = "auto") { if (!filter) { return {}; } if (typeof filter === "string") { try { return processObjectIdInFilter(JSON.parse(filter), objectIdMode); } catch { throw new Error("Invalid filter format: must be a valid JSON object"); } } if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) { return processObjectIdInFilter( filter, objectIdMode ); } throw new Error("Query filter must be a plain object or ObjectId"); } function isObjectIdField(fieldName) { const lowerFieldName = fieldName.toLowerCase(); return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id"); } function processObjectIdInFilter(filter, objectIdMode = "auto") { if (objectIdMode === "none") { const result2 = {}; for (const [key, value] of Object.entries(filter)) { if (typeof value === "string" && isISODateString(value)) { result2[key] = new Date(value); } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) { const dateString = value.substring(8, value.length - 2); if (isISODateString(dateString)) { result2[key] = new Date(dateString); } else { result2[key] = value; } } else if (typeof value === "object" && value !== null) { if (Array.isArray(value)) { result2[key] = value.map((item) => { if (typeof item === "string" && isISODateString(item)) { return new Date(item); } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) { const dateString = item.substring(8, item.length - 2); return isISODateString(dateString) ? new Date(dateString) : item; } return item; }); } else { result2[key] = processObjectIdInFilter( value, "none" ); } } else { result2[key] = value; } } return result2; } const result = {}; for (const [key, value] of Object.entries(filter)) { if (typeof value === "string" && isObjectIdString(value)) { if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) { result[key] = new ObjectId(value); } else { result[key] = value; } } else if (typeof value === "string" && isISODateString(value)) { result[key] = new Date(value); } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) { const dateString = value.substring(8, value.length - 2); if (isISODateString(dateString)) { result[key] = new Date(dateString); } else { result[key] = value; } } else if (typeof value === "object" && value !== null) { if (Array.isArray(value)) { result[key] = value.map((item) => { if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) { return new ObjectId(item); } else if (typeof item === "string" && isISODateString(item)) { return new Date(item); } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) { const dateString = item.substring(8, item.length - 2); return isISODateString(dateString) ? new Date(dateString) : item; } return item; }); } else { result[key] = processObjectIdInFilter( value, objectIdMode ); } } else { result[key] = value; } } return result; } function isObjectIdString(str) { return /^[0-9a-fA-F]{24}$/.test(str); } function isISODateString(str) { return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(str); } function formatResponse(data) { return { content: [ { type: "text", text: JSON.stringify(data, null, 2) } ] }; } function handleError(error, operation, collectionName) { const context = collectionName ? `collection ${collectionName}` : "operation"; if (error instanceof Error) { throw new Error(`Failed to ${operation} ${context}: ${error.message}`); } throw new Error(`Failed to ${operation} ${context}: Unknown error`); } async function handleQuery(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for query operation"); } const { filter, projection, limit, explain, sort } = args; const queryFilter = parseFilter(filter, objectIdMode); try { if (explain) { const explainResult = await collection.find(queryFilter, { projection, limit: limit || 100, sort }).explain(explain); return formatResponse(explainResult); } const cursor = collection.find(queryFilter, { projection, limit: limit || 100, sort }); const results = await cursor.toArray(); return formatResponse(results); } catch (error) { return handleError(error, "query", collection.collectionName); } } async function handleAggregate(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for aggregate operation"); } const { pipeline, explain } = args; if (!Array.isArray(pipeline)) { throw new Error("Pipeline must be an array"); } const processedPipeline = pipeline.map((stage) => { if (typeof stage === "object" && stage !== null) { return processObjectIdInFilter( stage, objectIdMode ); } return stage; }); try { if (explain) { const explainResult = await collection.aggregate(processedPipeline, { explain: { verbosity: explain } }).toArray(); return formatResponse(explainResult); } const results = await collection.aggregate(processedPipeline).toArray(); return formatResponse(results); } catch (error) { return handleError(error, "aggregate", collection.collectionName); } } async function handleUpdate(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for update operation"); } const { filter, update, upsert, multi } = args; const queryFilter = parseFilter(filter, objectIdMode); let processedUpdate = update; if (update && typeof update === "object" && !Array.isArray(update)) { processedUpdate = processObjectIdInFilter( update, objectIdMode ); } if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) { throw new Error("Update must be a valid MongoDB update document"); } const validUpdateOperators = [ "$set", "$unset", "$inc", "$push", "$pull", "$addToSet", "$pop", "$rename", "$mul" ]; const hasValidOperator = Object.keys(processedUpdate).some( (key) => validUpdateOperators.includes(key) ); if (!hasValidOperator) { throw new Error( "Update must include at least one valid update operator ($set, $unset, etc.)" ); } try { const options = { upsert: !!upsert, multi: !!multi }; const updateMethod = options.multi ? "updateMany" : "updateOne"; const result = await collection[updateMethod]( queryFilter, processedUpdate, options ); return formatResponse({ matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, upsertedCount: result.upsertedCount, upsertedId: result.upsertedId }); } catch (error) { return handleError(error, "update", collection.collectionName); } } async function handleServerInfo(db, isReadOnlyMode, args) { const { includeDebugInfo } = args; try { const buildInfo = await db.command({ buildInfo: 1 }); let serverStatus = null; if (includeDebugInfo) { serverStatus = await db.command({ serverStatus: 1 }); } const serverInfo = { version: buildInfo.version, gitVersion: buildInfo.gitVersion, modules: buildInfo.modules, allocator: buildInfo.allocator, javascriptEngine: buildInfo.javascriptEngine, sysInfo: buildInfo.sysInfo, storageEngines: buildInfo.storageEngines, debug: buildInfo.debug, maxBsonObjectSize: buildInfo.maxBsonObjectSize, openssl: buildInfo.openssl, buildEnvironment: buildInfo.buildEnvironment, bits: buildInfo.bits, ok: buildInfo.ok, status: {}, connectionInfo: { readOnlyMode: isReadOnlyMode, readPreference: isReadOnlyMode ? "secondary" : "primary" } }; if (serverStatus) { serverInfo.status = { host: serverStatus.host, version: serverStatus.version, process: serverStatus.process, pid: serverStatus.pid, uptime: serverStatus.uptime, uptimeMillis: serverStatus.uptimeMillis, uptimeEstimate: serverStatus.uptimeEstimate, localTime: serverStatus.localTime, connections: serverStatus.connections, network: serverStatus.network, memory: serverStatus.mem, storageEngine: serverStatus.storageEngine, security: serverStatus.security }; } return formatResponse(serverInfo); } catch (error) { return handleError(error, "get server information"); } } async function handleInsert(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for insert operation"); } const { documents, ordered, writeConcern, bypassDocumentValidation } = args; if (!Array.isArray(documents)) { throw new Error("Documents must be an array"); } if (documents.length === 0) { throw new Error("Documents array cannot be empty"); } if (!documents.every( (doc) => doc && typeof doc === "object" && !Array.isArray(doc) )) { throw new Error("Each document must be a valid MongoDB document object"); } const processedDocuments = documents.map( (doc) => processObjectIdInFilter(doc, objectIdMode) ); try { const options = { ordered: ordered !== false, // default to true if not specified writeConcern, bypassDocumentValidation }; const result = await collection.insertMany( processedDocuments, options ); return formatResponse({ acknowledged: result.acknowledged, insertedCount: result.insertedCount, insertedIds: result.insertedIds }); } catch (error) { if (error instanceof Error && error.name === "BulkWriteError") { const bulkError = error; return formatResponse({ error: "Bulk write error occurred", writeErrors: bulkError.writeErrors, insertedCount: bulkError.result?.nInserted || 0, failedCount: bulkError.result?.nFailedInserts || 0 }); } return handleError(error, "insert", collection.collectionName); } } async function handleCreateIndex(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for createIndex operation"); } const { indexes, commitQuorum, writeConcern } = args; if (!Array.isArray(indexes) || indexes.length === 0) { throw new Error("Indexes must be a non-empty array"); } if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) { throw new Error( "Write concern must be a valid MongoDB write concern object" ); } if (commitQuorum && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") { throw new Error("Commit quorum must be a string or number"); } const processedIndexes = indexes.map((index) => { if (index && typeof index === "object") { return processObjectIdInFilter( index, objectIdMode ); } return index; }); try { const indexOptions = { commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0 }; const result = await collection.createIndexes( processedIndexes, indexOptions ); return formatResponse({ acknowledged: result.acknowledged, createdIndexes: result.createdIndexes, numIndexesBefore: result.numIndexesBefore, numIndexesAfter: result.numIndexesAfter }); } catch (error) { return handleError(error, "create indexes", collection.collectionName); } } async function handleCount(collection, args, objectIdMode = "auto") { if (!collection) { throw new Error("Collection is required for count operation"); } const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args; const countQuery = parseFilter(query, objectIdMode); try { const options = { limit: typeof limit === "number" ? limit : void 0, skip: typeof skip === "number" ? skip : void 0, hint: typeof hint === "object" && hint !== null ? hint : void 0, readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0, maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0, collation: typeof collation === "object" && collation !== null ? collation : void 0 }; for (const key of Object.keys(options)) { if (options[key] === void 0) { delete options[key]; } } const count = await collection.countDocuments(countQuery, options); return formatResponse({ count, ok: 1 }); } catch (error) { return handleError(error, "count documents", collection.collectionName); } } async function handleListCollections(db, args, objectIdMode = "auto") { const { nameOnly, filter } = args; let processedFilter = filter; if (filter && typeof filter === "object") { processedFilter = processObjectIdInFilter( filter, objectIdMode ); } try { const options = processedFilter ? { filter: processedFilter } : {}; const collections = await db.listCollections(options).toArray(); const result = nameOnly ? collections.map((collection) => collection.name) : collections; return formatResponse(result); } catch (error) { return handleError(error, "list collections"); } } // src/schemas/completion.ts async function handleCompletionRequest({ request, client, db, isReadOnlyMode }) { const { ref, argument } = request.params; if (ref.type === "ref/prompt") { return handlePromptCompletion( client, db, isReadOnlyMode, ref.name, argument ); } if (ref.type === "ref/resource") { return handleResourceCompletion( client, db, isReadOnlyMode, ref.uri, argument ); } return emptyCompletionResult(); } async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) { if (!promptName) { return emptyCompletionResult(); } if (argument.name === "collection") { return await completeCollectionNames(argument.value, db, isReadOnlyMode); } return emptyCompletionResult(); } async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) { if (!promptName) { return emptyCompletionResult(); } if (argument.name === "collection") { return await completeCollectionNames(argument.value, db, isReadOnlyMode); } return emptyCompletionResult(); } async function completeCollectionNames(partialValue, db, isReadOnlyMode) { try { console.warn( `Completing collection names with partial value: ${partialValue}` ); const collections = await db.listCollections().toArray(); const matchingCollections = collections.map( (c) => c.name ).filter( (name) => ( // Filter out system collections !name.startsWith("system.") && // Match partial value name.toLowerCase().includes(partialValue.toLowerCase()) ) ).sort(); console.warn(`Found ${matchingCollections.length} matching collections`); const MAX_ITEMS = 100; const limitedResults = matchingCollections.slice(0, MAX_ITEMS); const hasMore = matchingCollections.length > MAX_ITEMS; return { completion: { values: limitedResults, total: matchingCollections.length, hasMore } }; } catch (error) { console.error("Error completing collection names:", error); return emptyCompletionResult(); } } function emptyCompletionResult() { return { completion: { values: [], total: 0, hasMore: false } }; } // src/schemas/ping.ts async function handlePingRequest({ request, client, db, isReadOnlyMode }) { try { if (!client) { throw new Error("MongoDB connection is not available"); } const pong = await db.command({ ping: 1 }); if (pong.ok !== 1) { throw new Error(`MongoDB ping failed: ${pong.errmsg}`); } return {}; } catch (error) { if (error instanceof Error) { throw new Error(`MongoDB ping failed: ${error.message}`); } throw new Error("MongoDB ping failed: Unknown error"); } } // src/schemas/prompts.ts async function handleListPromptsRequest({ request, client, db, isReadOnlyMode }) { return { prompts: [ { name: "analyze_collection", description: "Analyze a MongoDB collection structure and contents", arguments: [ { name: "collection", description: "Name of the collection to analyze", required: true } ] } ] }; } async function handleGetPromptRequest({ request, client, db, isReadOnlyMode }) { const { name, arguments: args = {} } = request.params; if (name !== "analyze_collection") { throw new Error("Unknown prompt"); } const collectionName = args.collection; if (!collectionName) { throw new Error("Collection name is required"); } try { const collection = db.collection(collectionName); if (collection.collectionName.startsWith("system.")) { throw new Error("Access to system collections is not allowed"); } const schemaSample = await collection.findOne({}); const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray(); const sampleDocs = await collection.find({}).limit(5).toArray(); const documentCount = stats[0]?.count ?? "unknown"; return { messages: [ { role: "user", content: { type: "text", text: `Please analyze the following MongoDB collection: Collection: ${collectionName} Schema: ${JSON.stringify(schemaSample, null, 2)} Stats: Document count: ${documentCount} Sample documents: ${JSON.stringify(sampleDocs, null, 2)}` } }, { role: "user", content: { type: "text", text: "Provide insights about the collection's structure, data types, and basic statistics." } } ] }; } catch (error) { const msg = error instanceof Error ? error.message : "Unknown error"; throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`); } } // src/schemas/resource.ts import { ObjectId as ObjectId2 } from "mongodb"; function detectMongoType(value) { if (value === null) return "null"; if (value === void 0) return "undefined"; if (value instanceof ObjectId2) return "ObjectId"; if (value instanceof Date) return "Date"; if (Array.isArray(value)) { if (value.length === 0) return "Array"; const elementTypes = new Set(value.map((item) => detectMongoType(item))); if (elementTypes.size === 1) { return `Array<${Array.from(elementTypes)[0]}>`; } return "Array<mixed>"; } if (typeof value === "object") { return "Document"; } return typeof value; } function inferSchemaFromSamples(documents) { if (!documents || documents.length === 0) { return { fields: [] }; } const fieldMap = /* @__PURE__ */ new Map(); for (const doc of documents) { for (const [key, value] of Object.entries(doc)) { if (!fieldMap.has(key)) { fieldMap.set(key, { name: key, types: /* @__PURE__ */ new Set([detectMongoType(value)]), nullable: false, // Store sample values for complex types samples: [value] }); } else { const fieldInfo = fieldMap.get(key); fieldInfo.types.add(detectMongoType(value)); if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some( (sample) => JSON.stringify(sample) === JSON.stringify(value) )) { fieldInfo.samples.push(value); } } } } for (const doc of documents) { for (const [key] of fieldMap.entries()) { if (!(key in doc)) { const fieldInfo = fieldMap.get(key); fieldInfo.nullable = true; } } } for (const [key, fieldInfo] of fieldMap.entries()) { if (fieldInfo.types.has("Document")) { const nestedDocs = documents.filter( (doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key]) ).map((doc) => doc[key]); if (nestedDocs.length > 0) { fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs); } } } const fields = Array.from(fieldMap.values()).map((fieldInfo) => { const result = { name: fieldInfo.name, types: Array.from(fieldInfo.types), nullable: fieldInfo.nullable, prevalence: `${Math.round( documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100 )}%`, examples: [] }; if (fieldInfo.nestedSchema) { result.nestedSchema = fieldInfo.nestedSchema; } const sampleValues = fieldInfo.samples.map((sample) => { if (sample instanceof ObjectId2) return sample.toString(); if (sample instanceof Date) return sample.toISOString(); if (typeof sample === "object") { return Array.isArray(sample) ? "[...]" : "{...}"; } return sample; }); result.examples = sampleValues; return result; }); return { fields }; } async function handleReadResourceRequest({ request, client, db, isReadOnlyMode }) { const url = new URL(request.params.uri); const collectionName = url.pathname.replace(/^\//, ""); try { const collection = db.collection(collectionName); const sampleSize = 100; let sampleDocuments = []; try { sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray(); } catch (sampleError) { console.warn( `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}` ); sampleDocuments = await collection.find({}).limit(sampleSize).toArray(); } const indexes = await collection.indexes(); const inferredSchema = inferSchemaFromSamples(sampleDocuments); let documentCount = null; try { documentCount = await Promise.race([ collection.countDocuments(), new Promise( (_, reject) => setTimeout( () => reject(new Error("Count operation timed out")), 5e3 ) ) ]); } catch (countError) { console.warn( `Count operation failed or timed out for ${collectionName}: ${countError}` ); try { const stats = await db.command({ collStats: collectionName }); documentCount = stats.count; } catch { documentCount = "unknown (count operation timed out)"; } } const schema = { type: "collection", name: collectionName, fields: inferredSchema.fields, indexes: indexes.map((idx) => ({ name: idx.name, keys: idx.key })), documentCount, sampleSize: sampleDocuments.length, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(schema, null, 2) } ] }; } catch (error) { if (error instanceof Error) { throw new Error( `Failed to read collection ${collectionName}: ${error.message}` ); } throw new Error( `Failed to read collection ${collectionName}: Unknown error` ); } } async function handleListResourcesRequest({ request, client, db, isReadOnlyMode }) { try { const collections = await db.listCollections().toArray(); return { resources: collections.map((collection) => ({ uri: `mongodb:///${collection.name}`, mimeType: "application/json", name: collection.name, description: `MongoDB collection: ${collection.name}` })) }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to list collections: ${error.message}`); } throw new Error("Failed to list collections: Unknown error"); } } // src/schemas/templates.ts async function handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode }) { return { resourceTemplates: [ { name: "mongodb_query", description: "Template for constructing MongoDB queries", uriTemplate: "mongodb:///{collection}", text: `To query MongoDB collections, you can use these operators: Filter operators: - $eq: Matches values equal to a specified value - $gt/$gte: Matches values greater than (or equal to) a specified value - $lt/$lte: Matches values less than (or equal to) a specified value - $in: Matches any of the values in an array - $nin: Matches none of the values in an array - $ne: Matches values not equal to a specified value - $exists: Matches documents that have the specified field Example queries: 1. Find documents where age > 21: { "age": { "$gt": 21 } } 2. Find documents with specific status: { "status": { "$in": ["active", "pending"] } } 3. Find documents with existing email: { "email": { "$exists": true } } 4. Find documents with dates: // Using ISO date string format { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } } // Using ISODate syntax { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } } Use these patterns to construct MongoDB queries.` } ] }; } // src/schemas/tools.ts async function handleListToolsRequest({ request, client, db, isReadOnlyMode }) { return { tools: [ { name: "query", description: "Execute a MongoDB query with optional execution plan analysis", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to query" }, filter: { type: "object", description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation" }, projection: { type: "object", description: "Fields to include/exclude" }, limit: { type: "number", description: "Maximum number of documents to return", default: 10 }, explain: { type: "string", description: "Optional: Get query execution information", enum: ["queryPlanner", "executionStats", "allPlansExecution"] }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection"] } }, { name: "aggregate", description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to aggregate" }, pipeline: { type: "array", description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation", items: { type: "object" } }, explain: { type: "string", description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)", enum: ["queryPlanner", "executionStats", "allPlansExecution"] }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection", "pipeline"] } }, { name: "update", description: "Update documents in a MongoDB collection", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to update" }, filter: { type: "object", description: "Filter to select documents to update. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation" }, update: { type: "object", description: "Update operations to apply ($set, $unset, $inc, etc.)" }, upsert: { type: "boolean", description: "Create a new document if no documents match the filter" }, multi: { type: "boolean", description: "Update multiple documents that match the filter" }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection", "filter", "update"] } }, { name: "serverInfo", description: "Get MongoDB server information including version, storage engine, and other details", inputSchema: { type: "object", properties: { includeDebugInfo: { type: "boolean", description: "Include additional debug information about the server" } } } }, { name: "insert", description: "Insert one or more documents into a MongoDB collection", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to insert into" }, documents: { type: "array", description: "Array of documents to insert", items: { type: "object" } }, ordered: { type: "boolean", description: "If true, perform ordered insert. If false, insert unordered" }, writeConcern: { type: "object", description: "Write concern for the insert operation" }, bypassDocumentValidation: { type: "boolean", description: "Allow insert to bypass schema validation" }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection", "documents"] } }, { name: "createIndex", description: "Create one or more indexes on a MongoDB collection", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Name of the collection to create indexes on" }, indexes: { type: "array", description: "Array of index specifications", items: { type: "object", properties: { key: { type: "object", description: "Index key pattern, e.g. { field: 1 }" }, name: { type: "string", description: "Optional: Name of the index" }, unique: { type: "boolean", description: "Optional: Creates a unique index" }, sparse: { type: "boolean", description: "Optional: Creates a sparse index" }, background: { type: "boolean", description: "Optional: Builds index in background" }, expireAfterSeconds: { type: "number", description: "TTL in seconds for documents" }, partialFilterExpression: { type: "object", description: "Filter expression for partial indexes" } }, required: ["key"] } }, writeConcern: { type: "object", description: "Write concern for index creation" }, commitQuorum: { type: "string", description: "Number of members required to create the index" }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection", "indexes"] } }, { name: "count", description: "Count documents in a collection matching a query", inputSchema: { type: "object", properties: { collection: { type: "string", description: "Collection name" }, query: { type: "object", description: "Query filter to count" }, limit: { type: "integer", description: "Max documents to count" }, skip: { type: "integer", description: "Docs to skip before counting" }, hint: { type: "object", description: "Index hint" }, readConcern: { type: "object", description: "Read concern option" }, maxTimeMS: { type: "integer", description: "Max execution time" }, collation: { type: "object", description: "Collation rules for comparison" }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } }, required: ["collection"] } }, { name: "listCollections", description: "List all collections in the MongoDB database", inputSchema: { type: "object", properties: { nameOnly: { type: "boolean", description: "If true, return only collection names" }, filter: { type: "object", description: "Filter for collections" }, objectIdMode: { type: "string", description: "Control how 24-character hex strings are handled", enum: ["auto", "none", "force"], default: "auto" } } } } ] }; } // src/server.ts function createServer(client, db, isReadOnlyMode = false, options = {}) { const server = new Server( { name: "mongodb", version: "2.0.2", ...options }, { capabilities: { completions: {}, resources: {}, tools: {}, prompts: {} }, ...options } ); server.setRequestHandler( PingRequestSchema, (request) => handlePingRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( ListResourcesRequestSchema, (request) => handleListResourcesRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( ReadResourceRequestSchema, (request) => handleReadResourceRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( ListToolsRequestSchema, (request) => handleListToolsRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( CallToolRequestSchema, (request) => handleCallToolRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( ListPromptsRequestSchema, (request) => handleListPromptsRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( GetPromptRequestSchema, (request) => handleGetPromptRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( ListResourceTemplatesRequestSchema, (request) => handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode }) ); server.setRequestHandler( CompleteRequestSchema, (request) => handleCompletionRequest({ request, client, db, isReadOnlyMode }) ); return server; } // src/index.ts var mongoClient = null; async function main() { const args = process.argv.slice(2); let connectionUrl = ""; let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false; let transportMode = "stdio"; let port = Number(process.env.MCP_PORT) || 3001; for (let i = 0; i < args.length; i++) { if (args[i] === "--read-only" || args[i] === "-r") { readOnlyMode = true; } else if (args[i] === "--transport" || args[i] === "-t") { const value = args[++i]; if (value !== "stdio" && value !== "http") { console.error("Invalid transport mode. Use 'stdio' or 'http'."); process.exit(1); } transportMode = value; } else if (args[i] === "--port" || args[i] === "-p") { port = Number(args[++i]); if (Number.isNaN(port)) { console.error("Invalid port number."); process.exit(1); } } else if (!connectionUrl) { connectionUrl = args[i]; } } if (!connectionUrl) { connectionUrl = process.env.MCP_MONGODB_URI || ""; } if (!connectionUrl) { console.error( "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable" ); console.error( "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]" ); console.error( " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command" ); process.exit(1); } if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) { console.error( "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'" ); process.exit(1); } try { const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB( connectionUrl, readOnlyMode ); mongoClient = client; if (!isConnected || !client || !db) { console.error("Failed to connect to MongoDB"); process.exit(1); } if (transportMode === "http") { await startHttpServer(client, db, isReadOnlyMode, port); } else { await startStdioServer(client, db, isReadOnlyMode); } } catch (error) { console.error("Failed to connect to MongoDB:", error); if (mongoClient) { await mongoClient.close(); } process.exit(1); } } async function startStdioServer(client, db, isReadOnlyMode) { const server = createServer(client, db, isReadOnlyMode); const transport = new StdioServerTransport(); await server.connect(transport); console.warn("Server connected successfully via stdio"); } async function startHttpServer(client, db, isReadOnlyMode, port) { const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use((req, res, next) => { const start = Date.now(); const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; const method = req.method; const url = req.url; let mcpMethod = "-"; if (req.body && typeof req.body === "object" && "method" in req.body) { mcpMethod = req.body.method; } res.on("finish", () => { const duration = Date.now() - start; console.log( `${(/* @__PURE__ */ new Date()).toISOString()} | ${ip} | ${method} ${url} | ${res.statusCode} | ${duration}ms | mcp:${mcpMethod}` ); }); next(); }); app.post("/mcp", async (req, res) => { const server = createServer(client, db, isReadOnlyMode); try { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 }); await server.connect(transport); await transport.handleRequest(req, res, req.body); res.on("close", () => { transport.close(); server.close(); }); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }); } } }); app.get("/mcp", async (_req, res) => { res.writeHead(405).end( JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed." }, id: null }) ); }); app.delete("/mcp", async (_req, res) => { res.writeHead(405).end( JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed." }, id: null }) ); }); app.listen(port, () => { console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`); console.log(`Endpoint: http://localhost:${port}/mcp`); }); } process.on("SIGINT", async () => { if (mongoClient) { await mongoClient.close(); } process.exit(0); }); process.on("SIGTERM", async () => { if (mongoClient) { await mongoClient.close(); } process.exit(0); }); main().catch((error) => { console.error("Server error:", error); process.exit(1); });