Skip to content

Commit 5008a00

Browse files
committed
Merge branch 'main' into ni/telemetry-hosting-mode
* main: feat: add more details about atlas connect flow - MCP-124 (#500) chore: extend library interfaces to allow injecting a custom connection error handler MCP-132 (#502) fix: start mcp even if connection fails - [MCP-140] (#503) fix: allow connect tool on readOnly mode (#499) chore: warn about the usage of deprecated cli arguments MCP-107 (#493) ci: add ipAccessList after creating project (#496)
2 parents 13fc7fc + 345efa4 commit 5008a00

File tree

22 files changed

+647
-137
lines changed

22 files changed

+647
-137
lines changed

.github/workflows/code_health.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }}
5555
MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }}
5656
MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }}
57-
run: npm test -- --exclude "tests/unit/**" --exclude "tests/integration/tools/mongodb/**" --exclude "tests/integration/*.ts"
57+
run: npm test -- tests/integration/tools/atlas
5858
- name: Upload test results
5959
uses: actions/upload-artifact@v4
6060
if: always()

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
}

src/common/config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,13 @@ function parseCliConfig(args: string[]): CliOptions {
253253
};
254254

255255
const positionalArguments = parsed._ ?? [];
256+
257+
// we use console.warn here because we still don't have our logging system configured
258+
// so we don't have a logger. For stdio, the warning will be received as a string in
259+
// the client and IDEs like VSCode do show the message in the log window. For HTTP,
260+
// it will be in the stdout of the process.
261+
warnAboutDeprecatedCliArgs({ ...parsed, _: positionalArguments }, console.warn);
262+
256263
// if we have a positional argument that matches a connection string
257264
// store it as the connection specifier and remove it from the argument
258265
// list, so it doesn't get misunderstood by the mongosh args-parser
@@ -264,6 +271,28 @@ function parseCliConfig(args: string[]): CliOptions {
264271
return parsed;
265272
}
266273

274+
export function warnAboutDeprecatedCliArgs(
275+
args: CliOptions &
276+
UserConfig & {
277+
_?: string[];
278+
},
279+
warn: (msg: string) => void
280+
): void {
281+
let usedDeprecatedArgument = false;
282+
// the first position argument should be used
283+
// instead of --connectionString, as it's how the mongosh works.
284+
if (args.connectionString) {
285+
usedDeprecatedArgument = true;
286+
warn(
287+
"The --connectionString argument is deprecated. Prefer using the first positional argument for the connection string or the MDB_MCP_CONNECTION_STRING environment variable."
288+
);
289+
}
290+
291+
if (usedDeprecatedArgument) {
292+
warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
293+
}
294+
}
295+
267296
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
268297
if (str === undefined) {
269298
return [] as unknown as T;

src/common/connectionErrorHandler.ts

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
}

0 commit comments

Comments
 (0)