Skip to content

Commit 616a32e

Browse files
committed
refactor: move apiClient into state
1 parent c8f8958 commit 616a32e

14 files changed

+107
-79
lines changed

src/common/atlas/apiClient.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import config from "../../config.js";
2-
import createClient, { FetchOptions, Middleware } from "openapi-fetch";
2+
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
33
import { AccessToken, ClientCredentials } from "simple-oauth2";
44

55
import { paths, operations } from "./openapi.js";
66

7+
const ATLAS_API_VERSION = "2025-03-12";
8+
79
export class ApiClientError extends Error {
810
response?: Response;
911

@@ -25,38 +27,32 @@ export class ApiClientError extends Error {
2527
}
2628

2729
export interface ApiClientOptions {
28-
credentials: {
30+
credentials?: {
2931
clientId: string;
3032
clientSecret: string;
3133
};
3234
baseUrl?: string;
35+
userAgent?: string;
3336
}
3437

3538
export class ApiClient {
36-
private client = createClient<paths>({
37-
baseUrl: config.apiBaseUrl,
38-
headers: {
39-
"User-Agent": config.userAgent,
40-
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
41-
},
42-
});
43-
private oauth2Client = new ClientCredentials({
44-
client: {
45-
id: this.options.credentials.clientId,
46-
secret: this.options.credentials.clientSecret,
47-
},
48-
auth: {
49-
tokenHost: this.options.baseUrl || config.apiBaseUrl,
50-
tokenPath: "/api/oauth/token",
51-
},
52-
});
39+
private options: {
40+
baseUrl: string;
41+
userAgent: string;
42+
credentials?: {
43+
clientId: string;
44+
clientSecret: string;
45+
};
46+
};
47+
private client: Client<paths>;
48+
private oauth2Client?: ClientCredentials;
5349
private accessToken?: AccessToken;
5450

5551
private getAccessToken = async () => {
56-
if (!this.accessToken || this.accessToken.expired()) {
52+
if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
5753
this.accessToken = await this.oauth2Client.getToken({});
5854
}
59-
return this.accessToken.token.access_token;
55+
return this.accessToken?.token.access_token as string | undefined;
6056
};
6157

6258
private authMiddleware = (apiClient: ApiClient): Middleware => ({
@@ -82,22 +78,51 @@ export class ApiClient {
8278
},
8379
});
8480

85-
constructor(private options: ApiClientOptions) {
86-
this.client.use(this.authMiddleware(this));
81+
constructor(options?: ApiClientOptions) {
82+
const defaultOptions = {
83+
baseUrl: "https://cloud.mongodb.com/",
84+
userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
85+
};
86+
87+
this.options = {
88+
...defaultOptions,
89+
...options,
90+
};
91+
92+
this.client = createClient<paths>({
93+
baseUrl: this.options.baseUrl,
94+
headers: {
95+
"User-Agent": this.options.userAgent,
96+
Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
97+
},
98+
});
99+
if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) {
100+
this.oauth2Client = new ClientCredentials({
101+
client: {
102+
id: this.options.credentials.clientId,
103+
secret: this.options.credentials.clientSecret,
104+
},
105+
auth: {
106+
tokenHost: this.options.baseUrl,
107+
tokenPath: "/api/oauth/token",
108+
},
109+
});
110+
this.client.use(this.authMiddleware(this));
111+
}
87112
this.client.use(this.errorMiddleware());
88113
}
89114

90115
async getIpInfo() {
91116
const accessToken = await this.getAccessToken();
92117

93118
const endpoint = "api/private/ipinfo";
94-
const url = new URL(endpoint, config.apiBaseUrl);
119+
const url = new URL(endpoint, this.options.baseUrl || config.apiBaseUrl);
95120
const response = await fetch(url, {
96121
method: "GET",
97122
headers: {
98123
Accept: "application/json",
99124
Authorization: `Bearer ${accessToken}`,
100-
"User-Agent": config.userAgent,
125+
"User-Agent": this.options.userAgent,
101126
},
102127
});
103128

src/config.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import os from "os";
33
import argv from "yargs-parser";
44

55
import packageJson from "../package.json" with { type: "json" };
6-
import fs from "fs";
76
import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";
87

98
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
109
// env variables.
1110
interface UserConfig {
12-
apiBaseUrl: string;
11+
apiBaseUrl?: string;
1312
apiClientId?: string;
1413
apiClientSecret?: string;
1514
logPath: string;
@@ -23,7 +22,6 @@ interface UserConfig {
2322
}
2423

2524
const defaults: UserConfig = {
26-
apiBaseUrl: "https://cloud.mongodb.com/",
2725
logPath: getLogPath(),
2826
connectOptions: {
2927
readConcern: "local",
@@ -41,17 +39,16 @@ const mergedUserConfig = {
4139

4240
const config = {
4341
...mergedUserConfig,
44-
atlasApiVersion: `2025-03-12`,
4542
version: packageJson.version,
46-
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
4743
};
4844

4945
export default config;
5046

5147
function getLogPath(): string {
52-
const localDataPath = (process.platform === "win32") ?
53-
path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
54-
: path.join(os.homedir(), ".mongodb");
48+
const localDataPath =
49+
process.platform === "win32"
50+
? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
51+
: path.join(os.homedir(), ".mongodb");
5552

5653
const logPath = path.join(localDataPath, "mongodb-mcp", ".app-logs");
5754

src/server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { ApiClient } from "./common/atlas/apiClient.js";
32
import defaultState, { State } from "./state.js";
43
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
54
import { registerAtlasTools } from "./tools/atlas/tools.js";
@@ -10,7 +9,6 @@ import { mongoLogId } from "mongodb-log-writer";
109

1110
export class Server {
1211
state: State = defaultState;
13-
apiClient?: ApiClient;
1412
private server?: McpServer;
1513

1614
async connect(transport: Transport) {
@@ -21,12 +19,12 @@ export class Server {
2119

2220
this.server.server.registerCapabilities({ logging: {} });
2321

24-
registerAtlasTools(this.server, this.state, this.apiClient);
22+
registerAtlasTools(this.server, this.state);
2523
registerMongoDBTools(this.server, this.state);
2624

2725
await this.server.connect(transport);
2826
await initializeLogger(this.server);
29-
27+
3028
logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`);
3129
}
3230

src/state.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
2+
import { ApiClient } from "./common/atlas/apiClient.js";
3+
import config from "./config.js";
24

35
export class State {
46
serviceProvider?: NodeDriverServiceProvider;
7+
apiClient?: ApiClient;
8+
9+
ensureApiClient(): asserts this is { apiClient: ApiClient } {
10+
if (!this.apiClient) {
11+
if (!config.apiClientId || !config.apiClientSecret) {
12+
throw new Error(
13+
"Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables."
14+
);
15+
}
16+
17+
this.apiClient = new ApiClient({
18+
baseUrl: config.apiBaseUrl,
19+
credentials: {
20+
clientId: config.apiClientId,
21+
clientSecret: config.apiClientSecret,
22+
},
23+
});
24+
}
25+
}
526
}
627

728
const defaultState = new State();

src/tools/atlas/atlasTool.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
11
import { ToolBase } from "../tool.js";
2-
import { ApiClient } from "../../common/atlas/apiClient.js";
32
import { State } from "../../state.js";
43

54
export abstract class AtlasToolBase extends ToolBase {
6-
constructor(
7-
state: State,
8-
protected apiClient?: ApiClient
9-
) {
5+
constructor(state: State) {
106
super(state);
117
}
12-
13-
protected ensureAuthenticated(): asserts this is { apiClient: ApiClient } {
14-
if (!this.apiClient) {
15-
throw new Error(
16-
"Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables."
17-
);
18-
}
19-
}
208
}

src/tools/atlas/createAccessList.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class CreateAccessListTool extends AtlasToolBase {
2626
comment,
2727
currentIpAddress,
2828
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
29-
this.ensureAuthenticated();
29+
this.state.ensureApiClient();
3030

3131
if (!ipAddresses?.length && !cidrBlocks?.length && !currentIpAddress) {
3232
throw new Error("One of ipAddresses, cidrBlocks, currentIpAddress must be provided.");
@@ -39,7 +39,7 @@ export class CreateAccessListTool extends AtlasToolBase {
3939
}));
4040

4141
if (currentIpAddress) {
42-
const currentIp = await this.apiClient.getIpInfo();
42+
const currentIp = await this.state.apiClient.getIpInfo();
4343
const input = {
4444
groupId: projectId,
4545
ipAddress: currentIp.currentIpv4Address,
@@ -56,7 +56,7 @@ export class CreateAccessListTool extends AtlasToolBase {
5656

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

59-
await this.apiClient.createProjectIpAccessList({
59+
await this.state.apiClient.createProjectIpAccessList({
6060
params: {
6161
path: {
6262
groupId: projectId,

src/tools/atlas/createDBUser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class CreateDBUserTool extends AtlasToolBase {
3333
roles,
3434
clusters,
3535
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
36-
this.ensureAuthenticated();
36+
this.state.ensureApiClient();
3737

3838
const input = {
3939
groupId: projectId,
@@ -53,7 +53,7 @@ export class CreateDBUserTool extends AtlasToolBase {
5353
: undefined,
5454
} as CloudDatabaseUser;
5555

56-
await this.apiClient.createDatabaseUser({
56+
await this.state.apiClient.createDatabaseUser({
5757
params: {
5858
path: {
5959
groupId: projectId,

src/tools/atlas/createFreeCluster.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class CreateFreeClusterTool extends AtlasToolBase {
1414
};
1515

1616
protected async execute({ projectId, name, region }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
17-
this.ensureAuthenticated();
17+
this.state.ensureApiClient();
1818

1919
const input = {
2020
groupId: projectId,
@@ -38,7 +38,7 @@ export class CreateFreeClusterTool extends AtlasToolBase {
3838
terminationProtectionEnabled: false,
3939
} as unknown as ClusterDescription20240805;
4040

41-
await this.apiClient.createCluster({
41+
await this.state.apiClient.createCluster({
4242
params: {
4343
path: {
4444
groupId: projectId,

src/tools/atlas/inspectAccessList.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export class InspectAccessListTool extends AtlasToolBase {
1111
};
1212

1313
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
14-
this.ensureAuthenticated();
14+
this.state.ensureApiClient();
1515

16-
const accessList = await this.apiClient.listProjectIpAccessLists({
16+
const accessList = await this.state.apiClient.listProjectIpAccessLists({
1717
params: {
1818
path: {
1919
groupId: projectId,

src/tools/atlas/inspectCluster.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export class InspectClusterTool extends AtlasToolBase {
1313
};
1414

1515
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
16-
this.ensureAuthenticated();
16+
this.state.ensureApiClient();
1717

18-
const cluster = await this.apiClient.getCluster({
18+
const cluster = await this.state.apiClient.getCluster({
1919
params: {
2020
path: {
2121
groupId: projectId,

0 commit comments

Comments
 (0)