mongodb-mcp-server 0.0.4 → 0.0.6
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/.github/CODEOWNERS +3 -0
- package/.github/dependabot.yml +10 -0
- package/.github/workflows/code_health.yaml +53 -22
- package/.github/workflows/code_health_fork.yaml +106 -0
- package/.github/workflows/codeql.yml +34 -0
- package/.github/workflows/lint.yml +37 -0
- package/.github/workflows/prepare_release.yaml +6 -4
- package/.github/workflows/publish.yaml +6 -3
- package/.prettierrc.json +1 -1
- package/README.md +18 -0
- package/dist/common/atlas/apiClient.js +28 -4
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/config.js +4 -7
- package/dist/config.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js +12 -7
- package/dist/index.js.map +1 -1
- package/dist/logger.js +72 -28
- package/dist/logger.js.map +1 -1
- package/dist/packageInfo.js +6 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/server.js +114 -10
- package/dist/server.js.map +1 -1
- package/dist/session.js +66 -16
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +15 -0
- package/dist/telemetry/constants.js.map +1 -0
- package/dist/telemetry/eventCache.js +53 -0
- package/dist/telemetry/eventCache.js.map +1 -0
- package/dist/telemetry/telemetry.js +97 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/telemetry/types.js +2 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/tools/atlas/atlasTool.js +8 -3
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/{createAccessList.js → create/createAccessList.js} +1 -2
- package/dist/tools/atlas/create/createAccessList.js.map +1 -0
- package/dist/tools/atlas/{createDBUser.js → create/createDBUser.js} +1 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -0
- package/dist/tools/atlas/{createFreeCluster.js → create/createFreeCluster.js} +5 -3
- package/dist/tools/atlas/create/createFreeCluster.js.map +1 -0
- package/dist/tools/atlas/{createProject.js → create/createProject.js} +1 -2
- package/dist/tools/atlas/create/createProject.js.map +1 -0
- package/dist/tools/atlas/metadata/connectCluster.js +97 -0
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -0
- package/dist/tools/atlas/{inspectAccessList.js → read/inspectAccessList.js} +1 -2
- package/dist/tools/atlas/read/inspectAccessList.js.map +1 -0
- package/dist/tools/atlas/{inspectCluster.js → read/inspectCluster.js} +1 -2
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -0
- package/dist/tools/atlas/{listClusters.js → read/listClusters.js} +1 -2
- package/dist/tools/atlas/read/listClusters.js.map +1 -0
- package/dist/tools/atlas/{listDBUsers.js → read/listDBUsers.js} +1 -2
- package/dist/tools/atlas/read/listDBUsers.js.map +1 -0
- package/dist/tools/atlas/{listOrgs.js → read/listOrgs.js} +1 -2
- package/dist/tools/atlas/read/listOrgs.js.map +1 -0
- package/dist/tools/atlas/{listProjects.js → read/listProjects.js} +11 -5
- package/dist/tools/atlas/read/listProjects.js.map +1 -0
- package/dist/tools/atlas/tools.js +12 -10
- package/dist/tools/atlas/tools.js.map +1 -1
- package/dist/tools/mongodb/create/insertMany.js +1 -1
- package/dist/tools/mongodb/create/insertMany.js.map +1 -1
- package/dist/tools/mongodb/delete/deleteMany.js +1 -2
- package/dist/tools/mongodb/delete/deleteMany.js.map +1 -1
- package/dist/tools/mongodb/metadata/collectionSchema.js +15 -13
- package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -1
- package/dist/tools/mongodb/metadata/collectionStorageSize.js +32 -3
- package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -1
- package/dist/tools/mongodb/metadata/connect.js +59 -72
- package/dist/tools/mongodb/metadata/connect.js.map +1 -1
- package/dist/tools/mongodb/metadata/dbStats.js +6 -1
- package/dist/tools/mongodb/metadata/dbStats.js.map +1 -1
- package/dist/tools/mongodb/metadata/explain.js +14 -7
- package/dist/tools/mongodb/metadata/explain.js.map +1 -1
- package/dist/tools/mongodb/metadata/logs.js +45 -0
- package/dist/tools/mongodb/metadata/logs.js.map +1 -0
- package/dist/tools/mongodb/mongodbTool.js +42 -36
- package/dist/tools/mongodb/mongodbTool.js.map +1 -1
- package/dist/tools/mongodb/read/aggregate.js +4 -4
- package/dist/tools/mongodb/read/aggregate.js.map +1 -1
- package/dist/tools/mongodb/read/collectionIndexes.js +24 -5
- package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -1
- package/dist/tools/mongodb/read/count.js +1 -2
- package/dist/tools/mongodb/read/count.js.map +1 -1
- package/dist/tools/mongodb/read/find.js +5 -6
- package/dist/tools/mongodb/read/find.js.map +1 -1
- package/dist/tools/mongodb/tools.js +6 -2
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/dist/tools/mongodb/update/renameCollection.js +28 -4
- package/dist/tools/mongodb/update/renameCollection.js.map +1 -1
- package/dist/tools/mongodb/update/updateMany.js +7 -11
- package/dist/tools/mongodb/update/updateMany.js.map +1 -1
- package/dist/tools/tool.js +68 -15
- package/dist/tools/tool.js.map +1 -1
- package/eslint.config.js +29 -10
- package/global.d.ts +1 -0
- package/package.json +7 -3
- package/scripts/apply.ts +9 -7
- package/scripts/filter.ts +3 -2
- package/src/common/atlas/apiClient.ts +44 -11
- package/src/config.ts +16 -17
- package/src/errors.ts +1 -1
- package/src/index.ts +12 -8
- package/src/logger.ts +92 -29
- package/src/packageInfo.ts +6 -0
- package/src/server.ts +160 -11
- package/src/session.ts +102 -22
- package/src/telemetry/constants.ts +15 -0
- package/src/telemetry/eventCache.ts +62 -0
- package/src/telemetry/telemetry.ts +125 -0
- package/src/telemetry/types.ts +77 -0
- package/src/tools/atlas/atlasTool.ts +7 -5
- package/src/tools/atlas/{createAccessList.ts → create/createAccessList.ts} +2 -4
- package/src/tools/atlas/{createDBUser.ts → create/createDBUser.ts} +3 -5
- package/src/tools/atlas/{createFreeCluster.ts → create/createFreeCluster.ts} +7 -6
- package/src/tools/atlas/{createProject.ts → create/createProject.ts} +3 -4
- package/src/tools/atlas/metadata/connectCluster.ts +114 -0
- package/src/tools/atlas/{inspectAccessList.ts → read/inspectAccessList.ts} +2 -4
- package/src/tools/atlas/{inspectCluster.ts → read/inspectCluster.ts} +3 -5
- package/src/tools/atlas/{listClusters.ts → read/listClusters.ts} +3 -5
- package/src/tools/atlas/{listDBUsers.ts → read/listDBUsers.ts} +3 -5
- package/src/tools/atlas/{listOrgs.ts → read/listOrgs.ts} +2 -4
- package/src/tools/atlas/{listProjects.ts → read/listProjects.ts} +15 -7
- package/src/tools/atlas/tools.ts +12 -10
- package/src/tools/mongodb/create/insertMany.ts +1 -1
- package/src/tools/mongodb/delete/deleteMany.ts +1 -2
- package/src/tools/mongodb/metadata/collectionSchema.ts +16 -14
- package/src/tools/mongodb/metadata/collectionStorageSize.ts +41 -3
- package/src/tools/mongodb/metadata/connect.ts +78 -76
- package/src/tools/mongodb/metadata/dbStats.ts +6 -1
- package/src/tools/mongodb/metadata/explain.ts +20 -7
- package/src/tools/mongodb/metadata/logs.ts +55 -0
- package/src/tools/mongodb/mongodbTool.ts +47 -40
- package/src/tools/mongodb/read/aggregate.ts +4 -4
- package/src/tools/mongodb/read/collectionIndexes.ts +29 -5
- package/src/tools/mongodb/read/count.ts +1 -2
- package/src/tools/mongodb/read/find.ts +5 -6
- package/src/tools/mongodb/tools.ts +6 -2
- package/src/tools/mongodb/update/renameCollection.ts +33 -4
- package/src/tools/mongodb/update/updateMany.ts +7 -11
- package/src/tools/tool.ts +89 -26
- package/tests/integration/helpers.ts +94 -107
- package/tests/integration/inMemoryTransport.ts +3 -2
- package/tests/integration/server.test.ts +75 -23
- package/tests/integration/tools/atlas/accessLists.test.ts +13 -15
- package/tests/integration/tools/atlas/atlasHelpers.ts +10 -13
- package/tests/integration/tools/atlas/clusters.test.ts +79 -16
- package/tests/integration/tools/atlas/dbUsers.test.ts +9 -9
- package/tests/integration/tools/atlas/orgs.test.ts +4 -4
- package/tests/integration/tools/atlas/projects.test.ts +10 -12
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +19 -62
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +41 -87
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +35 -78
- package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +25 -62
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +22 -71
- package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +29 -63
- package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +154 -0
- package/tests/integration/tools/mongodb/metadata/collectionStorageSize.test.ts +86 -0
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +88 -93
- package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +104 -0
- package/tests/integration/tools/mongodb/metadata/explain.test.ts +171 -0
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +28 -56
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +32 -26
- package/tests/integration/tools/mongodb/metadata/logs.test.ts +83 -0
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +176 -0
- package/tests/integration/tools/mongodb/read/aggregate.test.ts +99 -0
- package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +99 -0
- package/tests/integration/tools/mongodb/read/count.test.ts +31 -79
- package/tests/integration/tools/mongodb/read/find.test.ts +182 -0
- package/tests/integration/tools/mongodb/update/renameCollection.test.ts +194 -0
- package/tests/integration/tools/mongodb/update/updateMany.test.ts +238 -0
- package/tests/unit/telemetry.test.ts +200 -0
- package/tsconfig.jest.json +2 -1
- package/tsconfig.json +1 -1
- package/tsconfig.lint.json +8 -0
- package/dist/tools/atlas/createAccessList.js.map +0 -1
- package/dist/tools/atlas/createDBUser.js.map +0 -1
- package/dist/tools/atlas/createFreeCluster.js.map +0 -1
- package/dist/tools/atlas/createProject.js.map +0 -1
- package/dist/tools/atlas/inspectAccessList.js.map +0 -1
- package/dist/tools/atlas/inspectCluster.js.map +0 -1
- package/dist/tools/atlas/listClusters.js.map +0 -1
- package/dist/tools/atlas/listDBUsers.js.map +0 -1
- package/dist/tools/atlas/listOrgs.js.map +0 -1
- package/dist/tools/atlas/listProjects.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
|
|
2
|
+
// import { ConnectTool } from "./metadata/connect.js";
|
|
2
3
|
import { ListCollectionsTool } from "./metadata/listCollections.js";
|
|
3
4
|
import { CollectionIndexesTool } from "./read/collectionIndexes.js";
|
|
4
5
|
import { ListDatabasesTool } from "./metadata/listDatabases.js";
|
|
@@ -17,9 +18,11 @@ import { DropDatabaseTool } from "./delete/dropDatabase.js";
|
|
|
17
18
|
import { DropCollectionTool } from "./delete/dropCollection.js";
|
|
18
19
|
import { ExplainTool } from "./metadata/explain.js";
|
|
19
20
|
import { CreateCollectionTool } from "./create/createCollection.js";
|
|
21
|
+
import { LogsTool } from "./metadata/logs.js";
|
|
20
22
|
|
|
21
23
|
export const MongoDbTools = [
|
|
22
|
-
|
|
24
|
+
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
|
|
25
|
+
// ConnectTool,
|
|
23
26
|
ListCollectionsTool,
|
|
24
27
|
ListDatabasesTool,
|
|
25
28
|
CollectionIndexesTool,
|
|
@@ -38,4 +41,5 @@ export const MongoDbTools = [
|
|
|
38
41
|
DropCollectionTool,
|
|
39
42
|
ExplainTool,
|
|
40
43
|
CreateCollectionTool,
|
|
44
|
+
LogsTool,
|
|
41
45
|
];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { MongoDBToolBase } from "../mongodbTool.js";
|
|
3
|
+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
|
|
6
6
|
export class RenameCollectionTool extends MongoDBToolBase {
|
|
7
7
|
protected name = "rename-collection";
|
|
8
8
|
protected description = "Renames a collection in a MongoDB database";
|
|
9
9
|
protected argsShape = {
|
|
10
|
-
|
|
11
|
-
database: z.string().describe("Database name"),
|
|
10
|
+
...DbOperationArgs,
|
|
12
11
|
newName: z.string().describe("The new name for the collection"),
|
|
13
12
|
dropTarget: z.boolean().optional().default(false).describe("If true, drops the target collection if it exists"),
|
|
14
13
|
};
|
|
@@ -28,10 +27,40 @@ export class RenameCollectionTool extends MongoDBToolBase {
|
|
|
28
27
|
return {
|
|
29
28
|
content: [
|
|
30
29
|
{
|
|
31
|
-
text: `Collection
|
|
30
|
+
text: `Collection "${collection}" renamed to "${result.collectionName}" in database "${database}".`,
|
|
32
31
|
type: "text",
|
|
33
32
|
},
|
|
34
33
|
],
|
|
35
34
|
};
|
|
36
35
|
}
|
|
36
|
+
|
|
37
|
+
protected handleError(
|
|
38
|
+
error: unknown,
|
|
39
|
+
args: ToolArgs<typeof this.argsShape>
|
|
40
|
+
): Promise<CallToolResult> | CallToolResult {
|
|
41
|
+
if (error instanceof Error && "codeName" in error) {
|
|
42
|
+
switch (error.codeName) {
|
|
43
|
+
case "NamespaceNotFound":
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
text: `Cannot rename "${args.database}.${args.collection}" because it doesn't exist.`,
|
|
48
|
+
type: "text",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
case "NamespaceExists":
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
text: `Cannot rename "${args.database}.${args.collection}" to "${args.newName}" because the target collection already exists. If you want to overwrite it, set the "dropTarget" argument to true.`,
|
|
57
|
+
type: "text",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return super.handleError(error, args);
|
|
65
|
+
}
|
|
37
66
|
}
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { MongoDBToolBase } from "../mongodbTool.js";
|
|
3
|
+
import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
5
|
|
|
6
6
|
export class UpdateManyTool extends MongoDBToolBase {
|
|
7
7
|
protected name = "update-many";
|
|
8
8
|
protected description = "Updates all documents that match the specified filter for a collection";
|
|
9
9
|
protected argsShape = {
|
|
10
|
-
|
|
11
|
-
database: z.string().describe("Database name"),
|
|
10
|
+
...DbOperationArgs,
|
|
12
11
|
filter: z
|
|
13
|
-
.
|
|
14
|
-
.passthrough()
|
|
12
|
+
.record(z.string(), z.unknown())
|
|
15
13
|
.optional()
|
|
16
14
|
.describe(
|
|
17
15
|
"The selection criteria for the update, matching the syntax of the filter argument of db.collection.updateOne()"
|
|
18
16
|
),
|
|
19
17
|
update: z
|
|
20
|
-
.
|
|
21
|
-
.passthrough()
|
|
22
|
-
.optional()
|
|
18
|
+
.record(z.string(), z.unknown())
|
|
23
19
|
.describe("An update document describing the modifications to apply using update operator expressions"),
|
|
24
20
|
upsert: z
|
|
25
21
|
.boolean()
|
|
@@ -41,15 +37,15 @@ export class UpdateManyTool extends MongoDBToolBase {
|
|
|
41
37
|
});
|
|
42
38
|
|
|
43
39
|
let message = "";
|
|
44
|
-
if (result.matchedCount === 0) {
|
|
45
|
-
message =
|
|
40
|
+
if (result.matchedCount === 0 && result.modifiedCount === 0 && result.upsertedCount === 0) {
|
|
41
|
+
message = "No documents matched the filter.";
|
|
46
42
|
} else {
|
|
47
43
|
message = `Matched ${result.matchedCount} document(s).`;
|
|
48
44
|
if (result.modifiedCount > 0) {
|
|
49
45
|
message += ` Modified ${result.modifiedCount} document(s).`;
|
|
50
46
|
}
|
|
51
47
|
if (result.upsertedCount > 0) {
|
|
52
|
-
message += ` Upserted ${result.upsertedCount} document
|
|
48
|
+
message += ` Upserted ${result.upsertedCount} document with id: ${result.upsertedId?.toString()}.`;
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
51
|
|
package/src/tools/tool.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { z, type ZodRawShape, type ZodNever, AnyZodObject } from "zod";
|
|
2
|
+
import type { McpServer, RegisteredTool, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { Session } from "../session.js";
|
|
5
|
-
import logger from "../logger.js";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
5
|
+
import logger, { LogId } from "../logger.js";
|
|
6
|
+
import { Telemetry } from "../telemetry/telemetry.js";
|
|
7
|
+
import { type ToolEvent } from "../telemetry/types.js";
|
|
8
|
+
import { UserConfig } from "../config.js";
|
|
8
9
|
|
|
9
10
|
export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
|
|
10
11
|
|
|
11
|
-
export type OperationType = "metadata" | "read" | "create" | "delete" | "update"
|
|
12
|
+
export type OperationType = "metadata" | "read" | "create" | "delete" | "update";
|
|
12
13
|
export type ToolCategory = "mongodb" | "atlas";
|
|
13
14
|
|
|
14
15
|
export abstract class ToolBase {
|
|
@@ -24,7 +25,34 @@ export abstract class ToolBase {
|
|
|
24
25
|
|
|
25
26
|
protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
constructor(
|
|
29
|
+
protected readonly session: Session,
|
|
30
|
+
protected readonly config: UserConfig,
|
|
31
|
+
protected readonly telemetry: Telemetry
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates and emits a tool telemetry event
|
|
36
|
+
* @param startTime - Start time in milliseconds
|
|
37
|
+
* @param result - Whether the command succeeded or failed
|
|
38
|
+
* @param error - Optional error if the command failed
|
|
39
|
+
*/
|
|
40
|
+
private async emitToolEvent(startTime: number, result: CallToolResult): Promise<void> {
|
|
41
|
+
const duration = Date.now() - startTime;
|
|
42
|
+
const event: ToolEvent = {
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
source: "mdbmcp",
|
|
45
|
+
properties: {
|
|
46
|
+
...this.telemetry.getCommonProperties(),
|
|
47
|
+
command: this.name,
|
|
48
|
+
category: this.category,
|
|
49
|
+
component: "tool",
|
|
50
|
+
duration_ms: duration,
|
|
51
|
+
result: result.isError ? "failure" : "success",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
await this.telemetry.emitEvents([event]);
|
|
55
|
+
}
|
|
28
56
|
|
|
29
57
|
public register(server: McpServer): void {
|
|
30
58
|
if (!this.verifyAllowed()) {
|
|
@@ -32,39 +60,70 @@ export abstract class ToolBase {
|
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
|
|
63
|
+
const startTime = Date.now();
|
|
35
64
|
try {
|
|
36
|
-
|
|
37
|
-
logger.debug(
|
|
38
|
-
mongoLogId(1_000_006),
|
|
39
|
-
"tool",
|
|
40
|
-
`Executing ${this.name} with args: ${JSON.stringify(args)}`
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
return await this.execute(...args);
|
|
44
|
-
} catch (error: unknown) {
|
|
45
|
-
logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`);
|
|
65
|
+
logger.debug(LogId.toolExecute, "tool", `Executing ${this.name} with args: ${JSON.stringify(args)}`);
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
const result = await this.execute(...args);
|
|
68
|
+
await this.emitToolEvent(startTime, result);
|
|
69
|
+
return result;
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
logger.error(LogId.toolExecuteFailure, "tool", `Error executing ${this.name}: ${error as string}`);
|
|
72
|
+
const toolResult = await this.handleError(error, args[0] as ToolArgs<typeof this.argsShape>);
|
|
73
|
+
await this.emitToolEvent(startTime, toolResult).catch(() => {});
|
|
74
|
+
return toolResult;
|
|
48
75
|
}
|
|
49
76
|
};
|
|
50
77
|
|
|
51
78
|
server.tool(this.name, this.description, this.argsShape, callback);
|
|
79
|
+
|
|
80
|
+
// This is very similar to RegisteredTool.update, but without the bugs around the name.
|
|
81
|
+
// In the upstream update method, the name is captured in the closure and not updated when
|
|
82
|
+
// the tool name changes. This means that you only get one name update before things end up
|
|
83
|
+
// in a broken state.
|
|
84
|
+
this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => {
|
|
85
|
+
const tools = server["_registeredTools"] as { [toolName: string]: RegisteredTool };
|
|
86
|
+
const existingTool = tools[this.name];
|
|
87
|
+
|
|
88
|
+
if (updates.name && updates.name !== this.name) {
|
|
89
|
+
delete tools[this.name];
|
|
90
|
+
this.name = updates.name;
|
|
91
|
+
tools[this.name] = existingTool;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (updates.description) {
|
|
95
|
+
existingTool.description = updates.description;
|
|
96
|
+
this.description = updates.description;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (updates.inputSchema) {
|
|
100
|
+
existingTool.inputSchema = updates.inputSchema;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
server.sendToolListChanged();
|
|
104
|
+
};
|
|
52
105
|
}
|
|
53
106
|
|
|
107
|
+
protected update?: (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => void;
|
|
108
|
+
|
|
54
109
|
// Checks if a tool is allowed to run based on the config
|
|
55
|
-
|
|
110
|
+
protected verifyAllowed(): boolean {
|
|
56
111
|
let errorClarification: string | undefined;
|
|
57
|
-
|
|
112
|
+
|
|
113
|
+
// Check read-only mode first
|
|
114
|
+
if (this.config.readOnly && !["read", "metadata"].includes(this.operationType)) {
|
|
115
|
+
errorClarification = `read-only mode is enabled, its operation type, \`${this.operationType}\`,`;
|
|
116
|
+
} else if (this.config.disabledTools.includes(this.category)) {
|
|
58
117
|
errorClarification = `its category, \`${this.category}\`,`;
|
|
59
|
-
} else if (config.disabledTools.includes(this.operationType)) {
|
|
118
|
+
} else if (this.config.disabledTools.includes(this.operationType)) {
|
|
60
119
|
errorClarification = `its operation type, \`${this.operationType}\`,`;
|
|
61
|
-
} else if (config.disabledTools.includes(this.name)) {
|
|
120
|
+
} else if (this.config.disabledTools.includes(this.name)) {
|
|
62
121
|
errorClarification = `it`;
|
|
63
122
|
}
|
|
64
123
|
|
|
65
124
|
if (errorClarification) {
|
|
66
125
|
logger.debug(
|
|
67
|
-
|
|
126
|
+
LogId.toolDisabled,
|
|
68
127
|
"tool",
|
|
69
128
|
`Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
|
|
70
129
|
);
|
|
@@ -76,15 +135,19 @@ export abstract class ToolBase {
|
|
|
76
135
|
}
|
|
77
136
|
|
|
78
137
|
// This method is intended to be overridden by subclasses to handle errors
|
|
79
|
-
protected handleError(
|
|
138
|
+
protected handleError(
|
|
139
|
+
error: unknown,
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
141
|
+
args: ToolArgs<typeof this.argsShape>
|
|
142
|
+
): Promise<CallToolResult> | CallToolResult {
|
|
80
143
|
return {
|
|
81
144
|
content: [
|
|
82
145
|
{
|
|
83
146
|
type: "text",
|
|
84
147
|
text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
148
|
+
isError: true,
|
|
85
149
|
},
|
|
86
150
|
],
|
|
87
|
-
isError: true,
|
|
88
151
|
};
|
|
89
152
|
}
|
|
90
153
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { InMemoryTransport } from "./inMemoryTransport.js";
|
|
3
3
|
import { Server } from "../../src/server.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import fs from "fs/promises";
|
|
7
|
-
import { Session } from "../../src/session.js";
|
|
4
|
+
import { UserConfig } from "../../src/config.js";
|
|
5
|
+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
8
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
-
import {
|
|
10
|
-
import { toIncludeAllMembers } from "jest-extended";
|
|
11
|
-
import config from "../../src/config.js";
|
|
7
|
+
import { Session } from "../../src/session.js";
|
|
12
8
|
|
|
13
9
|
interface ParameterInfo {
|
|
14
10
|
name: string;
|
|
@@ -22,30 +18,22 @@ type ToolInfo = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
|
|
|
22
18
|
export interface IntegrationTest {
|
|
23
19
|
mcpClient: () => Client;
|
|
24
20
|
mcpServer: () => Server;
|
|
25
|
-
mongoClient: () => MongoClient;
|
|
26
|
-
connectionString: () => string;
|
|
27
|
-
connectMcpClient: () => Promise<void>;
|
|
28
|
-
randomDbName: () => string;
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
export function setupIntegrationTest(): IntegrationTest {
|
|
32
|
-
let mongoCluster: runner.MongoCluster | undefined;
|
|
33
|
-
let mongoClient: MongoClient | undefined;
|
|
34
|
-
|
|
23
|
+
export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
|
|
35
24
|
let mcpClient: Client | undefined;
|
|
36
25
|
let mcpServer: Server | undefined;
|
|
37
26
|
|
|
38
|
-
let randomDbName: string;
|
|
39
|
-
|
|
40
27
|
beforeAll(async () => {
|
|
28
|
+
const userConfig = getUserConfig();
|
|
41
29
|
const clientTransport = new InMemoryTransport();
|
|
42
30
|
const serverTransport = new InMemoryTransport();
|
|
43
31
|
|
|
44
32
|
await serverTransport.start();
|
|
45
33
|
await clientTransport.start();
|
|
46
34
|
|
|
47
|
-
clientTransport.output.pipeTo(serverTransport.input);
|
|
48
|
-
serverTransport.output.pipeTo(clientTransport.input);
|
|
35
|
+
void clientTransport.output.pipeTo(serverTransport.input);
|
|
36
|
+
void serverTransport.output.pipeTo(clientTransport.input);
|
|
49
37
|
|
|
50
38
|
mcpClient = new Client(
|
|
51
39
|
{
|
|
@@ -57,19 +45,35 @@ export function setupIntegrationTest(): IntegrationTest {
|
|
|
57
45
|
}
|
|
58
46
|
);
|
|
59
47
|
|
|
48
|
+
const session = new Session({
|
|
49
|
+
apiBaseUrl: userConfig.apiBaseUrl,
|
|
50
|
+
apiClientId: userConfig.apiClientId,
|
|
51
|
+
apiClientSecret: userConfig.apiClientSecret,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
userConfig.telemetry = "disabled";
|
|
60
55
|
mcpServer = new Server({
|
|
56
|
+
session,
|
|
57
|
+
userConfig,
|
|
61
58
|
mcpServer: new McpServer({
|
|
62
59
|
name: "test-server",
|
|
63
60
|
version: "1.2.3",
|
|
64
61
|
}),
|
|
65
|
-
session: new Session(),
|
|
66
62
|
});
|
|
67
63
|
await mcpServer.connect(serverTransport);
|
|
68
64
|
await mcpClient.connect(clientTransport);
|
|
69
65
|
});
|
|
70
66
|
|
|
71
|
-
beforeEach(
|
|
72
|
-
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
if (mcpServer) {
|
|
69
|
+
mcpServer.userConfig.telemetry = "disabled";
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
if (mcpServer) {
|
|
75
|
+
await mcpServer.session.close();
|
|
76
|
+
}
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
afterAll(async () => {
|
|
@@ -80,52 +84,6 @@ export function setupIntegrationTest(): IntegrationTest {
|
|
|
80
84
|
mcpServer = undefined;
|
|
81
85
|
});
|
|
82
86
|
|
|
83
|
-
afterEach(async () => {
|
|
84
|
-
await mcpServer?.session.close();
|
|
85
|
-
config.connectionString = undefined;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
beforeAll(async function () {
|
|
89
|
-
// Downloading Windows executables in CI takes a long time because
|
|
90
|
-
// they include debug symbols...
|
|
91
|
-
const tmpDir = path.join(__dirname, "..", "tmp");
|
|
92
|
-
await fs.mkdir(tmpDir, { recursive: true });
|
|
93
|
-
|
|
94
|
-
// On Windows, we may have a situation where mongod.exe is not fully released by the OS
|
|
95
|
-
// before we attempt to run it again, so we add a retry.
|
|
96
|
-
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
|
|
97
|
-
for (let i = 0; i < 10; i++) {
|
|
98
|
-
try {
|
|
99
|
-
mongoCluster = await MongoCluster.start({
|
|
100
|
-
tmpDir: dbsDir,
|
|
101
|
-
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
|
|
102
|
-
topology: "standalone",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
if (i < 5) {
|
|
108
|
-
// Just wait a little bit and retry
|
|
109
|
-
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
|
|
110
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
111
|
-
} else {
|
|
112
|
-
// If we still fail after 5 seconds, try another db dir
|
|
113
|
-
console.error(
|
|
114
|
-
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
|
|
115
|
-
);
|
|
116
|
-
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw new Error("Failed to start cluster after 10 attempts");
|
|
122
|
-
}, 120_000);
|
|
123
|
-
|
|
124
|
-
afterAll(async function () {
|
|
125
|
-
await mongoCluster?.close();
|
|
126
|
-
mongoCluster = undefined;
|
|
127
|
-
});
|
|
128
|
-
|
|
129
87
|
const getMcpClient = () => {
|
|
130
88
|
if (!mcpClient) {
|
|
131
89
|
throw new Error("beforeEach() hook not ran yet");
|
|
@@ -142,41 +100,25 @@ export function setupIntegrationTest(): IntegrationTest {
|
|
|
142
100
|
return mcpServer;
|
|
143
101
|
};
|
|
144
102
|
|
|
145
|
-
const getConnectionString = () => {
|
|
146
|
-
if (!mongoCluster) {
|
|
147
|
-
throw new Error("beforeAll() hook not ran yet");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return mongoCluster.connectionString;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
103
|
return {
|
|
154
104
|
mcpClient: getMcpClient,
|
|
155
105
|
mcpServer: getMcpServer,
|
|
156
|
-
mongoClient: () => {
|
|
157
|
-
if (!mongoClient) {
|
|
158
|
-
mongoClient = new MongoClient(getConnectionString());
|
|
159
|
-
}
|
|
160
|
-
return mongoClient;
|
|
161
|
-
},
|
|
162
|
-
connectionString: getConnectionString,
|
|
163
|
-
connectMcpClient: async () => {
|
|
164
|
-
await getMcpClient().callTool({
|
|
165
|
-
name: "connect",
|
|
166
|
-
arguments: { options: [{ connectionString: getConnectionString() }] },
|
|
167
|
-
});
|
|
168
|
-
},
|
|
169
|
-
randomDbName: () => randomDbName,
|
|
170
106
|
};
|
|
171
107
|
}
|
|
172
108
|
|
|
173
|
-
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
110
|
+
export function getResponseContent(content: unknown | { content: unknown }): string {
|
|
174
111
|
return getResponseElements(content)
|
|
175
112
|
.map((item) => item.text)
|
|
176
113
|
.join("\n");
|
|
177
114
|
}
|
|
178
115
|
|
|
179
|
-
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
117
|
+
export function getResponseElements(content: unknown | { content: unknown }): { type: string; text: string }[] {
|
|
118
|
+
if (typeof content === "object" && content !== null && "content" in content) {
|
|
119
|
+
content = (content as { content: unknown }).content;
|
|
120
|
+
}
|
|
121
|
+
|
|
180
122
|
expect(Array.isArray(content)).toBe(true);
|
|
181
123
|
|
|
182
124
|
const response = content as { type: string; text: string }[];
|
|
@@ -198,9 +140,9 @@ export async function connect(client: Client, connectionString: string): Promise
|
|
|
198
140
|
|
|
199
141
|
export function getParameters(tool: ToolInfo): ParameterInfo[] {
|
|
200
142
|
expect(tool.inputSchema.type).toBe("object");
|
|
201
|
-
|
|
143
|
+
expectDefined(tool.inputSchema.properties);
|
|
202
144
|
|
|
203
|
-
return Object.entries(tool.inputSchema.properties
|
|
145
|
+
return Object.entries(tool.inputSchema.properties)
|
|
204
146
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
205
147
|
.map(([key, value]) => {
|
|
206
148
|
expect(value).toHaveProperty("type");
|
|
@@ -218,24 +160,69 @@ export function getParameters(tool: ToolInfo): ParameterInfo[] {
|
|
|
218
160
|
});
|
|
219
161
|
}
|
|
220
162
|
|
|
221
|
-
export const
|
|
163
|
+
export const databaseParameters: ParameterInfo[] = [
|
|
222
164
|
{ name: "database", type: "string", description: "Database name", required: true },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
export const databaseCollectionParameters: ParameterInfo[] = [
|
|
168
|
+
...databaseParameters,
|
|
223
169
|
{ name: "collection", type: "string", description: "Collection name", required: true },
|
|
224
170
|
];
|
|
225
171
|
|
|
226
|
-
export
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
172
|
+
export const databaseCollectionInvalidArgs = [
|
|
173
|
+
{},
|
|
174
|
+
{ database: "test" },
|
|
175
|
+
{ collection: "foo" },
|
|
176
|
+
{ database: 123, collection: "foo" },
|
|
177
|
+
{ database: "test", collection: 123 },
|
|
178
|
+
{ database: [], collection: "foo" },
|
|
179
|
+
{ database: "test", collection: [] },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }];
|
|
183
|
+
|
|
184
|
+
export function validateToolMetadata(
|
|
185
|
+
integration: IntegrationTest,
|
|
186
|
+
name: string,
|
|
187
|
+
description: string,
|
|
188
|
+
parameters: ParameterInfo[]
|
|
189
|
+
): void {
|
|
190
|
+
it("should have correct metadata", async () => {
|
|
191
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
192
|
+
const tool = tools.find((tool) => tool.name === name);
|
|
193
|
+
expectDefined(tool);
|
|
194
|
+
expect(tool.description).toBe(description);
|
|
195
|
+
|
|
196
|
+
const toolParameters = getParameters(tool);
|
|
197
|
+
expect(toolParameters).toHaveLength(parameters.length);
|
|
198
|
+
expect(toolParameters).toIncludeAllMembers(parameters);
|
|
199
|
+
});
|
|
230
200
|
}
|
|
231
201
|
|
|
232
|
-
export function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
202
|
+
export function validateThrowsForInvalidArguments(
|
|
203
|
+
integration: IntegrationTest,
|
|
204
|
+
name: string,
|
|
205
|
+
args: { [x: string]: unknown }[]
|
|
206
|
+
): void {
|
|
207
|
+
describe("with invalid arguments", () => {
|
|
208
|
+
for (const arg of args) {
|
|
209
|
+
it(`throws a schema error for: ${JSON.stringify(arg)}`, async () => {
|
|
210
|
+
try {
|
|
211
|
+
await integration.mcpClient().callTool({ name, arguments: arg });
|
|
212
|
+
throw new Error("Expected an error to be thrown");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
expect((error as Error).message).not.toEqual("Expected an error to be thrown");
|
|
215
|
+
expect(error).toBeInstanceOf(McpError);
|
|
216
|
+
const mcpError = error as McpError;
|
|
217
|
+
expect(mcpError.code).toEqual(-32602);
|
|
218
|
+
expect(mcpError.message).toContain(`Invalid arguments for tool ${name}`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
240
222
|
});
|
|
241
223
|
}
|
|
224
|
+
|
|
225
|
+
/** Expects the argument being defined and asserts it */
|
|
226
|
+
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
|
|
227
|
+
expect(arg).toBeDefined();
|
|
228
|
+
}
|
|
@@ -39,6 +39,7 @@ export class InMemoryTransport implements Transport {
|
|
|
39
39
|
return Promise.resolve();
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
42
43
|
async close(): Promise<void> {
|
|
43
44
|
this.outputController.close();
|
|
44
45
|
this.onclose?.();
|
|
@@ -49,10 +50,10 @@ export class InMemoryTransport implements Transport {
|
|
|
49
50
|
sessionId?: string | undefined;
|
|
50
51
|
|
|
51
52
|
private static getPromise(): [Promise<void>, resolve: () => void] {
|
|
52
|
-
let resolve: () => void;
|
|
53
|
+
let resolve: () => void = () => {};
|
|
53
54
|
const promise = new Promise<void>((res) => {
|
|
54
55
|
resolve = res;
|
|
55
56
|
});
|
|
56
|
-
return [promise, resolve
|
|
57
|
+
return [promise, resolve];
|
|
57
58
|
}
|
|
58
59
|
}
|