Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/tools/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const CommonArgs = {
};

export const AtlasArgs = {
projectId: (): z.ZodString => CommonArgs.objectId("projectId"),
projectId: (): z.ZodString => CommonArgs.objectId("projectId").describe("Atlas project ID"),

organizationId: (): z.ZodString => CommonArgs.objectId("organizationId"),

Expand Down Expand Up @@ -68,3 +68,12 @@ export const AtlasArgs = {
password: (): z.ZodString =>
z.string().min(1, "Password is required").max(100, "Password must be 100 characters or less"),
};

export const ProjectAndClusterArgs = {
projectId: AtlasArgs.projectId(),
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
};

export const ProjectArgs = {
projectId: AtlasArgs.projectId(),
};
2 changes: 1 addition & 1 deletion src/tools/atlas/connect/connectCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function sleep(ms: number): Promise<void> {
}

export const ConnectClusterArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
projectId: AtlasArgs.projectId(),
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
};

Expand Down
4 changes: 2 additions & 2 deletions src/tools/atlas/create/createAccessList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { type OperationType, type ToolArgs } from "../../tool.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "../atlasTool.js";
import { makeCurrentIpAccessListEntry, DEFAULT_ACCESS_LIST_COMMENT } from "../../../common/atlas/accessListUtils.js";
import { AtlasArgs, CommonArgs } from "../../args.js";
import { AtlasArgs, CommonArgs, ProjectArgs } from "../../args.js";

export const CreateAccessListArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
...ProjectArgs,
ipAddresses: z.array(AtlasArgs.ipAddress()).describe("IP addresses to allow access from").optional(),
cidrBlocks: z.array(AtlasArgs.cidrBlock()).describe("CIDR blocks to allow access from").optional(),
currentIpAddress: z.boolean().describe("Add the current IP address").default(false),
Expand Down
6 changes: 3 additions & 3 deletions src/tools/atlas/create/createDBUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUti
import { AtlasArgs, CommonArgs } from "../../args.js";

