mcp-mongo-server 3.0.2 → 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 +32 -3
- package/build/index.js +438 -109
- package/package.json +3 -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,13 +100,41 @@ 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
139
|
**Server-side JavaScript.** The operators `$function`, `$where`, and
|
|
111
140
|
`$accumulator` are blocked by default in aggregation pipelines and in the
|
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",
|
|
@@ -119,6 +267,7 @@ async function handleCallToolRequest({
|
|
|
119
267
|
isReadOnlyMode,
|
|
120
268
|
allowCrossDb = false,
|
|
121
269
|
allowServerJs = false,
|
|
270
|
+
connections: connections2,
|
|
122
271
|
signal
|
|
123
272
|
}) {
|
|
124
273
|
const { name, arguments: args = {} } = request.params;
|
|
@@ -134,8 +283,32 @@ async function handleCallToolRequest({
|
|
|
134
283
|
args.sort = parseSort(args.sort);
|
|
135
284
|
}
|
|
136
285
|
Object.assign(args, filteredArgs);
|
|
137
|
-
|
|
286
|
+
if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
|
|
287
|
+
validateOperation(operation);
|
|
288
|
+
}
|
|
138
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
|
+
}
|
|
139
312
|
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
140
313
|
let collection = null;
|
|
141
314
|
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
@@ -161,7 +334,7 @@ async function handleCallToolRequest({
|
|
|
161
334
|
signal
|
|
162
335
|
);
|
|
163
336
|
} catch (error) {
|
|
164
|
-
if (error instanceof
|
|
337
|
+
if (error instanceof ProtocolError2 || signal?.aborted) {
|
|
165
338
|
throw error;
|
|
166
339
|
}
|
|
167
340
|
return toolExecutionError(
|
|
@@ -169,6 +342,40 @@ async function handleCallToolRequest({
|
|
|
169
342
|
);
|
|
170
343
|
}
|
|
171
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
|
+
}
|
|
172
379
|
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
|
|
173
380
|
switch (operation) {
|
|
174
381
|
case "query":
|
|
@@ -190,7 +397,13 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
|
|
|
190
397
|
signal
|
|
191
398
|
);
|
|
192
399
|
case "update":
|
|
193
|
-
return handleUpdate(
|
|
400
|
+
return handleUpdate(
|
|
401
|
+
collection,
|
|
402
|
+
args,
|
|
403
|
+
objectIdMode,
|
|
404
|
+
allowServerJs,
|
|
405
|
+
signal
|
|
406
|
+
);
|
|
194
407
|
case "serverInfo":
|
|
195
408
|
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
196
409
|
case "insert":
|
|
@@ -210,7 +423,10 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
|
|
|
210
423
|
case "convertTime":
|
|
211
424
|
return handleConvertTime(args);
|
|
212
425
|
default:
|
|
213
|
-
throw new
|
|
426
|
+
throw new ProtocolError2(
|
|
427
|
+
ProtocolErrorCode2.InvalidParams,
|
|
428
|
+
`Unknown tool: ${operation}`
|
|
429
|
+
);
|
|
214
430
|
}
|
|
215
431
|
}
|
|
216
432
|
function validateOperation(operation) {
|
|
@@ -226,7 +442,10 @@ function validateOperation(operation) {
|
|
|
226
442
|
"convertTime"
|
|
227
443
|
];
|
|
228
444
|
if (!validOperations.includes(operation)) {
|
|
229
|
-
throw new
|
|
445
|
+
throw new ProtocolError2(
|
|
446
|
+
ProtocolErrorCode2.InvalidParams,
|
|
447
|
+
`Unknown tool: ${operation}`
|
|
448
|
+
);
|
|
230
449
|
}
|
|
231
450
|
}
|
|
232
451
|
function validateCollection(collection) {
|
|
@@ -965,7 +1184,8 @@ async function handleListPromptsRequest({
|
|
|
965
1184
|
client,
|
|
966
1185
|
db,
|
|
967
1186
|
isReadOnlyMode,
|
|
968
|
-
signal
|
|
1187
|
+
signal,
|
|
1188
|
+
connectionSelection = false
|
|
969
1189
|
}) {
|
|
970
1190
|
return {
|
|
971
1191
|
prompts: [
|
|
@@ -977,7 +1197,17 @@ async function handleListPromptsRequest({
|
|
|
977
1197
|
name: "collection",
|
|
978
1198
|
description: "Name of the collection to analyze",
|
|
979
1199
|
required: true
|
|
980
|
-
}
|
|
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
|
+
] : []
|
|
981
1211
|
]
|
|
982
1212
|
}
|
|
983
1213
|
]
|
|
@@ -989,7 +1219,9 @@ async function handleGetPromptRequest({
|
|
|
989
1219
|
db,
|
|
990
1220
|
isReadOnlyMode,
|
|
991
1221
|
signal,
|
|
992
|
-
sendProgress
|
|
1222
|
+
sendProgress,
|
|
1223
|
+
connectionId,
|
|
1224
|
+
showTarget = false
|
|
993
1225
|
}) {
|
|
994
1226
|
const { name, arguments: args = {} } = request.params;
|
|
995
1227
|
if (name !== "analyze_collection") {
|
|
@@ -1021,7 +1253,9 @@ async function handleGetPromptRequest({
|
|
|
1021
1253
|
content: {
|
|
1022
1254
|
type: "text",
|
|
1023
1255
|
text: `Please analyze the following MongoDB collection:
|
|
1024
|
-
Collection: ${collectionName}
|
|
1256
|
+
Collection: ${collectionName}${showTarget ? `
|
|
1257
|
+
Connection: ${connectionId}
|
|
1258
|
+
Database: ${db.databaseName}` : ""}
|
|
1025
1259
|
|
|
1026
1260
|
Schema:
|
|
1027
1261
|
${JSON.stringify(schemaSample, null, 2)}
|
|
@@ -1050,8 +1284,8 @@ ${JSON.stringify(sampleDocs, null, 2)}`
|
|
|
1050
1284
|
|
|
1051
1285
|
// src/schemas/resource.ts
|
|
1052
1286
|
import {
|
|
1053
|
-
ProtocolError as
|
|
1054
|
-
ProtocolErrorCode as
|
|
1287
|
+
ProtocolError as ProtocolError3,
|
|
1288
|
+
ProtocolErrorCode as ProtocolErrorCode3
|
|
1055
1289
|
} from "@modelcontextprotocol/server";
|
|
1056
1290
|
import { ObjectId as ObjectId2 } from "mongodb";
|
|
1057
1291
|
function detectMongoType(value) {
|
|
@@ -1154,28 +1388,28 @@ async function handleReadResourceRequest({
|
|
|
1154
1388
|
try {
|
|
1155
1389
|
const url = new URL(request.params.uri);
|
|
1156
1390
|
if (url.protocol !== "mongodb:") {
|
|
1157
|
-
throw new
|
|
1158
|
-
|
|
1391
|
+
throw new ProtocolError3(
|
|
1392
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1159
1393
|
`Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
|
|
1160
1394
|
);
|
|
1161
1395
|
}
|
|
1162
1396
|
collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
1163
1397
|
} catch (error) {
|
|
1164
|
-
if (error instanceof
|
|
1165
|
-
throw new
|
|
1166
|
-
|
|
1398
|
+
if (error instanceof ProtocolError3) throw error;
|
|
1399
|
+
throw new ProtocolError3(
|
|
1400
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1167
1401
|
`Invalid resource URI: ${request.params.uri}`
|
|
1168
1402
|
);
|
|
1169
1403
|
}
|
|
1170
1404
|
if (!collectionName) {
|
|
1171
|
-
throw new
|
|
1172
|
-
|
|
1405
|
+
throw new ProtocolError3(
|
|
1406
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1173
1407
|
"Resource URI is missing a collection name"
|
|
1174
1408
|
);
|
|
1175
1409
|
}
|
|
1176
1410
|
if (collectionName.startsWith("system.")) {
|
|
1177
|
-
throw new
|
|
1178
|
-
|
|
1411
|
+
throw new ProtocolError3(
|
|
1412
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1179
1413
|
"Access to system collections is not allowed"
|
|
1180
1414
|
);
|
|
1181
1415
|
}
|
|
@@ -1339,9 +1573,10 @@ async function handleListToolsRequest({
|
|
|
1339
1573
|
client,
|
|
1340
1574
|
db,
|
|
1341
1575
|
isReadOnlyMode,
|
|
1342
|
-
signal
|
|
1576
|
+
signal,
|
|
1577
|
+
connectionSelection = false
|
|
1343
1578
|
}) {
|
|
1344
|
-
|
|
1579
|
+
const result = {
|
|
1345
1580
|
tools: [
|
|
1346
1581
|
{
|
|
1347
1582
|
name: "query",
|
|
@@ -1652,6 +1887,39 @@ async function handleListToolsRequest({
|
|
|
1652
1887
|
}
|
|
1653
1888
|
]
|
|
1654
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;
|
|
1655
1923
|
}
|
|
1656
1924
|
|
|
1657
1925
|
// src/server.ts
|
|
@@ -1662,12 +1930,13 @@ function createSendProgress(progressToken, notify) {
|
|
|
1662
1930
|
params: { progressToken, progress, total, message }
|
|
1663
1931
|
});
|
|
1664
1932
|
}
|
|
1665
|
-
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 };
|
|
1666
1935
|
const server = new Server(
|
|
1667
1936
|
{
|
|
1668
1937
|
name: "mongodb",
|
|
1669
1938
|
title: "MongoDB MCP Server",
|
|
1670
|
-
version: "3.0
|
|
1939
|
+
version: "3.1.0",
|
|
1671
1940
|
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
|
|
1672
1941
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1673
1942
|
...options
|
|
@@ -1692,23 +1961,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1692
1961
|
signal: ctx.mcpReq.signal
|
|
1693
1962
|
})
|
|
1694
1963
|
);
|
|
1695
|
-
server.setRequestHandler(
|
|
1696
|
-
|
|
1697
|
-
|
|
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({
|
|
1698
1973
|
request,
|
|
1699
1974
|
client,
|
|
1700
1975
|
db,
|
|
1701
1976
|
isReadOnlyMode,
|
|
1702
1977
|
signal: ctx.mcpReq.signal
|
|
1703
|
-
})
|
|
1704
|
-
);
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1705
1980
|
server.setRequestHandler(
|
|
1706
1981
|
"resources/read",
|
|
1707
|
-
(request, ctx) => handleReadResourceRequest({
|
|
1982
|
+
async (request, ctx) => handleReadResourceRequest({
|
|
1708
1983
|
request,
|
|
1709
|
-
|
|
1710
|
-
db,
|
|
1711
|
-
isReadOnlyMode,
|
|
1984
|
+
...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
|
|
1712
1985
|
signal: ctx.mcpReq.signal,
|
|
1713
1986
|
sendProgress: createSendProgress(
|
|
1714
1987
|
request.params._meta?.progressToken,
|
|
@@ -1723,6 +1996,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1723
1996
|
client,
|
|
1724
1997
|
db,
|
|
1725
1998
|
isReadOnlyMode,
|
|
1999
|
+
connectionSelection: !!connections2,
|
|
1726
2000
|
signal: ctx.mcpReq.signal
|
|
1727
2001
|
})
|
|
1728
2002
|
);
|
|
@@ -1735,6 +2009,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1735
2009
|
isReadOnlyMode,
|
|
1736
2010
|
allowCrossDb,
|
|
1737
2011
|
allowServerJs,
|
|
2012
|
+
connections: connections2,
|
|
1738
2013
|
signal: ctx.mcpReq.signal
|
|
1739
2014
|
})
|
|
1740
2015
|
);
|
|
@@ -1745,16 +2020,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1745
2020
|
client,
|
|
1746
2021
|
db,
|
|
1747
2022
|
isReadOnlyMode,
|
|
2023
|
+
connectionSelection: !!connections2,
|
|
1748
2024
|
signal: ctx.mcpReq.signal
|
|
1749
2025
|
})
|
|
1750
2026
|
);
|
|
1751
2027
|
server.setRequestHandler(
|
|
1752
2028
|
"prompts/get",
|
|
1753
|
-
(request, ctx) => handleGetPromptRequest({
|
|
2029
|
+
async (request, ctx) => handleGetPromptRequest({
|
|
1754
2030
|
request,
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
isReadOnlyMode,
|
|
2031
|
+
...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
|
|
2032
|
+
showTarget: !!connections2?.named,
|
|
1758
2033
|
signal: ctx.mcpReq.signal,
|
|
1759
2034
|
sendProgress: createSendProgress(
|
|
1760
2035
|
request.params._meta?.progressToken,
|
|
@@ -1762,31 +2037,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1762
2037
|
)
|
|
1763
2038
|
})
|
|
1764
2039
|
);
|
|
1765
|
-
server.setRequestHandler(
|
|
1766
|
-
|
|
1767
|
-
(request, ctx) => handleListResourceTemplatesRequest({
|
|
2040
|
+
server.setRequestHandler("resources/templates/list", async (request, ctx) => {
|
|
2041
|
+
const result = await handleListResourceTemplatesRequest({
|
|
1768
2042
|
request,
|
|
1769
2043
|
client,
|
|
1770
2044
|
db,
|
|
1771
2045
|
isReadOnlyMode,
|
|
1772
2046
|
signal: ctx.mcpReq.signal
|
|
1773
|
-
})
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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({
|
|
1778
2077
|
request,
|
|
1779
|
-
|
|
1780
|
-
db,
|
|
1781
|
-
isReadOnlyMode,
|
|
2078
|
+
...connections2 ? await connections2.resolve(selectors) : defaultTarget,
|
|
1782
2079
|
signal: ctx.mcpReq.signal
|
|
1783
|
-
})
|
|
1784
|
-
);
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
1785
2082
|
return server;
|
|
1786
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
|
+
}
|
|
1787
2125
|
|
|
1788
2126
|
// src/index.ts
|
|
1789
|
-
var
|
|
2127
|
+
var connections;
|
|
1790
2128
|
async function main() {
|
|
1791
2129
|
const args = process.argv.slice(2);
|
|
1792
2130
|
let connectionUrl = "";
|
|
@@ -1835,34 +2173,13 @@ async function main() {
|
|
|
1835
2173
|
if (!connectionUrl) {
|
|
1836
2174
|
connectionUrl = process.env.MCP_MONGODB_URI || "";
|
|
1837
2175
|
}
|
|
1838
|
-
if (!connectionUrl) {
|
|
1839
|
-
console.error(
|
|
1840
|
-
"Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
|
|
1841
|
-
);
|
|
1842
|
-
console.error(
|
|
1843
|
-
"Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
|
|
1844
|
-
);
|
|
1845
|
-
console.error(
|
|
1846
|
-
" or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
|
|
1847
|
-
);
|
|
1848
|
-
process.exit(1);
|
|
1849
|
-
}
|
|
1850
|
-
if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
|
|
1851
|
-
console.error(
|
|
1852
|
-
"Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
|
|
1853
|
-
);
|
|
1854
|
-
process.exit(1);
|
|
1855
|
-
}
|
|
1856
2176
|
try {
|
|
1857
|
-
|
|
2177
|
+
connections = new Connections(
|
|
1858
2178
|
connectionUrl,
|
|
2179
|
+
process.env.MCP_MONGODB_CONNECTIONS,
|
|
1859
2180
|
readOnlyMode
|
|
1860
2181
|
);
|
|
1861
|
-
|
|
1862
|
-
if (!isConnected || !client || !db) {
|
|
1863
|
-
console.error("Failed to connect to MongoDB");
|
|
1864
|
-
process.exit(1);
|
|
1865
|
-
}
|
|
2182
|
+
const { client, db, isReadOnlyMode } = await connections.resolve();
|
|
1866
2183
|
if (transportMode === "http") {
|
|
1867
2184
|
await startHttpServer(
|
|
1868
2185
|
client,
|
|
@@ -1870,6 +2187,7 @@ async function main() {
|
|
|
1870
2187
|
isReadOnlyMode,
|
|
1871
2188
|
allowCrossDb,
|
|
1872
2189
|
allowServerJs,
|
|
2190
|
+
connections,
|
|
1873
2191
|
port,
|
|
1874
2192
|
allowedOrigins,
|
|
1875
2193
|
jsonLimit,
|
|
@@ -1881,20 +2199,27 @@ async function main() {
|
|
|
1881
2199
|
db,
|
|
1882
2200
|
isReadOnlyMode,
|
|
1883
2201
|
allowCrossDb,
|
|
1884
|
-
allowServerJs
|
|
2202
|
+
allowServerJs,
|
|
2203
|
+
connections
|
|
1885
2204
|
);
|
|
1886
2205
|
}
|
|
1887
2206
|
} catch (error) {
|
|
1888
2207
|
console.error("Failed to connect to MongoDB:", error);
|
|
1889
|
-
|
|
1890
|
-
await mongoClient.close();
|
|
1891
|
-
}
|
|
2208
|
+
await connections?.close();
|
|
1892
2209
|
process.exit(1);
|
|
1893
2210
|
}
|
|
1894
2211
|
}
|
|
1895
|
-
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
|
|
2212
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2) {
|
|
1896
2213
|
serveStdio(
|
|
1897
|
-
() => createServer(
|
|
2214
|
+
() => createServer(
|
|
2215
|
+
client,
|
|
2216
|
+
db,
|
|
2217
|
+
isReadOnlyMode,
|
|
2218
|
+
allowCrossDb,
|
|
2219
|
+
allowServerJs,
|
|
2220
|
+
{},
|
|
2221
|
+
connections2
|
|
2222
|
+
)
|
|
1898
2223
|
);
|
|
1899
2224
|
console.warn("Server connected successfully via stdio");
|
|
1900
2225
|
}
|
|
@@ -1921,7 +2246,7 @@ function bearerTokenValid(authorization, expected) {
|
|
|
1921
2246
|
const wanted = Buffer.from(expected);
|
|
1922
2247
|
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
|
|
1923
2248
|
}
|
|
1924
|
-
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) {
|
|
1925
2250
|
const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
|
|
1926
2251
|
app.use((req, res, next) => {
|
|
1927
2252
|
const start = Date.now();
|
|
@@ -1972,7 +2297,15 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1972
2297
|
});
|
|
1973
2298
|
}
|
|
1974
2299
|
const mcpHandler = createMcpHandler(
|
|
1975
|
-
(_ctx) => createServer(
|
|
2300
|
+
(_ctx) => createServer(
|
|
2301
|
+
client,
|
|
2302
|
+
db,
|
|
2303
|
+
isReadOnlyMode,
|
|
2304
|
+
allowCrossDb,
|
|
2305
|
+
allowServerJs,
|
|
2306
|
+
{},
|
|
2307
|
+
connections2
|
|
2308
|
+
)
|
|
1976
2309
|
);
|
|
1977
2310
|
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
1978
2311
|
onerror: (error) => console.error("Error handling MCP request:", error)
|
|
@@ -1999,15 +2332,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1999
2332
|
});
|
|
2000
2333
|
}
|
|
2001
2334
|
process.on("SIGINT", async () => {
|
|
2002
|
-
|
|
2003
|
-
await mongoClient.close();
|
|
2004
|
-
}
|
|
2335
|
+
await connections?.close();
|
|
2005
2336
|
process.exit(0);
|
|
2006
2337
|
});
|
|
2007
2338
|
process.on("SIGTERM", async () => {
|
|
2008
|
-
|
|
2009
|
-
await mongoClient.close();
|
|
2010
|
-
}
|
|
2339
|
+
await connections?.close();
|
|
2011
2340
|
process.exit(0);
|
|
2012
2341
|
});
|
|
2013
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"
|