mcp-mongo-server 3.0.1 → 3.1.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 +39 -10
- package/build/index.js +489 -143
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-o
|
|
|
91
91
|
| Variable | Description |
|
|
92
92
|
|----------|-------------|
|
|
93
93
|
| `MCP_MONGODB_URI` | MongoDB connection URI (alternative to the argument) |
|
|
94
|
+
| `MCP_MONGODB_CONNECTIONS` | JSON object of named MongoDB connections and their database permissions |
|
|
94
95
|
| `MCP_MONGODB_READONLY` | Enable read-only mode (`"true"`) |
|
|
95
96
|
| `MCP_MONGODB_ALLOW_CROSS_DB` | Allow cross-database aggregation stages (`"true"`) |
|
|
96
97
|
| `MCP_MONGODB_ALLOW_SERVER_JS` | Allow server-side JavaScript operators (`"true"`) |
|
|
@@ -99,22 +100,50 @@ npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-o
|
|
|
99
100
|
| `MCP_HTTP_JSON_LIMIT` | Max HTTP request body size (default `10mb`) |
|
|
100
101
|
| `MCP_HTTP_AUTH_TOKEN` | Bearer token required to access the HTTP endpoint |
|
|
101
102
|
|
|
103
|
+
### Named connections
|
|
104
|
+
|
|
105
|
+
Set `MCP_MONGODB_CONNECTIONS` to a JSON string. This MCP client configuration
|
|
106
|
+
uses a local build and the two [Docker test databases](docs/development.md#local-mongodb-test-environment).
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"mongodb": {
|
|
112
|
+
"command": "node",
|
|
113
|
+
"args": [
|
|
114
|
+
"/absolute/path/to/mcp-mongo-server/build/index.js"
|
|
115
|
+
],
|
|
116
|
+
"env": {
|
|
117
|
+
"MCP_MONGODB_CONNECTIONS": "{\"default\":{\"uri\":\"mongodb://127.0.0.1:37017/mcp_test\"},\"analytics\":{\"uri\":\"mongodb://127.0.0.1:37018/mcp_test?directConnection=true\",\"databases\":[\"mcp_test\",\"mcp_test_secondary\"],\"readOnly\":true}}"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- Discover targets with `listConnections` and `listDatabases`. Pass `connectionId` and `database` to database tools and the collection analysis prompt. Omitted selectors use the `default` connection and its URI database.
|
|
125
|
+
- Omit `databases` to allow only the URI database. If supplied, the list must include the URI database. Global read-only mode applies to every connection.
|
|
126
|
+
- A positional URI or `MCP_MONGODB_URI` can supply `default` when the JSON contains only secondary connections. Named connections default to a 5-second server selection timeout. Set `serverSelectionTimeoutMS` in the URI to override it.
|
|
127
|
+
- If a connection is offline, `resources/list` reports its ID and fails. Tools targeting other connections remain usable.
|
|
128
|
+
|
|
102
129
|
## Security
|
|
103
130
|
|
|
104
|
-
**Database scope.** The server operates on the database
|
|
105
|
-
|
|
131
|
+
**Database scope.** The server operates on the selected database.
|
|
132
|
+
Named connections restrict selection to their configured databases.
|
|
133
|
+
Aggregation stages that reach another database (`$out`, `$merge`,
|
|
106
134
|
`$lookup` with an explicit `db`) are rejected by default, so a pipeline can't
|
|
107
135
|
quietly read from or write to databases you didn't point it at. Enable them
|
|
108
|
-
with `--allow-cross-db` if you need them.
|
|
136
|
+
with `--allow-cross-db` if you need them. Named connections still enforce their
|
|
137
|
+
database allowlist.
|
|
109
138
|
|
|
110
|
-
**Server-side JavaScript.** The
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
**Server-side JavaScript.** The operators `$function`, `$where`, and
|
|
140
|
+
`$accumulator` are blocked by default in aggregation pipelines and in the
|
|
141
|
+
filters for `query`, `count`, and `update`. In read-write mode,
|
|
142
|
+
`--allow-server-js` enables these operators.
|
|
114
143
|
|
|
115
|
-
**Read-only mode** blocks
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
**Read-only mode** blocks writes, including the `$out` and `$merge`
|
|
145
|
+
aggregation stages. It also blocks server-side JavaScript even when
|
|
146
|
+
`--allow-server-js` is set.
|
|
118
147
|
|
|
119
148
|
**Least privilege.** Application-level checks only go so far — the strongest
|
|
120
149
|
guarantee comes from the database. Connect with a MongoDB user scoped to just
|
package/build/index.js
CHANGED
|
@@ -7,36 +7,184 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
|
7
7
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
8
8
|
import { createMcpExpressApp } from "@modelcontextprotocol/express";
|
|
9
9
|
|
|
10
|
-
// src/
|
|
10
|
+
// src/connections.ts
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
11
13
|
import { MongoClient } from "mongodb";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
var Connections = class {
|
|
18
|
+
cursorScope = randomUUID();
|
|
19
|
+
named;
|
|
20
|
+
entries = /* @__PURE__ */ new Map();
|
|
21
|
+
closing;
|
|
22
|
+
constructor(legacyUri, configuration, readOnly) {
|
|
23
|
+
this.named = configuration !== void 0;
|
|
24
|
+
let config = {};
|
|
25
|
+
if (configuration !== void 0) {
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(configuration);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error("MCP_MONGODB_CONNECTIONS must be valid JSON");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(config))
|
|
33
|
+
throw new Error("MCP_MONGODB_CONNECTIONS must be an object");
|
|
34
|
+
if (legacyUri && Object.hasOwn(config, "default")) {
|
|
35
|
+
throw new Error("The legacy URI and named default connection conflict");
|
|
36
|
+
}
|
|
37
|
+
const entries = legacyUri ? { ...config, default: { uri: legacyUri } } : config;
|
|
38
|
+
if (!Object.hasOwn(entries, "default"))
|
|
39
|
+
throw new Error("A default MongoDB connection is required");
|
|
40
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
41
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
42
|
+
throw new Error("Invalid connection ID");
|
|
43
|
+
if (!isRecord(entry) || Object.keys(entry).some(
|
|
44
|
+
(key) => !["uri", "databases", "readOnly"].includes(key)
|
|
45
|
+
)) {
|
|
46
|
+
throw new Error(`Invalid configuration for connection '${id}'`);
|
|
47
|
+
}
|
|
48
|
+
const { uri, databases, readOnly: localReadOnly } = entry;
|
|
49
|
+
if (typeof uri !== "string" || !/^mongodb(?:\+srv)?:\/\//.test(uri)) {
|
|
50
|
+
throw new Error(`Invalid MongoDB URI for connection '${id}'`);
|
|
51
|
+
}
|
|
52
|
+
if (localReadOnly !== void 0 && typeof localReadOnly !== "boolean") {
|
|
53
|
+
throw new Error(`Invalid readOnly setting for connection '${id}'`);
|
|
54
|
+
}
|
|
55
|
+
let client;
|
|
56
|
+
const query = uri.indexOf("?");
|
|
57
|
+
const uriOptions = new URLSearchParams(
|
|
58
|
+
query < 0 ? "" : uri.slice(query + 1)
|
|
59
|
+
);
|
|
60
|
+
const hasSelectionTimeout = [...uriOptions.keys()].some(
|
|
61
|
+
(key) => key.toLowerCase() === "serverselectiontimeoutms"
|
|
62
|
+
);
|
|
63
|
+
const selectionTimeout = this.named && !hasSelectionTimeout ? 5e3 : void 0;
|
|
64
|
+
try {
|
|
65
|
+
client = new MongoClient(uri, {
|
|
66
|
+
serverSelectionTimeoutMS: selectionTimeout
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Invalid MongoDB URI for connection '${id}'`);
|
|
70
|
+
}
|
|
71
|
+
const defaultDatabase = client.db().databaseName;
|
|
72
|
+
if (databases !== void 0 && (!Array.isArray(databases) || !databases.includes(defaultDatabase) || databases.some(
|
|
73
|
+
(name) => typeof name !== "string" || !name.trim() || /[/\\. "$]/.test(name) || name.includes("\0")
|
|
74
|
+
))) {
|
|
75
|
+
throw new Error(`Invalid database allowlist for connection '${id}'`);
|
|
76
|
+
}
|
|
77
|
+
this.entries.set(id, {
|
|
78
|
+
connectionId: id,
|
|
79
|
+
uri,
|
|
80
|
+
selectionTimeout,
|
|
81
|
+
client,
|
|
82
|
+
defaultDatabase,
|
|
83
|
+
databases: databases === void 0 ? [defaultDatabase] : [...new Set(databases)].sort(),
|
|
84
|
+
readOnly: readOnly || localReadOnly === true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
connection(id = "default") {
|
|
89
|
+
const entry = typeof id === "string" ? this.entries.get(id) : void 0;
|
|
90
|
+
if (!entry) throw new Error("Unknown or invalid MongoDB connection ID");
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
list() {
|
|
94
|
+
return [...this.entries.keys()].sort().map((id) => {
|
|
95
|
+
const { connectionId, defaultDatabase, readOnly } = this.connection(id);
|
|
96
|
+
return { connectionId, defaultDatabase, readOnly };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
allowedDatabases(id) {
|
|
100
|
+
return this.connection(id).databases;
|
|
101
|
+
}
|
|
102
|
+
async resolve({ connectionId, database } = {}) {
|
|
103
|
+
const entry = this.connection(connectionId);
|
|
104
|
+
const name = database === void 0 ? entry.defaultDatabase : database;
|
|
105
|
+
if (typeof name !== "string" || !entry.databases.includes(name)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Database is invalid or not allowed on connection '${entry.connectionId}'`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (this.closing) throw new Error("MongoDB connections are shutting down");
|
|
111
|
+
if (!entry.pending) {
|
|
112
|
+
const client2 = entry.client ?? new MongoClient(entry.uri, {
|
|
113
|
+
serverSelectionTimeoutMS: entry.selectionTimeout
|
|
114
|
+
});
|
|
115
|
+
entry.client = client2;
|
|
116
|
+
entry.pending = client2.connect().catch(async () => {
|
|
117
|
+
await client2.close().catch(() => void 0);
|
|
118
|
+
entry.client = void 0;
|
|
119
|
+
entry.pending = void 0;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Failed to connect to MongoDB connection '${entry.connectionId}'`
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const client = await entry.pending;
|
|
18
126
|
return {
|
|
19
127
|
client,
|
|
20
|
-
db,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.error("Failed to connect to MongoDB:", error);
|
|
26
|
-
return {
|
|
27
|
-
client: null,
|
|
28
|
-
db: null,
|
|
29
|
-
isConnected: false,
|
|
30
|
-
isReadOnlyMode: readOnly
|
|
128
|
+
db: client.db(name),
|
|
129
|
+
isReadOnlyMode: entry.readOnly,
|
|
130
|
+
connectionId: entry.connectionId
|
|
31
131
|
};
|
|
32
132
|
}
|
|
133
|
+
async listDatabases(connectionId) {
|
|
134
|
+
const { db } = await this.resolve({ connectionId });
|
|
135
|
+
const allowed = this.allowedDatabases(connectionId);
|
|
136
|
+
const { databases } = await db.admin().listDatabases({
|
|
137
|
+
nameOnly: true,
|
|
138
|
+
authorizedDatabases: true,
|
|
139
|
+
filter: { name: { $in: [...allowed] } }
|
|
140
|
+
});
|
|
141
|
+
return databases.map(({ name }) => name).filter((name) => allowed.includes(name)).sort();
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
this.closing ??= Promise.allSettled(
|
|
145
|
+
[...this.entries.values()].map(async ({ client, pending }) => {
|
|
146
|
+
await pending?.catch(() => void 0);
|
|
147
|
+
await client?.close();
|
|
148
|
+
})
|
|
149
|
+
).then(() => void 0);
|
|
150
|
+
await this.closing;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function resourceSelectors(uri) {
|
|
154
|
+
let url;
|
|
155
|
+
try {
|
|
156
|
+
url = new URL(uri);
|
|
157
|
+
if (url.protocol !== "mongodb:") throw new Error("Invalid scheme");
|
|
158
|
+
decodeURIComponent(url.search);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new ProtocolError(
|
|
161
|
+
ProtocolErrorCode.InvalidParams,
|
|
162
|
+
"Invalid MongoDB resource URI"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const selectors = {};
|
|
166
|
+
for (const key of ["connectionId", "database"]) {
|
|
167
|
+
const values = url.searchParams.getAll(key);
|
|
168
|
+
if (values.length > 1) {
|
|
169
|
+
throw new ProtocolError(
|
|
170
|
+
ProtocolErrorCode.InvalidParams,
|
|
171
|
+
`Duplicate resource selector '${key}'`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (values.length === 1) selectors[key] = values[0];
|
|
175
|
+
}
|
|
176
|
+
return selectors;
|
|
33
177
|
}
|
|
34
178
|
|
|
35
179
|
// src/server.ts
|
|
36
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
ProtocolError as ProtocolError4,
|
|
182
|
+
ProtocolErrorCode as ProtocolErrorCode4,
|
|
183
|
+
Server
|
|
184
|
+
} from "@modelcontextprotocol/server";
|
|
37
185
|
|
|
38
186
|
// src/schemas/call.ts
|
|
39
|
-
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
187
|
+
import { ProtocolError as ProtocolError2, ProtocolErrorCode as ProtocolErrorCode2 } from "@modelcontextprotocol/server";
|
|
40
188
|
import { ObjectId } from "mongodb";
|
|
41
189
|
var COLLECTION_OPERATIONS = [
|
|
42
190
|
"query",
|
|
@@ -82,6 +230,14 @@ function findAggOperator(value, operators, depth = 0) {
|
|
|
82
230
|
}
|
|
83
231
|
return null;
|
|
84
232
|
}
|
|
233
|
+
function assertNoServerJs(value) {
|
|
234
|
+
const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
|
|
235
|
+
if (operator) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
85
241
|
function findCrossDbTarget(pipeline, connectedDb) {
|
|
86
242
|
const foreignDb = (target) => {
|
|
87
243
|
if (target && typeof target === "object" && "db" in target) {
|
|
@@ -111,6 +267,7 @@ async function handleCallToolRequest({
|
|
|
111
267
|
isReadOnlyMode,
|
|
112
268
|
allowCrossDb = false,
|
|
113
269
|
allowServerJs = false,
|
|
270
|
+
connections: connections2,
|
|
114
271
|
signal
|
|
115
272
|
}) {
|
|
116
273
|
const { name, arguments: args = {} } = request.params;
|
|
@@ -126,8 +283,32 @@ async function handleCallToolRequest({
|
|
|
126
283
|
args.sort = parseSort(args.sort);
|
|
127
284
|
}
|
|
128
285
|
Object.assign(args, filteredArgs);
|
|
129
|
-
|
|
286
|
+
if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
|
|
287
|
+
validateOperation(operation);
|
|
288
|
+
}
|
|
130
289
|
try {
|
|
290
|
+
signal?.throwIfAborted();
|
|
291
|
+
if (connections2 && name === "listConnections") {
|
|
292
|
+
return formatResponse({ connections: connections2.list() });
|
|
293
|
+
}
|
|
294
|
+
if (connections2 && name === "listDatabases") {
|
|
295
|
+
return formatResponse({
|
|
296
|
+
databases: await connections2.listDatabases(args.connectionId)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (connections2 && operation !== "convertTime") {
|
|
300
|
+
const target = await connections2.resolve(args);
|
|
301
|
+
db = target.db;
|
|
302
|
+
isReadOnlyMode = target.isReadOnlyMode;
|
|
303
|
+
if (connections2.named && operation === "aggregate" && Array.isArray(args.pipeline)) {
|
|
304
|
+
const allowed = connections2.allowedDatabases(target.connectionId);
|
|
305
|
+
checkNamedAggregationDatabases(
|
|
306
|
+
args.pipeline,
|
|
307
|
+
db.databaseName,
|
|
308
|
+
allowCrossDb ? allowed : [db.databaseName]
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
131
312
|
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
132
313
|
let collection = null;
|
|
133
314
|
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
@@ -153,7 +334,7 @@ async function handleCallToolRequest({
|
|
|
153
334
|
signal
|
|
154
335
|
);
|
|
155
336
|
} catch (error) {
|
|
156
|
-
if (error instanceof
|
|
337
|
+
if (error instanceof ProtocolError2 || signal?.aborted) {
|
|
157
338
|
throw error;
|
|
158
339
|
}
|
|
159
340
|
return toolExecutionError(
|
|
@@ -161,10 +342,50 @@ async function handleCallToolRequest({
|
|
|
161
342
|
);
|
|
162
343
|
}
|
|
163
344
|
}
|
|
345
|
+
function checkNamedAggregationDatabases(pipeline, database, allowed, depth = 0) {
|
|
346
|
+
if (depth > MAX_OBJECT_DEPTH)
|
|
347
|
+
throw new Error("Aggregation pipeline nesting is too deep");
|
|
348
|
+
for (const stage of pipeline) {
|
|
349
|
+
if (!stage || typeof stage !== "object") continue;
|
|
350
|
+
const { $out, $merge, $lookup, $unionWith, $facet } = stage;
|
|
351
|
+
for (const target of [$out, $merge, $lookup, $unionWith]) {
|
|
352
|
+
if (!target || typeof target !== "object") continue;
|
|
353
|
+
const value = target;
|
|
354
|
+
for (const reference of [value, value.into, value.from]) {
|
|
355
|
+
if (!reference || typeof reference !== "object" || !("db" in reference))
|
|
356
|
+
continue;
|
|
357
|
+
if (typeof reference.db !== "string" || !allowed.includes(reference.db)) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Aggregation references a database outside the allowed scope of '${database}'`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (Array.isArray(value.pipeline))
|
|
364
|
+
checkNamedAggregationDatabases(
|
|
365
|
+
value.pipeline,
|
|
366
|
+
database,
|
|
367
|
+
allowed,
|
|
368
|
+
depth + 1
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if ($facet && typeof $facet === "object") {
|
|
372
|
+
for (const nested of Object.values($facet)) {
|
|
373
|
+
if (Array.isArray(nested))
|
|
374
|
+
checkNamedAggregationDatabases(nested, database, allowed, depth + 1);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
164
379
|
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
|
|
165
380
|
switch (operation) {
|
|
166
381
|
case "query":
|
|
167
|
-
return handleQuery(
|
|
382
|
+
return handleQuery(
|
|
383
|
+
collection,
|
|
384
|
+
args,
|
|
385
|
+
objectIdMode,
|
|
386
|
+
allowServerJs && !isReadOnlyMode,
|
|
387
|
+
signal
|
|
388
|
+
);
|
|
168
389
|
case "aggregate":
|
|
169
390
|
return handleAggregate(
|
|
170
391
|
collection,
|
|
@@ -176,7 +397,13 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
|
|
|
176
397
|
signal
|
|
177
398
|
);
|
|
178
399
|
case "update":
|
|
179
|
-
return handleUpdate(
|
|
400
|
+
return handleUpdate(
|
|
401
|
+
collection,
|
|
402
|
+
args,
|
|
403
|
+
objectIdMode,
|
|
404
|
+
allowServerJs,
|
|
405
|
+
signal
|
|
406
|
+
);
|
|
180
407
|
case "serverInfo":
|
|
181
408
|
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
182
409
|
case "insert":
|
|
@@ -184,13 +411,22 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
|
|
|
184
411
|
case "createIndex":
|
|
185
412
|
return handleCreateIndex(collection, args, objectIdMode, signal);
|
|
186
413
|
case "count":
|
|
187
|
-
return handleCount(
|
|
414
|
+
return handleCount(
|
|
415
|
+
collection,
|
|
416
|
+
args,
|
|
417
|
+
objectIdMode,
|
|
418
|
+
allowServerJs && !isReadOnlyMode,
|
|
419
|
+
signal
|
|
420
|
+
);
|
|
188
421
|
case "listCollections":
|
|
189
422
|
return handleListCollections(db, args, objectIdMode, signal);
|
|
190
423
|
case "convertTime":
|
|
191
424
|
return handleConvertTime(args);
|
|
192
425
|
default:
|
|
193
|
-
throw new
|
|
426
|
+
throw new ProtocolError2(
|
|
427
|
+
ProtocolErrorCode2.InvalidParams,
|
|
428
|
+
`Unknown tool: ${operation}`
|
|
429
|
+
);
|
|
194
430
|
}
|
|
195
431
|
}
|
|
196
432
|
function validateOperation(operation) {
|
|
@@ -206,7 +442,10 @@ function validateOperation(operation) {
|
|
|
206
442
|
"convertTime"
|
|
207
443
|
];
|
|
208
444
|
if (!validOperations.includes(operation)) {
|
|
209
|
-
throw new
|
|
445
|
+
throw new ProtocolError2(
|
|
446
|
+
ProtocolErrorCode2.InvalidParams,
|
|
447
|
+
`Unknown tool: ${operation}`
|
|
448
|
+
);
|
|
210
449
|
}
|
|
211
450
|
}
|
|
212
451
|
function validateCollection(collection) {
|
|
@@ -377,7 +616,7 @@ function handleError(error, operation, collectionName) {
|
|
|
377
616
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
378
617
|
return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
|
|
379
618
|
}
|
|
380
|
-
async function handleQuery(collection, args, objectIdMode = "auto", signal) {
|
|
619
|
+
async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
381
620
|
if (!collection) {
|
|
382
621
|
throw new Error("Collection is required for query operation");
|
|
383
622
|
}
|
|
@@ -385,6 +624,9 @@ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
|
|
|
385
624
|
const limit = args.limit || 10;
|
|
386
625
|
const skip = args.skip || 0;
|
|
387
626
|
const queryFilter = parseFilter(filter, objectIdMode);
|
|
627
|
+
if (!allowServerJs) {
|
|
628
|
+
assertNoServerJs(queryFilter);
|
|
629
|
+
}
|
|
388
630
|
try {
|
|
389
631
|
if (explain) {
|
|
390
632
|
signal?.throwIfAborted();
|
|
@@ -440,12 +682,7 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
|
|
|
440
682
|
}
|
|
441
683
|
}
|
|
442
684
|
if (!allowServerJs) {
|
|
443
|
-
|
|
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
|
-
}
|
|
685
|
+
assertNoServerJs(pipeline);
|
|
449
686
|
}
|
|
450
687
|
if (!allowCrossDb) {
|
|
451
688
|
const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
|
|
@@ -486,12 +723,15 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
|
|
|
486
723
|
return handleError(error, "aggregate", collection.collectionName);
|
|
487
724
|
}
|
|
488
725
|
}
|
|
489
|
-
async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
|
|
726
|
+
async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
490
727
|
if (!collection) {
|
|
491
728
|
throw new Error("Collection is required for update operation");
|
|
492
729
|
}
|
|
493
730
|
const { filter, update, upsert, multi } = args;
|
|
494
731
|
const queryFilter = parseFilter(filter, objectIdMode);
|
|
732
|
+
if (!allowServerJs) {
|
|
733
|
+
assertNoServerJs(queryFilter);
|
|
734
|
+
}
|
|
495
735
|
let processedUpdate = update;
|
|
496
736
|
if (update && typeof update === "object" && !Array.isArray(update)) {
|
|
497
737
|
processedUpdate = processObjectIdInFilter(
|
|
@@ -652,7 +892,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
|
|
|
652
892
|
"Write concern must be a valid MongoDB write concern object"
|
|
653
893
|
);
|
|
654
894
|
}
|
|
655
|
-
if (commitQuorum && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
|
|
895
|
+
if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
|
|
656
896
|
throw new Error("Commit quorum must be a string or number");
|
|
657
897
|
}
|
|
658
898
|
const processedIndexes = indexes.map((index) => {
|
|
@@ -666,7 +906,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
|
|
|
666
906
|
});
|
|
667
907
|
try {
|
|
668
908
|
const indexOptions = {
|
|
669
|
-
commitQuorum
|
|
909
|
+
commitQuorum
|
|
670
910
|
};
|
|
671
911
|
signal?.throwIfAborted();
|
|
672
912
|
const createdIndexes = await collection.createIndexes(
|
|
@@ -682,12 +922,15 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
|
|
|
682
922
|
return handleError(error, "create indexes", collection.collectionName);
|
|
683
923
|
}
|
|
684
924
|
}
|
|
685
|
-
async function handleCount(collection, args, objectIdMode = "auto", signal) {
|
|
925
|
+
async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
686
926
|
if (!collection) {
|
|
687
927
|
throw new Error("Collection is required for count operation");
|
|
688
928
|
}
|
|
689
929
|
const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
|
|
690
930
|
const countQuery = parseFilter(query, objectIdMode);
|
|
931
|
+
if (!allowServerJs) {
|
|
932
|
+
assertNoServerJs(countQuery);
|
|
933
|
+
}
|
|
691
934
|
try {
|
|
692
935
|
const options = {
|
|
693
936
|
limit: typeof limit === "number" ? limit : void 0,
|
|
@@ -758,17 +1001,10 @@ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
|
758
1001
|
const { nameOnly, filter } = args;
|
|
759
1002
|
const skip = args.skip || 0;
|
|
760
1003
|
const limit = args.limit || 20;
|
|
761
|
-
let processedFilter = filter;
|
|
762
|
-
if (filter && typeof filter === "object") {
|
|
763
|
-
processedFilter = processObjectIdInFilter(
|
|
764
|
-
filter,
|
|
765
|
-
objectIdMode
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
1004
|
try {
|
|
769
|
-
const
|
|
1005
|
+
const processedFilter = parseFilter(filter, objectIdMode);
|
|
770
1006
|
signal?.throwIfAborted();
|
|
771
|
-
const collections = await db.listCollections(
|
|
1007
|
+
const collections = await db.listCollections(processedFilter).toArray();
|
|
772
1008
|
const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
|
|
773
1009
|
const total = allResults.length;
|
|
774
1010
|
const paged = allResults.slice(skip, skip + limit);
|
|
@@ -948,7 +1184,8 @@ async function handleListPromptsRequest({
|
|
|
948
1184
|
client,
|
|
949
1185
|
db,
|
|
950
1186
|
isReadOnlyMode,
|
|
951
|
-
signal
|
|
1187
|
+
signal,
|
|
1188
|
+
connectionSelection = false
|
|
952
1189
|
}) {
|
|
953
1190
|
return {
|
|
954
1191
|
prompts: [
|
|
@@ -960,7 +1197,17 @@ async function handleListPromptsRequest({
|
|
|
960
1197
|
name: "collection",
|
|
961
1198
|
description: "Name of the collection to analyze",
|
|
962
1199
|
required: true
|
|
963
|
-
}
|
|
1200
|
+
},
|
|
1201
|
+
...connectionSelection ? [
|
|
1202
|
+
{
|
|
1203
|
+
name: "connectionId",
|
|
1204
|
+
description: "Configured MongoDB connection ID"
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
name: "database",
|
|
1208
|
+
description: "Allowed MongoDB database name"
|
|
1209
|
+
}
|
|
1210
|
+
] : []
|
|
964
1211
|
]
|
|
965
1212
|
}
|
|
966
1213
|
]
|
|
@@ -972,7 +1219,9 @@ async function handleGetPromptRequest({
|
|
|
972
1219
|
db,
|
|
973
1220
|
isReadOnlyMode,
|
|
974
1221
|
signal,
|
|
975
|
-
sendProgress
|
|
1222
|
+
sendProgress,
|
|
1223
|
+
connectionId,
|
|
1224
|
+
showTarget = false
|
|
976
1225
|
}) {
|
|
977
1226
|
const { name, arguments: args = {} } = request.params;
|
|
978
1227
|
if (name !== "analyze_collection") {
|
|
@@ -1004,7 +1253,9 @@ async function handleGetPromptRequest({
|
|
|
1004
1253
|
content: {
|
|
1005
1254
|
type: "text",
|
|
1006
1255
|
text: `Please analyze the following MongoDB collection:
|
|
1007
|
-
Collection: ${collectionName}
|
|
1256
|
+
Collection: ${collectionName}${showTarget ? `
|
|
1257
|
+
Connection: ${connectionId}
|
|
1258
|
+
Database: ${db.databaseName}` : ""}
|
|
1008
1259
|
|
|
1009
1260
|
Schema:
|
|
1010
1261
|
${JSON.stringify(schemaSample, null, 2)}
|
|
@@ -1033,8 +1284,8 @@ ${JSON.stringify(sampleDocs, null, 2)}`
|
|
|
1033
1284
|
|
|
1034
1285
|
// src/schemas/resource.ts
|
|
1035
1286
|
import {
|
|
1036
|
-
ProtocolError as
|
|
1037
|
-
ProtocolErrorCode as
|
|
1287
|
+
ProtocolError as ProtocolError3,
|
|
1288
|
+
ProtocolErrorCode as ProtocolErrorCode3
|
|
1038
1289
|
} from "@modelcontextprotocol/server";
|
|
1039
1290
|
import { ObjectId as ObjectId2 } from "mongodb";
|
|
1040
1291
|
function detectMongoType(value) {
|
|
@@ -1137,28 +1388,28 @@ async function handleReadResourceRequest({
|
|
|
1137
1388
|
try {
|
|
1138
1389
|
const url = new URL(request.params.uri);
|
|
1139
1390
|
if (url.protocol !== "mongodb:") {
|
|
1140
|
-
throw new
|
|
1141
|
-
|
|
1391
|
+
throw new ProtocolError3(
|
|
1392
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1142
1393
|
`Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
|
|
1143
1394
|
);
|
|
1144
1395
|
}
|
|
1145
|
-
collectionName = url.pathname.replace(/^\//, "");
|
|
1396
|
+
collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
1146
1397
|
} catch (error) {
|
|
1147
|
-
if (error instanceof
|
|
1148
|
-
throw new
|
|
1149
|
-
|
|
1398
|
+
if (error instanceof ProtocolError3) throw error;
|
|
1399
|
+
throw new ProtocolError3(
|
|
1400
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1150
1401
|
`Invalid resource URI: ${request.params.uri}`
|
|
1151
1402
|
);
|
|
1152
1403
|
}
|
|
1153
1404
|
if (!collectionName) {
|
|
1154
|
-
throw new
|
|
1155
|
-
|
|
1405
|
+
throw new ProtocolError3(
|
|
1406
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1156
1407
|
"Resource URI is missing a collection name"
|
|
1157
1408
|
);
|
|
1158
1409
|
}
|
|
1159
1410
|
if (collectionName.startsWith("system.")) {
|
|
1160
|
-
throw new
|
|
1161
|
-
|
|
1411
|
+
throw new ProtocolError3(
|
|
1412
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1162
1413
|
"Access to system collections is not allowed"
|
|
1163
1414
|
);
|
|
1164
1415
|
}
|
|
@@ -1250,7 +1501,7 @@ async function handleListResourcesRequest({
|
|
|
1250
1501
|
signal?.throwIfAborted();
|
|
1251
1502
|
const collections = await db.listCollections().toArray();
|
|
1252
1503
|
const allResources = collections.map((collection) => ({
|
|
1253
|
-
uri: `mongodb:///${collection.name}`,
|
|
1504
|
+
uri: `mongodb:///${encodeURIComponent(collection.name)}`,
|
|
1254
1505
|
mimeType: "application/json",
|
|
1255
1506
|
name: collection.name,
|
|
1256
1507
|
description: `MongoDB collection: ${collection.name}`
|
|
@@ -1322,13 +1573,14 @@ async function handleListToolsRequest({
|
|
|
1322
1573
|
client,
|
|
1323
1574
|
db,
|
|
1324
1575
|
isReadOnlyMode,
|
|
1325
|
-
signal
|
|
1576
|
+
signal,
|
|
1577
|
+
connectionSelection = false
|
|
1326
1578
|
}) {
|
|
1327
|
-
|
|
1579
|
+
const result = {
|
|
1328
1580
|
tools: [
|
|
1329
1581
|
{
|
|
1330
1582
|
name: "query",
|
|
1331
|
-
description: "
|
|
1583
|
+
description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
|
|
1332
1584
|
inputSchema: {
|
|
1333
1585
|
type: "object",
|
|
1334
1586
|
properties: {
|
|
@@ -1375,7 +1627,7 @@ async function handleListToolsRequest({
|
|
|
1375
1627
|
},
|
|
1376
1628
|
{
|
|
1377
1629
|
name: "aggregate",
|
|
1378
|
-
description: "
|
|
1630
|
+
description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
|
|
1379
1631
|
inputSchema: {
|
|
1380
1632
|
type: "object",
|
|
1381
1633
|
properties: {
|
|
@@ -1407,7 +1659,7 @@ async function handleListToolsRequest({
|
|
|
1407
1659
|
},
|
|
1408
1660
|
{
|
|
1409
1661
|
name: "update",
|
|
1410
|
-
description: "
|
|
1662
|
+
description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
|
|
1411
1663
|
inputSchema: {
|
|
1412
1664
|
type: "object",
|
|
1413
1665
|
properties: {
|
|
@@ -1443,7 +1695,7 @@ async function handleListToolsRequest({
|
|
|
1443
1695
|
},
|
|
1444
1696
|
{
|
|
1445
1697
|
name: "serverInfo",
|
|
1446
|
-
description: "
|
|
1698
|
+
description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
|
|
1447
1699
|
inputSchema: {
|
|
1448
1700
|
type: "object",
|
|
1449
1701
|
properties: {
|
|
@@ -1456,7 +1708,7 @@ async function handleListToolsRequest({
|
|
|
1456
1708
|
},
|
|
1457
1709
|
{
|
|
1458
1710
|
name: "insert",
|
|
1459
|
-
description: "Insert
|
|
1711
|
+
description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
|
|
1460
1712
|
inputSchema: {
|
|
1461
1713
|
type: "object",
|
|
1462
1714
|
properties: {
|
|
@@ -1493,7 +1745,7 @@ async function handleListToolsRequest({
|
|
|
1493
1745
|
},
|
|
1494
1746
|
{
|
|
1495
1747
|
name: "createIndex",
|
|
1496
|
-
description: "Create
|
|
1748
|
+
description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
|
|
1497
1749
|
inputSchema: {
|
|
1498
1750
|
type: "object",
|
|
1499
1751
|
properties: {
|
|
@@ -1544,8 +1796,8 @@ async function handleListToolsRequest({
|
|
|
1544
1796
|
description: "Write concern for index creation"
|
|
1545
1797
|
},
|
|
1546
1798
|
commitQuorum: {
|
|
1547
|
-
type: "string",
|
|
1548
|
-
description: "
|
|
1799
|
+
type: ["string", "number"],
|
|
1800
|
+
description: "Replica-set commit quorum as a member count or a mode such as majority"
|
|
1549
1801
|
},
|
|
1550
1802
|
objectIdMode: {
|
|
1551
1803
|
type: "string",
|
|
@@ -1559,7 +1811,7 @@ async function handleListToolsRequest({
|
|
|
1559
1811
|
},
|
|
1560
1812
|
{
|
|
1561
1813
|
name: "count",
|
|
1562
|
-
description: "Count documents
|
|
1814
|
+
description: "Count documents matching a filter in one collection. Use aggregate for counts grouped by field.",
|
|
1563
1815
|
inputSchema: {
|
|
1564
1816
|
type: "object",
|
|
1565
1817
|
properties: {
|
|
@@ -1589,7 +1841,7 @@ async function handleListToolsRequest({
|
|
|
1589
1841
|
},
|
|
1590
1842
|
{
|
|
1591
1843
|
name: "listCollections",
|
|
1592
|
-
description: "List
|
|
1844
|
+
description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
|
|
1593
1845
|
inputSchema: {
|
|
1594
1846
|
type: "object",
|
|
1595
1847
|
properties: {
|
|
@@ -1635,6 +1887,39 @@ async function handleListToolsRequest({
|
|
|
1635
1887
|
}
|
|
1636
1888
|
]
|
|
1637
1889
|
};
|
|
1890
|
+
const { tools } = result;
|
|
1891
|
+
if (connectionSelection) {
|
|
1892
|
+
const connectionId = {
|
|
1893
|
+
type: "string",
|
|
1894
|
+
description: "Configured connection ID. Defaults to default."
|
|
1895
|
+
};
|
|
1896
|
+
const database = {
|
|
1897
|
+
type: "string",
|
|
1898
|
+
description: "Allowed database name. Defaults to the database in the connection URI."
|
|
1899
|
+
};
|
|
1900
|
+
for (const tool of tools) {
|
|
1901
|
+
if (tool.name !== "convertTime") {
|
|
1902
|
+
tool.inputSchema.properties = {
|
|
1903
|
+
...tool.inputSchema.properties,
|
|
1904
|
+
connectionId,
|
|
1905
|
+
database
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
tools.push(
|
|
1910
|
+
{
|
|
1911
|
+
name: "listConnections",
|
|
1912
|
+
description: "List configured connection IDs, default databases, and read-only policies.",
|
|
1913
|
+
inputSchema: { type: "object", properties: {} }
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
name: "listDatabases",
|
|
1917
|
+
description: "List existing authorized databases on a configured connection.",
|
|
1918
|
+
inputSchema: { type: "object", properties: { connectionId } }
|
|
1919
|
+
}
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
return result;
|
|
1638
1923
|
}
|
|
1639
1924
|
|
|
1640
1925
|
// src/server.ts
|
|
@@ -1645,12 +1930,13 @@ function createSendProgress(progressToken, notify) {
|
|
|
1645
1930
|
params: { progressToken, progress, total, message }
|
|
1646
1931
|
});
|
|
1647
1932
|
}
|
|
1648
|
-
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
|
|
1933
|
+
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2) {
|
|
1934
|
+
const defaultTarget = { client, db, isReadOnlyMode };
|
|
1649
1935
|
const server = new Server(
|
|
1650
1936
|
{
|
|
1651
1937
|
name: "mongodb",
|
|
1652
1938
|
title: "MongoDB MCP Server",
|
|
1653
|
-
version: "3.0
|
|
1939
|
+
version: "3.1.0",
|
|
1654
1940
|
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
|
|
1655
1941
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1656
1942
|
...options
|
|
@@ -1675,23 +1961,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1675
1961
|
signal: ctx.mcpReq.signal
|
|
1676
1962
|
})
|
|
1677
1963
|
);
|
|
1678
|
-
server.setRequestHandler(
|
|
1679
|
-
|
|
1680
|
-
|
|
1964
|
+
server.setRequestHandler("resources/list", (request, ctx) => {
|
|
1965
|
+
if (connections2?.named) {
|
|
1966
|
+
return listNamedResources(
|
|
1967
|
+
connections2,
|
|
1968
|
+
request.params?.cursor,
|
|
1969
|
+
ctx.mcpReq.signal
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
return handleListResourcesRequest({
|
|
1681
1973
|
request,
|
|
1682
1974
|
client,
|
|
1683
1975
|
db,
|
|
1684
1976
|
isReadOnlyMode,
|
|
1685
1977
|
signal: ctx.mcpReq.signal
|
|
1686
|
-
})
|
|
1687
|
-
);
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1688
1980
|
server.setRequestHandler(
|
|
1689
1981
|
"resources/read",
|
|
1690
|
-
(request, ctx) => handleReadResourceRequest({
|
|
1982
|
+
async (request, ctx) => handleReadResourceRequest({
|
|
1691
1983
|
request,
|
|
1692
|
-
|
|
1693
|
-
db,
|
|
1694
|
-
isReadOnlyMode,
|
|
1984
|
+
...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
|
|
1695
1985
|
signal: ctx.mcpReq.signal,
|
|
1696
1986
|
sendProgress: createSendProgress(
|
|
1697
1987
|
request.params._meta?.progressToken,
|
|
@@ -1706,6 +1996,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1706
1996
|
client,
|
|
1707
1997
|
db,
|
|
1708
1998
|
isReadOnlyMode,
|
|
1999
|
+
connectionSelection: !!connections2,
|
|
1709
2000
|
signal: ctx.mcpReq.signal
|
|
1710
2001
|
})
|
|
1711
2002
|
);
|
|
@@ -1718,6 +2009,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1718
2009
|
isReadOnlyMode,
|
|
1719
2010
|
allowCrossDb,
|
|
1720
2011
|
allowServerJs,
|
|
2012
|
+
connections: connections2,
|
|
1721
2013
|
signal: ctx.mcpReq.signal
|
|
1722
2014
|
})
|
|
1723
2015
|
);
|
|
@@ -1728,16 +2020,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1728
2020
|
client,
|
|
1729
2021
|
db,
|
|
1730
2022
|
isReadOnlyMode,
|
|
2023
|
+
connectionSelection: !!connections2,
|
|
1731
2024
|
signal: ctx.mcpReq.signal
|
|
1732
2025
|
})
|
|
1733
2026
|
);
|
|
1734
2027
|
server.setRequestHandler(
|
|
1735
2028
|
"prompts/get",
|
|
1736
|
-
(request, ctx) => handleGetPromptRequest({
|
|
2029
|
+
async (request, ctx) => handleGetPromptRequest({
|
|
1737
2030
|
request,
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
isReadOnlyMode,
|
|
2031
|
+
...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
|
|
2032
|
+
showTarget: !!connections2?.named,
|
|
1741
2033
|
signal: ctx.mcpReq.signal,
|
|
1742
2034
|
sendProgress: createSendProgress(
|
|
1743
2035
|
request.params._meta?.progressToken,
|
|
@@ -1745,31 +2037,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1745
2037
|
)
|
|
1746
2038
|
})
|
|
1747
2039
|
);
|
|
1748
|
-
server.setRequestHandler(
|
|
1749
|
-
|
|
1750
|
-
(request, ctx) => handleListResourceTemplatesRequest({
|
|
2040
|
+
server.setRequestHandler("resources/templates/list", async (request, ctx) => {
|
|
2041
|
+
const result = await handleListResourceTemplatesRequest({
|
|
1751
2042
|
request,
|
|
1752
2043
|
client,
|
|
1753
2044
|
db,
|
|
1754
2045
|
isReadOnlyMode,
|
|
1755
2046
|
signal: ctx.mcpReq.signal
|
|
1756
|
-
})
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2047
|
+
});
|
|
2048
|
+
if (connections2?.named) {
|
|
2049
|
+
result.resourceTemplates.push({
|
|
2050
|
+
...result.resourceTemplates[0],
|
|
2051
|
+
name: "mongodb_connection_collection",
|
|
2052
|
+
uriTemplate: "mongodb:///{collection}{?connectionId,database}"
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
return result;
|
|
2056
|
+
});
|
|
2057
|
+
server.setRequestHandler("completion/complete", async (request, ctx) => {
|
|
2058
|
+
const { ref, argument, context } = request.params;
|
|
2059
|
+
const selectors = {
|
|
2060
|
+
...context?.arguments,
|
|
2061
|
+
...ref.type === "ref/resource" ? resourceSelectors(ref.uri) : {}
|
|
2062
|
+
};
|
|
2063
|
+
if (connections2 && (argument.name === "connectionId" || argument.name === "database")) {
|
|
2064
|
+
const candidates = argument.name === "connectionId" ? connections2.list().map(({ connectionId }) => connectionId) : await connections2.listDatabases(selectors.connectionId);
|
|
2065
|
+
const matches = candidates.filter(
|
|
2066
|
+
(value) => value.startsWith(argument.value)
|
|
2067
|
+
);
|
|
2068
|
+
return {
|
|
2069
|
+
completion: {
|
|
2070
|
+
values: matches.slice(0, 100),
|
|
2071
|
+
total: matches.length,
|
|
2072
|
+
hasMore: matches.length > 100
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
return handleCompletionRequest({
|
|
1761
2077
|
request,
|
|
1762
|
-
|
|
1763
|
-
db,
|
|
1764
|
-
isReadOnlyMode,
|
|
2078
|
+
...connections2 ? await connections2.resolve(selectors) : defaultTarget,
|
|
1765
2079
|
signal: ctx.mcpReq.signal
|
|
1766
|
-
})
|
|
1767
|
-
);
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
1768
2082
|
return server;
|
|
1769
2083
|
}
|
|
2084
|
+
async function listNamedResources(connections2, cursor, signal) {
|
|
2085
|
+
const prefix = `${connections2.cursorScope}.`;
|
|
2086
|
+
const offsetCursor = cursor?.slice(prefix.length);
|
|
2087
|
+
const offset = decodeCursor(offsetCursor);
|
|
2088
|
+
if (cursor !== void 0 && (!cursor.startsWith(prefix) || !Number.isSafeInteger(offset) || encodeCursor(offset) !== offsetCursor)) {
|
|
2089
|
+
throw new ProtocolError4(
|
|
2090
|
+
ProtocolErrorCode4.InvalidParams,
|
|
2091
|
+
"Invalid resource cursor for the current connection configuration"
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
const resources = [];
|
|
2095
|
+
for (const { connectionId, defaultDatabase } of connections2.list()) {
|
|
2096
|
+
try {
|
|
2097
|
+
signal?.throwIfAborted();
|
|
2098
|
+
for (const database of await connections2.listDatabases(connectionId)) {
|
|
2099
|
+
const { db } = await connections2.resolve({ connectionId, database });
|
|
2100
|
+
const collections = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
2101
|
+
for (const name of collections.map(({ name: name2 }) => name2).sort()) {
|
|
2102
|
+
if (name.startsWith("system.")) continue;
|
|
2103
|
+
const query = connectionId === "default" && database === defaultDatabase ? "" : `?${new URLSearchParams({ connectionId, database })}`;
|
|
2104
|
+
resources.push({
|
|
2105
|
+
uri: `mongodb:///${encodeURIComponent(name)}${query}`,
|
|
2106
|
+
name,
|
|
2107
|
+
mimeType: "application/json",
|
|
2108
|
+
description: `MongoDB collection: ${connectionId}/${database}/${name}`
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
if (signal?.aborted) throw error;
|
|
2114
|
+
throw new Error(
|
|
2115
|
+
`Failed to list resources for MongoDB connection '${connectionId}'`
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const { items, nextCursor } = paginate(resources, offsetCursor);
|
|
2120
|
+
return {
|
|
2121
|
+
resources: items,
|
|
2122
|
+
...nextCursor ? { nextCursor: prefix + nextCursor } : {}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
1770
2125
|
|
|
1771
2126
|
// src/index.ts
|
|
1772
|
-
var
|
|
2127
|
+
var connections;
|
|
1773
2128
|
async function main() {
|
|
1774
2129
|
const args = process.argv.slice(2);
|
|
1775
2130
|
let connectionUrl = "";
|
|
@@ -1818,34 +2173,13 @@ async function main() {
|
|
|
1818
2173
|
if (!connectionUrl) {
|
|
1819
2174
|
connectionUrl = process.env.MCP_MONGODB_URI || "";
|
|
1820
2175
|
}
|
|
1821
|
-
if (!connectionUrl) {
|
|
1822
|
-
console.error(
|
|
1823
|
-
"Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
|
|
1824
|
-
);
|
|
1825
|
-
console.error(
|
|
1826
|
-
"Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
|
|
1827
|
-
);
|
|
1828
|
-
console.error(
|
|
1829
|
-
" or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
|
|
1830
|
-
);
|
|
1831
|
-
process.exit(1);
|
|
1832
|
-
}
|
|
1833
|
-
if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
|
|
1834
|
-
console.error(
|
|
1835
|
-
"Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
|
|
1836
|
-
);
|
|
1837
|
-
process.exit(1);
|
|
1838
|
-
}
|
|
1839
2176
|
try {
|
|
1840
|
-
|
|
2177
|
+
connections = new Connections(
|
|
1841
2178
|
connectionUrl,
|
|
2179
|
+
process.env.MCP_MONGODB_CONNECTIONS,
|
|
1842
2180
|
readOnlyMode
|
|
1843
2181
|
);
|
|
1844
|
-
|
|
1845
|
-
if (!isConnected || !client || !db) {
|
|
1846
|
-
console.error("Failed to connect to MongoDB");
|
|
1847
|
-
process.exit(1);
|
|
1848
|
-
}
|
|
2182
|
+
const { client, db, isReadOnlyMode } = await connections.resolve();
|
|
1849
2183
|
if (transportMode === "http") {
|
|
1850
2184
|
await startHttpServer(
|
|
1851
2185
|
client,
|
|
@@ -1853,6 +2187,7 @@ async function main() {
|
|
|
1853
2187
|
isReadOnlyMode,
|
|
1854
2188
|
allowCrossDb,
|
|
1855
2189
|
allowServerJs,
|
|
2190
|
+
connections,
|
|
1856
2191
|
port,
|
|
1857
2192
|
allowedOrigins,
|
|
1858
2193
|
jsonLimit,
|
|
@@ -1864,20 +2199,27 @@ async function main() {
|
|
|
1864
2199
|
db,
|
|
1865
2200
|
isReadOnlyMode,
|
|
1866
2201
|
allowCrossDb,
|
|
1867
|
-
allowServerJs
|
|
2202
|
+
allowServerJs,
|
|
2203
|
+
connections
|
|
1868
2204
|
);
|
|
1869
2205
|
}
|
|
1870
2206
|
} catch (error) {
|
|
1871
2207
|
console.error("Failed to connect to MongoDB:", error);
|
|
1872
|
-
|
|
1873
|
-
await mongoClient.close();
|
|
1874
|
-
}
|
|
2208
|
+
await connections?.close();
|
|
1875
2209
|
process.exit(1);
|
|
1876
2210
|
}
|
|
1877
2211
|
}
|
|
1878
|
-
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
|
|
2212
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2) {
|
|
1879
2213
|
serveStdio(
|
|
1880
|
-
() => createServer(
|
|
2214
|
+
() => createServer(
|
|
2215
|
+
client,
|
|
2216
|
+
db,
|
|
2217
|
+
isReadOnlyMode,
|
|
2218
|
+
allowCrossDb,
|
|
2219
|
+
allowServerJs,
|
|
2220
|
+
{},
|
|
2221
|
+
connections2
|
|
2222
|
+
)
|
|
1881
2223
|
);
|
|
1882
2224
|
console.warn("Server connected successfully via stdio");
|
|
1883
2225
|
}
|
|
@@ -1904,7 +2246,7 @@ function bearerTokenValid(authorization, expected) {
|
|
|
1904
2246
|
const wanted = Buffer.from(expected);
|
|
1905
2247
|
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
|
|
1906
2248
|
}
|
|
1907
|
-
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
|
|
2249
|
+
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, port, allowedOrigins, jsonLimit, authToken) {
|
|
1908
2250
|
const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
|
|
1909
2251
|
app.use((req, res, next) => {
|
|
1910
2252
|
const start = Date.now();
|
|
@@ -1955,7 +2297,15 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1955
2297
|
});
|
|
1956
2298
|
}
|
|
1957
2299
|
const mcpHandler = createMcpHandler(
|
|
1958
|
-
(_ctx) => createServer(
|
|
2300
|
+
(_ctx) => createServer(
|
|
2301
|
+
client,
|
|
2302
|
+
db,
|
|
2303
|
+
isReadOnlyMode,
|
|
2304
|
+
allowCrossDb,
|
|
2305
|
+
allowServerJs,
|
|
2306
|
+
{},
|
|
2307
|
+
connections2
|
|
2308
|
+
)
|
|
1959
2309
|
);
|
|
1960
2310
|
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
1961
2311
|
onerror: (error) => console.error("Error handling MCP request:", error)
|
|
@@ -1982,15 +2332,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1982
2332
|
});
|
|
1983
2333
|
}
|
|
1984
2334
|
process.on("SIGINT", async () => {
|
|
1985
|
-
|
|
1986
|
-
await mongoClient.close();
|
|
1987
|
-
}
|
|
2335
|
+
await connections?.close();
|
|
1988
2336
|
process.exit(0);
|
|
1989
2337
|
});
|
|
1990
2338
|
process.on("SIGTERM", async () => {
|
|
1991
|
-
|
|
1992
|
-
await mongoClient.close();
|
|
1993
|
-
}
|
|
2339
|
+
await connections?.close();
|
|
1994
2340
|
process.exit(0);
|
|
1995
2341
|
});
|
|
1996
2342
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-mongo-server",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "A Model Context Protocol server for MongoDB connections",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"format": "biome format --write .",
|
|
22
22
|
"format:check": "biome format .",
|
|
23
23
|
"lint": "biome lint .",
|
|
24
|
+
"test:connections": "bun test tests/connections.lifecycle.test.ts && node --test tests/connections.e2e.test.mjs",
|
|
25
|
+
"test:connections:legacy": "MCP_TEST_PROTOCOL=legacy node --test tests/connections.e2e.test.mjs",
|
|
24
26
|
"lint:fix": "biome lint --write .",
|
|
25
27
|
"check": "biome check --write .",
|
|
26
28
|
"inspector": "mcp-inspector --config inspector-config.json --server mongodb"
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"@biomejs/biome": "^2.5.5",
|
|
36
38
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
37
39
|
"@modelcontextprotocol/inspector": "^1.0.0",
|
|
40
|
+
"@types/express": "5",
|
|
38
41
|
"@types/node": "^26.1.1",
|
|
39
42
|
"tsup": "^8.5.1",
|
|
40
43
|
"typescript": "^5.9.3"
|