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 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
- docker run -e MCP_MONGODB_URI="mongodb://muhammed:kilic@localhost:27017/database" \
128
- -e MCP_MONGODB_READONLY="true" \
129
- mcp-mongo-server
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
 
@@ -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
- return filter;
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
- if (objectIdMode === "force" ||
145
- (objectIdMode === "auto" && isObjectIdField(key))) {
146
- result[key] = value.map((item) => typeof item === "string" && isObjectIdString(item)
147
- ? new ObjectId(item)
148
- : item);
149
- }
150
- else {
151
- result[key] = value;
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
- const sample = await collection.findOne({});
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
- const schema = sample
9
- ? {
10
- type: "collection",
11
- name: collectionName,
12
- fields: Object.entries(sample).map(([key, value]) => ({
13
- name: key,
14
- type: typeof value,
15
- })),
16
- indexes: indexes.map((idx) => ({
17
- name: idx.name,
18
- keys: idx.key,
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
- type: "collection",
23
- name: collectionName,
24
- fields: [],
25
- indexes: [],
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
  ],
@@ -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: { type: "object", description: "MongoDB query 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.2.0",
18
+ version: "1.3.0",
19
19
  ...options,
20
20
  }, {
21
21
  capabilities: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-mongo-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A Model Context Protocol server for MongoDB connections",
5
5
  "private": false,
6
6
  "type": "module",