Skip to content

Commit b1fbd3f

Browse files
authored
feat: refactor state, store it securely (#48)
1 parent 3b5f0b4 commit b1fbd3f

File tree

9 files changed

+275
-55
lines changed

9 files changed

+275
-55
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
@@ -59,6 +59,7 @@
5959
"dependencies": {
6060
"@mongodb-js/devtools-connect": "^3.7.2",
6161
"@mongosh/service-provider-node-driver": "^3.6.0",
62+
"@napi-rs/keyring": "^1.1.6",
6263
"@types/express": "^5.0.1",
6364
"bson": "^6.10.3",
6465
"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.credentials.auth.status) {
1212
case "not_auth":
1313
return false;
1414
case "requested":
1515
try {
16-
if (!state.auth.code) {
16+
if (!state.credentials.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.credentials.auth.code.device_code);
20+
return !!state.credentials.auth.token;
2121
} catch {
2222
return false;
2323
}
2424
case "issued":
25-
if (!state.auth.token) {
25+
if (!state.credentials.auth.token) {
2626
return false;
2727
}
2828
return await apiClient.validateToken();

src/server.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { ApiClient } from "./common/atlas/apiClient.js";
3-
import { State, saveState, loadState } from "./state.js";
3+
import defaultState, { State } from "./state.js";
44
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
55
import { registerAtlasTools } from "./tools/atlas/tools.js";
66
import { registerMongoDBTools } from "./tools/mongodb/index.js";
@@ -9,26 +9,27 @@ import logger, { initializeLogger } from "./logger.js";
99
import { mongoLogId } from "mongodb-log-writer";
1010

1111
export class Server {
12-
state: State | undefined = undefined;
12+
state: State = defaultState;
1313
apiClient: ApiClient | undefined = undefined;
1414
initialized: boolean = false;
1515

1616
private async init() {
1717
if (this.initialized) {
1818
return;
1919
}
20-
this.state = await loadState();
20+
21+
await this.state.loadCredentials();
2122

2223
this.apiClient = new ApiClient({
23-
token: this.state?.auth.token,
24+
token: this.state.credentials.auth.token,
2425
saveToken: async (token) => {
2526
if (!this.state) {
2627
throw new Error("State is not initialized");
2728
}
28-
this.state.auth.code = undefined;
29-
this.state.auth.token = token;
30-
this.state.auth.status = "issued";
31-
await saveState(this.state);
29+
this.state.credentials.auth.code = undefined;
30+
this.state.credentials.auth.token = token;
31+
this.state.credentials.auth.status = "issued";
32+
await this.state.persistCredentials();
3233
},
3334
});
3435

@@ -43,8 +44,8 @@ export class Server {
4344

4445
server.server.registerCapabilities({ logging: {} });
4546

46-
registerAtlasTools(server, this.state!, this.apiClient!);
47-
registerMongoDBTools(server, this.state!);
47+
registerAtlasTools(server, this.state, this.apiClient!);
48+
registerMongoDBTools(server, this.state);
4849

4950
return server;
5051
}

src/state.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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";
46

5-
export interface State {
7+
interface Credentials {
68
auth: {
79
status: "not_auth" | "requested" | "issued";
810
code?: OauthDeviceCode;
@@ -11,23 +13,33 @@ export interface State {
1113
connectionString?: string;
1214
}
1315

14-
export async function saveState(state: State): Promise<void> {
15-
await fs.writeFile(config.stateFile, JSON.stringify(state), { encoding: "utf-8" });
16-
}
16+
export class State {
17+
private entry = new AsyncEntry("mongodb-mcp", "credentials");
18+
credentials: Credentials = {
19+
auth: {
20+
status: "not_auth",
21+
},
22+
};
23+
serviceProvider?: NodeDriverServiceProvider;
1724

18-
export async function loadState(): Promise<State> {
19-
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-
};
29-
}
25+
public async persistCredentials(): Promise<void> {
26+
await this.entry.setPassword(JSON.stringify(this.credentials));
27+
}
3028

31-
throw err;
29+
public async loadCredentials(): Promise<boolean> {
30+
try {
31+
const data = await this.entry.getPassword();
32+
if (data) {
33+
this.credentials = JSON.parse(data);
34+
}
35+
36+
return true;
37+
} catch (err: unknown) {
38+
logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err}`);
39+
return false;
40+
}
3241
}
3342
}
43+
44+
const defaultState = new State();
45+
export default defaultState;

0 commit comments

Comments
 (0)