Skip to content

Commit cc01ac5

Browse files
committed
Merge branch 'main' into ni/logger-changes
2 parents c0b8d7a + a35d18d commit cc01ac5

File tree

20 files changed

+508
-122
lines changed

20 files changed

+508
-122
lines changed

.github/workflows/accuracy-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
path: .accuracy/test-summary.html
4646
- name: Comment summary on PR
4747
if: github.event_name == 'pull_request' && github.event.label.name == 'accuracy-tests'
48-
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2
48+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
4949
with:
5050
# Hides the previous comment and add a comment at the end
5151
hide_and_recreate: true

.github/workflows/dependabot_pr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: Dependabot PR
33
on:
4-
pull_request:
4+
pull_request_target:
55
types: [opened]
66
branches:
77
- main

.github/workflows/docker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Docker Buildx
1919
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
2020
- name: Login to Docker Hub
21-
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
21+
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
2222
with:
2323
username: "${{ secrets.DOCKERHUB_USERNAME }}"
2424
password: "${{ secrets.DOCKERHUB_PASSWORD }}"

.github/workflows/jira-issue.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
config: ${{ vars.PERMISSIONS_CONFIG }}
2121

2222
- name: Create JIRA ticket
23-
uses: mongodb/apix-action/create-jira@v10
23+
uses: mongodb/apix-action/create-jira@v12
2424
id: create
2525
continue-on-error: true
2626
with:

eslint.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export default defineConfig([
4848
rules: {
4949
"@typescript-eslint/switch-exhaustiveness-check": "error",
5050
"@typescript-eslint/no-non-null-assertion": "error",
51+
eqeqeq: "error",
52+
"no-self-compare": "error",
53+
"no-unassigned-vars": "error",
54+
"@typescript-eslint/await-thenable": "error",
5155
},
5256
},
5357
globalIgnores([

src/common/atlas/apiClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class ApiClient {
5858
private isAccessTokenValid(): boolean {
5959
return !!(
6060
this.accessToken &&
61-
this.accessToken.expires_at != undefined &&
61+
this.accessToken.expires_at !== undefined &&
6262
this.accessToken.expires_at > Date.now()
6363
);
6464
}
@@ -89,6 +89,7 @@ export class ApiClient {
8989
return request;
9090
} catch {
9191
// ignore not availble tokens, API will return 401
92+
return undefined;
9293
}
9394
},
9495
};
@@ -187,6 +188,8 @@ export class ApiClient {
187188
}
188189
return this.accessToken;
189190
}
191+
192+
return undefined;
190193
}
191194

192195
public async validateAccessToken(): Promise<void> {

src/common/atlas/cluster.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster {
5151
});
5252

5353
const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN";
54-
const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
54+
const clusterInstanceType = instanceSize === "M0" ? "FREE" : "DEDICATED";
5555

5656
return {
5757
name: cluster.name,
5858
instanceType: clusterInstanceType,
59-
instanceSize: clusterInstanceType == "DEDICATED" ? instanceSize : undefined,
59+
instanceSize: clusterInstanceType === "DEDICATED" ? instanceSize : undefined,
6060
state: cluster.stateName,
6161
mongoDBVersion: cluster.mongoDBVersion,
6262
connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard,

src/common/connectionManager.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { ConnectOptions } from "./config.js";
2+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
3+
import EventEmitter from "events";
4+
import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js";
5+
import { packageInfo } from "./packageInfo.js";
6+
import ConnectionString from "mongodb-connection-string-url";
7+
import { MongoClientOptions } from "mongodb";
8+
import { ErrorCodes, MongoDBError } from "./errors.js";
9+
10+
export interface AtlasClusterConnectionInfo {
11+
username: string;
12+
projectId: string;
13+
clusterName: string;
14+
expiryDate: Date;
15+
}
16+
17+
export interface ConnectionSettings extends ConnectOptions {
18+
connectionString: string;
19+
atlas?: AtlasClusterConnectionInfo;
20+
}
21+
22+
type ConnectionTag = "connected" | "connecting" | "disconnected" | "errored";
23+
type OIDCConnectionAuthType = "oidc-auth-flow" | "oidc-device-flow";
24+
export type ConnectionStringAuthType = "scram" | "ldap" | "kerberos" | OIDCConnectionAuthType | "x.509";
25+
26+
export interface ConnectionState {
27+
tag: ConnectionTag;
28+
connectionStringAuthType?: ConnectionStringAuthType;
29+
connectedAtlasCluster?: AtlasClusterConnectionInfo;
30+
}
31+
32+
export interface ConnectionStateConnected extends ConnectionState {
33+
tag: "connected";
34+
serviceProvider: NodeDriverServiceProvider;
35+
}
36+
37+
export interface ConnectionStateConnecting extends ConnectionState {
38+
tag: "connecting";
39+
serviceProvider: NodeDriverServiceProvider;
40+
oidcConnectionType: OIDCConnectionAuthType;
41+
oidcLoginUrl?: string;
42+
oidcUserCode?: string;
43+
}
44+
45+
export interface ConnectionStateDisconnected extends ConnectionState {
46+
tag: "disconnected";
47+
}
48+
49+
export interface ConnectionStateErrored extends ConnectionState {
50+
tag: "errored";
51+
errorReason: string;
52+
}
53+
54+
export type AnyConnectionState =
55+
| ConnectionStateConnected
56+
| ConnectionStateConnecting
57+
| ConnectionStateDisconnected
58+
| ConnectionStateErrored;
59+
60+
export interface ConnectionManagerEvents {
61+
"connection-requested": [AnyConnectionState];
62+
"connection-succeeded": [ConnectionStateConnected];
63+
"connection-timed-out": [ConnectionStateErrored];
64+
"connection-closed": [ConnectionStateDisconnected];
65+
"connection-errored": [ConnectionStateErrored];
66+
}
67+
68+
export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
69+
private state: AnyConnectionState;
70+
71+
constructor() {
72+
super();
73+
this.state = { tag: "disconnected" };
74+
}
75+
76+
async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
77+
this.emit("connection-requested", this.state);
78+
79+
if (this.state.tag === "connected" || this.state.tag === "connecting") {
80+
await this.disconnect();
81+
}
82+
83+
let serviceProvider: NodeDriverServiceProvider;
84+
try {
85+
settings = { ...settings };
86+
settings.connectionString = setAppNameParamIfMissing({
87+
connectionString: settings.connectionString,
88+
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
89+
});
90+
91+
serviceProvider = await NodeDriverServiceProvider.connect(settings.connectionString, {
92+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
93+
productName: "MongoDB MCP",
94+
readConcern: {
95+
level: settings.readConcern,
96+
},
97+
readPreference: settings.readPreference,
98+
writeConcern: {
99+
w: settings.writeConcern,
100+
},
101+
timeoutMS: settings.timeoutMS,
102+
proxy: { useEnvironmentVariableProxies: true },
103+
applyProxyToOIDC: true,
104+
});
105+
} catch (error: unknown) {
106+
const errorReason = error instanceof Error ? error.message : `${error as string}`;
107+
this.changeState("connection-errored", {
108+
tag: "errored",
109+
errorReason,
110+
connectedAtlasCluster: settings.atlas,
111+
});
112+
throw new MongoDBError(ErrorCodes.MisconfiguredConnectionString, errorReason);
113+
}
114+
115+
try {
116+
await serviceProvider?.runCommand?.("admin", { hello: 1 });
117+
118+
return this.changeState("connection-succeeded", {
119+
tag: "connected",
120+
connectedAtlasCluster: settings.atlas,
121+
serviceProvider,
122+
connectionStringAuthType: ConnectionManager.inferConnectionTypeFromSettings(settings),
123+
});
124+
} catch (error: unknown) {
125+
const errorReason = error instanceof Error ? error.message : `${error as string}`;
126+
this.changeState("connection-errored", {
127+
tag: "errored",
128+
errorReason,
129+
connectedAtlasCluster: settings.atlas,
130+
});
131+
throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, errorReason);
132+
}
133+
}
134+
135+
async disconnect(): Promise<ConnectionStateDisconnected | ConnectionStateErrored> {
136+
if (this.state.tag === "disconnected" || this.state.tag === "errored") {
137+
return this.state;
138+
}
139+
140+
if (this.state.tag === "connected" || this.state.tag === "connecting") {
141+
try {
142+
await this.state.serviceProvider?.close(true);
143+
} finally {
144+
this.changeState("connection-closed", {
145+
tag: "disconnected",
146+
});
147+
}
148+
}
149+
150+
return { tag: "disconnected" };
151+
}
152+
153+
get currentConnectionState(): AnyConnectionState {
154+
return this.state;
155+
}
156+
157+
changeState<Event extends keyof ConnectionManagerEvents, State extends ConnectionManagerEvents[Event][0]>(
158+
event: Event,
159+
newState: State
160+
): State {
161+
this.state = newState;
162+
// TypeScript doesn't seem to be happy with the spread operator and generics
163+
// eslint-disable-next-line
164+
this.emit(event, ...([newState] as any));
165+
return newState;
166+
}
167+
168+
static inferConnectionTypeFromSettings(settings: ConnectionSettings): ConnectionStringAuthType {
169+
const connString = new ConnectionString(settings.connectionString);
170+
const searchParams = connString.typedSearchParams<MongoClientOptions>();
171+
172+
switch (searchParams.get("authMechanism")) {
173+
case "MONGODB-OIDC": {
174+
return "oidc-auth-flow"; // TODO: depending on if we don't have a --browser later it can be oidc-device-flow
175+
}
176+
case "MONGODB-X509":
177+
return "x.509";
178+
case "GSSAPI":
179+
return "kerberos";
180+
case "PLAIN":
181+
if (searchParams.get("authSource") === "$external") {
182+
return "ldap";
183+
}
184+
return "scram";
185+
// default should catch also null, but eslint complains
186+
// about it.
187+
case null:
188+
default:
189+
return "scram";
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)