mongodb-mcp-server 0.0.8 → 0.1.1
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 +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
- package/.github/workflows/{lint.yml → check.yml} +22 -1
- package/.github/workflows/code_health.yaml +0 -22
- package/.github/workflows/code_health_fork.yaml +7 -63
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +11 -0
- package/README.md +41 -22
- package/dist/common/atlas/apiClient.js +141 -34
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/apiClientError.js +38 -5
- package/dist/common/atlas/apiClientError.js.map +1 -1
- package/dist/common/atlas/cluster.js +66 -0
- package/dist/common/atlas/cluster.js.map +1 -0
- package/dist/common/atlas/generatePassword.js +9 -0
- package/dist/common/atlas/generatePassword.js.map +1 -0
- package/dist/helpers/EJsonTransport.js +38 -0
- package/dist/helpers/EJsonTransport.js.map +1 -0
- package/dist/helpers/connectionOptions.js +10 -0
- package/dist/helpers/connectionOptions.js.map +1 -0
- package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
- package/dist/helpers/packageInfo.js.map +1 -0
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +2 -0
- package/dist/logger.js.map +1 -1
- package/dist/server.js +15 -11
- package/dist/server.js.map +1 -1
- package/dist/session.js +8 -3
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +1 -3
- package/dist/telemetry/constants.js.map +1 -1
- package/dist/telemetry/eventCache.js.map +1 -1
- package/dist/telemetry/telemetry.js +46 -4
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/atlasTool.js +38 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/create/createDBUser.js +19 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -1
- package/dist/tools/atlas/metadata/connectCluster.js +5 -22
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
- package/dist/tools/atlas/read/inspectCluster.js +4 -24
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
- package/dist/tools/atlas/read/listClusters.js +9 -18
- package/dist/tools/atlas/read/listClusters.js.map +1 -1
- package/dist/tools/mongodb/tools.js +2 -4
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/eslint.config.js +2 -1
- package/{jest.config.ts → jest.config.cjs} +1 -1
- package/package.json +4 -2
- package/scripts/apply.ts +4 -1
- package/scripts/filter.ts +4 -0
- package/src/common/atlas/apiClient.ts +179 -37
- package/src/common/atlas/apiClientError.ts +58 -7
- package/src/common/atlas/cluster.ts +95 -0
- package/src/common/atlas/generatePassword.ts +10 -0
- package/src/common/atlas/openapi.d.ts +438 -15
- package/src/helpers/EJsonTransport.ts +47 -0
- package/src/helpers/connectionOptions.ts +20 -0
- package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
- package/src/index.ts +7 -3
- package/src/logger.ts +2 -0
- package/src/server.ts +22 -14
- package/src/session.ts +8 -4
- package/src/telemetry/constants.ts +2 -3
- package/src/telemetry/eventCache.ts +1 -1
- package/src/telemetry/telemetry.ts +72 -6
- package/src/telemetry/types.ts +0 -1
- package/src/tools/atlas/atlasTool.ts +47 -1
- package/src/tools/atlas/create/createDBUser.ts +22 -2
- package/src/tools/atlas/metadata/connectCluster.ts +5 -27
- package/src/tools/atlas/read/inspectCluster.ts +4 -40
- package/src/tools/atlas/read/listClusters.ts +19 -36
- package/src/tools/mongodb/tools.ts +2 -4
- package/src/types/mongodb-connection-string-url.d.ts +69 -0
- package/tests/integration/helpers.ts +18 -2
- package/tests/integration/telemetry.test.ts +28 -0
- package/tests/integration/tools/atlas/dbUsers.test.ts +57 -32
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
- package/tests/integration/tools/mongodb/read/find.test.ts +28 -0
- package/tests/unit/EJsonTransport.test.ts +71 -0
- package/tests/unit/apiClient.test.ts +193 -0
- package/tests/unit/session.test.ts +65 -0
- package/tests/unit/telemetry.test.ts +165 -71
- package/tsconfig.build.json +1 -1
- package/dist/packageInfo.js.map +0 -1
- package/dist/telemetry/device-id.js +0 -20
- package/dist/telemetry/device-id.js.map +0 -1
- package/src/telemetry/device-id.ts +0 -21
|
@@ -2,7 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { AtlasToolBase } from "../atlasTool.js";
|
|
4
4
|
import { ToolArgs, OperationType } from "../../tool.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PaginatedClusterDescription20240805,
|
|
7
|
+
PaginatedOrgGroupView,
|
|
8
|
+
Group,
|
|
9
|
+
PaginatedFlexClusters20241113,
|
|
10
|
+
} from "../../../common/atlas/openapi.js";
|
|
11
|
+
import { formatCluster, formatFlexCluster } from "../../../common/atlas/cluster.js";
|
|
6
12
|
|
|
7
13
|
export class ListClustersTool extends AtlasToolBase {
|
|
8
14
|
protected name = "atlas-list-clusters";
|
|
@@ -73,43 +79,20 @@ ${rows}`,
|
|
|
73
79
|
};
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
private formatClustersTable(
|
|
77
|
-
|
|
82
|
+
private formatClustersTable(
|
|
83
|
+
project: Group,
|
|
84
|
+
clusters?: PaginatedClusterDescription20240805,
|
|
85
|
+
flexClusters?: PaginatedFlexClusters20241113
|
|
86
|
+
): CallToolResult {
|
|
87
|
+
// Check if both traditional clusters and flex clusters are absent
|
|
88
|
+
if (!clusters?.results?.length && !flexClusters?.results?.length) {
|
|
78
89
|
throw new Error("No clusters found.");
|
|
79
90
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const regionConfigs = (cluster.replicationSpecs || [])
|
|
86
|
-
.map(
|
|
87
|
-
(replicationSpec) =>
|
|
88
|
-
(replicationSpec.regionConfigs || []) as {
|
|
89
|
-
providerName: string;
|
|
90
|
-
electableSpecs?: {
|
|
91
|
-
instanceSize: string;
|
|
92
|
-
};
|
|
93
|
-
readOnlySpecs?: {
|
|
94
|
-
instanceSize: string;
|
|
95
|
-
};
|
|
96
|
-
}[]
|
|
97
|
-
)
|
|
98
|
-
.flat()
|
|
99
|
-
.map((regionConfig) => {
|
|
100
|
-
return {
|
|
101
|
-
providerName: regionConfig.providerName,
|
|
102
|
-
instanceSize:
|
|
103
|
-
regionConfig.electableSpecs?.instanceSize || regionConfig.readOnlySpecs?.instanceSize,
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const instanceSize =
|
|
108
|
-
(regionConfigs.length <= 0 ? undefined : regionConfigs[0].instanceSize) || "UNKNOWN";
|
|
109
|
-
|
|
110
|
-
const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
|
|
111
|
-
|
|
112
|
-
return `${cluster.name} | ${clusterInstanceType} | ${clusterInstanceType == "DEDICATED" ? instanceSize : "N/A"} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`;
|
|
91
|
+
const formattedClusters = clusters?.results?.map((cluster) => formatCluster(cluster)) || [];
|
|
92
|
+
const formattedFlexClusters = flexClusters?.results?.map((cluster) => formatFlexCluster(cluster)) || [];
|
|
93
|
+
const rows = [...formattedClusters, ...formattedFlexClusters]
|
|
94
|
+
.map((formattedCluster) => {
|
|
95
|
+
return `${formattedCluster.name || "Unknown"} | ${formattedCluster.instanceType} | ${formattedCluster.instanceSize || "N/A"} | ${formattedCluster.state || "UNKNOWN"} | ${formattedCluster.mongoDBVersion || "N/A"} | ${formattedCluster.connectionString || "N/A"}`;
|
|
113
96
|
})
|
|
114
97
|
.join("\n");
|
|
115
98
|
return {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// import { ConnectTool } from "./metadata/connect.js";
|
|
1
|
+
import { ConnectTool } from "./metadata/connect.js";
|
|
3
2
|
import { ListCollectionsTool } from "./metadata/listCollections.js";
|
|
4
3
|
import { CollectionIndexesTool } from "./read/collectionIndexes.js";
|
|
5
4
|
import { ListDatabasesTool } from "./metadata/listDatabases.js";
|
|
@@ -21,8 +20,7 @@ import { CreateCollectionTool } from "./create/createCollection.js";
|
|
|
21
20
|
import { LogsTool } from "./metadata/logs.js";
|
|
22
21
|
|
|
23
22
|
export const MongoDbTools = [
|
|
24
|
-
|
|
25
|
-
// ConnectTool,
|
|
23
|
+
ConnectTool,
|
|
26
24
|
ListCollectionsTool,
|
|
27
25
|
ListDatabasesTool,
|
|
28
26
|
CollectionIndexesTool,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare module "mongodb-connection-string-url" {
|
|
2
|
+
import { URL } from "whatwg-url";
|
|
3
|
+
import { redactConnectionString, ConnectionStringRedactionOptions } from "./redact";
|
|
4
|
+
export { redactConnectionString, ConnectionStringRedactionOptions };
|
|
5
|
+
declare class CaseInsensitiveMap<K extends string = string> extends Map<K, string> {
|
|
6
|
+
delete(name: K): boolean;
|
|
7
|
+
get(name: K): string | undefined;
|
|
8
|
+
has(name: K): boolean;
|
|
9
|
+
set(name: K, value: any): this;
|
|
10
|
+
_normalizeKey(name: any): K;
|
|
11
|
+
}
|
|
12
|
+
declare abstract class URLWithoutHost extends URL {
|
|
13
|
+
abstract get host(): never;
|
|
14
|
+
abstract set host(value: never);
|
|
15
|
+
abstract get hostname(): never;
|
|
16
|
+
abstract set hostname(value: never);
|
|
17
|
+
abstract get port(): never;
|
|
18
|
+
abstract set port(value: never);
|
|
19
|
+
abstract get href(): string;
|
|
20
|
+
abstract set href(value: string);
|
|
21
|
+
}
|
|
22
|
+
export interface ConnectionStringParsingOptions {
|
|
23
|
+
looseValidation?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare class ConnectionString extends URLWithoutHost {
|
|
26
|
+
_hosts: string[];
|
|
27
|
+
constructor(uri: string, options?: ConnectionStringParsingOptions);
|
|
28
|
+
get host(): never;
|
|
29
|
+
set host(_ignored: never);
|
|
30
|
+
get hostname(): never;
|
|
31
|
+
set hostname(_ignored: never);
|
|
32
|
+
get port(): never;
|
|
33
|
+
set port(_ignored: never);
|
|
34
|
+
get href(): string;
|
|
35
|
+
set href(_ignored: string);
|
|
36
|
+
get isSRV(): boolean;
|
|
37
|
+
get hosts(): string[];
|
|
38
|
+
set hosts(list: string[]);
|
|
39
|
+
toString(): string;
|
|
40
|
+
clone(): ConnectionString;
|
|
41
|
+
redact(options?: ConnectionStringRedactionOptions): ConnectionString;
|
|
42
|
+
typedSearchParams<T extends {}>(): {
|
|
43
|
+
append(name: keyof T & string, value: any): void;
|
|
44
|
+
delete(name: keyof T & string): void;
|
|
45
|
+
get(name: keyof T & string): string | null;
|
|
46
|
+
getAll(name: keyof T & string): string[];
|
|
47
|
+
has(name: keyof T & string): boolean;
|
|
48
|
+
set(name: keyof T & string, value: any): void;
|
|
49
|
+
keys(): IterableIterator<keyof T & string>;
|
|
50
|
+
values(): IterableIterator<string>;
|
|
51
|
+
entries(): IterableIterator<[keyof T & string, string]>;
|
|
52
|
+
_normalizeKey(name: keyof T & string): string;
|
|
53
|
+
[Symbol.iterator](): IterableIterator<[keyof T & string, string]>;
|
|
54
|
+
sort(): void;
|
|
55
|
+
forEach<THIS_ARG = void>(
|
|
56
|
+
callback: (this: THIS_ARG, value: string, name: string, searchParams: any) => void,
|
|
57
|
+
thisArg?: THIS_ARG | undefined
|
|
58
|
+
): void;
|
|
59
|
+
readonly [Symbol.toStringTag]: "URLSearchParams";
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export declare class CommaAndColonSeparatedRecord<
|
|
63
|
+
K extends {} = Record<string, unknown>,
|
|
64
|
+
> extends CaseInsensitiveMap<keyof K & string> {
|
|
65
|
+
constructor(from?: string | null);
|
|
66
|
+
toString(): string;
|
|
67
|
+
}
|
|
68
|
+
export default ConnectionString;
|
|
69
|
+
}
|
|
@@ -5,7 +5,9 @@ import { UserConfig } from "../../src/config.js";
|
|
|
5
5
|
import { McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { Session } from "../../src/session.js";
|
|
8
|
+
import { Telemetry } from "../../src/telemetry/telemetry.js";
|
|
8
9
|
import { config } from "../../src/config.js";
|
|
10
|
+
import { jest } from "@jest/globals";
|
|
9
11
|
|
|
10
12
|
interface ParameterInfo {
|
|
11
13
|
name: string;
|
|
@@ -56,14 +58,27 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
|
|
|
56
58
|
apiClientSecret: userConfig.apiClientSecret,
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
// Mock hasValidAccessToken for tests
|
|
62
|
+
if (userConfig.apiClientId && userConfig.apiClientSecret) {
|
|
63
|
+
const mockFn = jest.fn<() => Promise<boolean>>().mockResolvedValue(true);
|
|
64
|
+
// @ts-expect-error accessing private property for testing
|
|
65
|
+
session.apiClient.validateAccessToken = mockFn;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
userConfig.telemetry = "disabled";
|
|
69
|
+
|
|
70
|
+
const telemetry = Telemetry.create(session, userConfig);
|
|
71
|
+
|
|
59
72
|
mcpServer = new Server({
|
|
60
73
|
session,
|
|
61
74
|
userConfig,
|
|
75
|
+
telemetry,
|
|
62
76
|
mcpServer: new McpServer({
|
|
63
77
|
name: "test-server",
|
|
64
78
|
version: "5.2.3",
|
|
65
79
|
}),
|
|
66
80
|
});
|
|
81
|
+
|
|
67
82
|
await mcpServer.connect(serverTransport);
|
|
68
83
|
await mcpClient.connect(clientTransport);
|
|
69
84
|
});
|
|
@@ -117,7 +132,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
|
|
|
117
132
|
content = (content as { content: unknown }).content;
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
expect(
|
|
135
|
+
expect(content).toBeArray();
|
|
121
136
|
|
|
122
137
|
const response = content as { type: string; text: string }[];
|
|
123
138
|
for (const item of response) {
|
|
@@ -221,6 +236,7 @@ export function validateThrowsForInvalidArguments(
|
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
/** Expects the argument being defined and asserts it */
|
|
224
|
-
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined> {
|
|
239
|
+
export function expectDefined<T>(arg: T): asserts arg is Exclude<T, undefined | null> {
|
|
225
240
|
expect(arg).toBeDefined();
|
|
241
|
+
expect(arg).not.toBeNull();
|
|
226
242
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHmac } from "crypto";
|
|
2
|
+
import { Telemetry } from "../../src/telemetry/telemetry.js";
|
|
3
|
+
import { Session } from "../../src/session.js";
|
|
4
|
+
import { config } from "../../src/config.js";
|
|
5
|
+
import nodeMachineId from "node-machine-id";
|
|
6
|
+
|
|
7
|
+
describe("Telemetry", () => {
|
|
8
|
+
it("should resolve the actual machine ID", async () => {
|
|
9
|
+
const actualId: string = await nodeMachineId.machineId(true);
|
|
10
|
+
|
|
11
|
+
const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex");
|
|
12
|
+
|
|
13
|
+
const telemetry = Telemetry.create(
|
|
14
|
+
new Session({
|
|
15
|
+
apiBaseUrl: "",
|
|
16
|
+
}),
|
|
17
|
+
config
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
21
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
22
|
+
|
|
23
|
+
await telemetry.deviceIdPromise;
|
|
24
|
+
|
|
25
|
+
expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId);
|
|
26
|
+
expect(telemetry["isBufferingEvents"]).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { Session } from "../../../../src/session.js";
|
|
3
2
|
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
|
|
4
|
-
import { expectDefined } from "../../helpers.js";
|
|
3
|
+
import { expectDefined, getResponseElements } from "../../helpers.js";
|
|
4
|
+
import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js";
|
|
5
5
|
|
|
6
6
|
describeWithAtlas("db users", (integration) => {
|
|
7
|
-
const userName = "testuser-" + randomId;
|
|
8
7
|
withProject(integration, ({ getProjectId }) => {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
let userName: string;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
userName = "testuser-" + randomId;
|
|
11
|
+
});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const createUserWithMCP = async (password?: string): Promise<unknown> => {
|
|
14
|
+
return await integration.mcpClient().callTool({
|
|
15
|
+
name: "atlas-create-db-user",
|
|
16
|
+
arguments: {
|
|
17
|
+
projectId: getProjectId(),
|
|
18
|
+
username: userName,
|
|
19
|
+
password,
|
|
20
|
+
roles: [
|
|
21
|
+
{
|
|
22
|
+
roleName: "readWrite",
|
|
23
|
+
databaseName: "admin",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
20
26
|
},
|
|
21
27
|
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await integration.mcpServer().session.apiClient.deleteDatabaseUser({
|
|
33
|
+
params: {
|
|
34
|
+
path: {
|
|
35
|
+
groupId: getProjectId(),
|
|
36
|
+
username: userName,
|
|
37
|
+
databaseName: "admin",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Ignore 404 errors when deleting the user
|
|
43
|
+
if (!(error instanceof ApiClientError) || error.response?.status !== 404) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
});
|
|
23
48
|
|
|
24
49
|
describe("atlas-create-db-user", () => {
|
|
@@ -34,26 +59,24 @@ describeWithAtlas("db users", (integration) => {
|
|
|
34
59
|
expect(createDbUser.inputSchema.properties).toHaveProperty("roles");
|
|
35
60
|
expect(createDbUser.inputSchema.properties).toHaveProperty("clusters");
|
|
36
61
|
});
|
|
37
|
-
it("should create a database user", async () => {
|
|
38
|
-
const projectId = getProjectId();
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
63
|
+
it("should create a database user with supplied password", async () => {
|
|
64
|
+
const response = await createUserWithMCP("testpassword");
|
|
65
|
+
|
|
66
|
+
const elements = getResponseElements(response);
|
|
67
|
+
expect(elements).toHaveLength(1);
|
|
68
|
+
expect(elements[0].text).toContain("created successfully");
|
|
69
|
+
expect(elements[0].text).toContain(userName);
|
|
70
|
+
expect(elements[0].text).not.toContain("testpassword");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should create a database user with generated password", async () => {
|
|
74
|
+
const response = await createUserWithMCP();
|
|
75
|
+
const elements = getResponseElements(response);
|
|
76
|
+
expect(elements).toHaveLength(1);
|
|
77
|
+
expect(elements[0].text).toContain("created successfully");
|
|
78
|
+
expect(elements[0].text).toContain(userName);
|
|
79
|
+
expect(elements[0].text).toContain("with password: `");
|
|
57
80
|
});
|
|
58
81
|
});
|
|
59
82
|
describe("atlas-list-db-users", () => {
|
|
@@ -68,6 +91,8 @@ describeWithAtlas("db users", (integration) => {
|
|
|
68
91
|
it("returns database users by project", async () => {
|
|
69
92
|
const projectId = getProjectId();
|
|
70
93
|
|
|
94
|
+
await createUserWithMCP();
|
|
95
|
+
|
|
71
96
|
const response = (await integration
|
|
72
97
|
.mcpClient()
|
|
73
98
|
.callTool({ name: "atlas-list-db-users", arguments: { projectId } })) as CallToolResult;
|
|
@@ -2,8 +2,6 @@ import { describeWithMongoDB } from "../mongodbHelpers.js";
|
|
|
2
2
|
import { getResponseContent, validateThrowsForInvalidArguments, validateToolMetadata } from "../../../helpers.js";
|
|
3
3
|
import { config } from "../../../../../src/config.js";
|
|
4
4
|
|
|
5
|
-
// These tests are temporarily skipped because the connect tool is disabled for the initial release.
|
|
6
|
-
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
|
|
7
5
|
describeWithMongoDB(
|
|
8
6
|
"switchConnection tool",
|
|
9
7
|
(integration) => {
|
|
@@ -77,8 +75,7 @@ describeWithMongoDB(
|
|
|
77
75
|
(mdbIntegration) => ({
|
|
78
76
|
...config,
|
|
79
77
|
connectionString: mdbIntegration.connectionString(),
|
|
80
|
-
})
|
|
81
|
-
describe.skip
|
|
78
|
+
})
|
|
82
79
|
);
|
|
83
80
|
describeWithMongoDB(
|
|
84
81
|
"Connect tool",
|
|
@@ -127,6 +124,5 @@ describeWithMongoDB(
|
|
|
127
124
|
});
|
|
128
125
|
});
|
|
129
126
|
},
|
|
130
|
-
() => config
|
|
131
|
-
describe.skip
|
|
127
|
+
() => config
|
|
132
128
|
);
|
|
@@ -17,42 +17,32 @@ interface MongoDBIntegrationTest {
|
|
|
17
17
|
export function describeWithMongoDB(
|
|
18
18
|
name: string,
|
|
19
19
|
fn: (integration: IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise<void> }) => void,
|
|
20
|
-
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig
|
|
21
|
-
describeFn = describe
|
|
20
|
+
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig
|
|
22
21
|
) {
|
|
23
|
-
|
|
22
|
+
describe(name, () => {
|
|
24
23
|
const mdbIntegration = setupMongoDBIntegrationTest();
|
|
25
24
|
const integration = setupIntegrationTest(() => ({
|
|
26
25
|
...getUserConfig(mdbIntegration),
|
|
27
|
-
connectionString: mdbIntegration.connectionString(),
|
|
28
26
|
}));
|
|
29
27
|
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
integration.mcpServer().userConfig.connectionString = mdbIntegration.connectionString();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
28
|
fn({
|
|
35
29
|
...integration,
|
|
36
30
|
...mdbIntegration,
|
|
37
31
|
connectMcpClient: async () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
const { tools } = await integration.mcpClient().listTools();
|
|
33
|
+
if (tools.find((tool) => tool.name === "connect")) {
|
|
34
|
+
await integration.mcpClient().callTool({
|
|
35
|
+
name: "connect",
|
|
36
|
+
arguments: { connectionString: mdbIntegration.connectionString() },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
44
39
|
},
|
|
45
40
|
});
|
|
46
41
|
});
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
|
|
50
|
-
let mongoCluster:
|
|
51
|
-
| {
|
|
52
|
-
connectionString: string;
|
|
53
|
-
close: () => Promise<void>;
|
|
54
|
-
}
|
|
55
|
-
| undefined;
|
|
45
|
+
let mongoCluster: MongoCluster | undefined;
|
|
56
46
|
let mongoClient: MongoClient | undefined;
|
|
57
47
|
let randomDbName: string;
|
|
58
48
|
|
|
@@ -76,8 +66,6 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest {
|
|
|
76
66
|
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
|
|
77
67
|
for (let i = 0; i < 10; i++) {
|
|
78
68
|
try {
|
|
79
|
-
// TODO: Fix this type once mongodb-runner is updated.
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
81
69
|
mongoCluster = await MongoCluster.start({
|
|
82
70
|
tmpDir: dbsDir,
|
|
83
71
|
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
|
|
@@ -141,12 +129,15 @@ export function validateAutoConnectBehavior(
|
|
|
141
129
|
},
|
|
142
130
|
beforeEachImpl?: () => Promise<void>
|
|
143
131
|
): void {
|
|
144
|
-
|
|
145
|
-
describe.skip("when not connected", () => {
|
|
132
|
+
describe("when not connected", () => {
|
|
146
133
|
if (beforeEachImpl) {
|
|
147
134
|
beforeEach(() => beforeEachImpl());
|
|
148
135
|
}
|
|
149
136
|
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
integration.mcpServer().userConfig.connectionString = undefined;
|
|
139
|
+
});
|
|
140
|
+
|
|
150
141
|
it("connects automatically if connection string is configured", async () => {
|
|
151
142
|
integration.mcpServer().userConfig.connectionString = integration.connectionString();
|
|
152
143
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
validateToolMetadata,
|
|
5
5
|
validateThrowsForInvalidArguments,
|
|
6
6
|
getResponseElements,
|
|
7
|
+
expectDefined,
|
|
7
8
|
} from "../../../helpers.js";
|
|
8
9
|
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
|
|
9
10
|
|
|
@@ -171,6 +172,33 @@ describeWithMongoDB("find tool", (integration) => {
|
|
|
171
172
|
expect(JSON.parse(elements[i + 1].text).value).toEqual(i);
|
|
172
173
|
}
|
|
173
174
|
});
|
|
175
|
+
|
|
176
|
+
it("can find objects by $oid", async () => {
|
|
177
|
+
await integration.connectMcpClient();
|
|
178
|
+
|
|
179
|
+
const fooObject = await integration
|
|
180
|
+
.mongoClient()
|
|
181
|
+
.db(integration.randomDbName())
|
|
182
|
+
.collection("foo")
|
|
183
|
+
.findOne();
|
|
184
|
+
expectDefined(fooObject);
|
|
185
|
+
|
|
186
|
+
const response = await integration.mcpClient().callTool({
|
|
187
|
+
name: "find",
|
|
188
|
+
arguments: {
|
|
189
|
+
database: integration.randomDbName(),
|
|
190
|
+
collection: "foo",
|
|
191
|
+
filter: { _id: fooObject._id },
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const elements = getResponseElements(response.content);
|
|
196
|
+
expect(elements).toHaveLength(2);
|
|
197
|
+
expect(elements[0].text).toEqual('Found 1 documents in the collection "foo":');
|
|
198
|
+
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
200
|
+
expect(JSON.parse(elements[1].text).value).toEqual(fooObject.value);
|
|
201
|
+
});
|
|
174
202
|
});
|
|
175
203
|
|
|
176
204
|
validateAutoConnectBehavior(integration, "find", () => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Decimal128, MaxKey, MinKey, ObjectId, Timestamp, UUID } from "bson";
|
|
2
|
+
import { createEJsonTransport, EJsonReadBuffer } from "../../src/helpers/EJsonTransport.js";
|
|
3
|
+
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { Readable } from "stream";
|
|
7
|
+
import { ReadBuffer } from "@modelcontextprotocol/sdk/shared/stdio.js";
|
|
8
|
+
|
|
9
|
+
describe("EJsonTransport", () => {
|
|
10
|
+
let transport: StdioServerTransport;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
transport = createEJsonTransport();
|
|
13
|
+
await transport.start();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await transport.close();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("ejson deserializes messages", () => {
|
|
21
|
+
const messages: { message: JSONRPCMessage; extra?: { authInfo?: AuthInfo } }[] = [];
|
|
22
|
+
transport.onmessage = (
|
|
23
|
+
message,
|
|
24
|
+
extra?: {
|
|
25
|
+
authInfo?: AuthInfo;
|
|
26
|
+
}
|
|
27
|
+
) => {
|
|
28
|
+
messages.push({ message, extra });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
(transport["_stdin"] as Readable).emit(
|
|
32
|
+
"data",
|
|
33
|
+
Buffer.from(
|
|
34
|
+
'{"jsonrpc":"2.0","id":1,"method":"testMethod","params":{"oid":{"$oid":"681b741f13aa74a0687b5110"},"uuid":{"$uuid":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"},"date":{"$date":"2025-05-07T14:54:23.973Z"},"decimal":{"$numberDecimal":"1234567890987654321"},"int32":123,"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"timestamp":{"$timestamp":{"t":123,"i":456}}}}\n',
|
|
35
|
+
"utf-8"
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(messages.length).toBe(1);
|
|
40
|
+
const message = messages[0].message;
|
|
41
|
+
|
|
42
|
+
expect(message).toEqual({
|
|
43
|
+
jsonrpc: "2.0",
|
|
44
|
+
id: 1,
|
|
45
|
+
method: "testMethod",
|
|
46
|
+
params: {
|
|
47
|
+
oid: new ObjectId("681b741f13aa74a0687b5110"),
|
|
48
|
+
uuid: new UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"),
|
|
49
|
+
date: new Date(Date.parse("2025-05-07T14:54:23.973Z")),
|
|
50
|
+
decimal: new Decimal128("1234567890987654321"),
|
|
51
|
+
int32: 123,
|
|
52
|
+
maxKey: new MaxKey(),
|
|
53
|
+
minKey: new MinKey(),
|
|
54
|
+
timestamp: new Timestamp({ t: 123, i: 456 }),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("has _readBuffer field of type EJsonReadBuffer", () => {
|
|
60
|
+
expect(transport["_readBuffer"]).toBeDefined();
|
|
61
|
+
expect(transport["_readBuffer"]).toBeInstanceOf(EJsonReadBuffer);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("standard StdioServerTransport", () => {
|
|
65
|
+
it("has a _readBuffer field", () => {
|
|
66
|
+
const standardTransport = new StdioServerTransport();
|
|
67
|
+
expect(standardTransport["_readBuffer"]).toBeDefined();
|
|
68
|
+
expect(standardTransport["_readBuffer"]).toBeInstanceOf(ReadBuffer);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|