Skip to content

Commit 4900b02

Browse files
Implemented 'atlas-local-list-deployments'
1 parent 7a27749 commit 4900b02

File tree

7 files changed

+241
-6
lines changed

7 files changed

+241
-6
lines changed

package-lock.json

Lines changed: 97 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
@@ -96,6 +96,7 @@
9696
},
9797
"dependencies": {
9898
"@modelcontextprotocol/sdk": "^1.17.4",
99+
"@mongodb-js-preview/atlas-local": "^0.0.0-preview.1",
99100
"@mongodb-js/device-id": "^0.3.1",
100101
"@mongodb-js/devtools-connect": "^3.9.3",
101102
"@mongodb-js/devtools-proxy-support": "^0.5.2",

src/server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import type { Session } from "./common/session.js";
33
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
44
import { AtlasTools } from "./tools/atlas/tools.js";
5-
import { AtlasLocalTools } from "./tools/atlasLocal/tools.js";
5+
import { BuildAtlasLocalTools } from "./tools/atlasLocal/tools.js";
66
import { MongoDbTools } from "./tools/mongodb/tools.js";
77
import { Resources } from "./resources/resources.js";
88
import type { LogLevel } from "./common/logger.js";
@@ -62,7 +62,7 @@ export class Server {
6262
this.mcpServer.server.registerCapabilities({ logging: {}, resources: { listChanged: true, subscribe: true } });
6363

6464
// TODO: Eventually we might want to make tools reactive too instead of relying on custom logic.
65-
this.registerTools();
65+
await this.registerTools();
6666

6767
// This is a workaround for an issue we've seen with some models, where they'll see that everything in the `arguments`
6868
// object is optional, and then not pass it at all. However, the MCP server expects the `arguments` object to be if
@@ -193,8 +193,9 @@ export class Server {
193193
this.telemetry.emitEvents([event]).catch(() => {});
194194
}
195195

196-
private registerTools(): void {
197-
for (const toolConstructor of [...AtlasTools, ...AtlasLocalTools, ...MongoDbTools]) {
196+
private async registerTools(): Promise<void> {
197+
const atlasLocalTools = await BuildAtlasLocalTools();
198+
for (const toolConstructor of [...AtlasTools, ...atlasLocalTools, ...MongoDbTools]) {
198199
const tool = new toolConstructor(this.session, this.userConfig, this.telemetry);
199200
if (tool.register(this)) {
200201
this.tools.push(tool);

src/tools/atlasLocal/atlasLocalTool.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
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 { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import type AtlasLocal from "@mongodb-js-preview/atlas-local";
46

57
export abstract class AtlasLocalToolBase extends ToolBase {
68
public category: ToolCategory = "atlas-local";
9+
// Will be injected by BuildAtlasLocalTools() in atlasLocal/tools.ts
10+
public client?: AtlasLocal.Client;
11+
12+
protected verifyAllowed(): boolean {
13+
return this.client !== undefined && super.verifyAllowed();
14+
}
715

816
protected handleError(
917
error: unknown,
@@ -14,4 +22,11 @@ export abstract class AtlasLocalToolBase extends ToolBase {
1422
// For other types of errors, use the default error handling from the base class
1523
return super.handleError(error, args);
1624
}
25+
26+
protected resolveTelemetryMetadata(
27+
...args: Parameters<ToolCallback<typeof this.argsShape>>
28+
): TelemetryToolMetadata {
29+
// TODO: include deployment id in the metadata where possible
30+
return {};
31+
}
1732
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { AtlasLocalToolBase } from "../atlasLocalTool.js";
3+
import type { ToolArgs, OperationType, TelemetryToolMetadata } from "../../tool.js";
4+
import { formatUntrustedData } from "../../tool.js";
5+
import type { Deployment } from "@mongodb-js-preview/atlas-local";
6+
7+
export class ListDeploymentsTool extends AtlasLocalToolBase {
8+
public name = "atlas-local-list-deployments";
9+
protected description = "List MongoDB Atlas local deployments";
10+
public operationType: OperationType = "read";
11+
protected argsShape = {};
12+
13+
protected async execute({}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
14+
// Get the client
15+
const client = this.client;
16+
17+
// If the client is not found, throw an error
18+
// This should never happen, because the tool should have been disabled.
19+
// verifyAllowed in the base class returns false if the client is not found
20+
if (!client) {
21+
throw new Error("Atlas Local client not found, tool should have been disabled.");
22+
}
23+
24+
// List the deployments
25+
const deployments = await client.listDeployments();
26+
27+
// Format the deployments
28+
return this.formatDeploymentsTable(deployments);
29+
}
30+
31+
32+
private formatDeploymentsTable(
33+
deployments: Deployment[]
34+
): CallToolResult {
35+
// Check if deployments are absent
36+
if (!deployments?.length) {
37+
return {
38+
content: [{ type: "text", text: "No deployments found." }],
39+
};
40+
}
41+
42+
// Turn the deployments into a markdown table
43+
const rows = deployments
44+
.map((deployment) => {
45+
return `${deployment.name || "Unknown"} | ${deployment.state} | ${deployment.mongodbVersion}`
46+
})
47+
.join("\n");
48+
49+
return {
50+
content: formatUntrustedData(
51+
`Found ${deployments.length} deployments:`,
52+
`Deployment Name | State | MongoDB Version
53+
----------------|----------------|----------------
54+
${rows}`
55+
),
56+
};
57+
}
58+
}

src/tools/atlasLocal/tools.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,33 @@
1-
export const AtlasLocalTools = [];
1+
import { ListDeploymentsTool } from "./read/listDeployments.js";
2+
import type AtlasLocal from "@mongodb-js-preview/atlas-local";
3+
4+
// Don't use this directly, use BuildAtlasLocalTools instead
5+
const atlasLocalTools = [ListDeploymentsTool];
6+
7+
// Build the Atlas Local tools
8+
export const BuildAtlasLocalTools = async () => {
9+
// Initialize the Atlas Local client
10+
const client = await GetAtlasLocalClient();
11+
12+
// If the client is found, set it on the tools
13+
// On unsupported platforms, the client will be undefined
14+
if (client) {
15+
// Set the client on the tools
16+
atlasLocalTools.forEach(tool => {
17+
tool.prototype.client = client;
18+
});
19+
}
20+
21+
return atlasLocalTools;
22+
};
23+
24+
export const GetAtlasLocalClient = async (): Promise<AtlasLocal.Client | undefined> => {
25+
try {
26+
const { Client: AtlasLocalClient } = await import("@mongodb-js-preview/atlas-local");
27+
return AtlasLocalClient.connect();
28+
} catch (error) {
29+
// We only get here if the user is running atlas-local on a unsupported platform
30+
console.warn("Atlas Local native binding not available:", error);
31+
return undefined;
32+
}
33+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { defaultDriverOptions, defaultTestConfig, expectDefined, getResponseElements, setupIntegrationTest } from "../../helpers.js";
2+
import { describe, expect, it } from "vitest";
3+
4+
5+
6+
describe("atlas-local-list-deployments", () => {
7+
const integration = setupIntegrationTest(
8+
() => defaultTestConfig,
9+
() => defaultDriverOptions
10+
);
11+
12+
it("should have correct metadata", async () => {
13+
const { tools } = await integration.mcpClient().listTools();
14+
const listDeployments = tools.find((tool) => tool.name === "atlas-local-list-deployments");
15+
expectDefined(listDeployments);
16+
expect(listDeployments.inputSchema.type).toBe("object");
17+
expectDefined(listDeployments.inputSchema.properties);
18+
expect(listDeployments.inputSchema.properties).toEqual({});
19+
});
20+
21+
it("should not crash when calling the tool", async () => {
22+
const response = await integration.mcpClient().callTool({
23+
name: "atlas-local-list-deployments",
24+
arguments: {},
25+
});
26+
const elements = getResponseElements(response.content);
27+
expect(elements).toHaveLength(2);
28+
expect(elements[0]?.text).toMatch(/Found \d+ deployments/);
29+
expect(elements[1]?.text).toContain("Deployment Name | State | MongoDB Version\n----------------|----------------|----------------\n");
30+
});
31+
});

0 commit comments

Comments
 (0)