mcp-mongo-server 2.1.1 → 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 -18
- package/build/index.js +314 -191
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -5,51 +5,130 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
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.
|
|
9
9
|
|
|
10
10
|
## Demo
|
|
11
11
|
|
|
12
12
|
[](https://www.youtube.com/watch?v=FI-oE_voCpA)
|
|
13
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
|
+
|
|
14
20
|
## Key Features
|
|
15
21
|
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **Schema Inference**
|
|
19
|
-
- **Query & Aggregation**
|
|
20
|
-
- **Write Operations**
|
|
21
|
-
- **
|
|
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
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
- Node.js 20 or newer
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Point the server at your database — no install step needed:
|
|
24
38
|
|
|
25
39
|
```bash
|
|
26
|
-
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
|
|
27
47
|
```
|
|
28
48
|
|
|
29
49
|
## Usage
|
|
30
50
|
|
|
51
|
+
### Local (stdio)
|
|
52
|
+
|
|
53
|
+
This is the default, used by Claude Desktop, Cursor, and other local clients:
|
|
54
|
+
|
|
31
55
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
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`:
|
|
34
71
|
|
|
35
|
-
|
|
36
|
-
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"
|
|
37
74
|
```
|
|
38
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
|
+
|
|
39
89
|
### Environment Variables
|
|
40
90
|
|
|
41
91
|
| Variable | Description |
|
|
42
92
|
|----------|-------------|
|
|
43
|
-
| `MCP_MONGODB_URI` | MongoDB connection URI |
|
|
93
|
+
| `MCP_MONGODB_URI` | MongoDB connection URI (alternative to the argument) |
|
|
44
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.
|
|
45
124
|
|
|
46
125
|
## Documentation
|
|
47
126
|
|
|
48
|
-
- [Integration Guide](docs/integration.md)
|
|
49
|
-
- [Available Tools](docs/tools.md)
|
|
50
|
-
- [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
|
|
51
130
|
- [Contributing](CONTRIBUTING.md)
|
|
52
131
|
|
|
53
132
|
## License
|
|
54
133
|
|
|
55
|
-
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,40 +47,71 @@ var COLLECTION_OPERATIONS = [
|
|
|
64
47
|
"count"
|
|
65
48
|
];
|
|
66
49
|
var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
|
|
67
|
-
var
|
|
68
|
-
|
|
69
|
-
|
|
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([
|
|
70
53
|
"$function",
|
|
71
54
|
"$accumulator",
|
|
72
55
|
"$where"
|
|
73
56
|
]);
|
|
74
|
-
|
|
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
|
+
}
|
|
75
67
|
if (Array.isArray(value)) {
|
|
76
68
|
for (const item of value) {
|
|
77
|
-
const found =
|
|
69
|
+
const found = findAggOperator(item, operators, depth + 1);
|
|
78
70
|
if (found) return found;
|
|
79
71
|
}
|
|
80
72
|
return null;
|
|
81
73
|
}
|
|
82
74
|
if (value && typeof value === "object") {
|
|
83
75
|
for (const [key, nested] of Object.entries(value)) {
|
|
84
|
-
if (
|
|
76
|
+
if (operators.has(key)) {
|
|
85
77
|
return key;
|
|
86
78
|
}
|
|
87
|
-
const found =
|
|
79
|
+
const found = findAggOperator(nested, operators, depth + 1);
|
|
88
80
|
if (found) return found;
|
|
89
81
|
}
|
|
90
82
|
}
|
|
91
83
|
return null;
|
|
92
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
|
+
}
|
|
93
107
|
async function handleCallToolRequest({
|
|
94
108
|
request,
|
|
95
109
|
client,
|
|
96
110
|
db,
|
|
97
111
|
isReadOnlyMode,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
allowCrossDb = false,
|
|
113
|
+
allowServerJs = false,
|
|
114
|
+
signal
|
|
101
115
|
}) {
|
|
102
116
|
const { name, arguments: args = {} } = request.params;
|
|
103
117
|
const operation = name;
|
|
@@ -127,46 +141,19 @@ async function handleCallToolRequest({
|
|
|
127
141
|
validateCollection(collection);
|
|
128
142
|
}
|
|
129
143
|
signal?.throwIfAborted();
|
|
130
|
-
if (taskStore) {
|
|
131
|
-
const task = await taskStore.createTask({
|
|
132
|
-
ttl: taskTtl ?? void 0
|
|
133
|
-
});
|
|
134
|
-
(async () => {
|
|
135
|
-
try {
|
|
136
|
-
const result = await executeOperation(
|
|
137
|
-
operation,
|
|
138
|
-
collection,
|
|
139
|
-
db,
|
|
140
|
-
isReadOnlyMode,
|
|
141
|
-
args,
|
|
142
|
-
objectIdMode,
|
|
143
|
-
signal
|
|
144
|
-
);
|
|
145
|
-
await taskStore.storeTaskResult(task.taskId, "completed", result);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
try {
|
|
148
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
149
|
-
await taskStore.storeTaskResult(task.taskId, "failed", {
|
|
150
|
-
content: [{ type: "text", text: message }],
|
|
151
|
-
isError: true
|
|
152
|
-
});
|
|
153
|
-
} catch {
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
})();
|
|
157
|
-
return { task };
|
|
158
|
-
}
|
|
159
144
|
return await executeOperation(
|
|
160
145
|
operation,
|
|
161
146
|
collection,
|
|
162
147
|
db,
|
|
163
148
|
isReadOnlyMode,
|
|
149
|
+
allowCrossDb,
|
|
150
|
+
allowServerJs,
|
|
164
151
|
args,
|
|
165
152
|
objectIdMode,
|
|
166
153
|
signal
|
|
167
154
|
);
|
|
168
155
|
} catch (error) {
|
|
169
|
-
if (error instanceof
|
|
156
|
+
if (error instanceof ProtocolError || signal?.aborted) {
|
|
170
157
|
throw error;
|
|
171
158
|
}
|
|
172
159
|
return toolExecutionError(
|
|
@@ -174,7 +161,7 @@ async function handleCallToolRequest({
|
|
|
174
161
|
);
|
|
175
162
|
}
|
|
176
163
|
}
|
|
177
|
-
async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
|
|
164
|
+
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
|
|
178
165
|
switch (operation) {
|
|
179
166
|
case "query":
|
|
180
167
|
return handleQuery(collection, args, objectIdMode, signal);
|
|
@@ -184,6 +171,8 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, args,
|
|
|
184
171
|
args,
|
|
185
172
|
objectIdMode,
|
|
186
173
|
isReadOnlyMode,
|
|
174
|
+
allowCrossDb,
|
|
175
|
+
allowServerJs,
|
|
187
176
|
signal
|
|
188
177
|
);
|
|
189
178
|
case "update":
|
|
@@ -198,8 +187,10 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, args,
|
|
|
198
187
|
return handleCount(collection, args, objectIdMode, signal);
|
|
199
188
|
case "listCollections":
|
|
200
189
|
return handleListCollections(db, args, objectIdMode, signal);
|
|
190
|
+
case "convertTime":
|
|
191
|
+
return handleConvertTime(args);
|
|
201
192
|
default:
|
|
202
|
-
throw new
|
|
193
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
203
194
|
}
|
|
204
195
|
}
|
|
205
196
|
function validateOperation(operation) {
|
|
@@ -211,17 +202,18 @@ function validateOperation(operation) {
|
|
|
211
202
|
"insert",
|
|
212
203
|
"createIndex",
|
|
213
204
|
"count",
|
|
214
|
-
"listCollections"
|
|
205
|
+
"listCollections",
|
|
206
|
+
"convertTime"
|
|
215
207
|
];
|
|
216
208
|
if (!validOperations.includes(operation)) {
|
|
217
|
-
throw new
|
|
209
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
218
210
|
}
|
|
219
211
|
}
|
|
220
212
|
function validateCollection(collection) {
|
|
221
213
|
if (!collection.collectionName) {
|
|
222
214
|
throw new Error("Collection name cannot be empty");
|
|
223
215
|
}
|
|
224
|
-
if (collection.collectionName.startsWith("system.")) {
|
|
216
|
+
if (collection.collectionName.toLowerCase().startsWith("system.")) {
|
|
225
217
|
throw new Error("Access to system collections is not allowed");
|
|
226
218
|
}
|
|
227
219
|
}
|
|
@@ -268,7 +260,12 @@ function isObjectIdField(fieldName) {
|
|
|
268
260
|
const lowerFieldName = fieldName.toLowerCase();
|
|
269
261
|
return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
|
|
270
262
|
}
|
|
271
|
-
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
|
+
}
|
|
272
269
|
if (objectIdMode === "none") {
|
|
273
270
|
const result2 = {};
|
|
274
271
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -295,7 +292,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
|
295
292
|
} else {
|
|
296
293
|
result2[key] = processObjectIdInFilter(
|
|
297
294
|
value,
|
|
298
|
-
"none"
|
|
295
|
+
"none",
|
|
296
|
+
depth + 1
|
|
299
297
|
);
|
|
300
298
|
}
|
|
301
299
|
} else {
|
|
@@ -337,7 +335,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
|
337
335
|
} else {
|
|
338
336
|
result[key] = processObjectIdInFilter(
|
|
339
337
|
value,
|
|
340
|
-
objectIdMode
|
|
338
|
+
objectIdMode,
|
|
339
|
+
depth + 1
|
|
341
340
|
);
|
|
342
341
|
}
|
|
343
342
|
} else {
|
|
@@ -350,7 +349,9 @@ function isObjectIdString(str) {
|
|
|
350
349
|
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
351
350
|
}
|
|
352
351
|
function isISODateString(str) {
|
|
353
|
-
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
|
+
);
|
|
354
355
|
}
|
|
355
356
|
function formatResponse(data) {
|
|
356
357
|
return {
|
|
@@ -419,7 +420,7 @@ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
|
|
|
419
420
|
return handleError(error, "query", collection.collectionName);
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
|
-
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, signal) {
|
|
423
|
+
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
|
|
423
424
|
if (!collection) {
|
|
424
425
|
throw new Error("Collection is required for aggregate operation");
|
|
425
426
|
}
|
|
@@ -428,13 +429,32 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
|
|
|
428
429
|
throw new Error("Pipeline must be an array");
|
|
429
430
|
}
|
|
430
431
|
if (isReadOnlyMode) {
|
|
431
|
-
const forbidden =
|
|
432
|
+
const forbidden = findAggOperator(
|
|
433
|
+
pipeline,
|
|
434
|
+
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
435
|
+
);
|
|
432
436
|
if (forbidden) {
|
|
433
437
|
throw new Error(
|
|
434
438
|
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
435
439
|
);
|
|
436
440
|
}
|
|
437
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
|
+
}
|
|
438
458
|
const processedPipeline = pipeline.map((stage) => {
|
|
439
459
|
if (typeof stage === "object" && stage !== null) {
|
|
440
460
|
return processObjectIdInFilter(
|
|
@@ -536,8 +556,7 @@ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
|
536
556
|
bits: buildInfo.bits,
|
|
537
557
|
ok: buildInfo.ok,
|
|
538
558
|
connectionInfo: {
|
|
539
|
-
readOnlyMode: isReadOnlyMode
|
|
540
|
-
readPreference: isReadOnlyMode ? "secondary" : "primary"
|
|
559
|
+
readOnlyMode: isReadOnlyMode
|
|
541
560
|
}
|
|
542
561
|
};
|
|
543
562
|
if (includeDebugInfo) {
|
|
@@ -650,15 +669,14 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
|
|
|
650
669
|
commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
|
|
651
670
|
};
|
|
652
671
|
signal?.throwIfAborted();
|
|
653
|
-
const
|
|
672
|
+
const createdIndexes = await collection.createIndexes(
|
|
654
673
|
processedIndexes,
|
|
655
674
|
indexOptions
|
|
656
675
|
);
|
|
657
676
|
return formatResponse({
|
|
658
|
-
acknowledged:
|
|
659
|
-
createdIndexes
|
|
660
|
-
|
|
661
|
-
numIndexesAfter: result.numIndexesAfter
|
|
677
|
+
acknowledged: true,
|
|
678
|
+
createdIndexes,
|
|
679
|
+
indexCount: createdIndexes.length
|
|
662
680
|
});
|
|
663
681
|
} catch (error) {
|
|
664
682
|
return handleError(error, "create indexes", collection.collectionName);
|
|
@@ -694,6 +712,48 @@ async function handleCount(collection, args, objectIdMode = "auto", signal) {
|
|
|
694
712
|
return handleError(error, "count documents", collection.collectionName);
|
|
695
713
|
}
|
|
696
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
|
+
}
|
|
697
757
|
async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
698
758
|
const { nameOnly, filter } = args;
|
|
699
759
|
const skip = args.skip || 0;
|
|
@@ -972,6 +1032,10 @@ ${JSON.stringify(sampleDocs, null, 2)}`
|
|
|
972
1032
|
}
|
|
973
1033
|
|
|
974
1034
|
// src/schemas/resource.ts
|
|
1035
|
+
import {
|
|
1036
|
+
ProtocolError as ProtocolError2,
|
|
1037
|
+
ProtocolErrorCode as ProtocolErrorCode2
|
|
1038
|
+
} from "@modelcontextprotocol/server";
|
|
975
1039
|
import { ObjectId as ObjectId2 } from "mongodb";
|
|
976
1040
|
function detectMongoType(value) {
|
|
977
1041
|
if (value === null) return "null";
|
|
@@ -1069,8 +1133,35 @@ async function handleReadResourceRequest({
|
|
|
1069
1133
|
signal,
|
|
1070
1134
|
sendProgress
|
|
1071
1135
|
}) {
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
+
}
|
|
1074
1165
|
try {
|
|
1075
1166
|
const collection = db.collection(collectionName);
|
|
1076
1167
|
const sampleSize = 100;
|
|
@@ -1238,7 +1329,6 @@ async function handleListToolsRequest({
|
|
|
1238
1329
|
{
|
|
1239
1330
|
name: "query",
|
|
1240
1331
|
description: "Execute a MongoDB query with optional execution plan analysis",
|
|
1241
|
-
execution: { taskSupport: "optional" },
|
|
1242
1332
|
inputSchema: {
|
|
1243
1333
|
type: "object",
|
|
1244
1334
|
properties: {
|
|
@@ -1264,6 +1354,10 @@ async function handleListToolsRequest({
|
|
|
1264
1354
|
description: "Number of documents to skip before returning results",
|
|
1265
1355
|
default: 0
|
|
1266
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
|
+
},
|
|
1267
1361
|
explain: {
|
|
1268
1362
|
type: "string",
|
|
1269
1363
|
description: "Optional: Get query execution information",
|
|
@@ -1282,7 +1376,6 @@ async function handleListToolsRequest({
|
|
|
1282
1376
|
{
|
|
1283
1377
|
name: "aggregate",
|
|
1284
1378
|
description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
|
|
1285
|
-
execution: { taskSupport: "optional" },
|
|
1286
1379
|
inputSchema: {
|
|
1287
1380
|
type: "object",
|
|
1288
1381
|
properties: {
|
|
@@ -1315,7 +1408,6 @@ async function handleListToolsRequest({
|
|
|
1315
1408
|
{
|
|
1316
1409
|
name: "update",
|
|
1317
1410
|
description: "Update documents in a MongoDB collection",
|
|
1318
|
-
execution: { taskSupport: "optional" },
|
|
1319
1411
|
inputSchema: {
|
|
1320
1412
|
type: "object",
|
|
1321
1413
|
properties: {
|
|
@@ -1352,7 +1444,6 @@ async function handleListToolsRequest({
|
|
|
1352
1444
|
{
|
|
1353
1445
|
name: "serverInfo",
|
|
1354
1446
|
description: "Get MongoDB server information including version, storage engine, and other details",
|
|
1355
|
-
execution: { taskSupport: "optional" },
|
|
1356
1447
|
inputSchema: {
|
|
1357
1448
|
type: "object",
|
|
1358
1449
|
properties: {
|
|
@@ -1366,7 +1457,6 @@ async function handleListToolsRequest({
|
|
|
1366
1457
|
{
|
|
1367
1458
|
name: "insert",
|
|
1368
1459
|
description: "Insert one or more documents into a MongoDB collection",
|
|
1369
|
-
execution: { taskSupport: "optional" },
|
|
1370
1460
|
inputSchema: {
|
|
1371
1461
|
type: "object",
|
|
1372
1462
|
properties: {
|
|
@@ -1404,7 +1494,6 @@ async function handleListToolsRequest({
|
|
|
1404
1494
|
{
|
|
1405
1495
|
name: "createIndex",
|
|
1406
1496
|
description: "Create one or more indexes on a MongoDB collection",
|
|
1407
|
-
execution: { taskSupport: "optional" },
|
|
1408
1497
|
inputSchema: {
|
|
1409
1498
|
type: "object",
|
|
1410
1499
|
properties: {
|
|
@@ -1471,7 +1560,6 @@ async function handleListToolsRequest({
|
|
|
1471
1560
|
{
|
|
1472
1561
|
name: "count",
|
|
1473
1562
|
description: "Count documents in a collection matching a query",
|
|
1474
|
-
execution: { taskSupport: "optional" },
|
|
1475
1563
|
inputSchema: {
|
|
1476
1564
|
type: "object",
|
|
1477
1565
|
properties: {
|
|
@@ -1502,7 +1590,6 @@ async function handleListToolsRequest({
|
|
|
1502
1590
|
{
|
|
1503
1591
|
name: "listCollections",
|
|
1504
1592
|
description: "List all collections in the MongoDB database",
|
|
1505
|
-
execution: { taskSupport: "optional" },
|
|
1506
1593
|
inputSchema: {
|
|
1507
1594
|
type: "object",
|
|
1508
1595
|
properties: {
|
|
@@ -1532,29 +1619,39 @@ async function handleListToolsRequest({
|
|
|
1532
1619
|
}
|
|
1533
1620
|
}
|
|
1534
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
|
+
}
|
|
1535
1635
|
}
|
|
1536
1636
|
]
|
|
1537
1637
|
};
|
|
1538
1638
|
}
|
|
1539
1639
|
|
|
1540
1640
|
// src/server.ts
|
|
1541
|
-
function createSendProgress(
|
|
1542
|
-
const progressToken = extra._meta?.progressToken;
|
|
1641
|
+
function createSendProgress(progressToken, notify) {
|
|
1543
1642
|
if (progressToken === void 0) return void 0;
|
|
1544
|
-
return (progress, total, message) =>
|
|
1643
|
+
return (progress, total, message) => notify({
|
|
1545
1644
|
method: "notifications/progress",
|
|
1546
1645
|
params: { progressToken, progress, total, message }
|
|
1547
1646
|
});
|
|
1548
1647
|
}
|
|
1549
|
-
function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
1550
|
-
const taskStore = new InMemoryTaskStore();
|
|
1551
|
-
const taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
1648
|
+
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
|
|
1552
1649
|
const server = new Server(
|
|
1553
1650
|
{
|
|
1554
1651
|
name: "mongodb",
|
|
1555
1652
|
title: "MongoDB MCP Server",
|
|
1556
|
-
version: "
|
|
1557
|
-
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",
|
|
1558
1655
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1559
1656
|
...options
|
|
1560
1657
|
},
|
|
@@ -1563,110 +1660,109 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
|
1563
1660
|
completions: {},
|
|
1564
1661
|
resources: {},
|
|
1565
1662
|
tools: {},
|
|
1566
|
-
prompts: {}
|
|
1567
|
-
tasks: {
|
|
1568
|
-
list: {},
|
|
1569
|
-
cancel: {},
|
|
1570
|
-
requests: { tools: { call: {} } }
|
|
1571
|
-
}
|
|
1663
|
+
prompts: {}
|
|
1572
1664
|
},
|
|
1573
|
-
taskStore,
|
|
1574
|
-
taskMessageQueue,
|
|
1575
1665
|
...options
|
|
1576
1666
|
}
|
|
1577
1667
|
);
|
|
1578
1668
|
server.setRequestHandler(
|
|
1579
|
-
|
|
1580
|
-
(request,
|
|
1669
|
+
"ping",
|
|
1670
|
+
(request, ctx) => handlePingRequest({
|
|
1581
1671
|
request,
|
|
1582
1672
|
client,
|
|
1583
1673
|
db,
|
|
1584
1674
|
isReadOnlyMode,
|
|
1585
|
-
signal:
|
|
1675
|
+
signal: ctx.mcpReq.signal
|
|
1586
1676
|
})
|
|
1587
1677
|
);
|
|
1588
1678
|
server.setRequestHandler(
|
|
1589
|
-
|
|
1590
|
-
(request,
|
|
1679
|
+
"resources/list",
|
|
1680
|
+
(request, ctx) => handleListResourcesRequest({
|
|
1591
1681
|
request,
|
|
1592
1682
|
client,
|
|
1593
1683
|
db,
|
|
1594
1684
|
isReadOnlyMode,
|
|
1595
|
-
signal:
|
|
1685
|
+
signal: ctx.mcpReq.signal
|
|
1596
1686
|
})
|
|
1597
1687
|
);
|
|
1598
1688
|
server.setRequestHandler(
|
|
1599
|
-
|
|
1600
|
-
(request,
|
|
1689
|
+
"resources/read",
|
|
1690
|
+
(request, ctx) => handleReadResourceRequest({
|
|
1601
1691
|
request,
|
|
1602
1692
|
client,
|
|
1603
1693
|
db,
|
|
1604
1694
|
isReadOnlyMode,
|
|
1605
|
-
signal:
|
|
1606
|
-
sendProgress: createSendProgress(
|
|
1695
|
+
signal: ctx.mcpReq.signal,
|
|
1696
|
+
sendProgress: createSendProgress(
|
|
1697
|
+
request.params._meta?.progressToken,
|
|
1698
|
+
ctx.mcpReq.notify
|
|
1699
|
+
)
|
|
1607
1700
|
})
|
|
1608
1701
|
);
|
|
1609
1702
|
server.setRequestHandler(
|
|
1610
|
-
|
|
1611
|
-
(request,
|
|
1703
|
+
"tools/list",
|
|
1704
|
+
(request, ctx) => handleListToolsRequest({
|
|
1612
1705
|
request,
|
|
1613
1706
|
client,
|
|
1614
1707
|
db,
|
|
1615
1708
|
isReadOnlyMode,
|
|
1616
|
-
signal:
|
|
1709
|
+
signal: ctx.mcpReq.signal
|
|
1617
1710
|
})
|
|
1618
1711
|
);
|
|
1619
|
-
server.setRequestHandler(
|
|
1620
|
-
|
|
1621
|
-
|
|
1712
|
+
server.setRequestHandler(
|
|
1713
|
+
"tools/call",
|
|
1714
|
+
(request, ctx) => handleCallToolRequest({
|
|
1622
1715
|
request,
|
|
1623
1716
|
client,
|
|
1624
1717
|
db,
|
|
1625
1718
|
isReadOnlyMode,
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
})
|
|
1630
|
-
|
|
1719
|
+
allowCrossDb,
|
|
1720
|
+
allowServerJs,
|
|
1721
|
+
signal: ctx.mcpReq.signal
|
|
1722
|
+
})
|
|
1723
|
+
);
|
|
1631
1724
|
server.setRequestHandler(
|
|
1632
|
-
|
|
1633
|
-
(request,
|
|
1725
|
+
"prompts/list",
|
|
1726
|
+
(request, ctx) => handleListPromptsRequest({
|
|
1634
1727
|
request,
|
|
1635
1728
|
client,
|
|
1636
1729
|
db,
|
|
1637
1730
|
isReadOnlyMode,
|
|
1638
|
-
signal:
|
|
1731
|
+
signal: ctx.mcpReq.signal
|
|
1639
1732
|
})
|
|
1640
1733
|
);
|
|
1641
1734
|
server.setRequestHandler(
|
|
1642
|
-
|
|
1643
|
-
(request,
|
|
1735
|
+
"prompts/get",
|
|
1736
|
+
(request, ctx) => handleGetPromptRequest({
|
|
1644
1737
|
request,
|
|
1645
1738
|
client,
|
|
1646
1739
|
db,
|
|
1647
1740
|
isReadOnlyMode,
|
|
1648
|
-
signal:
|
|
1649
|
-
sendProgress: createSendProgress(
|
|
1741
|
+
signal: ctx.mcpReq.signal,
|
|
1742
|
+
sendProgress: createSendProgress(
|
|
1743
|
+
request.params._meta?.progressToken,
|
|
1744
|
+
ctx.mcpReq.notify
|
|
1745
|
+
)
|
|
1650
1746
|
})
|
|
1651
1747
|
);
|
|
1652
1748
|
server.setRequestHandler(
|
|
1653
|
-
|
|
1654
|
-
(request,
|
|
1749
|
+
"resources/templates/list",
|
|
1750
|
+
(request, ctx) => handleListResourceTemplatesRequest({
|
|
1655
1751
|
request,
|
|
1656
1752
|
client,
|
|
1657
1753
|
db,
|
|
1658
1754
|
isReadOnlyMode,
|
|
1659
|
-
signal:
|
|
1755
|
+
signal: ctx.mcpReq.signal
|
|
1660
1756
|
})
|
|
1661
1757
|
);
|
|
1662
1758
|
server.setRequestHandler(
|
|
1663
|
-
|
|
1664
|
-
(request,
|
|
1759
|
+
"completion/complete",
|
|
1760
|
+
(request, ctx) => handleCompletionRequest({
|
|
1665
1761
|
request,
|
|
1666
1762
|
client,
|
|
1667
1763
|
db,
|
|
1668
1764
|
isReadOnlyMode,
|
|
1669
|
-
signal:
|
|
1765
|
+
signal: ctx.mcpReq.signal
|
|
1670
1766
|
})
|
|
1671
1767
|
);
|
|
1672
1768
|
return server;
|
|
@@ -1678,12 +1774,20 @@ async function main() {
|
|
|
1678
1774
|
const args = process.argv.slice(2);
|
|
1679
1775
|
let connectionUrl = "";
|
|
1680
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;
|
|
1681
1779
|
let transportMode = "stdio";
|
|
1682
1780
|
let port = Number(process.env.MCP_PORT) || 3001;
|
|
1683
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 || "";
|
|
1684
1784
|
for (let i = 0; i < args.length; i++) {
|
|
1685
1785
|
if (args[i] === "--read-only" || args[i] === "-r") {
|
|
1686
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;
|
|
1687
1791
|
} else if (args[i] === "--transport" || args[i] === "-t") {
|
|
1688
1792
|
const value = args[++i];
|
|
1689
1793
|
if (value !== "stdio" && value !== "http") {
|
|
@@ -1701,6 +1805,12 @@ async function main() {
|
|
|
1701
1805
|
allowedOrigins.push(
|
|
1702
1806
|
...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
|
|
1703
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;
|
|
1704
1814
|
} else if (!connectionUrl) {
|
|
1705
1815
|
connectionUrl = args[i];
|
|
1706
1816
|
}
|
|
@@ -1737,9 +1847,25 @@ async function main() {
|
|
|
1737
1847
|
process.exit(1);
|
|
1738
1848
|
}
|
|
1739
1849
|
if (transportMode === "http") {
|
|
1740
|
-
await startHttpServer(
|
|
1850
|
+
await startHttpServer(
|
|
1851
|
+
client,
|
|
1852
|
+
db,
|
|
1853
|
+
isReadOnlyMode,
|
|
1854
|
+
allowCrossDb,
|
|
1855
|
+
allowServerJs,
|
|
1856
|
+
port,
|
|
1857
|
+
allowedOrigins,
|
|
1858
|
+
jsonLimit,
|
|
1859
|
+
authToken
|
|
1860
|
+
);
|
|
1741
1861
|
} else {
|
|
1742
|
-
await startStdioServer(
|
|
1862
|
+
await startStdioServer(
|
|
1863
|
+
client,
|
|
1864
|
+
db,
|
|
1865
|
+
isReadOnlyMode,
|
|
1866
|
+
allowCrossDb,
|
|
1867
|
+
allowServerJs
|
|
1868
|
+
);
|
|
1743
1869
|
}
|
|
1744
1870
|
} catch (error) {
|
|
1745
1871
|
console.error("Failed to connect to MongoDB:", error);
|
|
@@ -1749,10 +1875,10 @@ async function main() {
|
|
|
1749
1875
|
process.exit(1);
|
|
1750
1876
|
}
|
|
1751
1877
|
}
|
|
1752
|
-
async function startStdioServer(client, db, isReadOnlyMode) {
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1878
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
|
|
1879
|
+
serveStdio(
|
|
1880
|
+
() => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
|
|
1881
|
+
);
|
|
1756
1882
|
console.warn("Server connected successfully via stdio");
|
|
1757
1883
|
}
|
|
1758
1884
|
function isOriginAllowed(origin, allowedOrigins) {
|
|
@@ -1769,8 +1895,17 @@ function isOriginAllowed(origin, allowedOrigins) {
|
|
|
1769
1895
|
return false;
|
|
1770
1896
|
}
|
|
1771
1897
|
}
|
|
1772
|
-
|
|
1773
|
-
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 });
|
|
1774
1909
|
app.use((req, res, next) => {
|
|
1775
1910
|
const start = Date.now();
|
|
1776
1911
|
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
|
|
@@ -1803,56 +1938,44 @@ async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins)
|
|
|
1803
1938
|
}
|
|
1804
1939
|
next();
|
|
1805
1940
|
});
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
sessionIdGenerator: void 0
|
|
1811
|
-
});
|
|
1812
|
-
await server.connect(transport);
|
|
1813
|
-
await transport.handleRequest(req, res, req.body);
|
|
1814
|
-
res.on("close", () => {
|
|
1815
|
-
transport.close();
|
|
1816
|
-
server.close();
|
|
1817
|
-
});
|
|
1818
|
-
} catch (error) {
|
|
1819
|
-
console.error("Error handling MCP request:", error);
|
|
1820
|
-
if (!res.headersSent) {
|
|
1821
|
-
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({
|
|
1822
1945
|
jsonrpc: "2.0",
|
|
1823
1946
|
error: {
|
|
1824
|
-
code: -
|
|
1825
|
-
message: "
|
|
1947
|
+
code: -32001,
|
|
1948
|
+
message: "Unauthorized: missing or invalid bearer token"
|
|
1826
1949
|
},
|
|
1827
1950
|
id: null
|
|
1828
1951
|
});
|
|
1952
|
+
return;
|
|
1829
1953
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
message: "Method not allowed."
|
|
1839
|
-
},
|
|
1840
|
-
id: null
|
|
1841
|
-
})
|
|
1842
|
-
);
|
|
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)
|
|
1843
1962
|
});
|
|
1844
|
-
app.
|
|
1845
|
-
|
|
1846
|
-
|
|
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({
|
|
1847
1970
|
jsonrpc: "2.0",
|
|
1848
1971
|
error: {
|
|
1849
|
-
code: -
|
|
1850
|
-
message: "
|
|
1972
|
+
code: tooLarge ? -32600 : -32700,
|
|
1973
|
+
message: tooLarge ? "Request body exceeds the configured size limit" : `Invalid request body: ${err.message ?? "parse error"}`
|
|
1851
1974
|
},
|
|
1852
1975
|
id: null
|
|
1853
|
-
})
|
|
1854
|
-
|
|
1855
|
-
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
);
|
|
1856
1979
|
app.listen(port, () => {
|
|
1857
1980
|
console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`);
|
|
1858
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",
|