Skip to content

Commit 27bc07b

Browse files
authored
feat: add atlas access list tools (#32)
1 parent aad8b01 commit 27bc07b

File tree

7 files changed

+263
-1
lines changed

7 files changed

+263
-1
lines changed

scripts/filter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
2424
"listClusters",
2525
"createCluster",
2626
"listClustersForAllProjects",
27+
"listProjectIpAccessLists",
28+
"createProjectIpAccessList",
2729
];
2830

2931
const filteredPaths = {};

src/common/atlas/client.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
PaginatedAtlasGroupView,
77
ClusterDescription20240805,
88
PaginatedClusterDescription20240805,
9+
PaginatedNetworkAccessView,
10+
NetworkPermissionEntry,
911
} from "./openapi.js";
1012

1113
export interface OAuthToken {
@@ -65,7 +67,7 @@ export class ApiClient {
6567
credentials: !this.token?.access_token ? undefined : "include",
6668
headers: {
6769
"Content-Type": "application/json",
68-
Accept: "application/vnd.atlas.2025-04-07+json",
70+
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
6971
"User-Agent": config.userAgent,
7072
...authHeaders,
7173
},
@@ -269,6 +271,20 @@ export class ApiClient {
269271
return await this.do<PaginatedAtlasGroupView>("/groups");
270272
}
271273

274+
async listProjectIpAccessLists(groupId: string): Promise<PaginatedNetworkAccessView> {
275+
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`);
276+
}
277+
278+
async createProjectIpAccessList(
279+
groupId: string,
280+
entries: NetworkPermissionEntry[]
281+
): Promise<PaginatedNetworkAccessView> {
282+
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`, {
283+
method: "POST",
284+
body: JSON.stringify(entries),
285+
});
286+
}
287+
272288
async getProject(groupId: string): Promise<Group> {
273289
return await this.do<Group>(`/groups/${groupId}`);
274290
}

src/common/atlas/openapi.d.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ export interface paths {
6868
patch?: never;
6969
trace?: never;
7070
};
71+
"/api/atlas/v2/groups/{groupId}/accessList": {
72+
parameters: {
73+
query?: never;
74+
header?: never;
75+
path?: never;
76+
cookie?: never;
77+
};
78+
/**
79+
* Return Project IP Access List
80+
* @description Returns all access list entries from the specified project's IP access list. Each entry in the project's IP access list contains either one IP address or one CIDR-notated block of IP addresses. MongoDB Cloud only allows client connections to the cluster from entries in the project's IP access list. To use this resource, the requesting Service Account or API Key must have the Project Read Only or Project Charts Admin roles. This resource replaces the whitelist resource. MongoDB Cloud removed whitelists in July 2021. Update your applications to use this new resource. The `/groups/{GROUP-ID}/accessList` endpoint manages the database IP access list. This endpoint is distinct from the `orgs/{ORG-ID}/apiKeys/{API-KEY-ID}/accesslist` endpoint, which manages the access list for MongoDB Cloud organizations.
81+
*/
82+
get: operations["listProjectIpAccessLists"];
83+
put?: never;
84+
/**
85+
* Add Entries to Project IP Access List
86+
* @description Adds one or more access list entries to the specified project. MongoDB Cloud only allows client connections to the cluster from entries in the project's IP access list. Write each entry as either one IP address or one CIDR-notated block of IP addresses. To use this resource, the requesting Service Account or API Key must have the Project Owner or Project Charts Admin roles. This resource replaces the whitelist resource. MongoDB Cloud removed whitelists in July 2021. Update your applications to use this new resource. The `/groups/{GROUP-ID}/accessList` endpoint manages the database IP access list. This endpoint is distinct from the `orgs/{ORG-ID}/apiKeys/{API-KEY-ID}/accesslist` endpoint, which manages the access list for MongoDB Cloud organizations. This endpoint doesn't support concurrent `POST` requests. You must submit multiple `POST` requests synchronously.
87+
*/
88+
post: operations["createProjectIpAccessList"];
89+
delete?: never;
90+
options?: never;
91+
head?: never;
92+
patch?: never;
93+
trace?: never;
94+
};
7195
"/api/atlas/v2/groups/{groupId}/clusters": {
7296
parameters: {
7397
query?: never;
@@ -4711,6 +4735,28 @@ export interface components {
47114735
*/
47124736
type: "MONTHLY";
47134737
};
4738+
NetworkPermissionEntry: {
4739+
/** @description Unique string of the Amazon Web Services (AWS) security group that you want to add to the project's IP access list. Your IP access list entry can be one **awsSecurityGroup**, one **cidrBlock**, or one **ipAddress**. You must configure Virtual Private Connection (VPC) peering for your project before you can add an AWS security group to an IP access list. You cannot set AWS security groups as temporary access list entries. Don't set this parameter if you set **cidrBlock** or **ipAddress**. */
4740+
awsSecurityGroup?: string;
4741+
/** @description Range of IP addresses in Classless Inter-Domain Routing (CIDR) notation that you want to add to the project's IP access list. Your IP access list entry can be one **awsSecurityGroup**, one **cidrBlock**, or one **ipAddress**. Don't set this parameter if you set **awsSecurityGroup** or **ipAddress**. */
4742+
cidrBlock?: string;
4743+
/** @description Remark that explains the purpose or scope of this IP access list entry. */
4744+
comment?: string;
4745+
/**
4746+
* Format: date-time
4747+
* @description Date and time after which MongoDB Cloud deletes the temporary access list entry. This parameter expresses its value in the ISO 8601 timestamp format in UTC and can include the time zone designation. The date must be later than the current date but no later than one week after you submit this request. The resource returns this parameter if you specified an expiration date when creating this IP access list entry.
4748+
*/
4749+
deleteAfterDate?: string;
4750+
/**
4751+
* @description Unique 24-hexadecimal digit string that identifies the project that contains the IP access list to which you want to add one or more entries.
4752+
* @example 32b6e34b3d91647abb20e7b8
4753+
*/
4754+
readonly groupId?: string;
4755+
/** @description IP address that you want to add to the project's IP access list. Your IP access list entry can be one **awsSecurityGroup**, one **cidrBlock**, or one **ipAddress**. Don't set this parameter if you set **awsSecurityGroup** or **cidrBlock**. */
4756+
ipAddress?: string;
4757+
/** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */
4758+
readonly links?: components["schemas"]["Link"][];
4759+
};
47144760
/**
47154761
* On-Demand Cloud Provider Snapshot Source
47164762
* @description On-Demand Cloud Provider Snapshots as Source for a Data Lake Pipeline.
@@ -4898,6 +4944,17 @@ export interface components {
48984944
*/
48994945
readonly totalCount?: number;
49004946
};
4947+
PaginatedNetworkAccessView: {
4948+
/** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */
4949+
readonly links?: components["schemas"]["Link"][];
4950+
/** @description List of returned documents that MongoDB Cloud provides when completing this request. */
4951+
readonly results?: components["schemas"]["NetworkPermissionEntry"][];
4952+
/**
4953+
* Format: int32
4954+
* @description Total number of documents available. MongoDB Cloud omits this value if `includeCount` is set to `false`. The total number is an estimate and may not be exact.
4955+
*/
4956+
readonly totalCount?: number;
4957+
};
49014958
PaginatedOrgGroupView: {
49024959
/** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */
49034960
readonly links?: components["schemas"]["Link"][];
@@ -6851,6 +6908,7 @@ export type IngestionSource = components["schemas"]["IngestionSource"];
68516908
export type InvoiceLineItem = components["schemas"]["InvoiceLineItem"];
68526909
export type Link = components["schemas"]["Link"];
68536910
export type MonthlyScheduleView = components["schemas"]["MonthlyScheduleView"];
6911+
export type NetworkPermissionEntry = components["schemas"]["NetworkPermissionEntry"];
68546912
export type OnDemandCpsSnapshotSource = components["schemas"]["OnDemandCpsSnapshotSource"];
68556913
export type OnlineArchiveSchedule = components["schemas"]["OnlineArchiveSchedule"];
68566914
export type OrgActiveUserResponse = components["schemas"]["OrgActiveUserResponse"];
@@ -6860,6 +6918,7 @@ export type OrgUserResponse = components["schemas"]["OrgUserResponse"];
68606918
export type OrgUserRolesResponse = components["schemas"]["OrgUserRolesResponse"];
68616919
export type PaginatedAtlasGroupView = components["schemas"]["PaginatedAtlasGroupView"];
68626920
export type PaginatedClusterDescription20240805 = components["schemas"]["PaginatedClusterDescription20240805"];
6921+
export type PaginatedNetworkAccessView = components["schemas"]["PaginatedNetworkAccessView"];
68636922
export type PaginatedOrgGroupView = components["schemas"]["PaginatedOrgGroupView"];
68646923
export type PeriodicCpsSnapshotSource = components["schemas"]["PeriodicCpsSnapshotSource"];
68656924
export type ReplicationSpec20240805 = components["schemas"]["ReplicationSpec20240805"];
@@ -7094,6 +7153,89 @@ export interface operations {
70947153
500: components["responses"]["internalServerError"];
70957154
};
70967155
};
7156+
listProjectIpAccessLists: {
7157+
parameters: {
7158+
query?: {
7159+
/** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */
7160+
envelope?: components["parameters"]["envelope"];
7161+
/** @description Flag that indicates whether the response returns the total number of items (**totalCount**) in the response. */
7162+
includeCount?: components["parameters"]["includeCount"];
7163+
/** @description Number of items that the response returns per page. */
7164+
itemsPerPage?: components["parameters"]["itemsPerPage"];
7165+
/** @description Number of the page that displays the current set of the total objects that the response returns. */
7166+
pageNum?: components["parameters"]["pageNum"];
7167+
/** @description Flag that indicates whether the response body should be in the prettyprint format. */
7168+
pretty?: components["parameters"]["pretty"];
7169+
};
7170+
header?: never;
7171+
path: {
7172+
/** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access.
7173+
*
7174+
* **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */
7175+
groupId: components["parameters"]["groupId"];
7176+
};
7177+
cookie?: never;
7178+
};
7179+
requestBody?: never;
7180+
responses: {
7181+
/** @description OK */
7182+
200: {
7183+
headers: {
7184+
[name: string]: unknown;
7185+
};
7186+
content: {
7187+
"application/vnd.atlas.2023-01-01+json": components["schemas"]["PaginatedNetworkAccessView"];
7188+
};
7189+
};
7190+
401: components["responses"]["unauthorized"];
7191+
500: components["responses"]["internalServerError"];
7192+
};
7193+
};
7194+
createProjectIpAccessList: {
7195+
parameters: {
7196+
query?: {
7197+
/** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */
7198+
envelope?: components["parameters"]["envelope"];
7199+
/** @description Flag that indicates whether the response returns the total number of items (**totalCount**) in the response. */
7200+
includeCount?: components["parameters"]["includeCount"];
7201+
/** @description Number of items that the response returns per page. */
7202+
itemsPerPage?: components["parameters"]["itemsPerPage"];
7203+
/** @description Number of the page that displays the current set of the total objects that the response returns. */
7204+
pageNum?: components["parameters"]["pageNum"];
7205+
/** @description Flag that indicates whether the response body should be in the prettyprint format. */
7206+
pretty?: components["parameters"]["pretty"];
7207+
};
7208+
header?: never;
7209+
path: {
7210+
/** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access.
7211+
*
7212+
* **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */
7213+
groupId: components["parameters"]["groupId"];
7214+
};
7215+
cookie?: never;
7216+
};
7217+
/** @description One or more access list entries to add to the specified project. */
7218+
requestBody: {
7219+
content: {
7220+
"application/vnd.atlas.2023-01-01+json": components["schemas"]["NetworkPermissionEntry"][];
7221+
};
7222+
};
7223+
responses: {
7224+
/** @description OK */
7225+
200: {
7226+
headers: {
7227+
[name: string]: unknown;
7228+
};
7229+
content: {
7230+
"application/vnd.atlas.2023-01-01+json": components["schemas"]["PaginatedNetworkAccessView"];
7231+
};
7232+
};
7233+
400: components["responses"]["badRequest"];
7234+
401: components["responses"]["unauthorized"];
7235+
403: components["responses"]["forbidden"];
7236+
500: components["responses"]["internalServerError"];
7237+
};
7238+
};
70977239
listClusters: {
70987240
parameters: {
70997241
query?: {

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json
99
const packageJson = JSON.parse(packageMetadata);
1010

1111
export const config = {
12+
atlasApiVersion: `2025-03-12`,
1213
version: packageJson.version,
1314
apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/",
1415
clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",

src/tools/atlas/createAccessList.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { AtlasToolBase } from "./atlasTool.js";
4+
import { ToolArgs } from "../tool.js";
5+
6+
const DEFAULT_COMMENT = "Added by Atlas MCP";
7+
8+
export class CreateAccessListTool extends AtlasToolBase {
9+
protected name = "atlas-create-access-list";
10+
protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters.";
11+
protected argsShape = {
12+
projectId: z.string().describe("Atlas project ID"),
13+
ipAddresses: z
14+
.array(z.string().ip({ version: "v4" }))
15+
.describe("IP addresses to allow access from")
16+
.optional(),
17+
cidrBlocks: z.array(z.string().cidr()).describe("CIDR blocks to allow access from").optional(),
18+
comment: z.string().describe("Comment for the access list entries").default(DEFAULT_COMMENT).optional(),
19+
};
20+
21+
protected async execute({
22+
projectId,
23+
ipAddresses,
24+
cidrBlocks,
25+
comment,
26+
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
27+
await this.ensureAuthenticated();
28+
29+
if (!ipAddresses?.length && !cidrBlocks?.length) {
30+
throw new Error("Either ipAddresses or cidrBlocks must be provided.");
31+
}
32+
33+
const ipInputs = (ipAddresses || []).map((ipAddress) => ({
34+
groupId: projectId,
35+
ipAddress,
36+
comment: comment || DEFAULT_COMMENT,
37+
}));
38+
39+
const cidrInputs = (cidrBlocks || []).map((cidrBlock) => ({
40+
groupId: projectId,
41+
cidrBlock,
42+
comment: comment || DEFAULT_COMMENT,
43+
}));
44+
45+
const inputs = [...ipInputs, ...cidrInputs];
46+
47+
await this.apiClient.createProjectIpAccessList(projectId, inputs);
48+
49+
return {
50+
content: [
51+
{
52+
type: "text",
53+
text: `IP/CIDR ranges added to access list for project ${projectId}.`,
54+
},
55+
],
56+
};
57+
}
58+
}

src/tools/atlas/inspectAccessList.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { AtlasToolBase } from "./atlasTool.js";
4+
import { ToolArgs } from "../tool.js";
5+
6+
export class InspectAccessListTool extends AtlasToolBase {
7+
protected name = "atlas-inspect-access-list";
8+
protected description = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters.";
9+
protected argsShape = {
10+
projectId: z.string().describe("Atlas project ID"),
11+
};
12+
13+
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
14+
await this.ensureAuthenticated();
15+
16+
const accessList = await this.apiClient.listProjectIpAccessLists(projectId);
17+
18+
if (!accessList.results?.length) {
19+
throw new Error("No access list entries found.");
20+
}
21+
22+
return {
23+
content: [
24+
{
25+
type: "text",
26+
text:
27+
`IP ADDRESS | CIDR | COMMENT
28+
------|------|------
29+
` +
30+
(accessList.results || [])
31+
.map((entry) => {
32+
return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`;
33+
})
34+
.join("\n"),
35+
},
36+
],
37+
};
38+
}
39+
}

src/tools/atlas/tools.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ListClustersTool } from "./listClusters.js";
77
import { ListProjectsTool } from "./listProjects.js";
88
import { InspectClusterTool } from "./inspectCluster.js";
99
import { CreateFreeClusterTool } from "./createFreeCluster.js";
10+
import { CreateAccessListTool } from "./createAccessList.js";
11+
import { InspectAccessListTool } from "./inspectAccessList.js";
1012

1113
export function registerAtlasTools(server: McpServer, state: State, apiClient: ApiClient) {
1214
const tools: ToolBase[] = [
@@ -15,6 +17,8 @@ export function registerAtlasTools(server: McpServer, state: State, apiClient: A
1517
new ListProjectsTool(state, apiClient),
1618
new InspectClusterTool(state, apiClient),
1719
new CreateFreeClusterTool(state, apiClient),
20+
new CreateAccessListTool(state, apiClient),
21+
new InspectAccessListTool(state, apiClient),
1822
];
1923

2024
for (const tool of tools) {

0 commit comments

Comments
 (0)