Skip to content

Commit eecbdcf

Browse files
committed
feat: refactor state, store it securely
1 parent a031897 commit eecbdcf

File tree

9 files changed

+271
-44
lines changed

9 files changed

+271
-44
lines changed

package-lock.json

Lines changed: 216 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"dependencies": {
5454
"@mongodb-js/devtools-connect": "^3.7.2",
5555
"@mongosh/service-provider-node-driver": "^3.6.0",
56+
"@napi-rs/keyring": "^1.1.6",
5657
"@types/express": "^5.0.1",
5758
"bson": "^6.10.3",
5859
"mongodb": "^6.15.0",

src/common/atlas/auth.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ export async function ensureAuthenticated(state: State, apiClient: ApiClient): P
88
}
99

1010
export async function isAuthenticated(state: State, apiClient: ApiClient): Promise<boolean> {
11-
switch (state.auth.status) {
11+
switch (state.persistent.auth.status) {
1212
case "not_auth":
1313
return false;
1414
case "requested":
1515
try {
16-
if (!state.auth.code) {
16+
if (!state.persistent.auth.code) {
1717
return false;
1818
}
19-
await apiClient.retrieveToken(state.auth.code.device_code);
20-
return !!state.auth.token;
19+
await apiClient.retrieveToken(state.persistent.auth.code.device_code);
20+
return !!state.persistent.auth.token;
2121
} catch {
2222
return false;
2323
}
2424
case "issued":
25-
if (!state.auth.token) {
25+
if (!state.persistent.auth.token) {
2626
return false;
2727
}
2828
return await apiClient.validateToken();

src/server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export class Server {
2020
this.state = await loadState();
2121

2222
this.apiClient = new ApiClient({
23-
token: this.state?.auth.token,
23+
token: this.state.persistent.auth.token,
2424
saveToken: async (token) => {
2525
if (!this.state) {
2626
throw new Error("State is not initialized");
2727
}
28-
this.state.auth.code = undefined;
29-
this.state.auth.token = token;
30-
this.state.auth.status = "issued";
28+
this.state.persistent.auth.code = undefined;
29+
this.state.persistent.auth.token = token;
30+
this.state.persistent.auth.status = "issued";
3131
await saveState(this.state);
3232
},
3333
});

src/state.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,51 @@
1-
import fs from "fs/promises";
2-
import config from "./config.js";
31
import { OauthDeviceCode, OAuthToken } from "./common/atlas/apiClient.js";
2+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
3+
import { AsyncEntry } from "@napi-rs/keyring";
4+
import logger from "./logger.js";
5+
import { mongoLogId } from "mongodb-log-writer";
6+
7+
const entry = new AsyncEntry("mongodb-mcp", "credentials");
48

59
export interface State {
6-
auth: {
7-
status: "not_auth" | "requested" | "issued";
8-
code?: OauthDeviceCode;
9-
token?: OAuthToken;
10+
persistent: {
11+
auth: {
12+
status: "not_auth" | "requested" | "issued";
13+
code?: OauthDeviceCode;
14+
token?: OAuthToken;
15+
};
16+
connectionString?: string;
17+
};
18+
session: {
19+
serviceProvider?: NodeDriverServiceProvider;
1020
};
11-
connectionString?: string;
1221
}
1322

23+
const defaultState: State = {
24+
persistent: {
25+
auth: {
26+
status: "not_auth",
27+
},
28+
},
29+
session: {},
30+
};
31+
1432
export async function saveState(state: State): Promise<void> {
15-
await fs.writeFile(config.stateFile, JSON.stringify(state), { encoding: "utf-8" });
33+
await entry.setPassword(JSON.stringify(state.persistent));
1634
}
1735

1836
export async function loadState(): Promise<State> {
1937
try {
20-
const data = await fs.readFile(config.stateFile, "utf-8");
21-
return JSON.parse(data) as State;
22-
} catch (err: unknown) {
23-
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
24-
return {
25-
auth: {
26-
status: "not_auth",
27-
},
28-
};
38+
const data = await entry.getPassword();
39+
if (!data) {
40+
return defaultState;
2941
}
3042

31-
throw err;
43+
return {
44+
persistent: JSON.parse(data),
45+
session: {},
46+
};
47+
} catch (err: unknown) {
48+
logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err}`);
49+
return defaultState;
3250
}
3351
}

src/tools/atlas/auth.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export class AuthTool extends AtlasToolBase {
2525
try {
2626
const code = await this.apiClient.authenticate();
2727

28-
this.state.auth.status = "requested";
29-
this.state.auth.code = code;
30-
this.state.auth.token = undefined;
28+
this.state.persistent.auth.status = "requested";
29+
this.state.persistent.auth.code = code;
30+
this.state.persistent.auth.token = undefined;
3131

3232
await saveState(this.state);
3333

src/tools/mongodb/connect.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class ConnectTool extends MongoDBToolBase {
2121
protected async execute({
2222
connectionStringOrClusterName,
2323
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
24-
connectionStringOrClusterName ??= this.state.connectionString;
24+
connectionStringOrClusterName ??= this.state.persistent.connectionString;
2525
if (!connectionStringOrClusterName) {
2626
return {
2727
content: [
@@ -71,8 +71,8 @@ export class ConnectTool extends MongoDBToolBase {
7171
productName: "MongoDB MCP",
7272
});
7373

74-
this.mongodbState.serviceProvider = provider;
75-
this.state.connectionString = connectionString;
74+
this.state.session.serviceProvider = provider;
75+
this.state.persistent.connectionString = connectionString;
7676
await saveState(this.state);
7777
}
7878
}

0 commit comments

Comments
 (0)