mcp-mongo-server 2.1.0 → 3.0.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 +97 -20
- package/build/index.js +357 -198
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -4,54 +4,131 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|

|
|
7
|
-
[](https://smithery.ai/server/mcp-mongo-server)
|
|
8
|
-
[](https://mseep.ai/app/e274a3dd-7fe6-4440-8c43-043bae668251)
|
|
9
7
|
|
|
10
|
-
A Model Context Protocol server that
|
|
8
|
+
A Model Context Protocol (MCP) server that lets AI assistants work with your MongoDB databases. It exposes your collections, infers their schemas, and runs queries, aggregations, and writes through a standard interface — so tools like Claude Desktop and Cursor can read and reason about your data.
|
|
11
9
|
|
|
12
10
|
## Demo
|
|
13
11
|
|
|
14
12
|
[](https://www.youtube.com/watch?v=FI-oE_voCpA)
|
|
15
13
|
|
|
14
|
+
## Why use it
|
|
15
|
+
|
|
16
|
+
- **Talk to your database in plain language** — the assistant discovers your collections and their shape automatically.
|
|
17
|
+
- **Safe by default** — turn on read-only mode to let an assistant explore without any risk of changing data.
|
|
18
|
+
- **Works everywhere** — connects to standalone, replica set, sharded, and Atlas deployments, over plain or TLS connections.
|
|
19
|
+
|
|
16
20
|
## Key Features
|
|
17
21
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **Schema Inference**
|
|
21
|
-
- **Query & Aggregation**
|
|
22
|
-
- **Write Operations**
|
|
23
|
-
- **
|
|
22
|
+
- **Read-Only Mode** — blocks every write path (insert, update, index creation, and aggregation stages like `$out`/`$merge` that could modify data).
|
|
23
|
+
- **Smart ObjectId Handling** — configurable `auto`/`none`/`force` conversion of 24-character hex strings to ObjectIds.
|
|
24
|
+
- **Schema Inference** — automatic collection schema detection from document samples.
|
|
25
|
+
- **Query & Aggregation** — full query and aggregation pipeline support, with optional `explain` plans.
|
|
26
|
+
- **Write Operations** — insert, update, and index creation (when read-only mode is off).
|
|
27
|
+
- **Time Conversion** — a `convertTime` helper turns Unix timestamps and date strings into UTC/GMT/ISO, so date queries stay unambiguous across timezones.
|
|
28
|
+
- **Progress & Cancellation** — long operations report progress and can be cancelled mid-flight.
|
|
29
|
+
- **Two Transports** — run locally over stdio, or expose an HTTP endpoint for remote access.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
- Node.js 20 or newer
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Point the server at your database — no install step needed:
|
|
26
38
|
|
|
27
39
|
```bash
|
|
28
|
-
npx -y mcp-mongo-server mongodb://localhost:27017/
|
|
40
|
+
npx -y mcp-mongo-server mongodb://localhost:27017/mydatabase
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Explore safely, without any chance of changing data:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx -y mcp-mongo-server mongodb://localhost:27017/mydatabase --read-only
|
|
29
47
|
```
|
|
30
48
|
|
|
31
49
|
## Usage
|
|
32
50
|
|
|
51
|
+
### Local (stdio)
|
|
52
|
+
|
|
53
|
+
This is the default, used by Claude Desktop, Cursor, and other local clients:
|
|
54
|
+
|
|
33
55
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
npx -y mcp-mongo-server "mongodb://user:pass@localhost:27017/mydatabase"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Remote (HTTP)
|
|
60
|
+
|
|
61
|
+
Expose an HTTP endpoint at `/mcp` for remote or multi-client access:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx -y mcp-mongo-server "mongodb://user:pass@localhost:27017/mydatabase" --transport http --port 3001
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
By default, only requests without a browser `Origin` (CLIs, IDEs) and requests
|
|
68
|
+
from `localhost` are accepted; everything else is rejected with `403` to guard
|
|
69
|
+
against DNS-rebinding attacks. Allow specific browser origins with
|
|
70
|
+
`--allowed-origins`:
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
npx -y mcp-mongo-server mongodb
|
|
72
|
+
```bash
|
|
73
|
+
npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-origins "https://app.example.com"
|
|
39
74
|
```
|
|
40
75
|
|
|
76
|
+
### Options
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `--read-only`, `-r` | Block all write operations |
|
|
81
|
+
| `--allow-cross-db` | Allow aggregation `$out`/`$merge`/`$lookup` to target other databases (off by default) |
|
|
82
|
+
| `--allow-server-js` | Allow server-side JavaScript operators `$function`/`$where`/`$accumulator` (off by default) |
|
|
83
|
+
| `--transport`, `-t` | `stdio` (default) or `http` |
|
|
84
|
+
| `--port`, `-p` | HTTP port (default `3001`) |
|
|
85
|
+
| `--allowed-origins` | Comma-separated browser origins to allow in HTTP mode |
|
|
86
|
+
| `--json-limit` | Max HTTP request body size (default `10mb`) |
|
|
87
|
+
| `--auth-token` | Require `Authorization: Bearer <token>` on the HTTP endpoint |
|
|
88
|
+
|
|
41
89
|
### Environment Variables
|
|
42
90
|
|
|
43
91
|
| Variable | Description |
|
|
44
92
|
|----------|-------------|
|
|
45
|
-
| `MCP_MONGODB_URI` | MongoDB connection URI |
|
|
93
|
+
| `MCP_MONGODB_URI` | MongoDB connection URI (alternative to the argument) |
|
|
46
94
|
| `MCP_MONGODB_READONLY` | Enable read-only mode (`"true"`) |
|
|
95
|
+
| `MCP_MONGODB_ALLOW_CROSS_DB` | Allow cross-database aggregation stages (`"true"`) |
|
|
96
|
+
| `MCP_MONGODB_ALLOW_SERVER_JS` | Allow server-side JavaScript operators (`"true"`) |
|
|
97
|
+
| `MCP_PORT` | HTTP port |
|
|
98
|
+
| `MCP_HTTP_ALLOWED_ORIGINS` | Comma-separated browser origins to allow in HTTP mode |
|
|
99
|
+
| `MCP_HTTP_JSON_LIMIT` | Max HTTP request body size (default `10mb`) |
|
|
100
|
+
| `MCP_HTTP_AUTH_TOKEN` | Bearer token required to access the HTTP endpoint |
|
|
101
|
+
|
|
102
|
+
## Security
|
|
103
|
+
|
|
104
|
+
**Database scope.** The server operates on the database in your connection
|
|
105
|
+
string. Aggregation stages that reach another database (`$out`, `$merge`,
|
|
106
|
+
`$lookup` with an explicit `db`) are rejected by default, so a pipeline can't
|
|
107
|
+
quietly read from or write to databases you didn't point it at. Enable them
|
|
108
|
+
with `--allow-cross-db` if you need them.
|
|
109
|
+
|
|
110
|
+
**Server-side JavaScript.** The aggregation operators `$function`, `$where`,
|
|
111
|
+
and `$accumulator` run arbitrary JavaScript on the MongoDB server. They are
|
|
112
|
+
rejected by default; enable them with `--allow-server-js` if you trust the
|
|
113
|
+
pipelines being run.
|
|
114
|
+
|
|
115
|
+
**Read-only mode** blocks every write path, including aggregation stages that
|
|
116
|
+
write or run server-side JavaScript (`$out`, `$merge`, `$function`, `$where`,
|
|
117
|
+
`$accumulator`).
|
|
118
|
+
|
|
119
|
+
**Least privilege.** Application-level checks only go so far — the strongest
|
|
120
|
+
guarantee comes from the database. Connect with a MongoDB user scoped to just
|
|
121
|
+
the database you need, with read-only permissions when the assistant only needs
|
|
122
|
+
to explore. That way the database itself enforces the boundary, as defense in
|
|
123
|
+
depth.
|
|
47
124
|
|
|
48
125
|
## Documentation
|
|
49
126
|
|
|
50
|
-
- [Integration Guide](docs/integration.md)
|
|
51
|
-
- [Available Tools](docs/tools.md)
|
|
52
|
-
- [Development](docs/development.md)
|
|
127
|
+
- [Integration Guide](docs/integration.md) — Claude Desktop, Windsurf, Cursor, Docker
|
|
128
|
+
- [Available Tools](docs/tools.md) — query, aggregate, update, insert, and more
|
|
129
|
+
- [Development](docs/development.md) — setup, scripts, and debugging
|
|
53
130
|
- [Contributing](CONTRIBUTING.md)
|
|
54
131
|
|
|
55
132
|
## License
|
|
56
133
|
|
|
57
|
-
MIT
|
|
134
|
+
MIT — see [LICENSE](LICENSE) for details.
|
package/build/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { timingSafeEqual } from "crypto";
|
|
5
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
6
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
7
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
8
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/express";
|
|
7
9
|
|
|
8
10
|
// src/mongo.ts
|
|
9
|
-
import { MongoClient
|
|
11
|
+
import { MongoClient } from "mongodb";
|
|
10
12
|
async function connectToMongoDB(url, readOnly) {
|
|
11
13
|
try {
|
|
12
|
-
const
|
|
13
|
-
const client = new MongoClient(url, options);
|
|
14
|
+
const client = new MongoClient(url);
|
|
14
15
|
await client.connect();
|
|
15
16
|
const db = client.db();
|
|
16
17
|
console.warn(`Connected to MongoDB database: ${db.databaseName}`);
|
|
@@ -32,28 +33,10 @@ async function connectToMongoDB(url, readOnly) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// src/server.ts
|
|
35
|
-
import {
|
|
36
|
-
InMemoryTaskMessageQueue,
|
|
37
|
-
InMemoryTaskStore
|
|
38
|
-
} from "@modelcontextprotocol/sdk/experimental/tasks";
|
|
39
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
40
|
-
import {
|
|
41
|
-
CallToolRequestSchema,
|
|
42
|
-
CompleteRequestSchema,
|
|
43
|
-
GetPromptRequestSchema,
|
|
44
|
-
ListPromptsRequestSchema,
|
|
45
|
-
ListResourcesRequestSchema,
|
|
46
|
-
ListResourceTemplatesRequestSchema,
|
|
47
|
-
ListToolsRequestSchema,
|
|
48
|
-
PingRequestSchema,
|
|
49
|
-
ReadResourceRequestSchema
|
|
50
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
36
|
+
import { Server } from "@modelcontextprotocol/server";
|
|
51
37
|
|
|
52
38
|
// src/schemas/call.ts
|
|
53
|
-
import {
|
|
54
|
-
ErrorCode,
|
|
55
|
-
McpError
|
|
56
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
39
|
+
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
57
40
|
import { ObjectId } from "mongodb";
|
|
58
41
|
var COLLECTION_OPERATIONS = [
|
|
59
42
|
"query",
|
|
@@ -64,14 +47,71 @@ var COLLECTION_OPERATIONS = [
|
|
|
64
47
|
"count"
|
|
65
48
|
];
|
|
66
49
|
var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
|
|
50
|
+
var MAX_OBJECT_DEPTH = 100;
|
|
51
|
+
var WRITE_AGG_STAGES = /* @__PURE__ */ new Set(["$out", "$merge"]);
|
|
52
|
+
var SERVER_JS_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
53
|
+
"$function",
|
|
54
|
+
"$accumulator",
|
|
55
|
+
"$where"
|
|
56
|
+
]);
|
|
57
|
+
var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
58
|
+
...WRITE_AGG_STAGES,
|
|
59
|
+
...SERVER_JS_AGG_OPERATORS
|
|
60
|
+
]);
|
|
61
|
+
function findAggOperator(value, operators, depth = 0) {
|
|
62
|
+
if (depth > MAX_OBJECT_DEPTH) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
for (const item of value) {
|
|
69
|
+
const found = findAggOperator(item, operators, depth + 1);
|
|
70
|
+
if (found) return found;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (value && typeof value === "object") {
|
|
75
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
76
|
+
if (operators.has(key)) {
|
|
77
|
+
return key;
|
|
78
|
+
}
|
|
79
|
+
const found = findAggOperator(nested, operators, depth + 1);
|
|
80
|
+
if (found) return found;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function findCrossDbTarget(pipeline, connectedDb) {
|
|
86
|
+
const foreignDb = (target) => {
|
|
87
|
+
if (target && typeof target === "object" && "db" in target) {
|
|
88
|
+
const db = target.db;
|
|
89
|
+
if (typeof db === "string" && db && db !== connectedDb) return db;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
for (const stage of pipeline) {
|
|
94
|
+
if (!stage || typeof stage !== "object") continue;
|
|
95
|
+
const s = stage;
|
|
96
|
+
const out = foreignDb(s.$out);
|
|
97
|
+
if (out) return out;
|
|
98
|
+
const merge = s.$merge;
|
|
99
|
+
const mergeInto = foreignDb(merge?.into ?? s.$merge);
|
|
100
|
+
if (mergeInto) return mergeInto;
|
|
101
|
+
const lookup = s.$lookup;
|
|
102
|
+
const lookupFrom = foreignDb(lookup?.from);
|
|
103
|
+
if (lookupFrom) return lookupFrom;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
67
107
|
async function handleCallToolRequest({
|
|
68
108
|
request,
|
|
69
109
|
client,
|
|
70
110
|
db,
|
|
71
111
|
isReadOnlyMode,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
allowCrossDb = false,
|
|
113
|
+
allowServerJs = false,
|
|
114
|
+
signal
|
|
75
115
|
}) {
|
|
76
116
|
const { name, arguments: args = {} } = request.params;
|
|
77
117
|
const operation = name;
|
|
@@ -101,46 +141,19 @@ async function handleCallToolRequest({
|
|
|
101
141
|
validateCollection(collection);
|
|
102
142
|
}
|
|
103
143
|
signal?.throwIfAborted();
|
|
104
|
-
if (taskStore) {
|
|
105
|
-
const task = await taskStore.createTask({
|
|
106
|
-
ttl: taskTtl ?? void 0
|
|
107
|
-
});
|
|
108
|
-
(async () => {
|
|
109
|
-
try {
|
|
110
|
-
const result = await executeOperation(
|
|
111
|
-
operation,
|
|
112
|
-
collection,
|
|
113
|
-
db,
|
|
114
|
-
isReadOnlyMode,
|
|
115
|
-
args,
|
|
116
|
-
objectIdMode,
|
|
117
|
-
signal
|
|
118
|
-
);
|
|
119
|
-
await taskStore.storeTaskResult(task.taskId, "completed", result);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
try {
|
|
122
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
123
|
-
await taskStore.storeTaskResult(task.taskId, "failed", {
|
|
124
|
-
content: [{ type: "text", text: message }],
|
|
125
|
-
isError: true
|
|
126
|
-
});
|
|
127
|
-
} catch {
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
})();
|
|
131
|
-
return { task };
|
|
132
|
-
}
|
|
133
144
|
return await executeOperation(
|
|
134
145
|
operation,
|
|
135
146
|
collection,
|
|
136
147
|
db,
|
|
137
148
|
isReadOnlyMode,
|
|
149
|
+
allowCrossDb,
|
|
150
|
+
allowServerJs,
|
|
138
151
|
args,
|
|
139
152
|
objectIdMode,
|
|
140
153
|
signal
|
|
141
154
|
);
|
|
142
155
|
} catch (error) {
|
|
143
|
-
if (error instanceof
|
|
156
|
+
if (error instanceof ProtocolError || signal?.aborted) {
|
|
144
157
|
throw error;
|
|
145
158
|
}
|
|
146
159
|
return toolExecutionError(
|
|
@@ -148,12 +161,20 @@ async function handleCallToolRequest({
|
|
|
148
161
|
);
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
|
-
async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
|
|
164
|
+
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
|
|
152
165
|
switch (operation) {
|
|
153
166
|
case "query":
|
|
154
167
|
return handleQuery(collection, args, objectIdMode, signal);
|
|
155
168
|
case "aggregate":
|
|
156
|
-
return handleAggregate(
|
|
169
|
+
return handleAggregate(
|
|
170
|
+
collection,
|
|
171
|
+
args,
|
|
172
|
+
objectIdMode,
|
|
173
|
+
isReadOnlyMode,
|
|
174
|
+
allowCrossDb,
|
|
175
|
+
allowServerJs,
|
|
176
|
+
signal
|
|
177
|
+
);
|
|
157
178
|
case "update":
|
|
158
179
|
return handleUpdate(collection, args, objectIdMode, signal);
|
|
159
180
|
case "serverInfo":
|
|
@@ -166,8 +187,10 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, args,
|
|
|
166
187
|
return handleCount(collection, args, objectIdMode, signal);
|
|
167
188
|
case "listCollections":
|
|
168
189
|
return handleListCollections(db, args, objectIdMode, signal);
|
|
190
|
+
case "convertTime":
|
|
191
|
+
return handleConvertTime(args);
|
|
169
192
|
default:
|
|
170
|
-
throw new
|
|
193
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
171
194
|
}
|
|
172
195
|
}
|
|
173
196
|
function validateOperation(operation) {
|
|
@@ -179,17 +202,18 @@ function validateOperation(operation) {
|
|
|
179
202
|
"insert",
|
|
180
203
|
"createIndex",
|
|
181
204
|
"count",
|
|
182
|
-
"listCollections"
|
|
205
|
+
"listCollections",
|
|
206
|
+
"convertTime"
|
|
183
207
|
];
|
|
184
208
|
if (!validOperations.includes(operation)) {
|
|
185
|
-
throw new
|
|
209
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
function validateCollection(collection) {
|
|
189
213
|
if (!collection.collectionName) {
|
|
190
214
|
throw new Error("Collection name cannot be empty");
|
|
191
215
|
}
|
|
192
|
-
if (collection.collectionName.startsWith("system.")) {
|
|
216
|
+
if (collection.collectionName.toLowerCase().startsWith("system.")) {
|
|
193
217
|
throw new Error("Access to system collections is not allowed");
|
|
194
218
|
}
|
|
195
219
|
}
|
|
@@ -236,7 +260,12 @@ function isObjectIdField(fieldName) {
|
|
|
236
260
|
const lowerFieldName = fieldName.toLowerCase();
|
|
237
261
|
return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
|
|
238
262
|
}
|
|
239
|
-
function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
263
|
+
function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
|
|
264
|
+
if (depth > MAX_OBJECT_DEPTH) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
240
269
|
if (objectIdMode === "none") {
|
|
241
270
|
const result2 = {};
|
|
242
271
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -263,7 +292,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
|
263
292
|
} else {
|
|
264
293
|
result2[key] = processObjectIdInFilter(
|
|
265
294
|
value,
|
|
266
|
-
"none"
|
|
295
|
+
"none",
|
|
296
|
+
depth + 1
|
|
267
297
|
);
|
|
268
298
|
}
|
|
269
299
|
} else {
|
|
@@ -305,7 +335,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
|
305
335
|
} else {
|
|
306
336
|
result[key] = processObjectIdInFilter(
|
|
307
337
|
value,
|
|
308
|
-
objectIdMode
|
|
338
|
+
objectIdMode,
|
|
339
|
+
depth + 1
|
|
309
340
|
);
|
|
310
341
|
}
|
|
311
342
|
} else {
|
|
@@ -318,7 +349,9 @@ function isObjectIdString(str) {
|
|
|
318
349
|
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
319
350
|
}
|
|
320
351
|
function isISODateString(str) {
|
|
321
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(
|
|
352
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
|
|
353
|
+
str
|
|
354
|
+
);
|
|
322
355
|
}
|
|
323
356
|
function formatResponse(data) {
|
|
324
357
|
return {
|
|
@@ -387,7 +420,7 @@ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
|
|
|
387
420
|
return handleError(error, "query", collection.collectionName);
|
|
388
421
|
}
|
|
389
422
|
}
|
|
390
|
-
async function handleAggregate(collection, args, objectIdMode = "auto", signal) {
|
|
423
|
+
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
|
|
391
424
|
if (!collection) {
|
|
392
425
|
throw new Error("Collection is required for aggregate operation");
|
|
393
426
|
}
|
|
@@ -395,6 +428,33 @@ async function handleAggregate(collection, args, objectIdMode = "auto", signal)
|
|
|
395
428
|
if (!Array.isArray(pipeline)) {
|
|
396
429
|
throw new Error("Pipeline must be an array");
|
|
397
430
|
}
|
|
431
|
+
if (isReadOnlyMode) {
|
|
432
|
+
const forbidden = findAggOperator(
|
|
433
|
+
pipeline,
|
|
434
|
+
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
435
|
+
);
|
|
436
|
+
if (forbidden) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!allowServerJs) {
|
|
443
|
+
const js = findAggOperator(pipeline, SERVER_JS_AGG_OPERATORS);
|
|
444
|
+
if (js) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Server-side JavaScript operator '${js}' is not allowed. Enable it with --allow-server-js or MCP_MONGODB_ALLOW_SERVER_JS=true.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!allowCrossDb) {
|
|
451
|
+
const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
|
|
452
|
+
if (foreignDb) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
398
458
|
const processedPipeline = pipeline.map((stage) => {
|
|
399
459
|
if (typeof stage === "object" && stage !== null) {
|
|
400
460
|
return processObjectIdInFilter(
|
|
@@ -488,32 +548,27 @@ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
|
488
548
|
try {
|
|
489
549
|
signal?.throwIfAborted();
|
|
490
550
|
const buildInfo = await db.command({ buildInfo: 1 });
|
|
491
|
-
let serverStatus = null;
|
|
492
|
-
if (includeDebugInfo) {
|
|
493
|
-
signal?.throwIfAborted();
|
|
494
|
-
serverStatus = await db.command({ serverStatus: 1 });
|
|
495
|
-
}
|
|
496
551
|
const serverInfo = {
|
|
497
552
|
version: buildInfo.version,
|
|
498
553
|
gitVersion: buildInfo.gitVersion,
|
|
499
|
-
modules: buildInfo.modules,
|
|
500
|
-
allocator: buildInfo.allocator,
|
|
501
|
-
javascriptEngine: buildInfo.javascriptEngine,
|
|
502
|
-
sysInfo: buildInfo.sysInfo,
|
|
503
554
|
storageEngines: buildInfo.storageEngines,
|
|
504
|
-
debug: buildInfo.debug,
|
|
505
555
|
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
506
|
-
openssl: buildInfo.openssl,
|
|
507
|
-
buildEnvironment: buildInfo.buildEnvironment,
|
|
508
556
|
bits: buildInfo.bits,
|
|
509
557
|
ok: buildInfo.ok,
|
|
510
|
-
status: {},
|
|
511
558
|
connectionInfo: {
|
|
512
|
-
readOnlyMode: isReadOnlyMode
|
|
513
|
-
readPreference: isReadOnlyMode ? "secondary" : "primary"
|
|
559
|
+
readOnlyMode: isReadOnlyMode
|
|
514
560
|
}
|
|
515
561
|
};
|
|
516
|
-
if (
|
|
562
|
+
if (includeDebugInfo) {
|
|
563
|
+
signal?.throwIfAborted();
|
|
564
|
+
const serverStatus = await db.command({ serverStatus: 1 });
|
|
565
|
+
serverInfo.modules = buildInfo.modules;
|
|
566
|
+
serverInfo.allocator = buildInfo.allocator;
|
|
567
|
+
serverInfo.javascriptEngine = buildInfo.javascriptEngine;
|
|
568
|
+
serverInfo.sysInfo = buildInfo.sysInfo;
|
|
569
|
+
serverInfo.debug = buildInfo.debug;
|
|
570
|
+
serverInfo.openssl = buildInfo.openssl;
|
|
571
|
+
serverInfo.buildEnvironment = buildInfo.buildEnvironment;
|
|
517
572
|
serverInfo.status = {
|
|
518
573
|
host: serverStatus.host,
|
|
519
574
|
version: serverStatus.version,
|
|
@@ -614,15 +669,14 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
|
|
|
614
669
|
commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
|
|
615
670
|
};
|
|
616
671
|
signal?.throwIfAborted();
|
|
617
|
-
const
|
|
672
|
+
const createdIndexes = await collection.createIndexes(
|
|
618
673
|
processedIndexes,
|
|
619
674
|
indexOptions
|
|
620
675
|
);
|
|
621
676
|
return formatResponse({
|
|
622
|
-
acknowledged:
|
|
623
|
-
createdIndexes
|
|
624
|
-
|
|
625
|
-
numIndexesAfter: result.numIndexesAfter
|
|
677
|
+
acknowledged: true,
|
|
678
|
+
createdIndexes,
|
|
679
|
+
indexCount: createdIndexes.length
|
|
626
680
|
});
|
|
627
681
|
} catch (error) {
|
|
628
682
|
return handleError(error, "create indexes", collection.collectionName);
|
|
@@ -658,6 +712,48 @@ async function handleCount(collection, args, objectIdMode = "auto", signal) {
|
|
|
658
712
|
return handleError(error, "count documents", collection.collectionName);
|
|
659
713
|
}
|
|
660
714
|
}
|
|
715
|
+
function handleConvertTime(args) {
|
|
716
|
+
const { input } = args;
|
|
717
|
+
let date;
|
|
718
|
+
if (input === void 0 || input === null || input === "") {
|
|
719
|
+
date = /* @__PURE__ */ new Date();
|
|
720
|
+
} else if (typeof input === "number") {
|
|
721
|
+
date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
|
|
722
|
+
} else if (typeof input === "string") {
|
|
723
|
+
const trimmed = input.trim();
|
|
724
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
725
|
+
const n = Number(trimmed);
|
|
726
|
+
date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
|
|
727
|
+
} else {
|
|
728
|
+
date = new Date(trimmed);
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
throw new Error(
|
|
732
|
+
"convertTime 'input' must be a Unix timestamp (number) or a date string"
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (Number.isNaN(date.getTime())) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`Could not interpret '${String(input)}' as a date or Unix timestamp`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
741
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
742
|
+
const absMinutes = Math.abs(offsetMinutes);
|
|
743
|
+
const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
|
|
744
|
+
2,
|
|
745
|
+
"0"
|
|
746
|
+
)}:${String(absMinutes % 60).padStart(2, "0")}`;
|
|
747
|
+
return formatResponse({
|
|
748
|
+
iso: date.toISOString(),
|
|
749
|
+
utc: date.toUTCString(),
|
|
750
|
+
unixSeconds: Math.floor(date.getTime() / 1e3),
|
|
751
|
+
unixMillis: date.getTime(),
|
|
752
|
+
serverLocal: date.toString(),
|
|
753
|
+
serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
754
|
+
serverUtcOffset: offsetLabel
|
|
755
|
+
});
|
|
756
|
+
}
|
|
661
757
|
async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
662
758
|
const { nameOnly, filter } = args;
|
|
663
759
|
const skip = args.skip || 0;
|
|
@@ -936,6 +1032,10 @@ ${JSON.stringify(sampleDocs, null, 2)}`
|
|
|
936
1032
|
}
|
|
937
1033
|
|
|
938
1034
|
// src/schemas/resource.ts
|
|
1035
|
+
import {
|
|
1036
|
+
ProtocolError as ProtocolError2,
|
|
1037
|
+
ProtocolErrorCode as ProtocolErrorCode2
|
|
1038
|
+
} from "@modelcontextprotocol/server";
|
|
939
1039
|
import { ObjectId as ObjectId2 } from "mongodb";
|
|
940
1040
|
function detectMongoType(value) {
|
|
941
1041
|
if (value === null) return "null";
|
|
@@ -1033,8 +1133,35 @@ async function handleReadResourceRequest({
|
|
|
1033
1133
|
signal,
|
|
1034
1134
|
sendProgress
|
|
1035
1135
|
}) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1136
|
+
let collectionName;
|
|
1137
|
+
try {
|
|
1138
|
+
const url = new URL(request.params.uri);
|
|
1139
|
+
if (url.protocol !== "mongodb:") {
|
|
1140
|
+
throw new ProtocolError2(
|
|
1141
|
+
ProtocolErrorCode2.InvalidParams,
|
|
1142
|
+
`Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
collectionName = url.pathname.replace(/^\//, "");
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (error instanceof ProtocolError2) throw error;
|
|
1148
|
+
throw new ProtocolError2(
|
|
1149
|
+
ProtocolErrorCode2.InvalidParams,
|
|
1150
|
+
`Invalid resource URI: ${request.params.uri}`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
if (!collectionName) {
|
|
1154
|
+
throw new ProtocolError2(
|
|
1155
|
+
ProtocolErrorCode2.InvalidParams,
|
|
1156
|
+
"Resource URI is missing a collection name"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
if (collectionName.startsWith("system.")) {
|
|
1160
|
+
throw new ProtocolError2(
|
|
1161
|
+
ProtocolErrorCode2.InvalidParams,
|
|
1162
|
+
"Access to system collections is not allowed"
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1038
1165
|
try {
|
|
1039
1166
|
const collection = db.collection(collectionName);
|
|
1040
1167
|
const sampleSize = 100;
|
|
@@ -1202,7 +1329,6 @@ async function handleListToolsRequest({
|
|
|
1202
1329
|
{
|
|
1203
1330
|
name: "query",
|
|
1204
1331
|
description: "Execute a MongoDB query with optional execution plan analysis",
|
|
1205
|
-
execution: { taskSupport: "optional" },
|
|
1206
1332
|
inputSchema: {
|
|
1207
1333
|
type: "object",
|
|
1208
1334
|
properties: {
|
|
@@ -1228,6 +1354,10 @@ async function handleListToolsRequest({
|
|
|
1228
1354
|
description: "Number of documents to skip before returning results",
|
|
1229
1355
|
default: 0
|
|
1230
1356
|
},
|
|
1357
|
+
sort: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
|
|
1360
|
+
},
|
|
1231
1361
|
explain: {
|
|
1232
1362
|
type: "string",
|
|
1233
1363
|
description: "Optional: Get query execution information",
|
|
@@ -1246,7 +1376,6 @@ async function handleListToolsRequest({
|
|
|
1246
1376
|
{
|
|
1247
1377
|
name: "aggregate",
|
|
1248
1378
|
description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
|
|
1249
|
-
execution: { taskSupport: "optional" },
|
|
1250
1379
|
inputSchema: {
|
|
1251
1380
|
type: "object",
|
|
1252
1381
|
properties: {
|
|
@@ -1279,7 +1408,6 @@ async function handleListToolsRequest({
|
|
|
1279
1408
|
{
|
|
1280
1409
|
name: "update",
|
|
1281
1410
|
description: "Update documents in a MongoDB collection",
|
|
1282
|
-
execution: { taskSupport: "optional" },
|
|
1283
1411
|
inputSchema: {
|
|
1284
1412
|
type: "object",
|
|
1285
1413
|
properties: {
|
|
@@ -1316,7 +1444,6 @@ async function handleListToolsRequest({
|
|
|
1316
1444
|
{
|
|
1317
1445
|
name: "serverInfo",
|
|
1318
1446
|
description: "Get MongoDB server information including version, storage engine, and other details",
|
|
1319
|
-
execution: { taskSupport: "optional" },
|
|
1320
1447
|
inputSchema: {
|
|
1321
1448
|
type: "object",
|
|
1322
1449
|
properties: {
|
|
@@ -1330,7 +1457,6 @@ async function handleListToolsRequest({
|
|
|
1330
1457
|
{
|
|
1331
1458
|
name: "insert",
|
|
1332
1459
|
description: "Insert one or more documents into a MongoDB collection",
|
|
1333
|
-
execution: { taskSupport: "optional" },
|
|
1334
1460
|
inputSchema: {
|
|
1335
1461
|
type: "object",
|
|
1336
1462
|
properties: {
|
|
@@ -1368,7 +1494,6 @@ async function handleListToolsRequest({
|
|
|
1368
1494
|
{
|
|
1369
1495
|
name: "createIndex",
|
|
1370
1496
|
description: "Create one or more indexes on a MongoDB collection",
|
|
1371
|
-
execution: { taskSupport: "optional" },
|
|
1372
1497
|
inputSchema: {
|
|
1373
1498
|
type: "object",
|
|
1374
1499
|
properties: {
|
|
@@ -1435,7 +1560,6 @@ async function handleListToolsRequest({
|
|
|
1435
1560
|
{
|
|
1436
1561
|
name: "count",
|
|
1437
1562
|
description: "Count documents in a collection matching a query",
|
|
1438
|
-
execution: { taskSupport: "optional" },
|
|
1439
1563
|
inputSchema: {
|
|
1440
1564
|
type: "object",
|
|
1441
1565
|
properties: {
|
|
@@ -1466,7 +1590,6 @@ async function handleListToolsRequest({
|
|
|
1466
1590
|
{
|
|
1467
1591
|
name: "listCollections",
|
|
1468
1592
|
description: "List all collections in the MongoDB database",
|
|
1469
|
-
execution: { taskSupport: "optional" },
|
|
1470
1593
|
inputSchema: {
|
|
1471
1594
|
type: "object",
|
|
1472
1595
|
properties: {
|
|
@@ -1496,29 +1619,39 @@ async function handleListToolsRequest({
|
|
|
1496
1619
|
}
|
|
1497
1620
|
}
|
|
1498
1621
|
}
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
name: "convertTime",
|
|
1625
|
+
description: "Convert a Unix timestamp or date string to multiple formats (UTC ISO 8601, GMT, Unix seconds/milliseconds) and report the server's current time and timezone. Useful for building unambiguous date queries when the server and user are in different timezones. Omit 'input' to get the current time.",
|
|
1626
|
+
inputSchema: {
|
|
1627
|
+
type: "object",
|
|
1628
|
+
properties: {
|
|
1629
|
+
input: {
|
|
1630
|
+
type: "string",
|
|
1631
|
+
description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1499
1635
|
}
|
|
1500
1636
|
]
|
|
1501
1637
|
};
|
|
1502
1638
|
}
|
|
1503
1639
|
|
|
1504
1640
|
// src/server.ts
|
|
1505
|
-
function createSendProgress(
|
|
1506
|
-
const progressToken = extra._meta?.progressToken;
|
|
1641
|
+
function createSendProgress(progressToken, notify) {
|
|
1507
1642
|
if (progressToken === void 0) return void 0;
|
|
1508
|
-
return (progress, total, message) =>
|
|
1643
|
+
return (progress, total, message) => notify({
|
|
1509
1644
|
method: "notifications/progress",
|
|
1510
1645
|
params: { progressToken, progress, total, message }
|
|
1511
1646
|
});
|
|
1512
1647
|
}
|
|
1513
|
-
function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
1514
|
-
const taskStore = new InMemoryTaskStore();
|
|
1515
|
-
const taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
1648
|
+
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
|
|
1516
1649
|
const server = new Server(
|
|
1517
1650
|
{
|
|
1518
1651
|
name: "mongodb",
|
|
1519
1652
|
title: "MongoDB MCP Server",
|
|
1520
|
-
version: "
|
|
1521
|
-
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications,
|
|
1653
|
+
version: "3.0.0",
|
|
1654
|
+
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
|
|
1522
1655
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1523
1656
|
...options
|
|
1524
1657
|
},
|
|
@@ -1527,110 +1660,109 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
|
1527
1660
|
completions: {},
|
|
1528
1661
|
resources: {},
|
|
1529
1662
|
tools: {},
|
|
1530
|
-
prompts: {}
|
|
1531
|
-
tasks: {
|
|
1532
|
-
list: {},
|
|
1533
|
-
cancel: {},
|
|
1534
|
-
requests: { tools: { call: {} } }
|
|
1535
|
-
}
|
|
1663
|
+
prompts: {}
|
|
1536
1664
|
},
|
|
1537
|
-
taskStore,
|
|
1538
|
-
taskMessageQueue,
|
|
1539
1665
|
...options
|
|
1540
1666
|
}
|
|
1541
1667
|
);
|
|
1542
1668
|
server.setRequestHandler(
|
|
1543
|
-
|
|
1544
|
-
(request,
|
|
1669
|
+
"ping",
|
|
1670
|
+
(request, ctx) => handlePingRequest({
|
|
1545
1671
|
request,
|
|
1546
1672
|
client,
|
|
1547
1673
|
db,
|
|
1548
1674
|
isReadOnlyMode,
|
|
1549
|
-
signal:
|
|
1675
|
+
signal: ctx.mcpReq.signal
|
|
1550
1676
|
})
|
|
1551
1677
|
);
|
|
1552
1678
|
server.setRequestHandler(
|
|
1553
|
-
|
|
1554
|
-
(request,
|
|
1679
|
+
"resources/list",
|
|
1680
|
+
(request, ctx) => handleListResourcesRequest({
|
|
1555
1681
|
request,
|
|
1556
1682
|
client,
|
|
1557
1683
|
db,
|
|
1558
1684
|
isReadOnlyMode,
|
|
1559
|
-
signal:
|
|
1685
|
+
signal: ctx.mcpReq.signal
|
|
1560
1686
|
})
|
|
1561
1687
|
);
|
|
1562
1688
|
server.setRequestHandler(
|
|
1563
|
-
|
|
1564
|
-
(request,
|
|
1689
|
+
"resources/read",
|
|
1690
|
+
(request, ctx) => handleReadResourceRequest({
|
|
1565
1691
|
request,
|
|
1566
1692
|
client,
|
|
1567
1693
|
db,
|
|
1568
1694
|
isReadOnlyMode,
|
|
1569
|
-
signal:
|
|
1570
|
-
sendProgress: createSendProgress(
|
|
1695
|
+
signal: ctx.mcpReq.signal,
|
|
1696
|
+
sendProgress: createSendProgress(
|
|
1697
|
+
request.params._meta?.progressToken,
|
|
1698
|
+
ctx.mcpReq.notify
|
|
1699
|
+
)
|
|
1571
1700
|
})
|
|
1572
1701
|
);
|
|
1573
1702
|
server.setRequestHandler(
|
|
1574
|
-
|
|
1575
|
-
(request,
|
|
1703
|
+
"tools/list",
|
|
1704
|
+
(request, ctx) => handleListToolsRequest({
|
|
1576
1705
|
request,
|
|
1577
1706
|
client,
|
|
1578
1707
|
db,
|
|
1579
1708
|
isReadOnlyMode,
|
|
1580
|
-
signal:
|
|
1709
|
+
signal: ctx.mcpReq.signal
|
|
1581
1710
|
})
|
|
1582
1711
|
);
|
|
1583
|
-
server.setRequestHandler(
|
|
1584
|
-
|
|
1585
|
-
|
|
1712
|
+
server.setRequestHandler(
|
|
1713
|
+
"tools/call",
|
|
1714
|
+
(request, ctx) => handleCallToolRequest({
|
|
1586
1715
|
request,
|
|
1587
1716
|
client,
|
|
1588
1717
|
db,
|
|
1589
1718
|
isReadOnlyMode,
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
})
|
|
1594
|
-
|
|
1719
|
+
allowCrossDb,
|
|
1720
|
+
allowServerJs,
|
|
1721
|
+
signal: ctx.mcpReq.signal
|
|
1722
|
+
})
|
|
1723
|
+
);
|
|
1595
1724
|
server.setRequestHandler(
|
|
1596
|
-
|
|
1597
|
-
(request,
|
|
1725
|
+
"prompts/list",
|
|
1726
|
+
(request, ctx) => handleListPromptsRequest({
|
|
1598
1727
|
request,
|
|
1599
1728
|
client,
|
|
1600
1729
|
db,
|
|
1601
1730
|
isReadOnlyMode,
|
|
1602
|
-
signal:
|
|
1731
|
+
signal: ctx.mcpReq.signal
|
|
1603
1732
|
})
|
|
1604
1733
|
);
|
|
1605
1734
|
server.setRequestHandler(
|
|
1606
|
-
|
|
1607
|
-
(request,
|
|
1735
|
+
"prompts/get",
|
|
1736
|
+
(request, ctx) => handleGetPromptRequest({
|
|
1608
1737
|
request,
|
|
1609
1738
|
client,
|
|
1610
1739
|
db,
|
|
1611
1740
|
isReadOnlyMode,
|
|
1612
|
-
signal:
|
|
1613
|
-
sendProgress: createSendProgress(
|
|
1741
|
+
signal: ctx.mcpReq.signal,
|
|
1742
|
+
sendProgress: createSendProgress(
|
|
1743
|
+
request.params._meta?.progressToken,
|
|
1744
|
+
ctx.mcpReq.notify
|
|
1745
|
+
)
|
|
1614
1746
|
})
|
|
1615
1747
|
);
|
|
1616
1748
|
server.setRequestHandler(
|
|
1617
|
-
|
|
1618
|
-
(request,
|
|
1749
|
+
"resources/templates/list",
|
|
1750
|
+
(request, ctx) => handleListResourceTemplatesRequest({
|
|
1619
1751
|
request,
|
|
1620
1752
|
client,
|
|
1621
1753
|
db,
|
|
1622
1754
|
isReadOnlyMode,
|
|
1623
|
-
signal:
|
|
1755
|
+
signal: ctx.mcpReq.signal
|
|
1624
1756
|
})
|
|
1625
1757
|
);
|
|
1626
1758
|
server.setRequestHandler(
|
|
1627
|
-
|
|
1628
|
-
(request,
|
|
1759
|
+
"completion/complete",
|
|
1760
|
+
(request, ctx) => handleCompletionRequest({
|
|
1629
1761
|
request,
|
|
1630
1762
|
client,
|
|
1631
1763
|
db,
|
|
1632
1764
|
isReadOnlyMode,
|
|
1633
|
-
signal:
|
|
1765
|
+
signal: ctx.mcpReq.signal
|
|
1634
1766
|
})
|
|
1635
1767
|
);
|
|
1636
1768
|
return server;
|
|
@@ -1642,12 +1774,20 @@ async function main() {
|
|
|
1642
1774
|
const args = process.argv.slice(2);
|
|
1643
1775
|
let connectionUrl = "";
|
|
1644
1776
|
let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
|
|
1777
|
+
let allowCrossDb = process.env.MCP_MONGODB_ALLOW_CROSS_DB === "true" || false;
|
|
1778
|
+
let allowServerJs = process.env.MCP_MONGODB_ALLOW_SERVER_JS === "true" || false;
|
|
1645
1779
|
let transportMode = "stdio";
|
|
1646
1780
|
let port = Number(process.env.MCP_PORT) || 3001;
|
|
1647
1781
|
const allowedOrigins = (process.env.MCP_HTTP_ALLOWED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
1782
|
+
let jsonLimit = process.env.MCP_HTTP_JSON_LIMIT || "10mb";
|
|
1783
|
+
let authToken = process.env.MCP_HTTP_AUTH_TOKEN || "";
|
|
1648
1784
|
for (let i = 0; i < args.length; i++) {
|
|
1649
1785
|
if (args[i] === "--read-only" || args[i] === "-r") {
|
|
1650
1786
|
readOnlyMode = true;
|
|
1787
|
+
} else if (args[i] === "--allow-cross-db") {
|
|
1788
|
+
allowCrossDb = true;
|
|
1789
|
+
} else if (args[i] === "--allow-server-js") {
|
|
1790
|
+
allowServerJs = true;
|
|
1651
1791
|
} else if (args[i] === "--transport" || args[i] === "-t") {
|
|
1652
1792
|
const value = args[++i];
|
|
1653
1793
|
if (value !== "stdio" && value !== "http") {
|
|
@@ -1665,6 +1805,12 @@ async function main() {
|
|
|
1665
1805
|
allowedOrigins.push(
|
|
1666
1806
|
...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
|
|
1667
1807
|
);
|
|
1808
|
+
} else if (args[i] === "--json-limit") {
|
|
1809
|
+
const value = args[++i];
|
|
1810
|
+
if (value) jsonLimit = value;
|
|
1811
|
+
} else if (args[i] === "--auth-token") {
|
|
1812
|
+
const value = args[++i];
|
|
1813
|
+
if (value) authToken = value;
|
|
1668
1814
|
} else if (!connectionUrl) {
|
|
1669
1815
|
connectionUrl = args[i];
|
|
1670
1816
|
}
|
|
@@ -1701,9 +1847,25 @@ async function main() {
|
|
|
1701
1847
|
process.exit(1);
|
|
1702
1848
|
}
|
|
1703
1849
|
if (transportMode === "http") {
|
|
1704
|
-
await startHttpServer(
|
|
1850
|
+
await startHttpServer(
|
|
1851
|
+
client,
|
|
1852
|
+
db,
|
|
1853
|
+
isReadOnlyMode,
|
|
1854
|
+
allowCrossDb,
|
|
1855
|
+
allowServerJs,
|
|
1856
|
+
port,
|
|
1857
|
+
allowedOrigins,
|
|
1858
|
+
jsonLimit,
|
|
1859
|
+
authToken
|
|
1860
|
+
);
|
|
1705
1861
|
} else {
|
|
1706
|
-
await startStdioServer(
|
|
1862
|
+
await startStdioServer(
|
|
1863
|
+
client,
|
|
1864
|
+
db,
|
|
1865
|
+
isReadOnlyMode,
|
|
1866
|
+
allowCrossDb,
|
|
1867
|
+
allowServerJs
|
|
1868
|
+
);
|
|
1707
1869
|
}
|
|
1708
1870
|
} catch (error) {
|
|
1709
1871
|
console.error("Failed to connect to MongoDB:", error);
|
|
@@ -1713,10 +1875,10 @@ async function main() {
|
|
|
1713
1875
|
process.exit(1);
|
|
1714
1876
|
}
|
|
1715
1877
|
}
|
|
1716
|
-
async function startStdioServer(client, db, isReadOnlyMode) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1878
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
|
|
1879
|
+
serveStdio(
|
|
1880
|
+
() => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
|
|
1881
|
+
);
|
|
1720
1882
|
console.warn("Server connected successfully via stdio");
|
|
1721
1883
|
}
|
|
1722
1884
|
function isOriginAllowed(origin, allowedOrigins) {
|
|
@@ -1733,8 +1895,17 @@ function isOriginAllowed(origin, allowedOrigins) {
|
|
|
1733
1895
|
return false;
|
|
1734
1896
|
}
|
|
1735
1897
|
}
|
|
1736
|
-
|
|
1737
|
-
const
|
|
1898
|
+
function bearerTokenValid(authorization, expected) {
|
|
1899
|
+
const prefix = "Bearer ";
|
|
1900
|
+
if (!authorization?.startsWith(prefix)) {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
const provided = Buffer.from(authorization.slice(prefix.length));
|
|
1904
|
+
const wanted = Buffer.from(expected);
|
|
1905
|
+
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
|
|
1906
|
+
}
|
|
1907
|
+
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
|
|
1908
|
+
const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
|
|
1738
1909
|
app.use((req, res, next) => {
|
|
1739
1910
|
const start = Date.now();
|
|
1740
1911
|
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
|
|
@@ -1767,56 +1938,44 @@ async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins)
|
|
|
1767
1938
|
}
|
|
1768
1939
|
next();
|
|
1769
1940
|
});
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
sessionIdGenerator: void 0
|
|
1775
|
-
});
|
|
1776
|
-
await server.connect(transport);
|
|
1777
|
-
await transport.handleRequest(req, res, req.body);
|
|
1778
|
-
res.on("close", () => {
|
|
1779
|
-
transport.close();
|
|
1780
|
-
server.close();
|
|
1781
|
-
});
|
|
1782
|
-
} catch (error) {
|
|
1783
|
-
console.error("Error handling MCP request:", error);
|
|
1784
|
-
if (!res.headersSent) {
|
|
1785
|
-
res.status(500).json({
|
|
1941
|
+
if (authToken) {
|
|
1942
|
+
app.use((req, res, next) => {
|
|
1943
|
+
if (!bearerTokenValid(req.headers.authorization, authToken)) {
|
|
1944
|
+
res.status(401).set("WWW-Authenticate", 'Bearer realm="mcp", error="invalid_token"').json({
|
|
1786
1945
|
jsonrpc: "2.0",
|
|
1787
1946
|
error: {
|
|
1788
|
-
code: -
|
|
1789
|
-
message: "
|
|
1947
|
+
code: -32001,
|
|
1948
|
+
message: "Unauthorized: missing or invalid bearer token"
|
|
1790
1949
|
},
|
|
1791
1950
|
id: null
|
|
1792
1951
|
});
|
|
1952
|
+
return;
|
|
1793
1953
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
message: "Method not allowed."
|
|
1803
|
-
},
|
|
1804
|
-
id: null
|
|
1805
|
-
})
|
|
1806
|
-
);
|
|
1954
|
+
next();
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
const mcpHandler = createMcpHandler(
|
|
1958
|
+
(_ctx) => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
|
|
1959
|
+
);
|
|
1960
|
+
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
1961
|
+
onerror: (error) => console.error("Error handling MCP request:", error)
|
|
1807
1962
|
});
|
|
1808
|
-
app.
|
|
1809
|
-
|
|
1810
|
-
|
|
1963
|
+
app.all("/mcp", (req, res) => nodeHandler(req, res, req.body));
|
|
1964
|
+
app.use(
|
|
1965
|
+
(err, _req, res, next) => {
|
|
1966
|
+
if (res.headersSent) return next(err);
|
|
1967
|
+
const status = err.status ?? err.statusCode ?? 400;
|
|
1968
|
+
const tooLarge = err.type === "entity.too.large" || status === 413;
|
|
1969
|
+
res.status(tooLarge ? 413 : 400).json({
|
|
1811
1970
|
jsonrpc: "2.0",
|
|
1812
1971
|
error: {
|
|
1813
|
-
code: -
|
|
1814
|
-
message: "
|
|
1972
|
+
code: tooLarge ? -32600 : -32700,
|
|
1973
|
+
message: tooLarge ? "Request body exceeds the configured size limit" : `Invalid request body: ${err.message ?? "parse error"}`
|
|
1815
1974
|
},
|
|
1816
1975
|
id: null
|
|
1817
|
-
})
|
|
1818
|
-
|
|
1819
|
-
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
);
|
|
1820
1979
|
app.listen(port, () => {
|
|
1821
1980
|
console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`);
|
|
1822
1981
|
console.log(`Endpoint: http://localhost:${port}/mcp`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-mongo-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A Model Context Protocol server for MongoDB connections",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
7
10
|
"bin": {
|
|
8
11
|
"mongodb": "./build/index.js"
|
|
9
12
|
},
|
|
@@ -23,12 +26,14 @@
|
|
|
23
26
|
"inspector": "mcp-inspector --config inspector-config.json --server mongodb"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
+
"@modelcontextprotocol/express": "^2.0.0",
|
|
30
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
31
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
32
|
+
"mongodb": "^7.5.0"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"@biomejs/biome": "^2.5.5",
|
|
36
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
32
37
|
"@modelcontextprotocol/inspector": "^1.0.0",
|
|
33
38
|
"@types/node": "^26.1.1",
|
|
34
39
|
"tsup": "^8.5.1",
|