mcp-mongo-server 1.2.0 → 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.
- package/README.md +9 -3
- package/build/schemas/call.js +95 -11
- package/build/schemas/resource.js +170 -19
- package/build/schemas/templates.js +6 -0
- package/build/schemas/tools.js +6 -3
- package/build/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,9 +124,14 @@ Using environment variables in Claude Desktop configuration:
|
|
|
124
124
|
Using environment variables with Docker:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
# Build
|
|
128
|
+
docker build -t mcp-mongo-server .
|
|
129
|
+
|
|
130
|
+
# Run
|
|
131
|
+
docker run -it -d -e MCP_MONGODB_URI="mongodb://muhammed:kilic@localhost:27017/database" -e MCP_MONGODB_READONLY="true" mcp-mongo-server
|
|
132
|
+
|
|
133
|
+
# or edit docker-compose.yml and run
|
|
134
|
+
docker-compose up -d
|
|
130
135
|
```
|
|
131
136
|
|
|
132
137
|
## Integration with Claude Desktop
|
|
@@ -192,6 +197,7 @@ Add the server configuration to Claude Desktop's config file:
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
}
|
|
200
|
+
```
|
|
195
201
|
|
|
196
202
|
### GitHub Package Usage:
|
|
197
203
|
|
package/build/schemas/call.js
CHANGED
|
@@ -22,6 +22,10 @@ export async function handleCallToolRequest({ request, client, db, isReadOnlyMod
|
|
|
22
22
|
filteredArgs[key] = value;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
// Checking whether sort option provided is valid
|
|
26
|
+
if (args.sort) {
|
|
27
|
+
args.sort = parseSort(args.sort);
|
|
28
|
+
}
|
|
25
29
|
// Replace the original args with the filtered version
|
|
26
30
|
Object.assign(args, filteredArgs);
|
|
27
31
|
// Validate operation name
|
|
@@ -90,6 +94,20 @@ function checkReadOnlyMode(operation, isReadOnlyMode) {
|
|
|
90
94
|
throw new Error(`ReadonlyError: Operation '${operation}' is not allowed in read-only mode`);
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
function parseSort(sort) {
|
|
98
|
+
if (!sort)
|
|
99
|
+
return null;
|
|
100
|
+
if (typeof sort !== 'object' || sort === null || Array.isArray(sort)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const validSort = {};
|
|
104
|
+
for (const [key, value] of Object.entries(sort)) {
|
|
105
|
+
if (typeof value === 'number' && (value === 1 || value === -1)) {
|
|
106
|
+
validSort[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Object.keys(validSort).length > 0 ? validSort : null;
|
|
110
|
+
}
|
|
93
111
|
function parseFilter(filter, objectIdMode = "auto") {
|
|
94
112
|
if (!filter) {
|
|
95
113
|
return {};
|
|
@@ -122,7 +140,47 @@ function isObjectIdField(fieldName) {
|
|
|
122
140
|
function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
123
141
|
// If objectIdMode is "none", don't convert any strings to ObjectIds
|
|
124
142
|
if (objectIdMode === "none") {
|
|
125
|
-
|
|
143
|
+
// Create a new filter object to handle dates
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
146
|
+
if (typeof value === "string" && isISODateString(value)) {
|
|
147
|
+
// Convert ISO date string to Date object
|
|
148
|
+
result[key] = new Date(value);
|
|
149
|
+
}
|
|
150
|
+
else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
|
|
151
|
+
// Handle ISODate("2025-01-01T00:00:00Z") format
|
|
152
|
+
const dateString = value.substring(8, value.length - 2);
|
|
153
|
+
if (isISODateString(dateString)) {
|
|
154
|
+
result[key] = new Date(dateString);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
result[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (typeof value === "object" && value !== null) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
// For arrays, apply date conversion to each item
|
|
163
|
+
result[key] = value.map((item) => {
|
|
164
|
+
if (typeof item === "string" && isISODateString(item)) {
|
|
165
|
+
return new Date(item);
|
|
166
|
+
}
|
|
167
|
+
else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
|
|
168
|
+
const dateString = item.substring(8, item.length - 2);
|
|
169
|
+
return isISODateString(dateString) ? new Date(dateString) : item;
|
|
170
|
+
}
|
|
171
|
+
return item;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Process nested objects
|
|
176
|
+
result[key] = processObjectIdInFilter(value, "none");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
result[key] = value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
126
184
|
}
|
|
127
185
|
const result = {};
|
|
128
186
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -138,18 +196,37 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
|
138
196
|
result[key] = value;
|
|
139
197
|
}
|
|
140
198
|
}
|
|
199
|
+
else if (typeof value === "string" && isISODateString(value)) {
|
|
200
|
+
// Convert ISO date string to Date object
|
|
201
|
+
result[key] = new Date(value);
|
|
202
|
+
}
|
|
203
|
+
else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
|
|
204
|
+
// Handle ISODate("2025-01-01T00:00:00Z") format
|
|
205
|
+
const dateString = value.substring(8, value.length - 2);
|
|
206
|
+
if (isISODateString(dateString)) {
|
|
207
|
+
result[key] = new Date(dateString);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
result[key] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
141
213
|
else if (typeof value === "object" && value !== null) {
|
|
142
214
|
if (Array.isArray(value)) {
|
|
143
215
|
// For arrays, apply the same logic to each item
|
|
144
|
-
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
216
|
+
result[key] = value.map((item) => {
|
|
217
|
+
if (typeof item === "string" && isObjectIdString(item) &&
|
|
218
|
+
(objectIdMode === "force" || (objectIdMode === "auto" && isObjectIdField(key)))) {
|
|
219
|
+
return new ObjectId(item);
|
|
220
|
+
}
|
|
221
|
+
else if (typeof item === "string" && isISODateString(item)) {
|
|
222
|
+
return new Date(item);
|
|
223
|
+
}
|
|
224
|
+
else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
|
|
225
|
+
const dateString = item.substring(8, item.length - 2);
|
|
226
|
+
return isISODateString(dateString) ? new Date(dateString) : item;
|
|
227
|
+
}
|
|
228
|
+
return item;
|
|
229
|
+
});
|
|
153
230
|
}
|
|
154
231
|
else {
|
|
155
232
|
// Process nested objects
|
|
@@ -167,6 +244,11 @@ function isObjectIdString(str) {
|
|
|
167
244
|
// MongoDB ObjectId is typically a 24-character hex string
|
|
168
245
|
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
169
246
|
}
|
|
247
|
+
// Helper function to check if a string is in ISO date format
|
|
248
|
+
function isISODateString(str) {
|
|
249
|
+
// Check if string matches ISO 8601 format
|
|
250
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(str);
|
|
251
|
+
}
|
|
170
252
|
function formatResponse(data) {
|
|
171
253
|
return {
|
|
172
254
|
content: [
|
|
@@ -189,7 +271,7 @@ async function handleQuery(collection, args, objectIdMode = "auto") {
|
|
|
189
271
|
if (!collection) {
|
|
190
272
|
throw new Error("Collection is required for query operation");
|
|
191
273
|
}
|
|
192
|
-
const { filter, projection, limit, explain } = args;
|
|
274
|
+
const { filter, projection, limit, explain, sort } = args;
|
|
193
275
|
const queryFilter = parseFilter(filter, objectIdMode);
|
|
194
276
|
try {
|
|
195
277
|
if (explain) {
|
|
@@ -197,6 +279,7 @@ async function handleQuery(collection, args, objectIdMode = "auto") {
|
|
|
197
279
|
.find(queryFilter, {
|
|
198
280
|
projection,
|
|
199
281
|
limit: limit || 100,
|
|
282
|
+
sort,
|
|
200
283
|
})
|
|
201
284
|
.explain(explain);
|
|
202
285
|
return formatResponse(explainResult);
|
|
@@ -204,6 +287,7 @@ async function handleQuery(collection, args, objectIdMode = "auto") {
|
|
|
204
287
|
const cursor = collection.find(queryFilter, {
|
|
205
288
|
projection,
|
|
206
289
|
limit: limit || 100,
|
|
290
|
+
sort,
|
|
207
291
|
});
|
|
208
292
|
const results = await cursor.toArray();
|
|
209
293
|
return formatResponse(results);
|
|
@@ -1,29 +1,180 @@
|
|
|
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
|
+
}
|
|
1
120
|
export async function handleReadResourceRequest({ request, client, db, isReadOnlyMode, }) {
|
|
2
121
|
const url = new URL(request.params.uri);
|
|
3
122
|
const collectionName = url.pathname.replace(/^\//, "");
|
|
4
123
|
try {
|
|
5
124
|
const collection = db.collection(collectionName);
|
|
6
|
-
|
|
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
|
|
7
143
|
const indexes = await collection.indexes();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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)';
|
|
20
164
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
};
|
|
27
178
|
return {
|
|
28
179
|
contents: [
|
|
29
180
|
{
|
|
@@ -26,6 +26,12 @@ Example queries:
|
|
|
26
26
|
3. Find documents with existing email:
|
|
27
27
|
{ "email": { "$exists": true } }
|
|
28
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
|
+
|
|
29
35
|
Use these patterns to construct MongoDB queries.`,
|
|
30
36
|
},
|
|
31
37
|
],
|
package/build/schemas/tools.js
CHANGED
|
@@ -11,7 +11,10 @@ export async function handleListToolsRequest({ request, client, db, isReadOnlyMo
|
|
|
11
11
|
type: "string",
|
|
12
12
|
description: "Name of the collection to query",
|
|
13
13
|
},
|
|
14
|
-
filter: {
|
|
14
|
+
filter: {
|
|
15
|
+
type: "object",
|
|
16
|
+
description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
|
|
17
|
+
},
|
|
15
18
|
projection: {
|
|
16
19
|
type: "object",
|
|
17
20
|
description: "Fields to include/exclude",
|
|
@@ -48,7 +51,7 @@ export async function handleListToolsRequest({ request, client, db, isReadOnlyMo
|
|
|
48
51
|
},
|
|
49
52
|
pipeline: {
|
|
50
53
|
type: "array",
|
|
51
|
-
description: "Aggregation pipeline stages",
|
|
54
|
+
description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
|
|
52
55
|
items: {
|
|
53
56
|
type: "object",
|
|
54
57
|
},
|
|
@@ -80,7 +83,7 @@ export async function handleListToolsRequest({ request, client, db, isReadOnlyMo
|
|
|
80
83
|
},
|
|
81
84
|
filter: {
|
|
82
85
|
type: "object",
|
|
83
|
-
description: "Filter to select documents to update",
|
|
86
|
+
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",
|
|
84
87
|
},
|
|
85
88
|
update: {
|
|
86
89
|
type: "object",
|
package/build/server.js
CHANGED
|
@@ -15,7 +15,7 @@ import { handleCompletionRequest } from "./schemas/completion.js";
|
|
|
15
15
|
export function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
16
16
|
const server = new Server({
|
|
17
17
|
name: "mongodb",
|
|
18
|
-
version: "1.
|
|
18
|
+
version: "1.3.0",
|
|
19
19
|
...options,
|
|
20
20
|
}, {
|
|
21
21
|
capabilities: {
|