Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const LogId = {

toolUpdateFailure: mongoLogId(1_005_001),
resourceUpdateFailure: mongoLogId(1_005_002),
updateToolMetadata: mongoLogId(1_005_003),

streamableHttpTransportStarted: mongoLogId(1_006_001),
streamableHttpTransportSessionCloseFailure: mongoLogId(1_006_002),
Expand Down
55 changes: 42 additions & 13 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ export class Server {
}

async connect(transport: Transport): Promise<void> {
// Resources are now reactive, so we register them ASAP so they can listen to events like
await this.validateConfig();
// Register resources after the server is initialized so they can listen to events like
// connection events.
this.registerResources();
await this.validateConfig();

this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } });
this.mcpServer.server.registerCapabilities({
logging: {},
resources: { listChanged: true, subscribe: true },
instructions: this.getInstructions(),
});

// TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
this.registerTools();
Expand Down Expand Up @@ -134,17 +137,17 @@ export class Server {
message: `Server with version ${packageInfo.version} started with transport ${transport.constructor.name} and agent runner ${JSON.stringify(this.session.mcpClient)}`,
});

this.emitServerEvent("start", Date.now() - this.startTime);
this.emitServerTelemetryEvent("start", Date.now() - this.startTime);
};

this.mcpServer.server.onclose = (): void => {
const closeTime = Date.now();
this.emitServerEvent("stop", Date.now() - closeTime);
this.emitServerTelemetryEvent("stop", Date.now() - closeTime);
};

this.mcpServer.server.onerror = (error: Error): void => {
const closeTime = Date.now();
this.emitServerEvent("stop", Date.now() - closeTime, error);
this.emitServerTelemetryEvent("stop", Date.now() - closeTime, error);
};

await this.mcpServer.connect(transport);
Expand All @@ -161,17 +164,18 @@ export class Server {
}

public sendResourceUpdated(uri: string): void {
this.session.logger.info({
id: LogId.serverInitialized,
context: "resources",
message: `Resource updated: ${uri}`,
});

if (this.subscriptions.has(uri)) {
void this.mcpServer.server.sendResourceUpdated({ uri });
}
}

/**
* Emits a server event
* @param command - The server command (e.g., "start", "stop", "register", "deregister")
* @param additionalProperties - Additional properties specific to the event
*/
private emitServerEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
private emitServerTelemetryEvent(command: ServerCommand, commandDuration: number, error?: Error): void {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flyby: emitServerEvent feels misleading, so I renamed it

const event: ServerEvent = {
timestamp: new Date().toISOString(),
source: "mdbmcp",
Expand Down Expand Up @@ -239,6 +243,13 @@ export class Server {
// Validate API client credentials
if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
try {
if (!this.userConfig.apiBaseUrl.startsWith("https://")) {
const message =
"Failed to validate MongoDB Atlas the credentials from config: apiBaseUrl must start with https://";
console.error(message);
throw new Error(message);
}

await this.session.apiClient.validateAccessToken();
} catch (error) {
if (this.userConfig.connectionString === undefined) {
Expand All @@ -255,6 +266,24 @@ export class Server {
}
}

private getInstructions(): string {
let instructions = `
This is the MongoDB MCP server.
`;
if (this.userConfig.connectionString) {
instructions = `
This MCP server was configured with a MongoDB connection string, and you can assume that you are connected to a MongoDB cluster.
`;
}

if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) {
instructions = `
This MCP server was configured with MongoDB Atlas API credentials.`;
}

return instructions;
}

private async connectToConfigConnectionString(): Promise<void> {
if (this.userConfig.connectionString) {
try {
Expand Down
36 changes: 25 additions & 11 deletions src/tools/mongodb/connect/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MongoDBToolBase } from "../mongodbTool.js";
import type { ToolArgs, OperationType, ToolConstructorParams } from "../../tool.js";
import assert from "assert";
import type { Server } from "../../../server.js";
import { LogId } from "../../../common/logger.js";

const disconnectedSchema = z
.object({
Expand All @@ -27,7 +28,8 @@ const disconnectedName = "connect" as const;

const connectedDescription =
"Switch to a different MongoDB connection. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new instance.";
const disconnectedDescription = "Connect to a MongoDB instance";
const disconnectedDescription =
"Connect to a MongoDB instance. The config resource captures if the server is already connected to a MongoDB cluster. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new MongoDB cluster.";

export class ConnectTool extends MongoDBToolBase {
public name: typeof connectedName | typeof disconnectedName = disconnectedName;
Expand Down Expand Up @@ -84,18 +86,30 @@ export class ConnectTool extends MongoDBToolBase {
}

private updateMetadata(): void {
let name: string;
let description: string;
let inputSchema: z.ZodObject<z.ZodRawShape>;

if (this.session.isConnectedToMongoDB) {
this.update?.({
name: connectedName,
description: connectedDescription,
inputSchema: connectedSchema,
});
name = connectedName;
description = connectedDescription;
inputSchema = connectedSchema;
} else {
this.update?.({
name: disconnectedName,
description: disconnectedDescription,
inputSchema: disconnectedSchema,
});
name = disconnectedName;
description = disconnectedDescription;
inputSchema = disconnectedSchema;
}

this.session.logger.info({
id: LogId.updateToolMetadata,
context: "tool",
message: `Updating tool metadata to ${name}`,
});

this.update?.({
name: name,
description: description,
inputSchema: inputSchema,
});
}
}
21 changes: 13 additions & 8 deletions tests/integration/tools/mongodb/connect/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,19 @@ describeWithMongoDB(
describeWithMongoDB(
"Connect tool",
(integration) => {
validateToolMetadata(integration, "connect", "Connect to a MongoDB instance", [
{
name: "connectionString",
description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)",
type: "string",
required: true,
},
]);
validateToolMetadata(
integration,
"connect",
"Connect to a MongoDB instance. The config resource captures if the server is already connected to a MongoDB cluster. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new MongoDB cluster.",
[
{
name: "connectionString",
description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)",
type: "string",
required: true,
},
]
);

validateThrowsForInvalidArguments(integration, "connect", [{}, { connectionString: 123 }]);

Expand Down
Loading