Skip to content

chore: disable the connect tool #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 3 additions & 4 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ApiClientCredentials {

export interface ApiClientOptions {
credentials?: ApiClientCredentials;
baseUrl?: string;
baseUrl: string;
userAgent?: string;
}

Expand Down Expand Up @@ -63,12 +63,11 @@ export class ApiClient {
},
};

constructor(options?: ApiClientOptions) {
constructor(options: ApiClientOptions) {
this.options = {
...options,
baseUrl: options?.baseUrl || "https://cloud.mongodb.com/",
Copy link
Collaborator

Choose a reason for hiding this comment

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

the code was structured like that originally we moved https://cloud.mongodb.com/ to be part of apiClient to keep everything about the API in one place, any reason to move it back?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it makes more sense to have the defaults for the config options together rather than disconnected like that. That way we can see that the baseUrl option is actually always set and the exact value it is set to.

userAgent:
options?.userAgent ||
options.userAgent ||
`AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
};

Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb";
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
// env variables.
export interface UserConfig {
apiBaseUrl?: string;
apiBaseUrl: string;
apiClientId?: string;
apiClientSecret?: string;
telemetry?: "enabled" | "disabled";
Expand All @@ -24,6 +24,7 @@ export interface UserConfig {
}

const defaults: UserConfig = {
apiBaseUrl: "https://cloud.mongodb.com/",
logPath: getLogPath(),
connectOptions: {
readConcern: "local",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ try {
name: packageInfo.mcpServerName,
version: packageInfo.version,
});

const server = new Server({
mcpServer,
session,
Expand Down
30 changes: 29 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type ServerEvent } from "./telemetry/types.js";
import { type ServerCommand } from "./telemetry/types.js";
import { CallToolRequestSchema, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import assert from "assert";
import { connectToMongoDB } from "./tools/mongodb/mongodbTool.js";

export interface ServerOptions {
session: Session;
Expand All @@ -33,7 +34,7 @@ export class Server {
this.userConfig = userConfig;
}

async connect(transport: Transport) {
async connect(transport: Transport): Promise<void> {
this.mcpServer.server.registerCapabilities({ logging: {} });

this.registerTools();
Expand Down Expand Up @@ -88,6 +89,8 @@ export class Server {
const closeTime = Date.now();
this.emitServerEvent("stop", Date.now() - closeTime, error);
};

await this.validateConfig();
}

async close(): Promise<void> {
Expand Down Expand Up @@ -183,4 +186,29 @@ export class Server {
);
}
}

private async validateConfig(): Promise<void> {
const isAtlasConfigured = this.userConfig.apiClientId && this.userConfig.apiClientSecret;
const isMongoDbConfigured = this.userConfig.connectionString;
if (!isAtlasConfigured && !isMongoDbConfigured) {
console.error(
"Either Atlas Client Id or a MongoDB connection string must be configured - you can provide them as environment variables or as startup arguments. \n" +
"Provide the Atlas credentials as `MDB_MCP_API_CLIENT_ID` and `MDB_MCP_API_CLIENT_SECRET` environment variables or as `--apiClientId` and `--apiClientSecret` startup arguments. \n" +
"Provide the MongoDB connection string as `MDB_MCP_CONNECTION_STRING` environment variable or as `--connectionString` startup argument."
);
throw new Error("Either Atlas Client Id or a MongoDB connection string must be configured");
}

if (this.userConfig.connectionString) {
try {
await connectToMongoDB(this.userConfig.connectionString, this.userConfig, this.session);
} catch (error) {
console.error(
"Failed to connect to MongoDB instance using the connection string from the config: ",
error
);
throw new Error("Failed to connect to MongoDB instance using the connection string from the config");
}
}
}
}
4 changes: 2 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js";
import EventEmitter from "events";

export interface SessionOptions {
apiBaseUrl?: string;
apiBaseUrl: string;
apiClientId?: string;
apiClientSecret?: string;
}
Expand All @@ -20,7 +20,7 @@ export class Session extends EventEmitter<{
version: string;
};

constructor({ apiBaseUrl, apiClientId, apiClientSecret }: SessionOptions = {}) {
constructor({ apiBaseUrl, apiClientId, apiClientSecret }: SessionOptions) {
super();

const credentials: ApiClientCredentials | undefined =
Expand Down
36 changes: 21 additions & 15 deletions src/tools/mongodb/mongodbTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,31 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { ErrorCodes, MongoDBError } from "../../errors.js";
import logger, { LogId } from "../../logger.js";
import { UserConfig } from "../../config.js";
import { Session } from "../../session.js";

export const DbOperationArgs = {
database: z.string().describe("Database name"),
collection: z.string().describe("Collection name"),
};

export async function connectToMongoDB(connectionString: string, config: UserConfig, session: Session): Promise<void> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is slighly confusing to have connectToMongoDB pure function plus connectToMongoDB as member of MongoDBToolBase class on on the same file.

Have you considered moving this to session given it currently holds instance of NodeDriverServiceProvider ?

Copy link
Collaborator

@gagik gagik Apr 28, 2025

Choose a reason for hiding this comment

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

in similar realm, a solution could be to have a wrapper class for NodeDriverServiceProvider (with an unconnected state) live inside session similar to apiClient. And that client would have the connect function.

My thinking is session is the entry to i.e. apiClient is the "plugin" for Atlas stuff, databaseClient (or something like that) is the "plugin" for connections, each with their own modular states.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not opposed to this, but I'd suggest doing it separately - I'd prefer that this PR is the bare minimum that disables the connect tool, rather than a broader restructuring of the codebase.

const provider = await NodeDriverServiceProvider.connect(connectionString, {
productDocsLink: "https://docs.mongodb.com/todo-mcp",
productName: "MongoDB MCP",
readConcern: {
level: config.connectOptions.readConcern,
},
readPreference: config.connectOptions.readPreference,
writeConcern: {
w: config.connectOptions.writeConcern,
},
timeoutMS: config.connectOptions.timeoutMS,
});

session.serviceProvider = provider;
}

export abstract class MongoDBToolBase extends ToolBase {
protected category: ToolCategory = "mongodb";

Expand Down Expand Up @@ -70,20 +89,7 @@ export abstract class MongoDBToolBase extends ToolBase {
return super.handleError(error, args);
}

protected async connectToMongoDB(connectionString: string): Promise<void> {
const provider = await NodeDriverServiceProvider.connect(connectionString, {
productDocsLink: "https://docs.mongodb.com/todo-mcp",
productName: "MongoDB MCP",
readConcern: {
level: this.config.connectOptions.readConcern,
},
readPreference: this.config.connectOptions.readPreference,
writeConcern: {
w: this.config.connectOptions.writeConcern,
},
timeoutMS: this.config.connectOptions.timeoutMS,
});

this.session.serviceProvider = provider;
protected connectToMongoDB(connectionString: string): Promise<void> {
return connectToMongoDB(connectionString, this.config, this.session);
}
}
6 changes: 4 additions & 2 deletions src/tools/mongodb/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConnectTool } from "./metadata/connect.js";
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
// import { ConnectTool } from "./metadata/connect.js";
import { ListCollectionsTool } from "./metadata/listCollections.js";
import { CollectionIndexesTool } from "./read/collectionIndexes.js";
import { ListDatabasesTool } from "./metadata/listDatabases.js";
Expand All @@ -20,7 +21,8 @@ import { CreateCollectionTool } from "./create/createCollection.js";
import { LogsTool } from "./metadata/logs.js";

export const MongoDbTools = [
ConnectTool,
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
// ConnectTool,
ListCollectionsTool,
ListDatabasesTool,
CollectionIndexesTool,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "./inMemoryTransport.js";
import { Server } from "../../src/server.js";
import { config, UserConfig } from "../../src/config.js";

Check failure on line 4 in tests/integration/helpers.ts

View workflow job for this annotation

GitHub Actions / check-style

'config' is defined but never used
import { McpError } from "@modelcontextprotocol/sdk/types.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Session } from "../../src/session.js";
Expand All @@ -20,7 +20,7 @@
mcpServer: () => Server;
}

export function setupIntegrationTest(getUserConfig: () => UserConfig = () => config): IntegrationTest {
export function setupIntegrationTest(getUserConfig: () => UserConfig): IntegrationTest {
let mcpClient: Client | undefined;
let mcpServer: Server | undefined;

Expand Down
10 changes: 3 additions & 7 deletions tests/integration/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { expectDefined, setupIntegrationTest } from "./helpers.js";
import { config } from "../../src/config.js";
import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js";

describe("Server integration test", () => {
describe("without atlas", () => {
const integration = setupIntegrationTest(() => ({
...config,
apiClientId: undefined,
apiClientSecret: undefined,
}));

describeWithMongoDB("without atlas", (integration) => {
Copy link
Collaborator

@fmenezes fmenezes Apr 28, 2025

Choose a reason for hiding this comment

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

It was the way it was to be resilient against providing env vars on CI/terminal

Copy link
Collaborator

Choose a reason for hiding this comment

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

nvm I see you've changed describeAtlas

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean the undefining of the apiClientId/Secret - I can bring it back, the important thing here was to provide the connection string as otherwise the server was failing due to neither atlas credentials, nor connection string being configured.

it("should return positive number of tools and have no atlas tools", async () => {
const tools = await integration.mcpClient().listTools();
expectDefined(tools);
Expand All @@ -18,6 +13,7 @@ describe("Server integration test", () => {
expect(atlasTools.length).toBeLessThanOrEqual(0);
});
});

describe("with atlas", () => {
const integration = setupIntegrationTest(() => ({
...config,
Expand Down
8 changes: 7 additions & 1 deletion tests/integration/tools/atlas/atlasHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ObjectId } from "mongodb";
import { Group } from "../../../../src/common/atlas/openapi.js";
import { ApiClient } from "../../../../src/common/atlas/apiClient.js";
import { setupIntegrationTest, IntegrationTest } from "../../helpers.js";
import { config } from "../../../../src/config.js";

export type IntegrationTestFunction = (integration: IntegrationTest) => void;

Expand All @@ -11,7 +12,12 @@ export function sleep(ms: number) {

export function describeWithAtlas(name: string, fn: IntegrationTestFunction) {
const testDefinition = () => {
const integration = setupIntegrationTest();
const integration = setupIntegrationTest(() => ({
...config,
apiClientId: process.env.MDB_MCP_API_CLIENT_ID,
apiClientSecret: process.env.MDB_MCP_API_CLIENT_SECRET,
}));

describe(name, () => {
fn(integration);
});
Expand Down
84 changes: 46 additions & 38 deletions tests/integration/tools/mongodb/metadata/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describeWithMongoDB } from "../mongodbHelpers.js";
import { getResponseContent, validateThrowsForInvalidArguments, validateToolMetadata } from "../../../helpers.js";
import { config } from "../../../../../src/config.js";

// These tests are temporarily skipped because the connect tool is disabled for the initial release.
// TODO: https://github.com/mongodb-js/mongodb-mcp-server/issues/141 - reenable when the connect tool is reenabled
describeWithMongoDB(
"switchConnection tool",
(integration) => {
Expand Down Expand Up @@ -75,50 +77,56 @@ describeWithMongoDB(
(mdbIntegration) => ({
...config,
connectionString: mdbIntegration.connectionString(),
})
}),
describe.skip
);
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,
},
]);
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,
},
]);

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

it("doesn't have the switch-connection tool registered", async () => {
const { tools } = await integration.mcpClient().listTools();
const tool = tools.find((tool) => tool.name === "switch-connection");
expect(tool).toBeUndefined();
});
it("doesn't have the switch-connection tool registered", async () => {
const { tools } = await integration.mcpClient().listTools();
const tool = tools.find((tool) => tool.name === "switch-connection");
expect(tool).toBeUndefined();
});

describe("with connection string", () => {
it("connects to the database", async () => {
const response = await integration.mcpClient().callTool({
name: "connect",
arguments: {
connectionString: integration.connectionString(),
},
describe("with connection string", () => {
it("connects to the database", async () => {
const response = await integration.mcpClient().callTool({
name: "connect",
arguments: {
connectionString: integration.connectionString(),
},
});
const content = getResponseContent(response.content);
expect(content).toContain("Successfully connected");
});
const content = getResponseContent(response.content);
expect(content).toContain("Successfully connected");
});
});

describe("with invalid connection string", () => {
it("returns error message", async () => {
const response = await integration.mcpClient().callTool({
name: "connect",
arguments: { connectionString: "mongodb://localhost:12345" },
});
const content = getResponseContent(response.content);
expect(content).toContain("Error running connect");
describe("with invalid connection string", () => {
it("returns error message", async () => {
const response = await integration.mcpClient().callTool({
name: "connect",
arguments: { connectionString: "mongodb://localhost:12345" },
});
const content = getResponseContent(response.content);
expect(content).toContain("Error running connect");

// Should not suggest using the config connection string (because we don't have one)
expect(content).not.toContain("Your config lists a different connection string");
// Should not suggest using the config connection string (because we don't have one)
expect(content).not.toContain("Your config lists a different connection string");
});
});
});
});
},
() => config,
describe.skip
);
Loading
Loading