Skip to content

Commit 43cf255

Browse files
committed
MCP-5: Improve accesslist experience
1 parent 3d079c6 commit 43cf255

File tree

11 files changed

+99
-89
lines changed

11 files changed

+99
-89
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,12 +374,10 @@ To use the Atlas API tools, you'll need to create a service account in MongoDB A
374374
To learn more about Service Accounts, check the [MongoDB Atlas documentation](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/).
375375

376376
2. **Save Client Credentials:**
377-
378377
- After creation, you'll be shown the Client ID and Client Secret
379378
- **Important:** Copy and save the Client Secret immediately as it won't be displayed again
380379

381380
3. **Add Access List Entry:**
382-
383381
- Add your IP address to the API access list
384382

385383
4. **Configure the MCP Server:**

src/common/atlas/accessListUtils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ApiClient } from "./apiClient.js";
2+
import logger, { LogId } from "../logger.js";
3+
import { ApiClientError } from "./apiClientError.js";
4+
5+
export async function makeCurrentIpAccessListEntry(
6+
apiClient: ApiClient,
7+
projectId: string,
8+
comment: string = "Added by Atlas MCP"
9+
) {
10+
const { currentIpv4Address } = await apiClient.getIpInfo();
11+
return {
12+
groupId: projectId,
13+
ipAddress: currentIpv4Address,
14+
comment,
15+
};
16+
}
17+
18+
/**
19+
* Ensures the current public IP is in the access list for the given Atlas project.
20+
* If the IP is already present, this is a no-op.
21+
* @param apiClient The Atlas API client instance
22+
* @param projectId The Atlas project ID
23+
*/
24+
export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectId: string): Promise<void> {
25+
// Get the current public IP
26+
const entry = await makeCurrentIpAccessListEntry(apiClient, projectId, "Added by MCP pre-run access list helper");
27+
try {
28+
await apiClient.createProjectIpAccessList({
29+
params: { path: { groupId: projectId } },
30+
body: [entry],
31+
});
32+
logger.debug(
33+
LogId.atlasIpAccessListAdded,
34+
"atlas-connect-cluster",
35+
`IP access list created: ${JSON.stringify(entry)}`
36+
);
37+
} catch (err) {
38+
if (err instanceof ApiClientError && err.response?.status === 409) {
39+
// 409 Conflict: entry already exists, ignore
40+
return;
41+
}
42+
throw err;
43+
}
44+
}

src/common/atlas/ensureAccessList.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1 @@
1-
import { ApiClientError } from "./apiClientError.js";
2-
3-
/**
4-
* Ensures the current public IP is in the access list for the given Atlas project.
5-
* If the IP is already present, this is a no-op.
6-
* @param apiClient The Atlas API client instance
7-
* @param projectId The Atlas project ID
8-
*/
9-
export async function ensureCurrentIpInAccessList(apiClient: any, projectId: string): Promise<void> {
10-
// Get the current public IP
11-
const { currentIpv4Address } = await apiClient.getIpInfo();
12-
const entry = {
13-
groupId: projectId,
14-
ipAddress: currentIpv4Address,
15-
comment: "Added by MCP pre-run access list helper",
16-
};
17-
try {
18-
await apiClient.createProjectIpAccessList({
19-
params: { path: { groupId: projectId } },
20-
body: [entry],
21-
});
22-
} catch (err) {
23-
if (err instanceof ApiClientError && err.response?.status === 409) {
24-
// 409 Conflict: entry already exists, ignore
25-
return;
26-
}
27-
throw err;
28-
}
29-
}
1+
// This file has been migrated. ensureCurrentIpInAccessList is now in accessListUtils.ts

src/common/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const LogId = {
2020
atlasConnectAttempt: mongoLogId(1_001_005),
2121
atlasConnectSucceeded: mongoLogId(1_001_006),
2222
atlasApiRevokeFailure: mongoLogId(1_001_007),
23+
atlasIpAccessListAdded: mongoLogId(1_001_008),
2324

2425
telemetryDisabled: mongoLogId(1_002_001),
2526
telemetryEmitFailure: mongoLogId(1_002_002),

src/tools/atlas/connect/connectCluster.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ToolArgs, OperationType } from "../../tool.js";
55
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
66
import logger, { LogId } from "../../../common/logger.js";
77
import { inspectCluster } from "../../../common/atlas/cluster.js";
8-
import { ensureCurrentIpInAccessList } from "../../../common/atlas/ensureAccessList.js";
8+
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
99

1010
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
1111

