mcp-mongo-server
Version:
A Model Context Protocol server for MongoDB connections
1,522 lines (1,500 loc) • 47.4 kB
JavaScript
// 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);
});