UNPKG

smh-mongo-mcp-server

Version:

A Model Context Protocol server for MongoDB connections

298 lines (297 loc) 10.5 kB
import { ObjectId } from "mongodb"; const handleListResourcesRequest = async (db) => { try { const collections = await db.listCollections().toArray(); const collectionNames = collections.map((c) => c.name); return { contents: [ { uri: "resource:mongo-collections:text", text: `Available MongoDB collections:\n\n${collectionNames.join("\n")}`, }, ], }; } catch (error) { return { contents: [ { uri: "resource:mongo-collections:error", text: `Error fetching collections: ${error?.message || "Unknown error"}`, }, ], isError: true, }; } }; export { handleListResourcesRequest }; const handleListResourcesRequestTool = async (db) => { try { const collections = await db.listCollections().toArray(); const collectionNames = collections.map((c) => c.name); return { content: [ { type: "text", text: `Available MongoDB collections:\n\n${collectionNames.join("\n")}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error fetching collections: ${error?.message || "Unknown error"}`, }, ], isError: true, }; } }; export { handleListResourcesRequestTool }; const handleReadResourcesRequest = async (uri, collectionName, db) => { try { // const url = new URL(request.params.uri) // const collectionName = url.pathname.replace(/^\//, "") const collection = db.collection(collectionName); const sampleSize = 100; let sampleDocuments = []; try { sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray(); } catch (sampleError) { console.warn(`$sample failed, falling back: ${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('Timeout')), 5000)) ]); } catch { try { const stats = await db.command({ collStats: collectionName }); documentCount = stats.count; } catch { documentCount = "unknown (timeout)"; } } const schema = { type: "collection", name: collectionName, fields: inferredSchema.fields, indexes: indexes.map(idx => ({ name: idx.name, keys: idx.key, })), documentCount, sampleSize: sampleDocuments.length, lastUpdated: new Date().toISOString(), }; return { contents: [ { uri: uri, text: JSON.stringify(schema, null, 2), type: "application/json" } ] }; } catch (error) { console.error(`Error reading resource: ${error.message}`); return { contents: [ { uri: uri, text: `Failed to read resource: ${error?.message || "Unknown error"}`, type: "text/plain" }, ], isError: true, }; } }; export { handleReadResourcesRequest }; const handleReadResourcesRequestTool = async (collectionName, db) => { try { // const url = new URL(request.params.uri) // const collectionName = url.pathname.replace(/^\//, "") const collection = db.collection(collectionName); const sampleSize = 100; let sampleDocuments = []; try { sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray(); } catch (sampleError) { console.warn(`$sample failed, falling back: ${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('Timeout')), 5000)) ]); } catch { try { const stats = await db.command({ collStats: collectionName }); documentCount = stats.count; } catch { documentCount = "unknown (timeout)"; } } const schema = { type: "collection", name: collectionName, fields: inferredSchema.fields, indexes: indexes.map(idx => ({ name: idx.name, keys: idx.key, })), documentCount, sampleSize: sampleDocuments.length, lastUpdated: new Date().toISOString(), }; return { content: [ { type: "text", text: JSON.stringify(schema, null, 2) } ] }; } catch (error) { console.error(`Error reading resource: ${error.message}`); return { content: [ { type: "text", text: `Failed to read resource: ${error?.message || "Unknown error"}` }, ], isError: true, }; } }; export { handleReadResourcesRequestTool }; function detectMongoType(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (value instanceof ObjectId) return 'ObjectId'; if (value instanceof Date) return 'Date'; if (Array.isArray(value)) { if (value.length === 0) return 'Array'; // Check if array has consistent types 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') { // Handle nested documents return 'Document'; } return typeof value; } function inferSchemaFromSamples(documents) { if (!documents || documents.length === 0) { return { fields: [] }; } // Use a Map to store field information, with the key being the field name const fieldMap = new Map(); // Process each document to collect field information for (const doc of documents) { for (const [key, value] of Object.entries(doc)) { if (!fieldMap.has(key)) { // Initialize field info if we haven't seen this field before fieldMap.set(key, { name: key, types: new Set([detectMongoType(value)]), nullable: false, // Store sample values for complex types samples: [value], }); } else { // Update existing field info const fieldInfo = fieldMap.get(key); fieldInfo.types.add(detectMongoType(value)); // Store up to 3 different sample values if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some((sample) => JSON.stringify(sample) === JSON.stringify(value))) { fieldInfo.samples.push(value); } } } } // Check for nullable fields by seeing which fields are missing in some documents for (const doc of documents) { for (const [key] of fieldMap.entries()) { if (!(key in doc)) { const fieldInfo = fieldMap.get(key); fieldInfo.nullable = true; } } } // Process nested document schemas for (const [key, fieldInfo] of fieldMap.entries()) { if (fieldInfo.types.has('Document')) { // Extract nested documents for this field const nestedDocs = documents .filter((doc) => doc[key] && typeof doc[key] === 'object' && !Array.isArray(doc[key])) .map(doc => doc[key]); if (nestedDocs.length > 0) { // Recursively infer schema for nested documents fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs); } } } // Convert the Map to an array of field objects with additional info 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: [], }; // Include nested schema if available if (fieldInfo.nestedSchema) { result.nestedSchema = fieldInfo.nestedSchema; } // Include simplified sample values const sampleValues = fieldInfo.samples.map((sample) => { if (sample instanceof ObjectId) return sample.toString(); if (sample instanceof Date) return sample.toISOString(); if (typeof sample === 'object') { // For objects/arrays, just indicate type rather than full structure return Array.isArray(sample) ? '[...]' : '{...}'; } return sample; }); result.examples = sampleValues; return result; }); return { fields }; }