@@ -57,6 +57,19 @@ export class ConnectClusterTool extends AtlasToolBase {
5757
"atlas-connect-cluster",
5858
`error querying cluster: ${error.message}`
5959
);
60+
61+
// sometimes the error can be "error querying cluster: bad auth : Authentication failed."
62+
// which just means it's still propagating permissions to the cluster
63+
// in that case, we want to classify this as connecting.
64+
if (error.message.includes("Authentication failed")) {
65+
logger.debug(
66+
LogId.atlasConnectFailure,
67+
"atlas-connect-cluster",
68+
`assuming connecting to cluster: ${this.session.connectedAtlasCluster?.clusterName}`
69+
);
70+
return "connecting";
71+
}
72+
6073
return "unknown";
6174
}
6275
}

src/tools/atlas/create/createAccessList.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
5+
import { makeCurrentIpAccessListEntry } from "../../../common/atlas/accessListUtils.js";
56

67
const DEFAULT_COMMENT = "Added by Atlas MCP";
78

@@ -38,12 +39,11 @@ export class CreateAccessListTool extends AtlasToolBase {
3839
}));
3940

4041
if (currentIpAddress) {
41-
const currentIp = await this.session.apiClient.getIpInfo();
42-
const input = {
43-
groupId: projectId,
44-
ipAddress: currentIp.currentIpv4Address,
45-
comment: comment || DEFAULT_COMMENT,
46-
};
42+
const input = await makeCurrentIpAccessListEntry(
43+
this.session.apiClient,
44+
projectId,
45+
comment || DEFAULT_COMMENT
46+
);
4747
ipInputs.push(input);
4848
}
4949

src/tools/atlas/create/createDBUser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55
import { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
66
import { generateSecurePassword } from "../../../helpers/generatePassword.js";
7-
import { ensureCurrentIpInAccessList } from "../../../common/atlas/ensureAccessList.js";
7+
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
88

99
export class CreateDBUserTool extends AtlasToolBase {
1010
public name = "atlas-create-db-user";

src/tools/atlas/create/createFreeCluster.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55
import { ClusterDescription20240805 } from "../../../common/atlas/openapi.js";
6-
import { ensureCurrentIpInAccessList } from "../../../common/atlas/ensureAccessList.js";
6+
import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js";
77

88
export class CreateFreeClusterTool extends AtlasToolBase {
99
public name = "atlas-create-free-cluster";

tests/integration/tools/atlas/accessLists.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { describeWithAtlas, withProject } from "./atlasHelpers.js";
33
import { expectDefined } from "../../helpers.js";
4-
import { ensureCurrentIpInAccessList } from "../../../../src/common/atlas/ensureAccessList.js";
4+
import { ensureCurrentIpInAccessList } from "../../../../src/common/atlas/accessListUtils.js";
55

66
function generateRandomIp() {
77
const randomIp: number[] = [192];

tests/unit/accessListUtils.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* global jest */
2+
import { ApiClient } from "../../src/common/atlas/apiClient.js";
3+
import { ensureCurrentIpInAccessList } from "../../src/common/atlas/accessListUtils.js";
4+
import { jest } from "@jest/globals";
5+
6+
describe("ensureCurrentIpInAccessList", () => {
7+
it("should add the current IP to the access list", async () => {
8+
const apiClient = {
9+
getIpInfo: jest.fn().mockResolvedValue({ currentIpv4Address: "127.0.0.1" } as never),
10+
createProjectIpAccessList: jest.fn().mockResolvedValue(undefined as never),
11+
} as unknown as ApiClient;
12+
await ensureCurrentIpInAccessList(apiClient, "projectId");
13+
expect(apiClient.createProjectIpAccessList).toHaveBeenCalledWith({
14+
params: { path: { groupId: "projectId" } },
15+
body: [{ groupId: "projectId", ipAddress: "127.0.0.1", comment: "Added by MCP pre-run access list helper" }],
16+
});
17+
});
18+
19+
it("should not fail if the current IP is already in the access list", async () => {
20+
const apiClient = {
21+
getIpInfo: jest.fn().mockResolvedValue({ currentIpv4Address: "127.0.0.1" } as never),
22+
createProjectIpAccessList: jest.fn().mockRejectedValue({ response: { status: 409 } } as never),
23+
} as unknown as ApiClient;
24+
await ensureCurrentIpInAccessList(apiClient, "projectId");
25+
expect(apiClient.createProjectIpAccessList).toHaveBeenCalledWith({
26+
params: { path: { groupId: "projectId" } },
27+
body: [{ groupId: "projectId", ipAddress: "127.0.0.1", comment: "Added by MCP pre-run access list helper" }],
28+
});
29+
});
30+
});

0 commit comments

Comments
 (0)