mongodb-mcp-server 0.0.4
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/workflows/code_health.yaml +65 -0
- package/.github/workflows/prepare_release.yaml +45 -0
- package/.github/workflows/publish.yaml +70 -0
- package/.prettierignore +5 -0
- package/.prettierrc.json +37 -0
- package/.vscode/launch.json +17 -0
- package/CONTRIBUTING.md +185 -0
- package/LICENSE +202 -0
- package/README.md +234 -0
- package/dist/common/atlas/apiClient.js +147 -0
- package/dist/common/atlas/apiClient.js.map +1 -0
- package/dist/common/atlas/apiClientError.js +17 -0
- package/dist/common/atlas/apiClientError.js.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.js +97 -0
- package/dist/logger.js.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +30 -0
- package/dist/session.js.map +1 -0
- package/dist/tools/atlas/atlasTool.js +9 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -0
- package/dist/tools/atlas/createAccessList.js +64 -0
- package/dist/tools/atlas/createAccessList.js.map +1 -0
- package/dist/tools/atlas/createDBUser.js +58 -0
- package/dist/tools/atlas/createDBUser.js.map +1 -0
- package/dist/tools/atlas/createFreeCluster.js +51 -0
- package/dist/tools/atlas/createFreeCluster.js.map +1 -0
- package/dist/tools/atlas/createProject.js +53 -0
- package/dist/tools/atlas/createProject.js.map +1 -0
- package/dist/tools/atlas/inspectAccessList.js +41 -0
- package/dist/tools/atlas/inspectAccessList.js.map +1 -0
- package/dist/tools/atlas/inspectCluster.js +42 -0
- package/dist/tools/atlas/inspectCluster.js.map +1 -0
- package/dist/tools/atlas/listClusters.js +97 -0
- package/dist/tools/atlas/listClusters.js.map +1 -0
- package/dist/tools/atlas/listDBUsers.js +52 -0
- package/dist/tools/atlas/listDBUsers.js.map +1 -0
- package/dist/tools/atlas/listOrgs.js +30 -0
- package/dist/tools/atlas/listOrgs.js.map +1 -0
- package/dist/tools/atlas/listProjects.js +42 -0
- package/dist/tools/atlas/listProjects.js.map +1 -0
- package/dist/tools/atlas/tools.js +23 -0
- package/dist/tools/atlas/tools.js.map +1 -0
- package/dist/tools/mongodb/create/createCollection.js +23 -0
- package/dist/tools/mongodb/create/createCollection.js.map +1 -0
- package/dist/tools/mongodb/create/createIndex.js +33 -0
- package/dist/tools/mongodb/create/createIndex.js.map +1 -0
- package/dist/tools/mongodb/create/insertMany.js +33 -0
- package/dist/tools/mongodb/create/insertMany.js.map +1 -0
- package/dist/tools/mongodb/delete/deleteMany.js +31 -0
- package/dist/tools/mongodb/delete/deleteMany.js.map +1 -0
- package/dist/tools/mongodb/delete/dropCollection.js +25 -0
- package/dist/tools/mongodb/delete/dropCollection.js.map +1 -0
- package/dist/tools/mongodb/delete/dropDatabase.js +25 -0
- package/dist/tools/mongodb/delete/dropDatabase.js.map +1 -0
- package/dist/tools/mongodb/metadata/collectionSchema.js +38 -0
- package/dist/tools/mongodb/metadata/collectionSchema.js.map +1 -0
- package/dist/tools/mongodb/metadata/collectionStorageSize.js +28 -0
- package/dist/tools/mongodb/metadata/collectionStorageSize.js.map +1 -0
- package/dist/tools/mongodb/metadata/connect.js +86 -0
- package/dist/tools/mongodb/metadata/connect.js.map +1 -0
- package/dist/tools/mongodb/metadata/dbStats.js +28 -0
- package/dist/tools/mongodb/metadata/dbStats.js.map +1 -0
- package/dist/tools/mongodb/metadata/explain.js +77 -0
- package/dist/tools/mongodb/metadata/explain.js.map +1 -0
- package/dist/tools/mongodb/metadata/listCollections.js +35 -0
- package/dist/tools/mongodb/metadata/listCollections.js.map +1 -0
- package/dist/tools/mongodb/metadata/listDatabases.js +23 -0
- package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -0
- package/dist/tools/mongodb/mongodbTool.js +58 -0
- package/dist/tools/mongodb/mongodbTool.js.map +1 -0
- package/dist/tools/mongodb/read/aggregate.js +38 -0
- package/dist/tools/mongodb/read/aggregate.js.map +1 -0
- package/dist/tools/mongodb/read/collectionIndexes.js +23 -0
- package/dist/tools/mongodb/read/collectionIndexes.js.map +1 -0
- package/dist/tools/mongodb/read/count.js +34 -0
- package/dist/tools/mongodb/read/count.js.map +1 -0
- package/dist/tools/mongodb/read/find.js +51 -0
- package/dist/tools/mongodb/read/find.js.map +1 -0
- package/dist/tools/mongodb/tools.js +41 -0
- package/dist/tools/mongodb/tools.js.map +1 -0
- package/dist/tools/mongodb/update/renameCollection.js +31 -0
- package/dist/tools/mongodb/update/renameCollection.js.map +1 -0
- package/dist/tools/mongodb/update/updateMany.js +56 -0
- package/dist/tools/mongodb/update/updateMany.js.map +1 -0
- package/dist/tools/tool.js +56 -0
- package/dist/tools/tool.js.map +1 -0
- package/eslint.config.js +35 -0
- package/jest.config.js +22 -0
- package/package.json +76 -0
- package/scripts/apply.ts +129 -0
- package/scripts/filter.ts +67 -0
- package/scripts/generate.sh +11 -0
- package/src/common/atlas/apiClient.ts +202 -0
- package/src/common/atlas/apiClientError.ts +21 -0
- package/src/common/atlas/openapi.d.ts +5849 -0
- package/src/config.ts +124 -0
- package/src/errors.ts +13 -0
- package/src/index.ts +30 -0
- package/src/logger.ts +117 -0
- package/src/server.ts +64 -0
- package/src/session.ts +37 -0
- package/src/tools/atlas/atlasTool.ts +10 -0
- package/src/tools/atlas/createAccessList.ts +78 -0
- package/src/tools/atlas/createDBUser.ts +70 -0
- package/src/tools/atlas/createFreeCluster.ts +55 -0
- package/src/tools/atlas/createProject.ts +63 -0
- package/src/tools/atlas/inspectAccessList.ts +44 -0
- package/src/tools/atlas/inspectCluster.ts +47 -0
- package/src/tools/atlas/listClusters.ts +104 -0
- package/src/tools/atlas/listDBUsers.ts +62 -0
- package/src/tools/atlas/listOrgs.ts +34 -0
- package/src/tools/atlas/listProjects.ts +46 -0
- package/src/tools/atlas/tools.ts +23 -0
- package/src/tools/mongodb/create/createCollection.ts +26 -0
- package/src/tools/mongodb/create/createIndex.ts +41 -0
- package/src/tools/mongodb/create/insertMany.ts +40 -0
- package/src/tools/mongodb/delete/deleteMany.ts +38 -0
- package/src/tools/mongodb/delete/dropCollection.ts +27 -0
- package/src/tools/mongodb/delete/dropDatabase.ts +26 -0
- package/src/tools/mongodb/metadata/collectionSchema.ts +41 -0
- package/src/tools/mongodb/metadata/collectionStorageSize.ts +30 -0
- package/src/tools/mongodb/metadata/connect.ts +94 -0
- package/src/tools/mongodb/metadata/dbStats.ts +30 -0
- package/src/tools/mongodb/metadata/explain.ts +90 -0
- package/src/tools/mongodb/metadata/listCollections.ts +38 -0
- package/src/tools/mongodb/metadata/listDatabases.ts +26 -0
- package/src/tools/mongodb/mongodbTool.ts +69 -0
- package/src/tools/mongodb/read/aggregate.ts +45 -0
- package/src/tools/mongodb/read/collectionIndexes.ts +24 -0
- package/src/tools/mongodb/read/count.ts +39 -0
- package/src/tools/mongodb/read/find.ts +62 -0
- package/src/tools/mongodb/tools.ts +41 -0
- package/src/tools/mongodb/update/renameCollection.ts +37 -0
- package/src/tools/mongodb/update/updateMany.ts +65 -0
- package/src/tools/tool.ts +90 -0
- package/src/types/mongodb-redact.d.ts +4 -0
- package/tests/integration/helpers.ts +241 -0
- package/tests/integration/inMemoryTransport.ts +58 -0
- package/tests/integration/server.test.ts +35 -0
- package/tests/integration/tools/atlas/accessLists.test.ts +100 -0
- package/tests/integration/tools/atlas/atlasHelpers.ts +110 -0
- package/tests/integration/tools/atlas/clusters.test.ts +122 -0
- package/tests/integration/tools/atlas/dbUsers.test.ts +80 -0
- package/tests/integration/tools/atlas/orgs.test.ts +24 -0
- package/tests/integration/tools/atlas/projects.test.ts +80 -0
- package/tests/integration/tools/mongodb/create/createCollection.test.ts +138 -0
- package/tests/integration/tools/mongodb/create/createIndex.test.ts +249 -0
- package/tests/integration/tools/mongodb/create/insertMany.test.ts +141 -0
- package/tests/integration/tools/mongodb/delete/deleteMany.test.ts +191 -0
- package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +118 -0
- package/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +114 -0
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +137 -0
- package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +104 -0
- package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +67 -0
- package/tests/integration/tools/mongodb/read/count.test.ts +138 -0
- package/tsconfig.jest.json +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z, ZodNever, ZodRawShape } from "zod";
|
|
3
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { Session } from "../session.js";
|
|
5
|
+
import logger from "../logger.js";
|
|
6
|
+
import { mongoLogId } from "mongodb-log-writer";
|
|
7
|
+
import config from "../config.js";
|
|
8
|
+
|
|
9
|
+
export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
|
|
10
|
+
|
|
11
|
+
export type OperationType = "metadata" | "read" | "create" | "delete" | "update" | "cluster";
|
|
12
|
+
export type ToolCategory = "mongodb" | "atlas";
|
|
13
|
+
|
|
14
|
+
export abstract class ToolBase {
|
|
15
|
+
protected abstract name: string;
|
|
16
|
+
|
|
17
|
+
protected abstract category: ToolCategory;
|
|
18
|
+
|
|
19
|
+
protected abstract operationType: OperationType;
|
|
20
|
+
|
|
21
|
+
protected abstract description: string;
|
|
22
|
+
|
|
23
|
+
protected abstract argsShape: ZodRawShape;
|
|
24
|
+
|
|
25
|
+
protected abstract execute(...args: Parameters<ToolCallback<typeof this.argsShape>>): Promise<CallToolResult>;
|
|
26
|
+
|
|
27
|
+
protected constructor(protected session: Session) {}
|
|
28
|
+
|
|
29
|
+
public register(server: McpServer): void {
|
|
30
|
+
if (!this.verifyAllowed()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const callback: ToolCallback<typeof this.argsShape> = async (...args) => {
|
|
35
|
+
try {
|
|
36
|
+
// TODO: add telemetry here
|
|
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}`);
|
|
46
|
+
|
|
47
|
+
return await this.handleError(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
server.tool(this.name, this.description, this.argsShape, callback);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checks if a tool is allowed to run based on the config
|
|
55
|
+
private verifyAllowed(): boolean {
|
|
56
|
+
let errorClarification: string | undefined;
|
|
57
|
+
if (config.disabledTools.includes(this.category)) {
|
|
58
|
+
errorClarification = `its category, \`${this.category}\`,`;
|
|
59
|
+
} else if (config.disabledTools.includes(this.operationType)) {
|
|
60
|
+
errorClarification = `its operation type, \`${this.operationType}\`,`;
|
|
61
|
+
} else if (config.disabledTools.includes(this.name)) {
|
|
62
|
+
errorClarification = `it`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (errorClarification) {
|
|
66
|
+
logger.debug(
|
|
67
|
+
mongoLogId(1_000_010),
|
|
68
|
+
"tool",
|
|
69
|
+
`Prevented registration of ${this.name} because ${errorClarification} is disabled in the config`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// This method is intended to be overridden by subclasses to handle errors
|
|
79
|
+
protected handleError(error: unknown): Promise<CallToolResult> | CallToolResult {
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `Error running ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
isError: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { InMemoryTransport } from "./inMemoryTransport.js";
|
|
3
|
+
import { Server } from "../../src/server.js";
|
|
4
|
+
import runner, { MongoCluster } from "mongodb-runner";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
import { Session } from "../../src/session.js";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { MongoClient, ObjectId } from "mongodb";
|
|
10
|
+
import { toIncludeAllMembers } from "jest-extended";
|
|
11
|
+
import config from "../../src/config.js";
|
|
12
|
+
|
|
13
|
+
interface ParameterInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ToolInfo = Awaited<ReturnType<Client["listTools"]>>["tools"][number];
|
|
21
|
+
|
|
22
|
+
export interface IntegrationTest {
|
|
23
|
+
mcpClient: () => Client;
|
|
24
|
+
mcpServer: () => Server;
|
|
25
|
+
mongoClient: () => MongoClient;
|
|
26
|
+
connectionString: () => string;
|
|
27
|
+
connectMcpClient: () => Promise<void>;
|
|
28
|
+
randomDbName: () => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setupIntegrationTest(): IntegrationTest {
|
|
32
|
+
let mongoCluster: runner.MongoCluster | undefined;
|
|
33
|
+
let mongoClient: MongoClient | undefined;
|
|
34
|
+
|
|
35
|
+
let mcpClient: Client | undefined;
|
|
36
|
+
let mcpServer: Server | undefined;
|
|
37
|
+
|
|
38
|
+
let randomDbName: string;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
const clientTransport = new InMemoryTransport();
|
|
42
|
+
const serverTransport = new InMemoryTransport();
|
|
43
|
+
|
|
44
|
+
await serverTransport.start();
|
|
45
|
+
await clientTransport.start();
|
|
46
|
+
|
|
47
|
+
clientTransport.output.pipeTo(serverTransport.input);
|
|
48
|
+
serverTransport.output.pipeTo(clientTransport.input);
|
|
49
|
+
|
|
50
|
+
mcpClient = new Client(
|
|
51
|
+
{
|
|
52
|
+
name: "test-client",
|
|
53
|
+
version: "1.2.3",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
capabilities: {},
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
mcpServer = new Server({
|
|
61
|
+
mcpServer: new McpServer({
|
|
62
|
+
name: "test-server",
|
|
63
|
+
version: "1.2.3",
|
|
64
|
+
}),
|
|
65
|
+
session: new Session(),
|
|
66
|
+
});
|
|
67
|
+
await mcpServer.connect(serverTransport);
|
|
68
|
+
await mcpClient.connect(clientTransport);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
beforeEach(async () => {
|
|
72
|
+
randomDbName = new ObjectId().toString();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await mcpClient?.close();
|
|
77
|
+
mcpClient = undefined;
|
|
78
|
+
|
|
79
|
+
await mcpServer?.close();
|
|
80
|
+
mcpServer = undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
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
|
+
const getMcpClient = () => {
|
|
130
|
+
if (!mcpClient) {
|
|
131
|
+
throw new Error("beforeEach() hook not ran yet");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return mcpClient;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getMcpServer = () => {
|
|
138
|
+
if (!mcpServer) {
|
|
139
|
+
throw new Error("beforeEach() hook not ran yet");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return mcpServer;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getConnectionString = () => {
|
|
146
|
+
if (!mongoCluster) {
|
|
147
|
+
throw new Error("beforeAll() hook not ran yet");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return mongoCluster.connectionString;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
mcpClient: getMcpClient,
|
|
155
|
+
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
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getResponseContent(content: unknown): string {
|
|
174
|
+
return getResponseElements(content)
|
|
175
|
+
.map((item) => item.text)
|
|
176
|
+
.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getResponseElements(content: unknown): { type: string; text: string }[] {
|
|
180
|
+
expect(Array.isArray(content)).toBe(true);
|
|
181
|
+
|
|
182
|
+
const response = content as { type: string; text: string }[];
|
|
183
|
+
for (const item of response) {
|
|
184
|
+
expect(item).toHaveProperty("type");
|
|
185
|
+
expect(item).toHaveProperty("text");
|
|
186
|
+
expect(item.type).toBe("text");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return response;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function connect(client: Client, connectionString: string): Promise<void> {
|
|
193
|
+
await client.callTool({
|
|
194
|
+
name: "connect",
|
|
195
|
+
arguments: { connectionStringOrClusterName: connectionString },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getParameters(tool: ToolInfo): ParameterInfo[] {
|
|
200
|
+
expect(tool.inputSchema.type).toBe("object");
|
|
201
|
+
expect(tool.inputSchema.properties).toBeDefined();
|
|
202
|
+
|
|
203
|
+
return Object.entries(tool.inputSchema.properties!)
|
|
204
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
205
|
+
.map(([key, value]) => {
|
|
206
|
+
expect(value).toHaveProperty("type");
|
|
207
|
+
expect(value).toHaveProperty("description");
|
|
208
|
+
|
|
209
|
+
const typedValue = value as { type: string; description: string };
|
|
210
|
+
expect(typeof typedValue.type).toBe("string");
|
|
211
|
+
expect(typeof typedValue.description).toBe("string");
|
|
212
|
+
return {
|
|
213
|
+
name: key,
|
|
214
|
+
type: typedValue.type,
|
|
215
|
+
description: typedValue.description,
|
|
216
|
+
required: (tool.inputSchema.required as string[])?.includes(key) ?? false,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const dbOperationParameters: ParameterInfo[] = [
|
|
222
|
+
{ name: "database", type: "string", description: "Database name", required: true },
|
|
223
|
+
{ name: "collection", type: "string", description: "Collection name", required: true },
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
export function validateParameters(tool: ToolInfo, parameters: ParameterInfo[]): void {
|
|
227
|
+
const toolParameters = getParameters(tool);
|
|
228
|
+
expect(toolParameters).toHaveLength(parameters.length);
|
|
229
|
+
expect(toolParameters).toIncludeAllMembers(parameters);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function describeAtlas(name: number | string | Function | jest.FunctionLike, fn: jest.EmptyFunction) {
|
|
233
|
+
if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) {
|
|
234
|
+
return describe.skip("atlas", () => {
|
|
235
|
+
describe(name, fn);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return describe("atlas", () => {
|
|
239
|
+
describe(name, fn);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
|
|
4
|
+
export class InMemoryTransport implements Transport {
|
|
5
|
+
private outputController: ReadableStreamDefaultController<JSONRPCMessage>;
|
|
6
|
+
|
|
7
|
+
private startPromise: Promise<unknown>;
|
|
8
|
+
|
|
9
|
+
public output: ReadableStream<JSONRPCMessage>;
|
|
10
|
+
public input: WritableStream<JSONRPCMessage>;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
const [inputReady, inputResolve] = InMemoryTransport.getPromise();
|
|
14
|
+
const [outputReady, outputResolve] = InMemoryTransport.getPromise();
|
|
15
|
+
|
|
16
|
+
this.output = new ReadableStream<JSONRPCMessage>({
|
|
17
|
+
start: (controller) => {
|
|
18
|
+
this.outputController = controller;
|
|
19
|
+
outputResolve();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.input = new WritableStream<JSONRPCMessage>({
|
|
24
|
+
write: (message) => this.onmessage?.(message),
|
|
25
|
+
start: () => {
|
|
26
|
+
inputResolve();
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.startPromise = Promise.all([inputReady, outputReady]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
await this.startPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
send(message: JSONRPCMessage): Promise<void> {
|
|
38
|
+
this.outputController.enqueue(message);
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async close(): Promise<void> {
|
|
43
|
+
this.outputController.close();
|
|
44
|
+
this.onclose?.();
|
|
45
|
+
}
|
|
46
|
+
onclose?: (() => void) | undefined;
|
|
47
|
+
onerror?: ((error: Error) => void) | undefined;
|
|
48
|
+
onmessage?: ((message: JSONRPCMessage) => void) | undefined;
|
|
49
|
+
sessionId?: string | undefined;
|
|
50
|
+
|
|
51
|
+
private static getPromise(): [Promise<void>, resolve: () => void] {
|
|
52
|
+
let resolve: () => void;
|
|
53
|
+
const promise = new Promise<void>((res) => {
|
|
54
|
+
resolve = res;
|
|
55
|
+
});
|
|
56
|
+
return [promise, resolve!];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { setupIntegrationTest } from "./helpers";
|
|
2
|
+
|
|
3
|
+
describe("Server integration test", () => {
|
|
4
|
+
const integration = setupIntegrationTest();
|
|
5
|
+
|
|
6
|
+
describe("list capabilities", () => {
|
|
7
|
+
it("should return positive number of tools", async () => {
|
|
8
|
+
const tools = await integration.mcpClient().listTools();
|
|
9
|
+
expect(tools).toBeDefined();
|
|
10
|
+
expect(tools.tools.length).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return no resources", async () => {
|
|
14
|
+
await expect(() => integration.mcpClient().listResources()).rejects.toMatchObject({
|
|
15
|
+
message: "MCP error -32601: Method not found",
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return no prompts", async () => {
|
|
20
|
+
await expect(() => integration.mcpClient().listPrompts()).rejects.toMatchObject({
|
|
21
|
+
message: "MCP error -32601: Method not found",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return capabilities", async () => {
|
|
26
|
+
const capabilities = integration.mcpClient().getServerCapabilities();
|
|
27
|
+
expect(capabilities).toBeDefined();
|
|
28
|
+
expect(capabilities?.completions).toBeUndefined();
|
|
29
|
+
expect(capabilities?.experimental).toBeUndefined();
|
|
30
|
+
expect(capabilities?.tools).toBeDefined();
|
|
31
|
+
expect(capabilities?.logging).toBeDefined();
|
|
32
|
+
expect(capabilities?.prompts).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { Session } from "../../../../src/session.js";
|
|
3
|
+
import { describeAtlas, withProject } from "./atlasHelpers.js";
|
|
4
|
+
|
|
5
|
+
function generateRandomIp() {
|
|
6
|
+
const randomIp: number[] = [192];
|
|
7
|
+
for (let i = 0; i < 3; i++) {
|
|
8
|
+
randomIp.push(Math.floor(Math.random() * 256));
|
|
9
|
+
}
|
|
10
|
+
return randomIp.join(".");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describeAtlas("ip access lists", (integration) => {
|
|
14
|
+
withProject(integration, ({ getProjectId }) => {
|
|
15
|
+
const ips = [generateRandomIp(), generateRandomIp()];
|
|
16
|
+
const cidrBlocks = [generateRandomIp() + "/16", generateRandomIp() + "/24"];
|
|
17
|
+
const values = [...ips, ...cidrBlocks];
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
const session: Session = integration.mcpServer().session;
|
|
21
|
+
session.ensureAuthenticated();
|
|
22
|
+
const ipInfo = await session.apiClient.getIpInfo();
|
|
23
|
+
values.push(ipInfo.currentIpv4Address);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
const session: Session = integration.mcpServer().session;
|
|
28
|
+
session.ensureAuthenticated();
|
|
29
|
+
|
|
30
|
+
const projectId = getProjectId();
|
|
31
|
+
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
await session.apiClient.deleteProjectIpAccessList({
|
|
34
|
+
params: {
|
|
35
|
+
path: {
|
|
36
|
+
groupId: projectId,
|
|
37
|
+
entryValue: value,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("atlas-create-access-list", () => {
|
|
45
|
+
it("should have correct metadata", async () => {
|
|
46
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
47
|
+
const createAccessList = tools.find((tool) => tool.name === "atlas-create-access-list")!;
|
|
48
|
+
expect(createAccessList).toBeDefined();
|
|
49
|
+
expect(createAccessList.inputSchema.type).toBe("object");
|
|
50
|
+
expect(createAccessList.inputSchema.properties).toBeDefined();
|
|
51
|
+
expect(createAccessList.inputSchema.properties).toHaveProperty("projectId");
|
|
52
|
+
expect(createAccessList.inputSchema.properties).toHaveProperty("ipAddresses");
|
|
53
|
+
expect(createAccessList.inputSchema.properties).toHaveProperty("cidrBlocks");
|
|
54
|
+
expect(createAccessList.inputSchema.properties).toHaveProperty("currentIpAddress");
|
|
55
|
+
expect(createAccessList.inputSchema.properties).toHaveProperty("comment");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should create an access list", async () => {
|
|
59
|
+
const projectId = getProjectId();
|
|
60
|
+
|
|
61
|
+
const response = (await integration.mcpClient().callTool({
|
|
62
|
+
name: "atlas-create-access-list",
|
|
63
|
+
arguments: {
|
|
64
|
+
projectId,
|
|
65
|
+
ipAddresses: ips,
|
|
66
|
+
cidrBlocks: cidrBlocks,
|
|
67
|
+
currentIpAddress: true,
|
|
68
|
+
},
|
|
69
|
+
})) as CallToolResult;
|
|
70
|
+
expect(response.content).toBeArray();
|
|
71
|
+
expect(response.content).toHaveLength(1);
|
|
72
|
+
expect(response.content[0].text).toContain("IP/CIDR ranges added to access list");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("atlas-inspect-access-list", () => {
|
|
77
|
+
it("should have correct metadata", async () => {
|
|
78
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
79
|
+
const inspectAccessList = tools.find((tool) => tool.name === "atlas-inspect-access-list")!;
|
|
80
|
+
expect(inspectAccessList).toBeDefined();
|
|
81
|
+
expect(inspectAccessList.inputSchema.type).toBe("object");
|
|
82
|
+
expect(inspectAccessList.inputSchema.properties).toBeDefined();
|
|
83
|
+
expect(inspectAccessList.inputSchema.properties).toHaveProperty("projectId");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns access list data", async () => {
|
|
87
|
+
const projectId = getProjectId();
|
|
88
|
+
|
|
89
|
+
const response = (await integration
|
|
90
|
+
.mcpClient()
|
|
91
|
+
.callTool({ name: "atlas-inspect-access-list", arguments: { projectId } })) as CallToolResult;
|
|
92
|
+
expect(response.content).toBeArray();
|
|
93
|
+
expect(response.content).toHaveLength(1);
|
|
94
|
+
for (const value of values) {
|
|
95
|
+
expect(response.content[0].text).toContain(value);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
import { Group } from "../../../../src/common/atlas/openapi.js";
|
|
3
|
+
import { ApiClient } from "../../../../src/common/atlas/apiClient.js";
|
|
4
|
+
import { setupIntegrationTest, IntegrationTest } from "../../helpers.js";
|
|
5
|
+
import { Session } from "../../../../src/session.js";
|
|
6
|
+
|
|
7
|
+
export type IntegrationTestFunction = (integration: IntegrationTest) => void;
|
|
8
|
+
|
|
9
|
+
export function sleep(ms: number) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function describeAtlas(name: number | string | Function | jest.FunctionLike, fn: IntegrationTestFunction) {
|
|
14
|
+
const testDefinition = () => {
|
|
15
|
+
const integration = setupIntegrationTest();
|
|
16
|
+
describe(name, () => {
|
|
17
|
+
fn(integration);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length) {
|
|
22
|
+
return describe.skip("atlas", testDefinition);
|
|
23
|
+
}
|
|
24
|
+
return describe("atlas", testDefinition);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProjectTestArgs {
|
|
28
|
+
getProjectId: () => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ProjectTestFunction = (args: ProjectTestArgs) => void;
|
|
32
|
+
|
|
33
|
+
export function withProject(integration: IntegrationTest, fn: ProjectTestFunction) {
|
|
34
|
+
return describe("project", () => {
|
|
35
|
+
let projectId: string = "";
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
const session: Session = integration.mcpServer().session;
|
|
39
|
+
session.ensureAuthenticated();
|
|
40
|
+
|
|
41
|
+
const apiClient = session.apiClient;
|
|
42
|
+
const group = await createProject(apiClient);
|
|
43
|
+
projectId = group.id || "";
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(async () => {
|
|
47
|
+
const session: Session = integration.mcpServer().session;
|
|
48
|
+
session.ensureAuthenticated();
|
|
49
|
+
|
|
50
|
+
const apiClient = session.apiClient;
|
|
51
|
+
await apiClient.deleteProject({
|
|
52
|
+
params: {
|
|
53
|
+
path: {
|
|
54
|
+
groupId: projectId,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const args = {
|
|
61
|
+
getProjectId: () => projectId,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe("with project", () => {
|
|
65
|
+
fn(args);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseTable(text: string): Record<string, string>[] {
|
|
71
|
+
const data = text
|
|
72
|
+
.split("\n")
|
|
73
|
+
.filter((line) => line.trim() !== "")
|
|
74
|
+
.map((line) => line.split("|").map((cell) => cell.trim()));
|
|
75
|
+
|
|
76
|
+
const headers = data[0];
|
|
77
|
+
return data
|
|
78
|
+
.filter((_, index) => index >= 2)
|
|
79
|
+
.map((cells) => {
|
|
80
|
+
const row = {};
|
|
81
|
+
cells.forEach((cell, index) => {
|
|
82
|
+
row[headers[index]] = cell;
|
|
83
|
+
});
|
|
84
|
+
return row;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const randomId = new ObjectId().toString();
|
|
89
|
+
|
|
90
|
+
async function createProject(apiClient: ApiClient): Promise<Group> {
|
|
91
|
+
const projectName: string = `testProj-` + randomId;
|
|
92
|
+
|
|
93
|
+
const orgs = await apiClient.listOrganizations();
|
|
94
|
+
if (!orgs?.results?.length || !orgs.results[0].id) {
|
|
95
|
+
throw new Error("No orgs found");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const group = await apiClient.createProject({
|
|
99
|
+
body: {
|
|
100
|
+
name: projectName,
|
|
101
|
+
orgId: orgs.results[0].id,
|
|
102
|
+
} as Group,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!group?.id) {
|
|
106
|
+
throw new Error("Failed to create project");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return group;
|
|
110
|
+
}
|