Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8a2db27

Browse files
feat(atlas-local): Added Atlas Local List Deployments tool (#520)
1 parent a27f2d4 commit 8a2db27

File tree

12 files changed

+346
-6
lines changed

12 files changed

+346
-6
lines changed

‎package-lock.json

Lines changed: 98 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"node": "^20.19.0 || ^22.12.0 || >= 23.0.0"
122122
},
123123
"optionalDependencies": {
124+
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.1",
124125
"kerberos": "^2.2.2"
125126
}
126127
}

‎src/common/session.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
1616
import { ErrorCodes, MongoDBError } from "./errors.js";
1717
import type { ExportsManager } from "./exportsManager.js";
18+
import type { Client } from "@mongodb-js-preview/atlas-local";
1819
import type { Keychain } from "./keychain.js";
1920

2021
export interface SessionOptions {
@@ -46,6 +47,7 @@ export class Session extends EventEmitter<SessionEvents> {
4647
version?: string;
4748
title?: string;
4849
};
50+
atlasLocalClient?: Client;
4951

5052
public logger: CompositeLogger;
5153

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

104+
setAtlasLocalClient(atlasLocalClient: Client): void {
105+
this.atlasLocalClient = atlasLocalClient;
106+
}
107+
102108
async disconnect(): Promise<void> {
103109
const atlasCluster = this.connectedAtlasCluster;
104110

‎src/server.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
UnsubscribeRequestSchema,
2020
} from "@modelcontextprotocol/sdk/types.js";
2121
import assert from "assert";
22-
import type { ToolBase } from "./tools/tool.js";
22+
import type { ToolBase,ToolConstructor } from "./tools/tool.js";
2323
import { validateConnectionString } from "./helpers/connectionOptions.js";
2424
import { packageInfo } from "./common/packageInfo.js";
2525
import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js";
@@ -69,6 +69,9 @@ export class Server {
6969
// TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
7070
this.registerTools();
7171

72+
// Atlas Local tools are optional and require async initialization
73+
void this.registerAtlasLocalTools();
74+
7275
// This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
7376
// object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
7477
// the tool accepts any arguments, even if they're all optional.
@@ -197,8 +200,41 @@ export class Server {
197200
this.telemetry.emitEvents([event]).catch(() => {});
198201
}
199202

203+
private async registerAtlasLocalTools(): Promise<void> {
204+
// If Atlas Local tools are disabled, don't attempt to connect to the client
205+
if (this.userConfig.disabledTools.includes("atlas-local")) {
206+
return;
207+
}
208+
209+
try {
210+
// Import Atlas Local client asyncronously
211+
// This will fail on unsupported platforms
212+
const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local");
213+
214+
// Connect to Atlas Local client
215+
// This will fail if docker is not running
216+
const client = AtlasLocalClient.connect();
217+
218+
// Set Atlas Local client
219+
this.session.setAtlasLocalClient(client);
220+
221+
// Register Atlas Local tools
222+
this.registerToolInstances(AtlasLocalTools);
223+
} catch (error) {
224+
console.warn(
225+
"Failed to initialize Atlas Local client, atlas-local tools will be disabled (error: ",
226+
error,
227+
")"
228+
);
229+
}
230+
}
231+
200232
private registerTools(): void {
201-
for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) {
233+
this.registerToolInstances([...AtlasTools, ...MongoDbTools]);
234+
}
235+
236+
private registerToolInstances(tools: Array<ToolConstructor>): void {
237+
for (const toolConstructor of tools) {
202238
const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
203239
if (tool.register(this)) {
204240
this.tools.push(tool);

‎src/tools/atlasLocal/atlasLocalTool.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
11
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2-
import type { ToolArgs, ToolCategory } from "../tool.js";
2+
import type { TelemetryToolMetadata,ToolArgs, ToolCategory } from "../tool.js";
33
import { ToolBase } from "../tool.js";
4+
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import type { Client } from "@mongodb-js-preview/atlas-local";
46

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

10+
protected verifyAllowed(): boolean {
11+
return this.session.atlasLocalClient !== undefined && super.verifyAllowed();
12+
}
13+
14+
protected async execute(): Promise<CallToolResult> {
15+
// Get the client
16+
const client = this.session.atlasLocalClient;
17+
18+
// If the client is not found, throw an error
19+
// This should never happen:
20+
// - atlas-local tools are only added after the client is set
21+
// this means that if we were unable to get the client, the tool will not be registered
22+
// - in case the tool was registered by accident
23+
// verifyAllowed in the base class would still return false preventing the tool from being registered,
24+
// preventing the tool from being executed
25+
if (!client) {
26+
return {
27+
content: [
28+
{
29+
type: "text",
30+
text: `Something went wrong on our end, this tool should have been disabled but it was not.
31+
please log a ticket here: https://github.com/mongodb-js/mongodb-mcp-server/issues/new?template=bug_report.yml`,
32+
},
33+
],
34+
isError: true,
35+
};
36+
}
37+
38+
return this.executeWithAtlasLocalClient(client);
39+
}
40+
41+
protected abstract executeWithAtlasLocalClient(client: Client): Promise<CallToolResult>;
42+
843
protected handleError(
944
error: unknown,
1045
args: ToolArgs<typeof this.argsShape>
@@ -14,4 +49,12 @@ export abstract class AtlasLocalToolBase extends ToolBase {
1449
// For other types of errors, use the default error handling from the base class
1550
return super.handleError(error, args);
1651
}
52+
53+
protected resolveTelemetryMetadata(
54+
...args: Parameters<ToolCallback<typeof this.argsShape>>
55+
): TelemetryToolMetadata {
56+
// TODO: include deployment id in the metadata where possible
57+
void args; // this shuts up the eslint rule until we implement the TODO above
58+
return {};
59+
}
1760
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { AtlasLocalToolBase } from "../atlasLocalTool.js";
3+
import type { OperationType } from "../../tool.js";
4+
import { formatUntrustedData } from "../../tool.js";
5+
import type { Deployment } from "@mongodb-js-preview/atlas-local";
6+
import type { Client } from "@mongodb-js-preview/atlas-local";
7+
8+
export class ListDeploymentsTool extends AtlasLocalToolBase {
9+
public name = "atlas-local-list-deployments";
10+
protected description = "List MongoDB Atlas local deployments";
11+
public operationType: OperationType = "read";
12+
protected argsShape = {};
13+
14+
protected async executeWithAtlasLocalClient(client: Client): Promise<CallToolResult> {
15+
// List the deployments
16+
const deployments = await client.listDeployments();
17+
18+
// Format the deployments
19+
return this.formatDeploymentsTable(deployments);
20+
}
21+
22+
private formatDeploymentsTable(deployments: Deployment[]): CallToolResult {
23+
// Check if deployments are absent
24+
if (!deployments?.length) {
25+
return {
26+
content: [{ type: "text", text: "No deployments found." }],
27+
};
28+
}
29+
30+
// Turn the deployments into a markdown table
31+
const rows = deployments
32+
.map((deployment) => {
33+
return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}`;
34+
})
35+
.join("\n");
36+
37+
return {
38+
content: formatUntrustedData(
39+
`Found ${deployments.length} deployments:`,
40+
`Deployment Name | State | MongoDB Version
41+
----------------|----------------|----------------
42+
${rows}`
43+
),
44+
};
45+
}
46+
}

‎src/tools/atlasLocal/tools.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export const AtlasLocalTools = [];
1+
import { ListDeploymentsTool } from "./read/listDeployments.js";
2+
3+
export const AtlasLocalTools = [ListDeploymentsTool];

‎src/tools/tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export type TelemetryToolMetadata = {
1818
orgId?: string;
1919
};
2020

21+
export type ToolConstructor = new (session: Session, config: UserConfig, telemetry: Telemetry) => ToolBase;
22+
2123
export abstract class ToolBase {
2224
public abstract name: string;
2325

‎tests/integration/helpers.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { MCPConnectionManager } from "../../src/common/connectionManager.js";
1515
import { DeviceId } from "../../src/helpers/deviceId.js";
1616
import { connectionErrorHandler } from "../../src/common/connectionErrorHandler.js";
1717
import { Keychain } from "../../src/common/keychain.js";
18+
import type { Client as AtlasLocalClient } from "@mongodb-js-preview/atlas-local";
1819

1920
interface ParameterInfo {
2021
name: string;
@@ -345,6 +346,37 @@ export function waitUntil<T extends ConnectionState>(
345346
});
346347
}
347348

349+
export function waitUntilMcpClientIsSet(
350+
mcpServer: Server,
351+
signal: AbortSignal,
352+
timeout: number = 5000
353+
): Promise<AtlasLocalClient> {
354+
let ts: NodeJS.Timeout | undefined;
355+
356+
const timeoutSignal = AbortSignal.timeout(timeout);
357+
const combinedSignal = AbortSignal.any([signal, timeoutSignal]);
358+
359+
return new Promise<AtlasLocalClient>((resolve, reject) => {
360+
ts = setInterval(() => {
361+
if (combinedSignal.aborted) {
362+
return reject(new Error(`Aborted: ${combinedSignal.reason}`));
363+
}
364+
365+
// wait until session.client != undefined
366+
// do not wait more than 1 second, should take a few milliseconds at most
367+
// try every 50ms to see if the client is set, if it's not set after 1 second, throw an error
368+
const client = mcpServer.session.atlasLocalClient;
369+
if (client) {
370+
return resolve(client);
371+
}
372+
}, 100);
373+
}).finally(() => {
374+
if (ts !== undefined) {
375+
clearInterval(ts);
376+
}
377+
});
378+
}
379+
348380
export function getDataFromUntrustedContent(content: string): string {
349381
const regex = /^[\t]*<untrusted-user-data-[0-9a-f\\-]*>(?<data>.*)^[\t]*<\/untrusted-user-data-[0-9a-f\\-]*>/gms;
350382
const match = regex.exec(content);

‎tests/integration/server.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ describe("Server integration test", () => {
1111
expectDefined(tools);
1212
expect(tools.tools.length).toBeGreaterThan(0);
1313

14-
const atlasTools = tools.tools.filter((tool) => tool.name.startsWith("atlas-"));
14+
const atlasTools = tools.tools.filter(
15+
(tool) => tool.name.startsWith("atlas-") && !tool.name.startsWith("atlas-local-")
16+
);
1517
expect(atlasTools.length).toBeLessThanOrEqual(0);
1618
});
1719
},

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /