Skip to content

Commit 254d75a

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/mongosh/arg-parser-3.16.0
2 parents 8e7c58f + 345efa4 commit 254d75a

File tree

17 files changed

+550
-131
lines changed

17 files changed

+550
-131
lines changed

src/common/atlas/accessListUtils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export async function makeCurrentIpAccessListEntry(
2222
* If the IP is already present, this is a no-op.
2323
* @param apiClient The Atlas API client instance
2424
* @param projectId The Atlas project ID
25+
* @returns Promise<boolean> - true if a new IP access list entry was created, false if it already existed
2526
*/
26-
export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectId: string): Promise<void> {
27+
export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectId: string): Promise<boolean> {
2728
const entry = await makeCurrentIpAccessListEntry(apiClient, projectId, DEFAULT_ACCESS_LIST_COMMENT);
2829
try {
2930
await apiClient.createProjectIpAccessList({
@@ -35,6 +36,7 @@ export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectI
3536
context: "accessListUtils",
3637
message: `IP access list created: ${JSON.stringify(entry)}`,
3738
});
39+
return true;
3840
} catch (err) {
3941
if (err instanceof ApiClientError && err.response?.status === 409) {
4042
// 409 Conflict: entry already exists, log info
@@ -43,12 +45,13 @@ export async function ensureCurrentIpInAccessList(apiClient: ApiClient, projectI
4345
context: "accessListUtils",
4446
message: `IP address ${entry.ipAddress} is already present in the access list for project ${projectId}.`,
4547
});
46-
return;
48+
return false;
4749
}
4850
apiClient.logger.warning({
4951
id: LogId.atlasIpAccessListAddFailure,
5052
context: "accessListUtils",
5153
message: `Error adding IP access list: ${err instanceof Error ? err.message : String(err)}`,
5254
});
5355
}
56+
return false;
5457
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { ErrorCodes, type MongoDBError } from "./errors.js";
3+
import type { AnyConnectionState } from "./connectionManager.js";
4+
import type { ToolBase } from "../tools/tool.js";
5+
6+
export type ConnectionErrorHandler = (
7+
error: MongoDBError<ErrorCodes.NotConnectedToMongoDB | ErrorCodes.MisconfiguredConnectionString>,
8+
additionalContext: ConnectionErrorHandlerContext
9+
) => ConnectionErrorUnhandled | ConnectionErrorHandled;
10+
11+
export type ConnectionErrorHandlerContext = { availableTools: ToolBase[]; connectionState: AnyConnectionState };
12+
export type ConnectionErrorUnhandled = { errorHandled: false };
13+
export type ConnectionErrorHandled = { errorHandled: true; result: CallToolResult };
14+
15+
export const connectionErrorHandler: ConnectionErrorHandler = (error, { availableTools, connectionState }) => {
16+
const connectTools = availableTools
17+
.filter((t) => t.operationType === "connect")
18+
.sort((a, b) => a.category.localeCompare(b.category)); // Sort Atlas tools before MongoDB tools
19+
20+
// Find the first Atlas connect tool if available and suggest to the LLM to use it.
21+
// Note: if we ever have multiple Atlas connect tools, we may want to refine this logic to select the most appropriate one.
22+
const atlasConnectTool = connectTools?.find((t) => t.category === "atlas");
23+
const llmConnectHint = atlasConnectTool
24+
? `Note to LLM: prefer using the "${atlasConnectTool.name}" tool to connect to an Atlas cluster over using a connection string. Make sure to ask the user to specify a cluster name they want to connect to or ask them if they want to use the "list-clusters" tool to list all their clusters. Do not invent cluster names or connection strings unless the user has explicitly specified them. If they've previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same cluster/connection.`
25+
: "Note to LLM: do not invent connection strings and explicitly ask the user to provide one. If they have previously connected to MongoDB using MCP, you can ask them if they want to reconnect using the same connection string.";
26+
27+
const connectToolsNames = connectTools?.map((t) => `"${t.name}"`).join(", ");
28+
const additionalPromptForConnectivity: { type: "text"; text: string }[] = [];
29+
30+
if (connectionState.tag === "connecting" && connectionState.oidcConnectionType) {
31+
additionalPromptForConnectivity.push({
32+
type: "text",
33+
text: `The user needs to finish their OIDC connection by opening '${connectionState.oidcLoginUrl}' in the browser and use the following user code: '${connectionState.oidcUserCode}'`,
34+
});
35+
} else {
36+
additionalPromptForConnectivity.push({
37+
type: "text",
38+
text: connectToolsNames
39+
? `Please use one of the following tools: ${connectToolsNames} to connect to a MongoDB instance or update the MCP server configuration to include a connection string. ${llmConnectHint}`
40+
: "There are no tools available to connect. Please update the configuration to include a connection string and restart the server.",
41+
});
42+
}
43+
44+
switch (error.code) {
45+
case ErrorCodes.NotConnectedToMongoDB:
46+
return {
47+
errorHandled: true,
48+
result: {
49+
content: [
50+
{
51+
type: "text",
52+
text: "You need to connect to a MongoDB instance before you can access its data.",
53+
},
54+
...additionalPromptForConnectivity,
55+
],
56+
isError: true,
57+
},
58+
};
59+
case ErrorCodes.MisconfiguredConnectionString:
60+
return {
61+
errorHandled: true,
62+
result: {
63+
content: [
64+
{
65+
type: "text",
66+
text: "The configured connection string is not valid. Please check the connection string and confirm it points to a valid MongoDB instance.",
67+
},
68+
{
69+
type: "text",
70+
text: connectTools
71+
? `Alternatively, you can use one of the following tools: ${connectToolsNames} to connect to a MongoDB instance. ${llmConnectHint}`
72+
: "Please update the configuration to use a valid connection string and restart the server.",
73+
},
74+
],
75+
isError: true,
76+
},
77+
};
78+
79+
default:
80+
return { errorHandled: false };
81+
}
82+
};

src/common/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ export enum ErrorCodes {
44
ForbiddenCollscan = 1_000_002,
55
}
66

7-
export class MongoDBError extends Error {
7+
export class MongoDBError<ErrorCode extends ErrorCodes = ErrorCodes> extends Error {
88
constructor(
9-
public code: ErrorCodes,
9+
public code: ErrorCode,
1010
message: string
1111
) {
1212
super(message);

src/common/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const LogId = {
4141

4242
mongodbConnectFailure: mongoLogId(1_004_001),
4343
mongodbDisconnectFailure: mongoLogId(1_004_002),
44+
mongodbConnectTry: mongoLogId(1_004_003),
4445

4546
toolUpdateFailure: mongoLogId(1_005_001),
4647
resourceUpdateFailure: mongoLogId(1_005_002),

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ async function main(): Promise<void> {
4949
assertHelpMode();
5050
assertVersionMode();
5151

52-
const transportRunner = config.transport === "stdio" ? new StdioRunner(config) : new StreamableHttpRunner(config);
52+
const transportRunner =
53+
config.transport === "stdio"
54+
? new StdioRunner({
55+
userConfig: config,
56+
})
57+
: new StreamableHttpRunner({
58+
userConfig: config,
59+
});
5360
const shutdown = (): void => {
5461
transportRunner.logger.info({
5562
id: LogId.serverCloseRequested,

src/lib.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ export {
1111
type ConnectionStateErrored,
1212
type ConnectionManagerFactoryFn,
1313
} from "./common/connectionManager.js";
14+
export type {
15+
ConnectionErrorHandler,
16+
ConnectionErrorHandled,
17+
ConnectionErrorUnhandled,
18+
ConnectionErrorHandlerContext,
19+
} from "./common/connectionErrorHandler.js";
20+
export { ErrorCodes } from "./common/errors.js";
1421
export { Telemetry } from "./telemetry/telemetry.js";

src/server.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import {
2020
import assert from "assert";
2121
import type { ToolBase } from "./tools/tool.js";
2222
import { validateConnectionString } from "./helpers/connectionOptions.js";
23+
import { packageInfo } from "./common/packageInfo.js";
24+
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
2325

2426
export interface ServerOptions {
2527
session: Session;
2628
userConfig: UserConfig;
2729
mcpServer: McpServer;
2830
telemetry: Telemetry;
31+
connectionErrorHandler: ConnectionErrorHandler;
2932
}
3033

3134
export class Server {
@@ -34,6 +37,7 @@ export class Server {
3437
private readonly telemetry: Telemetry;
3538
public readonly userConfig: UserConfig;
3639
public readonly tools: ToolBase[] = [];
40+
public readonly connectionErrorHandler: ConnectionErrorHandler;
3741

3842
private _mcpLogLevel: LogLevel = "debug";
3943

@@ -44,12 +48,13 @@ export class Server {
4448
private readonly startTime: number;
4549
private readonly subscriptions = new Set<string>();
4650

47-
constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
51+
constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler }: ServerOptions) {
4852
this.startTime = Date.now();
4953
this.session = session;
5054
this.telemetry = telemetry;
5155
this.mcpServer = mcpServer;
5256
this.userConfig = userConfig;
57+
this.connectionErrorHandler = connectionErrorHandler;
5358
}
5459

5560
async connect(transport: Transport): Promise<void> {
@@ -119,11 +124,10 @@ export class Server {
119124
this.session.setMcpClient(this.mcpServer.server.getClientVersion());
120125
// Placed here to start the connection to the config connection string as soon as the server is initialized.
121126
void this.connectToConfigConnectionString();
122-
123127
this.session.logger.info({
124128
id: LogId.serverInitialized,
125129
context: "server",
126-
message: `Server started with transport ${transport.constructor.name} and agent runner ${this.session.mcpClient?.name}`,
130+
message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
127131
});
128132

129133
this.emitServerEvent("start", Date.now() - this.startTime);
@@ -244,15 +248,21 @@ export class Server {
244248
private async connectToConfigConnectionString(): Promise<void> {
245249
if (this.userConfig.connectionString) {
246250
try {
251+
this.session.logger.info({
252+
id: LogId.mongodbConnectTry,
253+
context: "server",
254+
message: `Detected a MongoDB connection string in the configuration, trying to connect...`,
255+
});
247256
await this.session.connectToMongoDB({
248257
connectionString: this.userConfig.connectionString,
249258
});
250259
} catch (error) {
251-
console.error(
252-
"Failed to connect to MongoDB instance using the connection string from the config: ",
253-
error
254-
);
255-
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
260+
// We don't throw an error here because we want to allow the server to start even if the connection string is invalid.
261+
this.session.logger.error({
262+
id: LogId.mongodbConnectFailure,
263+
context: "server",
264+
message: `Failed to connect to MongoDB instance using the connection string from the config: ${error instanceof Error ? error.message : String(error)}`,
265+
});
256266
}
257267
}
258268
}

src/tools/atlas/connect/connectCluster.ts

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import type { AtlasClusterConnectionInfo } from "../../../common/connectionManag
1010
import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js";
1111

1212
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
13+
const addedIpAccessListMessage =
14+
"Note: Your current IP address has been added to the Atlas project's IP access list to enable secure connection.";
15+
16+
const createdUserMessage =
17+
"Note: A temporary user has been created to enable secure connection to the cluster. For more information, see https://dochub.mongodb.org/core/mongodb-mcp-server-tools-considerations\n\nNote to LLM Agent: it is important to include the following link in your response to the user in case they want to get more information about the temporary user created: https://dochub.mongodb.org/core/mongodb-mcp-server-tools-considerations";
1318

1419
function sleep(ms: number): Promise<void> {
1520
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -190,19 +195,35 @@ export class ConnectClusterTool extends AtlasToolBase {
190195
}
191196

192197
protected async execute({ projectId, clusterName }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
193-
await ensureCurrentIpInAccessList(this.session.apiClient, projectId);
198+
const ipAccessListUpdated = await ensureCurrentIpInAccessList(this.session.apiClient, projectId);
199+
let createdUser = false;
200+
194201
for (let i = 0; i < 60; i++) {
195202
const state = this.queryConnection(projectId, clusterName);
196203
switch (state) {
197204
case "connected": {
198-
return {
199-
content: [
200-
{
201-
type: "text",
202-
text: `Connected to cluster "${clusterName}".`,
203-
},
204-
],
205-
};
205+
const content: CallToolResult["content"] = [
206+
{
207+
type: "text",
208+
text: `Connected to cluster "${clusterName}".`,
209+
},
210+
];
211+
212+
if (ipAccessListUpdated) {
213+
content.push({
214+
type: "text",
215+
text: addedIpAccessListMessage,
216+
});
217+
}
218+
219+
if (createdUser) {
220+
content.push({
221+
type: "text",
222+
text: createdUserMessage,
223+
});
224+
}
225+
226+
return { content };
206227
}
207228
case "connecting":
208229
case "unknown": {
@@ -214,6 +235,7 @@ export class ConnectClusterTool extends AtlasToolBase {
214235
await this.session.disconnect();
215236
const { connectionString, atlas } = await this.prepareClusterConnection(projectId, clusterName);
216237

238+
createdUser = true;
217239
// try to connect for about 5 minutes asynchronously
218240
void this.connectToCluster(connectionString, atlas).catch((err: unknown) => {
219241
const error = err instanceof Error ? err : new Error(String(err));
@@ -230,21 +252,31 @@ export class ConnectClusterTool extends AtlasToolBase {
230252
await sleep(500);
231253
}
232254

233-
return {
234-
content: [
235-
{
236-
type: "text" as const,
237-
text: `Attempting to connect to cluster "${clusterName}"...`,
238-
},
239-
{
240-
type: "text" as const,
241-
text: `Warning: Provisioning a user and connecting to the cluster may take more time, please check again in a few seconds.`,
242-
},
243-
{
244-
type: "text" as const,
245-
text: `Warning: Make sure your IP address was enabled in the allow list setting of the Atlas cluster.`,
246-
},
247-
],
248-
};
255+
const content: CallToolResult["content"] = [
256+
{
257+
type: "text" as const,
258+
text: `Attempting to connect to cluster "${clusterName}"...`,
259+
},
260+
{
261+
type: "text" as const,
262+
text: `Warning: Provisioning a user and connecting to the cluster may take more time, please check again in a few seconds.`,
263+
},
264+
];
265+
266+
if (ipAccessListUpdated) {
267+
content.push({
268+
type: "text" as const,
269+
text: addedIpAccessListMessage,
270+
});
271+
}
272+
273+
if (createdUser) {
274+
content.push({
275+
type: "text" as const,
276+
text: createdUserMessage,
277+
});
278+
}
279+
280+
return { content };
249281
}
250282
}

0 commit comments

Comments
 (0)