mcp-mongo-server 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Handles completion requests from the Model Context Protocol
3
+ * @param options The request options
4
+ * @returns Completion result with matching values
5
+ */
6
+ export async function handleCompletionRequest({ request, client, db, isReadOnlyMode, }) {
7
+ const { ref, argument } = request.params;
8
+ // Handle completions for prompts
9
+ if (ref.type === "ref/prompt") {
10
+ return handlePromptCompletion(client, db, isReadOnlyMode, ref.name, argument);
11
+ }
12
+ // Handle completions for resources
13
+ if (ref.type === "ref/resource") {
14
+ return handleResourceCompletion(client, db, isReadOnlyMode, ref.uri, argument);
15
+ }
16
+ // Default empty response if reference type is not supported
17
+ return emptyCompletionResult();
18
+ }
19
+ /**
20
+ * Handle completion requests for prompts
21
+ * @param client MongoDB client
22
+ * @param db MongoDB database
23
+ * @param isReadOnlyMode Whether the database is in read-only mode
24
+ * @param promptName Name of the prompt
25
+ * @param argument Argument being completed
26
+ * @returns Completion result
27
+ */
28
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
29
+ if (!promptName) {
30
+ return emptyCompletionResult();
31
+ }
32
+ // Handle collection name completions
33
+ if (argument.name === "collection") {
34
+ return await completeCollectionNames(argument.value, db, isReadOnlyMode);
35
+ }
36
+ // Add other prompt completions here as needed
37
+ return emptyCompletionResult();
38
+ }
39
+ /**
40
+ * Handle completion requests for resources
41
+ * @param client MongoDB client
42
+ * @param db MongoDB database
43
+ * @param isReadOnlyMode Whether the database is in read-only mode
44
+ * @param uri Resource URI
45
+ * @param argument Argument being completed
46
+ * @returns Completion result
47
+ */
48
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
49
+ if (!promptName) {
50
+ return emptyCompletionResult();
51
+ }
52
+ // Handle collection name completions
53
+ if (argument.name === "collection") {
54
+ return await completeCollectionNames(argument.value, db, isReadOnlyMode);
55
+ }
56
+ // Add other prompt completions here as needed
57
+ return emptyCompletionResult();
58
+ }
59
+ /**
60
+ * Get collection name completions for a partial value
61
+ * @param partialValue Partial collection name
62
+ * @param db MongoDB database
63
+ * @param isReadOnlyMode Whether the database is in read-only mode
64
+ * @returns Completion result with matching collection names
65
+ */
66
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
67
+ try {
68
+ console.warn(`Completing collection names with partial value: ${partialValue}`);
69
+ // Get list of collections
70
+ const collections = await db.listCollections().toArray();
71
+ // Filter collections by partial value (case insensitive)
72
+ const matchingCollections = collections
73
+ .map((c) => c.name)
74
+ .filter((name) =>
75
+ // Filter out system collections
76
+ !name.startsWith("system.") &&
77
+ // Match partial value
78
+ name.toLowerCase().includes(partialValue.toLowerCase()))
79
+ // Sort alphabetically
80
+ .sort();
81
+ console.warn(`Found ${matchingCollections.length} matching collections`);
82
+ // Limit to 100 items as per spec
83
+ const MAX_ITEMS = 100;
84
+ const limitedResults = matchingCollections.slice(0, MAX_ITEMS);
85
+ const hasMore = matchingCollections.length > MAX_ITEMS;
86
+ return {
87
+ completion: {
88
+ values: limitedResults,
89
+ total: matchingCollections.length,
90
+ hasMore,
91
+ },
92
+ };
93
+ }
94
+ catch (error) {
95
+ console.error("Error completing collection names:", error);
96
+ // Return empty result on error
97
+ return emptyCompletionResult();
98
+ }
99
+ }
100
+ /**
101
+ * Create an empty completion result
102
+ * @returns Empty completion result object
103
+ */
104
+ function emptyCompletionResult() {
105
+ return {
106
+ completion: {
107
+ values: [],
108
+ total: 0,
109
+ hasMore: false,
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,20 @@
1
+ export async function handlePingRequest({ request, client, db, isReadOnlyMode, }) {
2
+ try {
3
+ // Check MongoDB connection
4
+ if (!client) {
5
+ throw new Error("MongoDB connection is not available");
6
+ }
7
+ // Ping MongoDB to verify connection
8
+ const pong = await db.command({ ping: 1 });
9
+ if (pong.ok !== 1) {
10
+ throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
11
+ }
12
+ return {};
13
+ }
14
+ catch (error) {
15
+ if (error instanceof Error) {
16
+ throw new Error(`MongoDB ping failed: ${error.message}`);
17
+ }
18
+ throw new Error("MongoDB ping failed: Unknown error");
19
+ }
20
+ }
@@ -0,0 +1,71 @@
1
+ export async function handleListPromptsRequest({ request, client, db, isReadOnlyMode, }) {
2
+ return {
3
+ prompts: [
4
+ {
5
+ name: "analyze_collection",
6
+ description: "Analyze a MongoDB collection structure and contents",
7
+ arguments: [
8
+ {
9
+ name: "collection",
10
+ description: "Name of the collection to analyze",
11
+ required: true,
12
+ },
13
+ ],
14
+ },
15
+ ],
16
+ };
17
+ }
18
+ export async function handleGetPromptRequest({ request, client, db, isReadOnlyMode, }) {
19
+ const { name, arguments: args = {} } = request.params;
20
+ if (name !== "analyze_collection") {
21
+ throw new Error("Unknown prompt");
22
+ }
23
+ const collectionName = args.collection;
24
+ if (!collectionName) {
25
+ throw new Error("Collection name is required");
26
+ }
27
+ try {
28
+ const collection = db.collection(collectionName);
29
+ if (collection.collectionName.startsWith("system.")) {
30
+ throw new Error("Access to system collections is not allowed");
31
+ }
32
+ const schemaSample = await collection.findOne({});
33
+ const stats = await collection
34
+ .aggregate([{ $collStats: { count: {} } }])
35
+ .toArray();
36
+ const sampleDocs = await collection.find({}).limit(5).toArray();
37
+ const documentCount = stats[0]?.count ?? "unknown";
38
+ return {
39
+ messages: [
40
+ {
41
+ role: "user",
42
+ content: {
43
+ type: "text",
44
+ text: `Please analyze the following MongoDB collection:
45
+ Collection: ${collectionName}
46
+
47
+ Schema:
48
+ ${JSON.stringify(schemaSample, null, 2)}
49
+
50
+ Stats:
51
+ Document count: ${documentCount}
52
+
53
+ Sample documents:
54
+ ${JSON.stringify(sampleDocs, null, 2)}`,
55
+ },
56
+ },
57
+ {
58
+ role: "user",
59
+ content: {
60
+ type: "text",
61
+ text: "Provide insights about the collection's structure, data types, and basic statistics.",
62
+ },
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ catch (error) {
68
+ const msg = error instanceof Error ? error.message : "Unknown error";
69
+ throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
70
+ }
71
+ }
@@ -0,0 +1,213 @@
1
+ import { ObjectId } from "mongodb";
2
+ /**
3
+ * Detects the MongoDB-specific type of a value
4
+ * @param value The value to detect the type of
5
+ * @returns A string representing the detected type
6
+ */
7
+ function detectMongoType(value) {
8
+ if (value === null)
9
+ return 'null';
10
+ if (value === undefined)
11
+ return 'undefined';
12
+ if (value instanceof ObjectId)
13
+ return 'ObjectId';
14
+ if (value instanceof Date)
15
+ return 'Date';
16
+ if (Array.isArray(value)) {
17
+ if (value.length === 0)
18
+ return 'Array';
19
+ // Check if array has consistent types
20
+ const elementTypes = new Set(value.map(item => detectMongoType(item)));
21
+ if (elementTypes.size === 1) {
22
+ return `Array<${Array.from(elementTypes)[0]}>`;
23
+ }
24
+ return 'Array<mixed>';
25
+ }
26
+ if (typeof value === 'object') {
27
+ // Handle nested documents
28
+ return 'Document';
29
+ }
30
+ return typeof value;
31
+ }
32
+ /**
33
+ * Helper function to infer a schema from multiple documents
34
+ * @param documents Array of sample documents from the collection
35
+ * @returns Inferred schema with field names and types
36
+ */
37
+ function inferSchemaFromSamples(documents) {
38
+ if (!documents || documents.length === 0) {
39
+ return { fields: [] };
40
+ }
41
+ // Use a Map to store field information, with the key being the field name
42
+ const fieldMap = new Map();
43
+ // Process each document to collect field information
44
+ for (const doc of documents) {
45
+ for (const [key, value] of Object.entries(doc)) {
46
+ if (!fieldMap.has(key)) {
47
+ // Initialize field info if we haven't seen this field before
48
+ fieldMap.set(key, {
49
+ name: key,
50
+ types: new Set([detectMongoType(value)]),
51
+ nullable: false,
52
+ // Store sample values for complex types
53
+ samples: [value],
54
+ });
55
+ }
56
+ else {
57
+ // Update existing field info
58
+ const fieldInfo = fieldMap.get(key);
59
+ fieldInfo.types.add(detectMongoType(value));
60
+ // Store up to 3 different sample values
61
+ if (fieldInfo.samples.length < 3 &&
62
+ !fieldInfo.samples.some((sample) => JSON.stringify(sample) === JSON.stringify(value))) {
63
+ fieldInfo.samples.push(value);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ // Check for nullable fields by seeing which fields are missing in some documents
69
+ for (const doc of documents) {
70
+ for (const [key] of fieldMap.entries()) {
71
+ if (!(key in doc)) {
72
+ const fieldInfo = fieldMap.get(key);
73
+ fieldInfo.nullable = true;
74
+ }
75
+ }
76
+ }
77
+ // Process nested document schemas
78
+ for (const [key, fieldInfo] of fieldMap.entries()) {
79
+ if (fieldInfo.types.has('Document')) {
80
+ // Extract nested documents for this field
81
+ const nestedDocs = documents
82
+ .filter(doc => doc[key] && typeof doc[key] === 'object' && !Array.isArray(doc[key]))
83
+ .map(doc => doc[key]);
84
+ if (nestedDocs.length > 0) {
85
+ // Recursively infer schema for nested documents
86
+ fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
87
+ }
88
+ }
89
+ }
90
+ // Convert the Map to an array of field objects with additional info
91
+ const fields = Array.from(fieldMap.values()).map(fieldInfo => {
92
+ const result = {
93
+ name: fieldInfo.name,
94
+ types: Array.from(fieldInfo.types),
95
+ nullable: fieldInfo.nullable,
96
+ prevalence: Math.round((documents.filter(doc => fieldInfo.name in doc).length / documents.length) * 100) + '%',
97
+ examples: [],
98
+ };
99
+ // Include nested schema if available
100
+ if (fieldInfo.nestedSchema) {
101
+ result.nestedSchema = fieldInfo.nestedSchema;
102
+ }
103
+ // Include simplified sample values
104
+ const sampleValues = fieldInfo.samples.map((sample) => {
105
+ if (sample instanceof ObjectId)
106
+ return sample.toString();
107
+ if (sample instanceof Date)
108
+ return sample.toISOString();
109
+ if (typeof sample === 'object') {
110
+ // For objects/arrays, just indicate type rather than full structure
111
+ return Array.isArray(sample) ? '[...]' : '{...}';
112
+ }
113
+ return sample;
114
+ });
115
+ result.examples = sampleValues;
116
+ return result;
117
+ });
118
+ return { fields };
119
+ }
120
+ export async function handleReadResourceRequest({ request, client, db, isReadOnlyMode, }) {
121
+ const url = new URL(request.params.uri);
122
+ const collectionName = url.pathname.replace(/^\//, "");
123
+ try {
124
+ const collection = db.collection(collectionName);
125
+ // Set sample size for schema inference
126
+ const sampleSize = 100;
127
+ let sampleDocuments = [];
128
+ try {
129
+ // First try using MongoDB's $sample aggregation to get a diverse set of documents
130
+ sampleDocuments = await collection
131
+ .aggregate([{ $sample: { size: sampleSize } }])
132
+ .toArray();
133
+ }
134
+ catch (sampleError) {
135
+ // Fallback to sequential scan if $sample is not available
136
+ console.warn(`$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`);
137
+ sampleDocuments = await collection
138
+ .find({})
139
+ .limit(sampleSize)
140
+ .toArray();
141
+ }
142
+ // Get indexes for the collection
143
+ const indexes = await collection.indexes();
144
+ // Infer schema from samples
145
+ const inferredSchema = inferSchemaFromSamples(sampleDocuments);
146
+ // Get document count with timeout protection
147
+ let documentCount = null;
148
+ try {
149
+ // Set a timeout for the count operation
150
+ documentCount = await Promise.race([
151
+ collection.countDocuments(),
152
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Count operation timed out')), 5000))
153
+ ]);
154
+ }
155
+ catch (countError) {
156
+ console.warn(`Count operation failed or timed out for ${collectionName}: ${countError}`);
157
+ // Estimate count based on sample size and collection stats
158
+ try {
159
+ const stats = await db.command({ collStats: collectionName });
160
+ documentCount = stats.count;
161
+ }
162
+ catch {
163
+ documentCount = 'unknown (count operation timed out)';
164
+ }
165
+ }
166
+ const schema = {
167
+ type: "collection",
168
+ name: collectionName,
169
+ fields: inferredSchema.fields,
170
+ indexes: indexes.map((idx) => ({
171
+ name: idx.name,
172
+ keys: idx.key,
173
+ })),
174
+ documentCount: documentCount,
175
+ sampleSize: sampleDocuments.length,
176
+ lastUpdated: new Date().toISOString(),
177
+ };
178
+ return {
179
+ contents: [
180
+ {
181
+ uri: request.params.uri,
182
+ mimeType: "application/json",
183
+ text: JSON.stringify(schema, null, 2),
184
+ },
185
+ ],
186
+ };
187
+ }
188
+ catch (error) {
189
+ if (error instanceof Error) {
190
+ throw new Error(`Failed to read collection ${collectionName}: ${error.message}`);
191
+ }
192
+ throw new Error(`Failed to read collection ${collectionName}: Unknown error`);
193
+ }
194
+ }
195
+ export async function handleListResourcesRequest({ request, client, db, isReadOnlyMode, }) {
196
+ try {
197
+ const collections = await db.listCollections().toArray();
198
+ return {
199
+ resources: collections.map((collection) => ({
200
+ uri: `mongodb:///${collection.name}`,
201
+ mimeType: "application/json",
202
+ name: collection.name,
203
+ description: `MongoDB collection: ${collection.name}`,
204
+ })),
205
+ };
206
+ }
207
+ catch (error) {
208
+ if (error instanceof Error) {
209
+ throw new Error(`Failed to list collections: ${error.message}`);
210
+ }
211
+ throw new Error("Failed to list collections: Unknown error");
212
+ }
213
+ }
@@ -0,0 +1,39 @@
1
+ export async function handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode, }) {
2
+ return {
3
+ resourceTemplates: [
4
+ {
5
+ name: "mongodb_query",
6
+ description: "Template for constructing MongoDB queries",
7
+ uriTemplate: "mongodb:///{collection}",
8
+ text: `To query MongoDB collections, you can use these operators:
9
+
10
+ Filter operators:
11
+ - $eq: Matches values equal to a specified value
12
+ - $gt/$gte: Matches values greater than (or equal to) a specified value
13
+ - $lt/$lte: Matches values less than (or equal to) a specified value
14
+ - $in: Matches any of the values in an array
15
+ - $nin: Matches none of the values in an array
16
+ - $ne: Matches values not equal to a specified value
17
+ - $exists: Matches documents that have the specified field
18
+
19
+ Example queries:
20
+ 1. Find documents where age > 21:
21
+ { "age": { "$gt": 21 } }
22
+
23
+ 2. Find documents with specific status:
24
+ { "status": { "$in": ["active", "pending"] } }
25
+
26
+ 3. Find documents with existing email:
27
+ { "email": { "$exists": true } }
28
+
29
+ 4. Find documents with dates:
30
+ // Using ISO date string format
31
+ { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
32
+ // Using ISODate syntax
33
+ { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
34
+
35
+ Use these patterns to construct MongoDB queries.`,
36
+ },
37
+ ],
38
+ };
39
+ }