Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7a27749
feat: Adds scaffolding for atlas-local tools MCP-155 (#498)
Luke-Sanderson Sep 1, 2025
a27f2d4
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Sep 5, 2025
8a2db27
feat(atlas-local): Added Atlas Local List Deployments tool (#520)
jeroenvervaeke Sep 9, 2025
6938a8f
feat(atlas-local): Add Atlas Local List Deployments tool (#538)
Luke-Sanderson Sep 11, 2025
e774083
feat(atlas-local): Add Atlas Local Create Deployment tool (#546)
Luke-Sanderson Sep 12, 2025
bb717ab
feat(atlas-local): Accuracy tests for Atlas Local (#554)
Luke-Sanderson Sep 15, 2025
0d14679
feat(atlas-local): Split Atlas Local tests from Atlas (#552)
Luke-Sanderson Sep 15, 2025
cb5d335
feat(atlas-local): Adds Atlas Local Connect Deployment tool (#612)
cveticm Oct 7, 2025
91c11cc
feat: added atlas local deployment id to telemetry (#627)
jeroenvervaeke Oct 9, 2025
8efb029
refactor: Replace z.string with CommonArgs.string (#629)
Luke-Sanderson Oct 9, 2025
c0f3e49
Merge branch 'main' into feat-MCP-40
Luke-Sanderson Oct 9, 2025
cac14ae
Merge branch 'main' into feat-MCP-40
Luke-Sanderson Oct 9, 2025
bd4a061
Merge branch 'main' into feat-MCP-40
Luke-Sanderson Oct 9, 2025
14ade77
fix: knip ignore atlas-local dependency
Luke-Sanderson Oct 9, 2025
5c5c2e7
Merge branch 'main' into feat-MCP-40
Luke-Sanderson Oct 10, 2025
5afc312
docs(atlas-local): Adds the Atlas Local Tools to the readme
Luke-Sanderson Oct 10, 2025
c4a44e0
fix: Small changes from PR suggestions
Luke-Sanderson Oct 10, 2025
9eb0c1a
refactor: Move all lookupDeploymentId calls to resolveTelemetryMetadata
Luke-Sanderson Oct 10, 2025
74d2218
refactor: Update atlas local error handling
Luke-Sanderson Oct 10, 2025
eb488ff
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Oct 14, 2025
e33df95
move atlas local client initialization to 'TransportRunnerBase'
jeroenvervaeke Oct 16, 2025
c3efcd1
use the @mongodb-js/atlas-local package instead of the @mongodb-js-pr…
jeroenvervaeke Oct 16, 2025
123ab82
fix bug with fetching deployment id caused by refactor
jeroenvervaeke Oct 17, 2025
eda00a2
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Oct 17, 2025
a8e10bd
only lookup deploymentid in atlas local delete tool when telemetry is…
jeroenvervaeke Oct 20, 2025
c29581f
fix integration tests
jeroenvervaeke Oct 20, 2025
e38743f
fix error message in listSearchIndexes test
jeroenvervaeke Oct 20, 2025
08eb04c
removed debug log
jeroenvervaeke Oct 20, 2025
a197b08
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Oct 20, 2025
30aabae
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Oct 20, 2025
3e46dbb
adressed pr comments in atlasLocal.ts
jeroenvervaeke Oct 21, 2025
035d4c7
addressed PR comments in atlasLocalTools + simplified telemetry, clos…
jeroenvervaeke Oct 21, 2025
4730bfb
reverted emitToolEvent to be sync again
jeroenvervaeke Oct 21, 2025
e0c8a72
move AtlasLocalTools to toolConstructors
jeroenvervaeke Oct 21, 2025
13b989e
return json instead of markdown table for list deployments
jeroenvervaeke Oct 21, 2025
78fd715
fix list deployments test
jeroenvervaeke Oct 21, 2025
596a783
Merge branch 'main' into feat-MCP-40
jeroenvervaeke Oct 21, 2025
f20c09f
Update src/tools/atlasLocal/read/listDeployments.ts
jeroenvervaeke Oct 21, 2025
318e429
Update src/tools/atlasLocal/atlasLocalTool.ts
jeroenvervaeke Oct 21, 2025
939cfd5
Update src/tools/atlasLocal/atlasLocalTool.ts
jeroenvervaeke Oct 21, 2025
d535757
ran npm run fix, and cleaned up code after accepting suggestions
jeroenvervaeke Oct 21, 2025
db06cc1
fix accuracy tests
jeroenvervaeke Oct 21, 2025
04e505c
updated listDeployments.test.ts after accepting suggestions
jeroenvervaeke Oct 21, 2025
5ae9405
addressed test comments
jeroenvervaeke Oct 21, 2025
3de5b26
added comment describing test in 'should return an error when creatin…
jeroenvervaeke Oct 21, 2025
e3c6708
ran 'npm run fix'
jeroenvervaeke Oct 21, 2025
56d981d
prefixed list deployments accuacy prompts with 'local'
jeroenvervaeke Oct 21, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(atlas-local): Added Atlas Local List Deployments tool (#520)
  • Loading branch information
jeroenvervaeke authored Sep 9, 2025
commit 8a2db27677c437b7162c7e43339eb0efc067136d
98 changes: 98 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"node": "^20.19.0 || ^22.12.0 || >= 23.0.0"
},
"optionalDependencies": {
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.1",
"kerberos": "^2.2.2"
}
}
6 changes: 6 additions & 0 deletions src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
import { ErrorCodes, MongoDBError } from "./errors.js";
import type { ExportsManager } from "./exportsManager.js";
import type { Client } from "@mongodb-js-preview/atlas-local";
import type { Keychain } from "./keychain.js";

export interface SessionOptions {
Expand Down Expand Up @@ -46,6 +47,7 @@ export class Session extends EventEmitter<SessionEvents> {
version?: string;
title?: string;
};
atlasLocalClient?: Client;

public logger: CompositeLogger;

Expand Down Expand Up @@ -99,6 +101,10 @@ export class Session extends EventEmitter<SessionEvents> {
this.connectionManager.setClientName(this.mcpClient.name || "unknown");
}

setAtlasLocalClient(atlasLocalClient: Client): void {
this.atlasLocalClient = atlasLocalClient;
}

async disconnect(): Promise<void> {
const atlasCluster = this.connectedAtlasCluster;

Expand Down
40 changes: 38 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
UnsubscribeRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import assert from "assert";
import type { ToolBase } from "./tools/tool.js";
import type { ToolBase, ToolConstructor } from "./tools/tool.js";
import { validateConnectionString } from "./helpers/connectionOptions.js";
import { packageInfo } from "./common/packageInfo.js";
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
Expand Down Expand Up @@ -69,6 +69,9 @@ export class Server {
// TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
this.registerTools();

// Atlas Local tools are optional and require async initialization
void this.registerAtlasLocalTools();

// This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
// object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
// the tool accepts any arguments, even if they're all optional.
Expand Down Expand Up @@ -197,8 +200,41 @@ export class Server {
this.telemetry.emitEvents([event]).catch(() => {});
}

private async registerAtlasLocalTools(): Promise<void> {
// If Atlas Local tools are disabled, don't attempt to connect to the client
if (this.userConfig.disabledTools.includes("atlas-local")) {
return;
}

try {
// Import Atlas Local client asyncronously
// This will fail on unsupported platforms
const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local");

// Connect to Atlas Local client
// This will fail if docker is not running
const client = AtlasLocalClient.connect();

// Set Atlas Local client
this.session.setAtlasLocalClient(client);

// Register Atlas Local tools
this.registerToolInstances(AtlasLocalTools);
} catch (error) {
console.warn(
"Failed to initialize Atlas Local client, atlas-local tools will be disabled (error: ",
error,
")"
);
}
}

private registerTools(): void {
for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) {
this.registerToolInstances([...AtlasTools, ...MongoDbTools]);
}

private registerToolInstances(tools: Array<ToolConstructor>): void {
for (const toolConstructor of tools) {
const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
if (tool.register(this)) {
this.tools.push(tool);
Expand Down
45 changes: 44 additions & 1 deletion src/tools/atlasLocal/atlasLocalTool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { ToolArgs, ToolCategory } from "../tool.js";
import type { TelemetryToolMetadata, ToolArgs, ToolCategory } from "../tool.js";
import { ToolBase } from "../tool.js";
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Client } from "@mongodb-js-preview/atlas-local";

export abstract class AtlasLocalToolBase extends ToolBase {
public category: ToolCategory = "atlas-local";

protected verifyAllowed(): boolean {
return this.session.atlasLocalClient !== undefined && super.verifyAllowed();
}

protected async execute(): Promise<CallToolResult> {
// Get the client
const client = this.session.atlasLocalClient;

// If the client is not found, throw an error
// This should never happen:
// - atlas-local tools are only added after the client is set
// this means that if we were unable to get the client, the tool will not be registered
// - in case the tool was registered by accident
// verifyAllowed in the base class would still return false preventing the tool from being registered,
// preventing the tool from being executed
if (!client) {
return {
content: [
{
type: "text",
text: `Something went wrong on our end, this tool should have been disabled but it was not.
please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issues/new?template=bug_report.yml`,
},
],
isError: true,
};
}

return this.executeWithAtlasLocalClient(client);
}

protected abstract executeWithAtlasLocalClient(client: Client): Promise<CallToolResult>;

protected handleError(
error: unknown,
args: ToolArgs<typeof this.argsShape>
Expand All @@ -14,4 +49,12 @@ export abstract class AtlasLocalToolBase extends ToolBase {
// For other types of errors, use the default error handling from the base class
return super.handleError(error, args);
}

protected resolveTelemetryMetadata(
...args: Parameters<ToolCallback<typeof this.argsShape>>
): TelemetryToolMetadata {
// TODO: include deployment id in the metadata where possible
void args; // this shuts up the eslint rule until we implement the TODO above
return {};
}
}
46 changes: 46 additions & 0 deletions src/tools/atlasLocal/read/listDeployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { AtlasLocalToolBase } from "../atlasLocalTool.js";
import type { OperationType } from "../../tool.js";
import { formatUntrustedData } from "../../tool.js";
import type { Deployment } from "@mongodb-js-preview/atlas-local";
import type { Client } from "@mongodb-js-preview/atlas-local";

export class ListDeploymentsTool extends AtlasLocalToolBase {
public name = "atlas-local-list-deployments";
protected description = "List MongoDB Atlas local deployments";
public operationType: OperationType = "read";
protected argsShape = {};

protected async executeWithAtlasLocalClient(client: Client): Promise<CallToolResult> {
// List the deployments
const deployments = await client.listDeployments();

// Format the deployments
return this.formatDeploymentsTable(deployments);
}

private formatDeploymentsTable(deployments: Deployment[]): CallToolResult {
// Check if deployments are absent
if (!deployments?.length) {
return {
content: [{ type: "text", text: "No deployments found." }],
};
}

// Turn the deployments into a markdown table
const rows = deployments
.map((deployment) => {
return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}`;
})
.join("\n");

return {
content: formatUntrustedData(
`Found ${deployments.length} deployments:`,
`Deployment Name | State | MongoDB Version
----------------|----------------|----------------
${rows}`
),
};
}
}
4 changes: 3 additions & 1 deletion src/tools/atlasLocal/tools.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const AtlasLocalTools = [];
import { ListDeploymentsTool } from "./read/listDeployments.js";

export const AtlasLocalTools = [ListDeploymentsTool];
2 changes: 2 additions & 0 deletions src/tools/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type TelemetryToolMetadata = {
orgId?: string;
};

export type ToolConstructor = new (session: Session, config: UserConfig, telemetry: Telemetry) => ToolBase;

export abstract class ToolBase {
public abstract name: string;

Expand Down
32 changes: 32 additions & 0 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MCPConnectionManager } from "../../src/common/connectionManager.js";
import { DeviceId } from "../../src/helpers/deviceId.js";
import { connectionErrorHandler } from "../../src/common/connectionErrorHandler.js";
import { Keychain } from "../../src/common/keychain.js";
import type { Client as AtlasLocalClient } from "@mongodb-js-preview/atlas-local";

interface ParameterInfo {
name: string;
Expand Down Expand Up @@ -345,6 +346,37 @@ export function waitUntil<T extends ConnectionState>(
});
}

export function waitUntilMcpClientIsSet(
mcpServer: Server,
signal: AbortSignal,
timeout: number = 5000
): Promise<AtlasLocalClient> {
let ts: NodeJS.Timeout | undefined;

const timeoutSignal = AbortSignal.timeout(timeout);
const combinedSignal = AbortSignal.any([signal, timeoutSignal]);

return new Promise<AtlasLocalClient>((resolve, reject) => {
ts = setInterval(() => {
if (combinedSignal.aborted) {
return reject(new Error(`Aborted: ${combinedSignal.reason}`));
}

// wait until session.client != undefined
// do not wait more than 1 second, should take a few milliseconds at most
// try every 50ms to see if the client is set, if it's not set after 1 second, throw an error
const client = mcpServer.session.atlasLocalClient;
if (client) {
return resolve(client);
}
}, 100);
}).finally(() => {
if (ts !== undefined) {
clearInterval(ts);
}
});
}

export function getDataFromUntrustedContent(content: string): string {
const regex = /^[ \t]*<untrusted-user-data-[0-9a-f\\-]*>(?<data>.*)^[ \t]*<\/untrusted-user-data-[0-9a-f\\-]*>/gms;
const match = regex.exec(content);
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ describe("Server integration test", () => {
expectDefined(tools);
expect(tools.tools.length).toBeGreaterThan(0);

const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-"));
const atlasTools = tools.tools.filter(
(tool) => tool.name.startsWith("atlas-") && !tool.name.startsWith("atlas-local-")
);
expect(atlasTools.length).toBeLessThanOrEqual(0);
});
},
Expand Down
Loading