Skip to content

feat: add atlas access list tools #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 10, 2025
Merged
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
2 changes: 2 additions & 0 deletions scripts/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
"listClusters",
"createCluster",
"listClustersForAllProjects",
"listProjectIpAccessLists",
"createProjectIpAccessList",
];

const filteredPaths = {};
Expand Down
18 changes: 17 additions & 1 deletion src/common/atlas/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
PaginatedAtlasGroupView,
ClusterDescription20240805,
PaginatedClusterDescription20240805,
PaginatedNetworkAccessView,
NetworkPermissionEntry,
} from "./openapi.js";

export interface OAuthToken {
Expand Down Expand Up @@ -65,7 +67,7 @@ export class ApiClient {
credentials: !this.token?.access_token ? undefined : "include",
headers: {
"Content-Type": "application/json",
Accept: "application/vnd.atlas.2025-04-07+json",
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
"User-Agent": config.userAgent,
...authHeaders,
},
Expand Down Expand Up @@ -269,6 +271,20 @@ export class ApiClient {
return await this.do<PaginatedAtlasGroupView>("/groups");
}

async listProjectIpAccessLists(groupId: string): Promise<PaginatedNetworkAccessView> {
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`);
}

async createProjectIpAccessList(
groupId: string,
entries: NetworkPermissionEntry[]
): Promise<PaginatedNetworkAccessView> {
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`, {
method: "POST",
body: JSON.stringify(entries),
});
}

async getProject(groupId: string): Promise<Group> {
return await this.do<Group>(`/groups/${groupId}`);
}
Expand Down
142 changes: 142 additions & 0 deletions src/common/atlas/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/atlas/v2/groups/{groupId}/accessList": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Return Project IP Access List
* @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.
*/
get: operations["listProjectIpAccessLists"];
put?: never;
/**
* Add Entries to Project IP Access List
* @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.
*/
post: operations["createProjectIpAccessList"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/atlas/v2/groups/{groupId}/clusters": {
parameters: {
query?: never;
Expand Down Expand Up @@ -4711,6 +4735,28 @@ export interface components {
*/
type: "MONTHLY";
};
NetworkPermissionEntry: {
/** @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**. */
awsSecurityGroup?: string;
/** @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**. */
cidrBlock?: string;
/** @description Remark that explains the purpose or scope of this IP access list entry. */
comment?: string;
/**
* Format: date-time
* @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.
*/
deleteAfterDate?: string;
/**
* @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.
* @example 32b6e34b3d91647abb20e7b8
*/
readonly groupId?: string;
/** @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**. */
ipAddress?: string;
/** @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. */
readonly links?: components["schemas"]["Link"][];
};
/**
* On-Demand Cloud Provider Snapshot Source
* @description On-Demand Cloud Provider Snapshots as Source for a Data Lake Pipeline.
Expand Down Expand Up @@ -4898,6 +4944,17 @@ export interface components {
*/
readonly totalCount?: number;
};
PaginatedNetworkAccessView: {
/** @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. */
readonly links?: components["schemas"]["Link"][];
/** @description List of returned documents that MongoDB Cloud provides when completing this request. */
readonly results?: components["schemas"]["NetworkPermissionEntry"][];
/**
* Format: int32
* @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.
*/
readonly totalCount?: number;
};
PaginatedOrgGroupView: {
/** @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. */
readonly links?: components["schemas"]["Link"][];
Expand Down Expand Up @@ -6851,6 +6908,7 @@ export type IngestionSource = components["schemas"]["IngestionSource"];
export type InvoiceLineItem = components["schemas"]["InvoiceLineItem"];
export type Link = components["schemas"]["Link"];
export type MonthlyScheduleView = components["schemas"]["MonthlyScheduleView"];
export type NetworkPermissionEntry = components["schemas"]["NetworkPermissionEntry"];
export type OnDemandCpsSnapshotSource = components["schemas"]["OnDemandCpsSnapshotSource"];
export type OnlineArchiveSchedule = components["schemas"]["OnlineArchiveSchedule"];
export type OrgActiveUserResponse = components["schemas"]["OrgActiveUserResponse"];
Expand All @@ -6860,6 +6918,7 @@ export type OrgUserResponse = components["schemas"]["OrgUserResponse"];
export type OrgUserRolesResponse = components["schemas"]["OrgUserRolesResponse"];
export type PaginatedAtlasGroupView = components["schemas"]["PaginatedAtlasGroupView"];
export type PaginatedClusterDescription20240805 = components["schemas"]["PaginatedClusterDescription20240805"];
export type PaginatedNetworkAccessView = components["schemas"]["PaginatedNetworkAccessView"];
export type PaginatedOrgGroupView = components["schemas"]["PaginatedOrgGroupView"];
export type PeriodicCpsSnapshotSource = components["schemas"]["PeriodicCpsSnapshotSource"];
export type ReplicationSpec20240805 = components["schemas"]["ReplicationSpec20240805"];
Expand Down Expand Up @@ -7094,6 +7153,89 @@ export interface operations {
500: components["responses"]["internalServerError"];
};
};
listProjectIpAccessLists: {
parameters: {
query?: {
/** @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. */
envelope?: components["parameters"]["envelope"];
/** @description Flag that indicates whether the response returns the total number of items (**totalCount**) in the response. */
includeCount?: components["parameters"]["includeCount"];
/** @description Number of items that the response returns per page. */
itemsPerPage?: components["parameters"]["itemsPerPage"];
/** @description Number of the page that displays the current set of the total objects that the response returns. */
pageNum?: components["parameters"]["pageNum"];
/** @description Flag that indicates whether the response body should be in the prettyprint format. */
pretty?: components["parameters"]["pretty"];
};
header?: never;
path: {
/** @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.
*
* **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. */
groupId: components["parameters"]["groupId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/vnd.atlas.2023-01-01+json": components["schemas"]["PaginatedNetworkAccessView"];
};
};
401: components["responses"]["unauthorized"];
500: components["responses"]["internalServerError"];
};
};
createProjectIpAccessList: {
parameters: {
query?: {
/** @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. */
envelope?: components["parameters"]["envelope"];
/** @description Flag that indicates whether the response returns the total number of items (**totalCount**) in the response. */
includeCount?: components["parameters"]["includeCount"];
/** @description Number of items that the response returns per page. */
itemsPerPage?: components["parameters"]["itemsPerPage"];
/** @description Number of the page that displays the current set of the total objects that the response returns. */
pageNum?: components["parameters"]["pageNum"];
/** @description Flag that indicates whether the response body should be in the prettyprint format. */
pretty?: components["parameters"]["pretty"];
};
header?: never;
path: {
/** @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.
*
* **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. */
groupId: components["parameters"]["groupId"];
};
cookie?: never;
};
/** @description One or more access list entries to add to the specified project. */
requestBody: {
content: {
"application/vnd.atlas.2023-01-01+json": components["schemas"]["NetworkPermissionEntry"][];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/vnd.atlas.2023-01-01+json": components["schemas"]["PaginatedNetworkAccessView"];
};
};
400: components["responses"]["badRequest"];
401: components["responses"]["unauthorized"];
403: components["responses"]["forbidden"];
500: components["responses"]["internalServerError"];
};
};
listClusters: {
parameters: {
query?: {
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json
const packageJson = JSON.parse(packageMetadata);

export const config = {
atlasApiVersion: `2025-03-12`,
version: packageJson.version,
apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/",
clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
Expand Down
58 changes: 58 additions & 0 deletions src/tools/atlas/createAccessList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "./atlasTool.js";
import { ToolArgs } from "../tool.js";

const DEFAULT_COMMENT = "Added by Atlas MCP";

export class CreateAccessListTool extends AtlasToolBase {
protected name = "atlas-create-access-list";
protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters.";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
ipAddresses: z
.array(z.string().ip({ version: "v4" }))
.describe("IP addresses to allow access from")
.optional(),
cidrBlocks: z.array(z.string().cidr()).describe("CIDR blocks to allow access from").optional(),
comment: z.string().describe("Comment for the access list entries").default(DEFAULT_COMMENT).optional(),
};

protected async execute({
projectId,
ipAddresses,
cidrBlocks,
comment,
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
await this.ensureAuthenticated();

if (!ipAddresses?.length && !cidrBlocks?.length) {
throw new Error("Either ipAddresses or cidrBlocks must be provided.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we default to the device's public IP address? Alternatively, we can lookup the current IP address and respond with a prompt whether to add it - something like:

return {
  content: [{
    type: "text",
    text: "Either an IP address or a CIDR block must be provided. The current device's public IP address is: a.b.c.d - do you want to use that?"
  }]
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing out, I'll handle current ips on a follow up PR, I'll check if makes sense to have a resource for this one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added #36

}

const ipInputs = (ipAddresses || []).map((ipAddress) => ({
groupId: projectId,
ipAddress,
comment: comment || DEFAULT_COMMENT,
}));

const cidrInputs = (cidrBlocks || []).map((cidrBlock) => ({
groupId: projectId,
cidrBlock,
comment: comment || DEFAULT_COMMENT,
}));

const inputs = [...ipInputs, ...cidrInputs];

await this.apiClient.createProjectIpAccessList(projectId, inputs);

return {
content: [
{
type: "text",
text: `IP/CIDR ranges added to access list for project ${projectId}.`,
},
],
};
}
}
39 changes: 39 additions & 0 deletions src/tools/atlas/inspectAccessList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasToolBase } from "./atlasTool.js";
import { ToolArgs } from "../tool.js";

export class InspectAccessListTool extends AtlasToolBase {
protected name = "atlas-inspect-access-list";
protected description = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters.";
protected argsShape = {
projectId: z.string().describe("Atlas project ID"),
};

protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
await this.ensureAuthenticated();

const accessList = await this.apiClient.listProjectIpAccessLists(projectId);

if (!accessList.results?.length) {
throw new Error("No access list entries found.");
}

return {
content: [
{
type: "text",
text:
`IP ADDRESS | CIDR | COMMENT
------|------|------
` +
(accessList.results || [])
.map((entry) => {
return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`;
})
.join("\n"),
},
],
};
}
}
4 changes: 4 additions & 0 deletions src/tools/atlas/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ListClustersTool } from "./listClusters.js";
import { ListProjectsTool } from "./listProjects.js";
import { InspectClusterTool } from "./inspectCluster.js";
import { CreateFreeClusterTool } from "./createFreeCluster.js";
import { CreateAccessListTool } from "./createAccessList.js";
import { InspectAccessListTool } from "./inspectAccessList.js";

export function registerAtlasTools(server: McpServer, state: State, apiClient: ApiClient) {
const tools: ToolBase[] = [
Expand All @@ -15,6 +17,8 @@ export function registerAtlasTools(server: McpServer, state: State, apiClient: A
new ListProjectsTool(state, apiClient),
new InspectClusterTool(state, apiClient),
new CreateFreeClusterTool(state, apiClient),
new CreateAccessListTool(state, apiClient),
new InspectAccessListTool(state, apiClient),
];

for (const tool of tools) {
Expand Down