export const CreateDBUserArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
projectId: AtlasArgs.projectId(),
username: AtlasArgs.username().describe("Username for the new user"),
// Models will generate overly simplistic passwords like SecurePassword123 or
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
Expand All @@ -26,7 +26,7 @@ export const CreateDBUserArgs = {
collectionName: CommonArgs.string().describe("Collection name").optional(),
})
)
.describe("Roles for the new user"),
.describe("Roles for the new database user"),
clusters: z
.array(AtlasArgs.clusterName())
.describe("Clusters to assign the user to, leave empty for access to all clusters")
Expand All @@ -35,7 +35,7 @@ export const CreateDBUserArgs = {

export class CreateDBUserTool extends AtlasToolBase {
public name = "atlas-create-db-user";
protected description = "Create an MongoDB Atlas database user";
protected description = "Create a MongoDB Atlas database user";
public operationType: OperationType = "create";
protected argsShape = {
...CreateDBUserArgs,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/atlas/read/inspectAccessList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AtlasToolBase } from "../atlasTool.js";
import { AtlasArgs } from "../../args.js";

export const InspectAccessListArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
projectId: AtlasArgs.projectId(),
};

export class InspectAccessListTool extends AtlasToolBase {
Expand Down
9 changes: 2 additions & 7 deletions src/tools/atlas/read/inspectCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ import { type OperationType, type ToolArgs, formatUntrustedData } from "../../to
import { AtlasToolBase } from "../atlasTool.js";
import type { Cluster } from "../../../common/atlas/cluster.js";
import { inspectCluster } from "../../../common/atlas/cluster.js";
import { AtlasArgs } from "../../args.js";

export const InspectClusterArgs = {
projectId: AtlasArgs.projectId().describe("Atlas project ID"),
clusterName: AtlasArgs.clusterName().describe("Atlas cluster name"),
};
import { ProjectAndClusterArgs } from "../../args.js";

export class InspectClusterTool extends AtlasToolBase {
public name = "atlas-inspect-cluster";
protected description = "Inspect MongoDB Atlas cluster";
public operationType: OperationType = "read";
protected argsShape = {
...InspectClusterArgs,
...ProjectAndClusterArgs,
};

protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
Expand Down
22 changes: 21 additions & 1 deletion tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ export function setupIntegrationTest(
keychain: new Keychain(),
});

// Mock hasValidAccessToken for tests
// Mock API Client for tests
if (!userConfig.apiClientId && !userConfig.apiClientSecret) {
userConfig.apiClientId = "test";
userConfig.apiClientSecret = "test";
const mockFn = vi.fn().mockResolvedValue(true);
session.apiClient.validateAccessToken = mockFn;
}
Expand Down Expand Up @@ -235,6 +237,16 @@ export const databaseCollectionParameters: ParameterInfo[] = [
{ name: "collection", type: "string", description: "Collection name", required: true },
];

export const projectIdParameters: ParameterInfo[] = [
{ name: "projectId", type: "string", description: "Atlas project ID", required: true },
];

export const createClusterParameters: ParameterInfo[] = [
{ name: "name", type: "string", description: "Name of the cluster", required: true },
{ name: "projectId", type: "string", description: "Atlas project ID to create the cluster in", required: true },
{ name: "region", type: "string", description: "Region of the cluster", required: false },
];

export const databaseCollectionInvalidArgs = [
{},
{ database: "test" },
Expand All @@ -245,6 +257,14 @@ export const databaseCollectionInvalidArgs = [
{ database: "test", collection: [] },
];

export const projectIdInvalidArgs = [
{},
{ projectId: 123 },
{ projectId: [] },
{ projectId: "!✅invalid" },
{ projectId: "invalid-test-project-id" },
];

export const databaseInvalidArgs = [{}, { database: 123 }, { database: [] }];

export function validateToolMetadata(
Expand Down
27 changes: 18 additions & 9 deletions tests/integration/tools/atlas/alerts.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { expectDefined, getResponseElements } from "../../helpers.js";
import {
getResponseElements,
projectIdInvalidArgs,
validateThrowsForInvalidArguments,
validateToolMetadata,
} from "../../helpers.js";
import { parseTable, describeWithAtlas, withProject } from "./atlasHelpers.js";
import { expect, it } from "vitest";
import { expect, it, describe } from "vitest";

describeWithAtlas("atlas-list-alerts", (integration) => {
it("should have correct metadata", async () => {
const { tools } = await integration.mcpClient().listTools();
const listAlerts = tools.find((tool) => tool.name === "atlas-list-alerts");
expectDefined(listAlerts);
expect(listAlerts.inputSchema.type).toBe("object");
expectDefined(listAlerts.inputSchema.properties);
expect(listAlerts.inputSchema.properties).toHaveProperty("projectId");
describe("should have correct metadata and validate invalid arguments", () => {
validateToolMetadata(integration, "atlas-list-alerts", "List MongoDB Atlas alerts", [
{
name: "projectId",
type: "string",
description: "Atlas project ID to list alerts for",
required: true,
},
]);

validateThrowsForInvalidArguments(integration, "atlas-list-alerts", projectIdInvalidArgs);
});

withProject(integration, ({ getProjectId }) => {
Expand Down
23 changes: 17 additions & 6 deletions tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import { afterAll, beforeAll, describe } from "vitest";
export type IntegrationTestFunction = (integration: IntegrationTest) => void;

export function describeWithAtlas(name: string, fn: IntegrationTestFunction): void {
const describeFn =
!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length
? describe.skip
: describe;
describeFn(name, () => {
describe(name, () => {
const integration = setupIntegrationTest(
() => ({
...defaultTestConfig,
Expand All @@ -34,8 +30,23 @@ interface ProjectTestArgs {

type ProjectTestFunction = (args: ProjectTestArgs) => void;

export function withCredentials(integration: IntegrationTest, fn: IntegrationTestFunction): SuiteCollector<object> {
const describeFn =
!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length
? describe.skip
: describe;
return describeFn("with credentials", () => {
fn(integration);
});
}

export function withProject(integration: IntegrationTest, fn: ProjectTestFunction): SuiteCollector<object> {
return describe("with project", () => {
const describeFn =
!process.env.MDB_MCP_API_CLIENT_ID?.length || !process.env.MDB_MCP_API_CLIENT_SECRET?.length
? describe.skip
: describe;

return describeFn("with project", () => {
let projectId: string = "";
let ipAddress: string = "";

Expand Down
23 changes: 22 additions & 1 deletion tests/integration/tools/atlas/clusters.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { Session } from "../../../../src/common/session.js";
import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js";
import {
expectDefined,
getResponseElements,
getDataFromUntrustedContent,
createClusterParameters,
projectIdInvalidArgs,
validateThrowsForInvalidArguments,
validateToolMetadata,
} from "../../helpers.js";
import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js";
import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
Expand Down Expand Up @@ -57,6 +65,19 @@ async function waitCluster(
}

describeWithAtlas("clusters", (integration) => {
describe("should have correct metadata and validate invalid arguments", () => {
validateToolMetadata(
integration,
"atlas-create-free-cluster",
"Create a free MongoDB Atlas cluster",
createClusterParameters
);

expect(() => {
validateThrowsForInvalidArguments(integration, "atlas-create-free-cluster", projectIdInvalidArgs);
}).not.toThrow();
});

withProject(integration, ({ getProjectId, getIpAddress }) => {
const clusterName = "ClusterTest-" + randomId;

Expand Down
11 changes: 10 additions & 1 deletion tests/integration/tools/atlas/dbUsers.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
import { expectDefined, getResponseElements } from "../../helpers.js";
import {
expectDefined,
getResponseElements,
projectIdInvalidArgs,
validateThrowsForInvalidArguments,
} from "../../helpers.js";
import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Keychain } from "../../../../src/common/keychain.js";

describeWithAtlas("db users", (integration) => {
describe("should have correct metadata and validate invalid arguments", () => {
validateThrowsForInvalidArguments(integration, "atlas-create-db-user", projectIdInvalidArgs);
});

withProject(integration, ({ getProjectId }) => {
let userName: string;
beforeEach(() => {
Expand Down
32 changes: 17 additions & 15 deletions tests/integration/tools/atlas/orgs.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js";
import { parseTable, describeWithAtlas } from "./atlasHelpers.js";
import { parseTable, describeWithAtlas, withCredentials } from "./atlasHelpers.js";
import { describe, expect, it } from "vitest";

describeWithAtlas("orgs", (integration) => {
describe("atlas-list-orgs", () => {
it("should have correct metadata", async () => {
const { tools } = await integration.mcpClient().listTools();
const listOrgs = tools.find((tool) => tool.name === "atlas-list-orgs");
expectDefined(listOrgs);
});
withCredentials(integration, () => {
describe("atlas-list-orgs", () => {
it("should have correct metadata", async () => {
const { tools } = await integration.mcpClient().listTools();
const listOrgs = tools.find((tool) => tool.name === "atlas-list-orgs");
expectDefined(listOrgs);
});

it("returns org names", async () => {
const response = await integration.mcpClient().callTool({ name: "atlas-list-orgs", arguments: {} });
const elements = getResponseElements(response);
expect(elements[0]?.text).toContain("Found 1 organizations");
expect(elements[1]?.text).toContain("<untrusted-user-data-");
const data = parseTable(getDataFromUntrustedContent(elements[1]?.text ?? ""));
expect(data).toHaveLength(1);
expect(data[0]?.["Organization Name"]).toEqual("MongoDB MCP Test");
it("returns org names", async () => {
const response = await integration.mcpClient().callTool({ name: "atlas-list-orgs", arguments: {} });
const elements = getResponseElements(response);
expect(elements[0]?.text).toContain("Found 1 organizations");

Check failure on line 17 in tests/integration/tools/atlas/orgs.test.ts

View workflow job for this annotation

GitHub Actions / Run Atlas tests

tests/integration/tools/atlas/orgs.test.ts > orgs > with credentials > atlas-list-orgs > returns org names

AssertionError: expected 'Unable to authenticate with MongoDB A…' to contain 'Found 1 organizations' - Expected + Received - Found 1 organizations + Unable to authenticate with MongoDB Atlas, API error: [401 Unauthorized] error calling Atlas API: Unauthorized; You are not authorized for this resource. + + Hint: Your API credentials may be invalid, expired or lack permissions. + Please check your Atlas API credentials and ensure they have the appropriate permissions. + For more information on setting up API keys, visit: https://www.mongodb.com/docs/atlas/configure-api-access/ ❯ tests/integration/tools/atlas/orgs.test.ts:17:43
expect(elements[1]?.text).toContain("<untrusted-user-data-");
const data = parseTable(getDataFromUntrustedContent(elements[1]?.text ?? ""));
expect(data).toHaveLength(1);
expect(data[0]?.["Organization Name"]).toEqual("MongoDB MCP Test");
});
});
});
});
Loading
Loading