From 2572bc3cde75aeddf710dd81b2cac809cf286e55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:38:14 +0200 Subject: [PATCH 01/15] chore(deps): bump @modelcontextprotocol/sdk from 1.18.0 to 1.19.1 (#614) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08e04b9ed..67d15d63e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2144,9 +2144,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.0.tgz", - "integrity": "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz", + "integrity": "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", From 6c5168d35796149daef09dbb06338e87257f58ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:40:00 +0200 Subject: [PATCH 02/15] chore(deps): bump mongodb-redact from 1.2.0 to 1.2.1 (#613) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67d15d63e..f03b02d86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10690,9 +10690,9 @@ "optional": true }, "node_modules/mongodb-redact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.2.0.tgz", - "integrity": "sha512-UVJBlVNEF/8UhZ/SwR+KJXqf6pVY0b0M9aBa+1cwdRAoFFqH5NZUhMdzaXCCvhY2hoPtZ32Z7vYMoDl6Msmm/g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mongodb-redact/-/mongodb-redact-1.2.1.tgz", + "integrity": "sha512-waZV5KuNXSihjIu3mgewjAxhOejDRq7W4CEbd9eb5abpKIKxP4sZm29tOaxVoCsNYhicYm4Aw9aHNERCW8uIyQ==", "license": "Apache-2.0", "dependencies": { "regexp.escape": "^2.0.1" From 6d7a8ad2844449063befac375441e410d1b25a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:41:49 +0100 Subject: [PATCH 03/15] chore(deps): bump peter-evans/create-or-update-comment from 4.0.0 to 5.0.0 (#616) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/jira-issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jira-issue.yml b/.github/workflows/jira-issue.yml index 3760b59c4..f66431ae0 100644 --- a/.github/workflows/jira-issue.yml +++ b/.github/workflows/jira-issue.yml @@ -54,7 +54,7 @@ jobs: - name: Add comment if: steps.create.outputs.issue-key - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.issue.number }} body: | @@ -104,7 +104,7 @@ jobs: transition-id: 61 - name: Add closure comment if: steps.close_jira_ticket.outcome == 'success' - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: issue-number: ${{ github.event.issue.number }} body: | From 543301c2d500895fd76f4b5caf1808d453f7170f Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Wed, 8 Oct 2025 12:24:25 +0200 Subject: [PATCH 04/15] chore: allow providing Voyage AI API key from config and tests to ensure unusable tools are not registered MCP-233 MCP-237 (#622) --- README.md | 1 + src/common/config.ts | 3 + src/server.ts | 17 +++- .../tools/mongodb/mongodbTool.test.ts | 82 ++++++++++++------- 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f8ea9afd6..469e33754 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | 300000 | Time in milliseconds after which an export is considered expired and eligible for cleanup. | | `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | 120000 | Time in milliseconds between export cleanup cycles that remove expired export files. | | `atlasTemporaryDatabaseUserLifetimeMs` | `MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS` | 14400000 | Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted. | +| `voyageApiKey` | `MDB_VOYAGE_API_KEY` | | API key for communicating with Voyage AI. Used for generating embeddings for Vector search. | #### Logger Options diff --git a/src/common/config.ts b/src/common/config.ts index cbac900c4..efcc7b4a6 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -53,6 +53,7 @@ const OPTIONS = { "exportsPath", "exportTimeoutMs", "exportCleanupIntervalMs", + "voyageApiKey", ], boolean: [ "apiDeprecationErrors", @@ -181,6 +182,7 @@ export interface UserConfig extends CliOptions { maxDocumentsPerQuery: number; maxBytesPerQuery: number; atlasTemporaryDatabaseUserLifetimeMs: number; + voyageApiKey: string; } export const defaultUserConfig: UserConfig = { @@ -210,6 +212,7 @@ export const defaultUserConfig: UserConfig = { maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours + voyageApiKey: "", }; export const config = setupUserConfig({ diff --git a/src/server.ts b/src/server.ts index 458bcd28b..794fb9867 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,7 +18,7 @@ import { UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import assert from "assert"; -import type { ToolBase } from "./tools/tool.js"; +import type { ToolBase, ToolConstructorParams } from "./tools/tool.js"; import { validateConnectionString } from "./helpers/connectionOptions.js"; import { packageInfo } from "./common/packageInfo.js"; import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js"; @@ -31,6 +31,7 @@ export interface ServerOptions { telemetry: Telemetry; elicitation: Elicitation; connectionErrorHandler: ConnectionErrorHandler; + toolConstructors?: (new (params: ToolConstructorParams) => ToolBase)[]; } export class Server { @@ -39,6 +40,7 @@ export class Server { private readonly telemetry: Telemetry; public readonly userConfig: UserConfig; public readonly elicitation: Elicitation; + private readonly toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[]; public readonly tools: ToolBase[] = []; public readonly connectionErrorHandler: ConnectionErrorHandler; @@ -51,7 +53,15 @@ export class Server { private readonly startTime: number; private readonly subscriptions = new Set(); - constructor({ session, mcpServer, userConfig, telemetry, connectionErrorHandler, elicitation }: ServerOptions) { + constructor({ + session, + mcpServer, + userConfig, + telemetry, + connectionErrorHandler, + elicitation, + toolConstructors, + }: ServerOptions) { this.startTime = Date.now(); this.session = session; this.telemetry = telemetry; @@ -59,6 +69,7 @@ export class Server { this.userConfig = userConfig; this.elicitation = elicitation; this.connectionErrorHandler = connectionErrorHandler; + this.toolConstructors = toolConstructors ?? [...AtlasTools, ...MongoDbTools]; } async connect(transport: Transport): Promise { @@ -206,7 +217,7 @@ export class Server { } private registerTools(): void { - for (const toolConstructor of [...AtlasTools, ...MongoDbTools]) { + for (const toolConstructor of this.toolConstructors) { const tool = new toolConstructor({ session: this.session, config: this.userConfig, diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index f2e4930a2..32ec0f6d1 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -1,9 +1,9 @@ -import { vi, it, describe, beforeEach, afterEach, type MockedFunction, afterAll, expect } from "vitest"; +import { vi, it, describe, beforeEach, afterEach, afterAll, expect } from "vitest"; import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { MongoDBToolBase } from "../../../../src/tools/mongodb/mongodbTool.js"; -import { type OperationType } from "../../../../src/tools/tool.js"; +import { type ToolBase, type ToolConstructorParams, type OperationType } from "../../../../src/tools/tool.js"; import { defaultDriverOptions, type UserConfig } from "../../../../src/common/config.js"; import { MCPConnectionManager } from "../../../../src/common/connectionManager.js"; import { Session } from "../../../../src/common/session.js"; @@ -19,6 +19,7 @@ import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js"; import { ErrorCodes } from "../../../../src/common/errors.js"; import { Keychain } from "../../../../src/common/keychain.js"; import { Elicitation } from "../../../../src/elicitation.js"; +import { MongoDbTools } from "../../../../src/tools/mongodb/tools.js"; const injectedErrorHandler: ConnectionErrorHandler = (error) => { switch (error.code) { @@ -51,29 +52,45 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => { } }; -describe("MongoDBTool implementations", () => { - const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []); - const executeStub: MockedFunction<() => Promise> = vi - .fn() - .mockResolvedValue({ content: [{ type: "text", text: "Something" }] }); - class RandomTool extends MongoDBToolBase { - name = "Random"; - operationType: OperationType = "read"; - protected description = "This is a tool."; - protected argsShape = {}; - public async execute(): Promise { - await this.ensureConnected(); - return executeStub(); +class RandomTool extends MongoDBToolBase { + name = "Random"; + operationType: OperationType = "read"; + protected description = "This is a tool."; + protected argsShape = {}; + public async execute(): Promise { + await this.ensureConnected(); + return { content: [{ type: "text", text: "Something" }] }; + } +} + +class UnusableVoyageTool extends MongoDBToolBase { + name = "UnusableVoyageTool"; + operationType: OperationType = "read"; + protected description = "This is a Voyage tool."; + protected argsShape = {}; + + override verifyAllowed(): boolean { + if (this.config.voyageApiKey.trim()) { + return super.verifyAllowed(); } + return false; + } + public async execute(): Promise { + await this.ensureConnected(); + return { content: [{ type: "text", text: "Something" }] }; } +} + +describe("MongoDBTool implementations", () => { + const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []); - let tool: RandomTool | undefined; let mcpClient: Client | undefined; let mcpServer: Server | undefined; let deviceId: DeviceId | undefined; async function cleanupAndStartServer( config: Partial | undefined = {}, + toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[] = [...MongoDbTools, RandomTool], errorHandler: ConnectionErrorHandler | undefined = connectionErrorHandler ): Promise { await cleanup(); @@ -126,16 +143,9 @@ describe("MongoDBTool implementations", () => { mcpServer: internalMcpServer, connectionErrorHandler: errorHandler, elicitation, + toolConstructors, }); - tool = new RandomTool({ - session, - config: userConfig, - telemetry, - elicitation, - }); - tool.register(mcpServer); - await mcpServer.connect(serverTransport); await mcpClient.connect(clientTransport); } @@ -150,8 +160,6 @@ describe("MongoDBTool implementations", () => { deviceId?.close(); deviceId = undefined; - - tool = undefined; } beforeEach(async () => { @@ -232,7 +240,7 @@ describe("MongoDBTool implementations", () => { describe("when MCP is using injected connection error handler", () => { beforeEach(async () => { - await cleanupAndStartServer(defaultTestConfig, injectedErrorHandler); + await cleanupAndStartServer(defaultTestConfig, [...MongoDbTools, RandomTool], injectedErrorHandler); }); describe("and comes across a MongoDB Error - NotConnectedToMongoDB", () => { @@ -256,7 +264,11 @@ describe("MongoDBTool implementations", () => { describe("and comes across a MongoDB Error - MisconfiguredConnectionString", () => { it("should handle the error", async () => { // This is a misconfigured connection string - await cleanupAndStartServer({ connectionString: "mongodb://localhost:1234" }, injectedErrorHandler); + await cleanupAndStartServer( + { connectionString: "mongodb://localhost:1234" }, + [...MongoDbTools, RandomTool], + injectedErrorHandler + ); const toolResponse = await mcpClient?.callTool({ name: "Random", arguments: {}, @@ -278,6 +290,7 @@ describe("MongoDBTool implementations", () => { // This is a misconfigured connection string await cleanupAndStartServer( { connectionString: mdbIntegration.connectionString(), indexCheck: true }, + [...MongoDbTools, RandomTool], injectedErrorHandler ); const toolResponse = await mcpClient?.callTool({ @@ -299,4 +312,17 @@ describe("MongoDBTool implementations", () => { }); }); }); + + describe("when a tool is not usable", () => { + it("should not even be registered", async () => { + await cleanupAndStartServer( + { connectionString: mdbIntegration.connectionString(), indexCheck: true }, + [RandomTool, UnusableVoyageTool], + injectedErrorHandler + ); + const tools = await mcpClient?.listTools({}); + expect(tools?.tools).toHaveLength(1); + expect(tools?.tools.find((tool) => tool.name === "UnusableVoyageTool")).toBeUndefined(); + }); + }); }); From 46bf661d5bb77e5244b46a6a10ea9f22592a0567 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Wed, 8 Oct 2025 13:17:37 +0200 Subject: [PATCH 05/15] chore: add isError annotation for handled errors (#623) --- src/tools/mongodb/metadata/collectionStorageSize.ts | 1 + src/tools/mongodb/read/collectionIndexes.ts | 1 + src/tools/mongodb/update/renameCollection.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/tools/mongodb/metadata/collectionStorageSize.ts b/src/tools/mongodb/metadata/collectionStorageSize.ts index 91a1c51e0..c38ccc076 100644 --- a/src/tools/mongodb/metadata/collectionStorageSize.ts +++ b/src/tools/mongodb/metadata/collectionStorageSize.ts @@ -42,6 +42,7 @@ export class CollectionStorageSizeTool extends MongoDBToolBase { type: "text", }, ], + isError: true, }; } diff --git a/src/tools/mongodb/read/collectionIndexes.ts b/src/tools/mongodb/read/collectionIndexes.ts index 818561917..84b8b1dbb 100644 --- a/src/tools/mongodb/read/collectionIndexes.ts +++ b/src/tools/mongodb/read/collectionIndexes.ts @@ -37,6 +37,7 @@ export class CollectionIndexesTool extends MongoDBToolBase { type: "text", }, ], + isError: true, }; } diff --git a/src/tools/mongodb/update/renameCollection.ts b/src/tools/mongodb/update/renameCollection.ts index be142e443..4992a3227 100644 --- a/src/tools/mongodb/update/renameCollection.ts +++ b/src/tools/mongodb/update/renameCollection.ts @@ -48,6 +48,7 @@ export class RenameCollectionTool extends MongoDBToolBase { type: "text", }, ], + isError: true, }; case "NamespaceExists": return { @@ -57,6 +58,7 @@ export class RenameCollectionTool extends MongoDBToolBase { type: "text", }, ], + isError: true, }; } } From c12de89b1edd7f9123b100d78e985aa7d6c591d9 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 15:20:30 +0200 Subject: [PATCH 06/15] feat(search): Add a new tool to list search and vector search indexes MCP-235 (#610) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/code-health-fork.yml | 5 + .github/workflows/code-health.yml | 5 + package-lock.json | 927 +++++++++++++++++- package.json | 1 + src/tools/mongodb/search/listSearchIndexes.ts | 81 ++ src/tools/mongodb/tools.ts | 2 + tests/accuracy/listSearchIndexes.test.ts | 28 + tests/accuracy/sdk/describeAccuracyTests.ts | 2 +- .../common/connectionManager.oidc.test.ts | 3 +- tests/integration/helpers.ts | 4 + .../integration/tools/atlas/clusters.test.ts | 6 +- .../tools/mongodb/mongodbClusterProcess.ts | 103 ++ .../tools/mongodb/mongodbHelpers.ts | 71 +- .../tools/mongodb/mongodbTool.test.ts | 2 +- .../mongodb/search/listSearchIndexes.test.ts | 171 ++++ 15 files changed, 1322 insertions(+), 89 deletions(-) create mode 100644 src/tools/mongodb/search/listSearchIndexes.ts create mode 100644 tests/accuracy/listSearchIndexes.test.ts create mode 100644 tests/integration/tools/mongodb/mongodbClusterProcess.ts create mode 100644 tests/integration/tools/mongodb/search/listSearchIndexes.test.ts diff --git a/.github/workflows/code-health-fork.yml b/.github/workflows/code-health-fork.yml index f5ef5f763..a07b0d902 100644 --- a/.github/workflows/code-health-fork.yml +++ b/.github/workflows/code-health-fork.yml @@ -20,6 +20,11 @@ jobs: - uses: GitHubSecurityLab/actions-permissions/monitor@v1 if: matrix.os == 'ubuntu-latest' - uses: actions/checkout@v5 + - uses: docker/setup-docker-action@v4 + if: matrix.os == 'ubuntu-latest' + name: Setup Docker Environment + with: + set-host: true - uses: actions/setup-node@v5 with: node-version-file: package.json diff --git a/.github/workflows/code-health.yml b/.github/workflows/code-health.yml index 2dfba473d..fc8b4b153 100644 --- a/.github/workflows/code-health.yml +++ b/.github/workflows/code-health.yml @@ -21,6 +21,11 @@ jobs: - uses: GitHubSecurityLab/actions-permissions/monitor@v1 if: matrix.os == 'ubuntu-latest' - uses: actions/checkout@v5 + - uses: docker/setup-docker-action@v4 + if: matrix.os == 'ubuntu-latest' + name: Setup Docker Environment + with: + set-host: true - uses: actions/setup-node@v5 with: node-version-file: package.json diff --git a/package-lock.json b/package-lock.json index f03b02d86..7a6a7d015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "proper-lockfile": "^4.1.2", "semver": "^7.7.2", "simple-git": "^3.28.0", + "testcontainers": "^11.7.1", "tsx": "^4.20.5", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0", @@ -1072,6 +1073,13 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1833,6 +1841,58 @@ "dev": true, "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1998,6 +2058,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", @@ -5296,6 +5367,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.44", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.44.tgz", + "integrity": "sha512-fUpIHlsbYpxAJb285xx3vp7q5wf5mjqSn3cYwl/MhiM+DB99OdO5sOCPlO0PjO+TyOtphPs7tMVLU/RtOo/JjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5420,6 +5514,43 @@ "@types/send": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", + "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.129", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", + "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stylis": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", @@ -6117,6 +6248,181 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -6224,6 +6530,13 @@ "js-tokens": "^9.0.1" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -6233,6 +6546,13 @@ "node": ">= 0.4" } }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6268,6 +6588,95 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-fs": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz", + "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6546,6 +6955,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6922,6 +7341,36 @@ "node": ">=18" } }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7056,6 +7505,33 @@ "node": ">=10.0.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -7167,9 +7643,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7497,26 +7973,103 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "license": "BSD-3-Clause", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/docker-compose": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz", + "integrity": "sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, "engines": { - "node": ">=0.3.1" + "node": ">= 6" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "node_modules/dockerode": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.9.tgz", + "integrity": "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/dompurify": { @@ -8152,6 +8705,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -8263,6 +8826,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8735,6 +9305,19 @@ "node": ">=6" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -10088,6 +10671,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10128,6 +10764,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10455,8 +11105,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/mobx": { "version": "6.13.7", @@ -11906,6 +12556,23 @@ "dev": true, "license": "ISC" }, + "node_modules/properties-reader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", + "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -11948,8 +12615,8 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -12224,6 +12891,39 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -13330,12 +14030,41 @@ "rxjs": "^7.8.1" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -13408,6 +14137,18 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13837,11 +14578,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -13853,8 +14594,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -13865,6 +14606,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -13880,7 +14622,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -13890,15 +14631,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/tar-fs/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13912,8 +14653,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -14096,6 +14837,107 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/testcontainers": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.7.1.tgz", + "integrity": "sha512-fjut+07G4Avp6Lly/6hQePpUpQFv9ZyQd+7JC5iCDKg+dWa2Sw7fXD3pBrkzslYFfKqGx9M6kyIaLpg9VeMsjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.44", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.3.0", + "dockerode": "^4.0.8", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5", + "undici": "^7.16.0" + } + }, + "node_modules/testcontainers/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/testcontainers/node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/testcontainers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/testcontainers/node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/throttleit": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", @@ -14208,6 +15050,16 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -15355,8 +16207,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -15478,6 +16328,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 89a590c09..2801da8de 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "proper-lockfile": "^4.1.2", "semver": "^7.7.2", "simple-git": "^3.28.0", + "testcontainers": "^11.7.1", "tsx": "^4.20.5", "typescript": "^5.9.2", "typescript-eslint": "^8.41.0", diff --git a/src/tools/mongodb/search/listSearchIndexes.ts b/src/tools/mongodb/search/listSearchIndexes.ts new file mode 100644 index 000000000..1b520d523 --- /dev/null +++ b/src/tools/mongodb/search/listSearchIndexes.ts @@ -0,0 +1,81 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { ToolArgs, OperationType } from "../../tool.js"; +import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; +import { formatUntrustedData } from "../../tool.js"; +import { EJSON } from "bson"; + +export type SearchIndexStatus = { + name: string; + type: string; + status: string; + queryable: boolean; + latestDefinition: Document; +}; + +export class ListSearchIndexesTool extends MongoDBToolBase { + public name = "list-search-indexes"; + protected description = "Describes the search and vector search indexes for a single collection"; + protected argsShape = DbOperationArgs; + public operationType: OperationType = "metadata"; + + protected async execute({ database, collection }: ToolArgs): Promise { + const provider = await this.ensureConnected(); + const indexes = await provider.getSearchIndexes(database, collection); + const trimmedIndexDefinitions = this.pickRelevantInformation(indexes); + + if (trimmedIndexDefinitions.length > 0) { + return { + content: formatUntrustedData( + `Found ${trimmedIndexDefinitions.length} search and vector search indexes in ${database}.${collection}`, + trimmedIndexDefinitions.map((index) => EJSON.stringify(index)).join("\n") + ), + }; + } else { + return { + content: formatUntrustedData( + "Could not retrieve search indexes", + `There are no search or vector search indexes in ${database}.${collection}` + ), + }; + } + } + + protected verifyAllowed(): boolean { + // Only enable this on tests for now. + return process.env.VITEST === "true"; + } + + /** + * Atlas Search index status contains a lot of information that is not relevant for the agent at this stage. + * Like for example, the status on each of the dedicated nodes. We only care about the main status, if it's + * queryable and the index name. We are also picking the index definition as it can be used by the agent to + * understand which fields are available for searching. + **/ + protected pickRelevantInformation(indexes: Record[]): SearchIndexStatus[] { + return indexes.map((index) => ({ + name: (index["name"] ?? "default") as string, + type: (index["type"] ?? "UNKNOWN") as string, + status: (index["status"] ?? "UNKNOWN") as string, + queryable: (index["queryable"] ?? false) as boolean, + latestDefinition: index["latestDefinition"] as Document, + })); + } + + protected handleError( + error: unknown, + args: ToolArgs + ): Promise | CallToolResult { + if (error instanceof Error && "codeName" in error && error.codeName === "SearchNotEnabled") { + return { + content: [ + { + text: "This MongoDB cluster does not support Search Indexes. Make sure you are using an Atlas Cluster, either remotely in Atlas or using the Atlas Local image, or your cluster supports MongoDB Search.", + type: "text", + isError: true, + }, + ], + }; + } + return super.handleError(error, args); + } +} diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index 00575ee05..1567fd4f8 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -19,6 +19,7 @@ import { ExplainTool } from "./metadata/explain.js"; import { CreateCollectionTool } from "./create/createCollection.js"; import { LogsTool } from "./metadata/logs.js"; import { ExportTool } from "./read/export.js"; +import { ListSearchIndexesTool } from "./search/listSearchIndexes.js"; export const MongoDbTools = [ ConnectTool, @@ -42,4 +43,5 @@ export const MongoDbTools = [ CreateCollectionTool, LogsTool, ExportTool, + ListSearchIndexesTool, ]; diff --git a/tests/accuracy/listSearchIndexes.test.ts b/tests/accuracy/listSearchIndexes.test.ts new file mode 100644 index 000000000..6f4a2d1ce --- /dev/null +++ b/tests/accuracy/listSearchIndexes.test.ts @@ -0,0 +1,28 @@ +import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js"; + +describeAccuracyTests([ + { + prompt: "how many search indexes do I have in the collection mydb.mycoll?", + expectedToolCalls: [ + { + toolName: "list-search-indexes", + parameters: { + database: "mydb", + collection: "mycoll", + }, + }, + ], + }, + { + prompt: "which vector search indexes do I have in mydb.mycoll?", + expectedToolCalls: [ + { + toolName: "list-search-indexes", + parameters: { + database: "mydb", + collection: "mycoll", + }, + }, + ], + }, +]); diff --git a/tests/accuracy/sdk/describeAccuracyTests.ts b/tests/accuracy/sdk/describeAccuracyTests.ts index 6617a84f7..8073518b4 100644 --- a/tests/accuracy/sdk/describeAccuracyTests.ts +++ b/tests/accuracy/sdk/describeAccuracyTests.ts @@ -63,7 +63,7 @@ export function describeAccuracyTests(accuracyTestConfigs: AccuracyTestConfig[]) eachModel(`$displayName`, function (model) { const configsWithDescriptions = getConfigsWithDescriptions(accuracyTestConfigs); const accuracyRunId = `${process.env.MDB_ACCURACY_RUN_ID}`; - const mdbIntegration = setupMongoDBIntegrationTest({}, []); + const mdbIntegration = setupMongoDBIntegrationTest(); const { populateTestData, cleanupTestDatabases } = prepareTestData(mdbIntegration); let commitSHA: string; diff --git a/tests/integration/common/connectionManager.oidc.test.ts b/tests/integration/common/connectionManager.oidc.test.ts index de1801a56..9f30cf32e 100644 --- a/tests/integration/common/connectionManager.oidc.test.ts +++ b/tests/integration/common/connectionManager.oidc.test.ts @@ -144,8 +144,7 @@ describe.skipIf(process.platform !== "linux")("ConnectionManager OIDC Tests", as defaults: {}, }), }), - { enterprise: true, version: mongodbVersion }, - serverArgs + { runner: true, downloadOptions: { enterprise: true, version: mongodbVersion }, serverArgs } ); } diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index d62354a83..6282851cf 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -382,3 +382,7 @@ export function getDataFromUntrustedContent(content: string): string { } return match.groups.data.trim(); } + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 0e666be7d..0c7b75596 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,13 +1,9 @@ import type { Session } from "../../../../src/common/session.js"; -import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; +import { expectDefined, getDataFromUntrustedContent, getResponseElements, sleep } from "../../helpers.js"; import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js"; import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - async function deleteCluster( session: Session, projectId: string, diff --git a/tests/integration/tools/mongodb/mongodbClusterProcess.ts b/tests/integration/tools/mongodb/mongodbClusterProcess.ts new file mode 100644 index 000000000..b0f7ee863 --- /dev/null +++ b/tests/integration/tools/mongodb/mongodbClusterProcess.ts @@ -0,0 +1,103 @@ +import fs from "fs/promises"; +import path from "path"; +import type { MongoClusterOptions } from "mongodb-runner"; +import { GenericContainer } from "testcontainers"; +import { MongoCluster } from "mongodb-runner"; +import { ShellWaitStrategy } from "testcontainers/build/wait-strategies/shell-wait-strategy.js"; + +export type MongoRunnerConfiguration = { + runner: true; + downloadOptions: MongoClusterOptions["downloadOptions"]; + serverArgs: string[]; +}; + +export type MongoSearchConfiguration = { search: true; image?: string }; +export type MongoClusterConfiguration = MongoRunnerConfiguration | MongoSearchConfiguration; + +const DOWNLOAD_RETRIES = 10; + +export class MongoDBClusterProcess { + static async spinUp(config: MongoClusterConfiguration): Promise { + if (MongoDBClusterProcess.isSearchOptions(config)) { + const runningContainer = await new GenericContainer(config.image ?? "mongodb/mongodb-atlas-local:8") + .withExposedPorts(27017) + .withCommand(["/usr/local/bin/runner", "server"]) + .withWaitStrategy(new ShellWaitStrategy(`mongosh --eval 'db.test.getSearchIndexes()'`)) + .start(); + + return new MongoDBClusterProcess( + () => runningContainer.stop(), + () => `mongodb://${runningContainer.getHost()}:${runningContainer.getMappedPort(27017)}` + ); + } else if (MongoDBClusterProcess.isMongoRunnerOptions(config)) { + const { downloadOptions, serverArgs } = config; + + const tmpDir = path.join(__dirname, "..", "..", "..", "tmp"); + await fs.mkdir(tmpDir, { recursive: true }); + let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs"); + for (let i = 0; i < DOWNLOAD_RETRIES; i++) { + try { + const mongoCluster = await MongoCluster.start({ + tmpDir: dbsDir, + logDir: path.join(tmpDir, "mongodb-runner", "logs"), + topology: "standalone", + version: downloadOptions?.version ?? "8.0.12", + downloadOptions, + args: serverArgs, + }); + + return new MongoDBClusterProcess( + () => mongoCluster.close(), + () => mongoCluster.connectionString + ); + } catch (err) { + if (i < 5) { + // Just wait a little bit and retry + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + // If we still fail after 5 seconds, try another db dir + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.` + ); + dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`); + } + } + } + throw new Error(`Could not download cluster with configuration: ${JSON.stringify(config)}`); + } else { + throw new Error(`Unsupported configuration: ${JSON.stringify(config)}`); + } + } + + private constructor( + private readonly tearDownFunction: () => Promise, + private readonly connectionStringFunction: () => string + ) {} + + connectionString(): string { + return this.connectionStringFunction(); + } + + async close(): Promise { + await this.tearDownFunction(); + } + + static isConfigurationSupportedInCurrentEnv(config: MongoClusterConfiguration): boolean { + if (MongoDBClusterProcess.isSearchOptions(config) && process.env.GITHUB_ACTIONS === "true") { + return process.platform === "linux"; + } + + return true; + } + + private static isSearchOptions(opt: MongoClusterConfiguration): opt is MongoSearchConfiguration { + return (opt as MongoSearchConfiguration)?.search === true; + } + + private static isMongoRunnerOptions(opt: MongoClusterConfiguration): opt is MongoRunnerConfiguration { + return (opt as MongoRunnerConfiguration)?.runner === true; + } +} diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index 60961df32..e3a332ae8 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -1,5 +1,3 @@ -import type { MongoClusterOptions } from "mongodb-runner"; -import { MongoCluster } from "mongodb-runner"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs/promises"; @@ -16,6 +14,8 @@ import { import type { UserConfig, DriverOptions } from "../../../../src/common/config.js"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { EJSON } from "bson"; +import { MongoDBClusterProcess } from "./mongodbClusterProcess.js"; +import type { MongoClusterConfiguration } from "./mongodbClusterProcess.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -49,6 +49,12 @@ const testDataPaths = [ }, ]; +const DEFAULT_MONGODB_PROCESS_OPTIONS: MongoClusterConfiguration = { + runner: true, + downloadOptions: { enterprise: false }, + serverArgs: [], +}; + interface MongoDBIntegrationTest { mongoClient: () => MongoClient; connectionString: () => string; @@ -58,16 +64,17 @@ interface MongoDBIntegrationTest { export type MongoDBIntegrationTestCase = IntegrationTest & MongoDBIntegrationTest & { connectMcpClient: () => Promise }; +export type MongoSearchConfiguration = { search: true; image?: string }; + export function describeWithMongoDB( name: string, fn: (integration: MongoDBIntegrationTestCase) => void, getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig, getDriverOptions: (mdbIntegration: MongoDBIntegrationTest) => DriverOptions = () => defaultDriverOptions, - downloadOptions: MongoClusterOptions["downloadOptions"] = { enterprise: false }, - serverArgs: string[] = [] + downloadOptions: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS ): void { - describe(name, () => { - const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions, serverArgs); + describe.skipIf(!MongoDBClusterProcess.isConfigurationSupportedInCurrentEnv(downloadOptions))(name, () => { + const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions); const integration = setupIntegrationTest( () => ({ ...getUserConfig(mdbIntegration), @@ -94,10 +101,9 @@ export function describeWithMongoDB( } export function setupMongoDBIntegrationTest( - downloadOptions: MongoClusterOptions["downloadOptions"], - serverArgs: string[] + configuration: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS ): MongoDBIntegrationTest { - let mongoCluster: MongoCluster | undefined; + let mongoCluster: MongoDBClusterProcess | undefined; let mongoClient: MongoClient | undefined; let randomDbName: string; @@ -111,44 +117,7 @@ export function setupMongoDBIntegrationTest( }); beforeAll(async function () { - // Downloading Windows executables in CI takes a long time because - // they include debug symbols... - const tmpDir = path.join(__dirname, "..", "..", "..", "tmp"); - await fs.mkdir(tmpDir, { recursive: true }); - - // On Windows, we may have a situation where mongod.exe is not fully released by the OS - // before we attempt to run it again, so we add a retry. - let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs"); - for (let i = 0; i < 10; i++) { - try { - mongoCluster = await MongoCluster.start({ - tmpDir: dbsDir, - logDir: path.join(tmpDir, "mongodb-runner", "logs"), - topology: "standalone", - version: downloadOptions?.version ?? "8.0.12", - downloadOptions, - args: serverArgs, - }); - - return; - } catch (err) { - if (i < 5) { - // Just wait a little bit and retry - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } else { - // If we still fail after 5 seconds, try another db dir - console.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.` - ); - dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`); - } - } - } - - throw new Error("Failed to start cluster after 10 attempts"); + mongoCluster = await MongoDBClusterProcess.spinUp(configuration); }, 120_000); afterAll(async function () { @@ -161,7 +130,7 @@ export function setupMongoDBIntegrationTest( throw new Error("beforeAll() hook not ran yet"); } - return mongoCluster.connectionString; + return mongoCluster.connectionString(); }; return { @@ -172,7 +141,6 @@ export function setupMongoDBIntegrationTest( return mongoClient; }, connectionString: getConnectionString, - randomDbName: () => randomDbName, }; } @@ -268,6 +236,11 @@ export function prepareTestData(integration: MongoDBIntegrationTest): { }; } +export function getSingleDocFromUntrustedContent(content: string): T { + const data = getDataFromUntrustedContent(content); + return EJSON.parse(data, { relaxed: true }) as T; +} + export function getDocsFromUntrustedContent(content: string): T[] { const data = getDataFromUntrustedContent(content); return EJSON.parse(data, { relaxed: true }) as T[]; diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index 32ec0f6d1..ea43345cd 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -82,7 +82,7 @@ class UnusableVoyageTool extends MongoDBToolBase { } describe("MongoDBTool implementations", () => { - const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []); + const mdbIntegration = setupMongoDBIntegrationTest(); let mcpClient: Client | undefined; let mcpServer: Server | undefined; diff --git a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts new file mode 100644 index 000000000..97571c0a9 --- /dev/null +++ b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts @@ -0,0 +1,171 @@ +import { describeWithMongoDB, getSingleDocFromUntrustedContent } from "../mongodbHelpers.js"; +import { describe, it, expect, beforeEach } from "vitest"; +import { + getResponseContent, + databaseCollectionParameters, + validateToolMetadata, + validateThrowsForInvalidArguments, + databaseCollectionInvalidArgs, + sleep, + getDataFromUntrustedContent, +} from "../../../helpers.js"; +import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; +import type { SearchIndexStatus } from "../../../../../src/tools/mongodb/search/listSearchIndexes.js"; + +const SEARCH_RETRIES = 200; +const SEARCH_TIMEOUT = 20_000; + +describeWithMongoDB("list search indexes tool in local MongoDB", (integration) => { + validateToolMetadata( + integration, + "list-search-indexes", + "Describes the search and vector search indexes for a single collection", + databaseCollectionParameters + ); + + validateThrowsForInvalidArguments(integration, "list-search-indexes", databaseCollectionInvalidArgs); + + it("fails for clusters without MongoDB Search", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "list-search-indexes", + arguments: { database: "any", collection: "foo" }, + }); + const content = getResponseContent(response.content); + expect(content).toEqual( + "This MongoDB cluster does not support Search Indexes. Make sure you are using an Atlas Cluster, either remotely in Atlas or using the Atlas Local image, or your cluster supports MongoDB Search." + ); + }); +}); + +describeWithMongoDB( + "list search indexes tool in Atlas", + (integration) => { + let provider: NodeDriverServiceProvider; + + beforeEach(async ({ signal }) => { + await integration.connectMcpClient(); + provider = integration.mcpServer().session.serviceProvider; + await waitUntilSearchIsReady(provider, signal); + }); + + describe("when the collection does not exist", () => { + it("returns an empty list of indexes", async () => { + const response = await integration.mcpClient().callTool({ + name: "list-search-indexes", + arguments: { database: "any", collection: "foo" }, + }); + const responseContent = getResponseContent(response.content); + const content = getDataFromUntrustedContent(responseContent); + expect(responseContent).toContain("Could not retrieve search indexes"); + expect(content).toEqual("There are no search or vector search indexes in any.foo"); + }); + }); + + describe("when there are no indexes", () => { + it("returns an empty list of indexes", async () => { + const response = await integration.mcpClient().callTool({ + name: "list-search-indexes", + arguments: { database: "any", collection: "foo" }, + }); + const responseContent = getResponseContent(response.content); + const content = getDataFromUntrustedContent(responseContent); + expect(responseContent).toContain("Could not retrieve search indexes"); + expect(content).toEqual("There are no search or vector search indexes in any.foo"); + }); + }); + + describe("when there are indexes", () => { + beforeEach(async () => { + await provider.insertOne("any", "foo", { field1: "yay" }); + await provider.createSearchIndexes("any", "foo", [{ definition: { mappings: { dynamic: true } } }]); + }); + + it("returns the list of existing indexes", { timeout: SEARCH_TIMEOUT }, async () => { + const response = await integration.mcpClient().callTool({ + name: "list-search-indexes", + arguments: { database: "any", collection: "foo" }, + }); + const content = getResponseContent(response.content); + const indexDefinition = getSingleDocFromUntrustedContent(content); + + expect(indexDefinition?.name).toEqual("default"); + expect(indexDefinition?.type).toEqual("search"); + expect(indexDefinition?.latestDefinition).toEqual({ mappings: { dynamic: true, fields: {} } }); + }); + + it( + "returns the list of existing indexes and detects if they are queryable", + { timeout: SEARCH_TIMEOUT }, + async ({ signal }) => { + await waitUntilIndexIsQueryable(provider, "any", "foo", "default", signal); + + const response = await integration.mcpClient().callTool({ + name: "list-search-indexes", + arguments: { database: "any", collection: "foo" }, + }); + + const content = getResponseContent(response.content); + const indexDefinition = getSingleDocFromUntrustedContent(content); + + expect(indexDefinition?.name).toEqual("default"); + expect(indexDefinition?.type).toEqual("search"); + expect(indexDefinition?.latestDefinition).toEqual({ mappings: { dynamic: true, fields: {} } }); + expect(indexDefinition?.queryable).toEqual(true); + expect(indexDefinition?.status).toEqual("READY"); + } + ); + }); + }, + undefined, // default user config + undefined, // default driver config + { search: true } // use a search cluster +); + +async function waitUntilSearchIsReady(provider: NodeDriverServiceProvider, abortSignal: AbortSignal): Promise { + let lastError: unknown = null; + + for (let i = 0; i < SEARCH_RETRIES && !abortSignal.aborted; i++) { + try { + await provider.insertOne("tmp", "test", { field1: "yay" }); + await provider.createSearchIndexes("tmp", "test", [{ definition: { mappings: { dynamic: true } } }]); + return; + } catch (err) { + lastError = err; + await sleep(100); + } + } + + throw new Error(`Search Management Index is not ready.\nlastError: ${JSON.stringify(lastError)}`); +} + +async function waitUntilIndexIsQueryable( + provider: NodeDriverServiceProvider, + database: string, + collection: string, + indexName: string, + abortSignal: AbortSignal +): Promise { + let lastIndexStatus: unknown = null; + let lastError: unknown = null; + + for (let i = 0; i < SEARCH_RETRIES && !abortSignal.aborted; i++) { + try { + const [indexStatus] = await provider.getSearchIndexes(database, collection, indexName); + lastIndexStatus = indexStatus; + + if (indexStatus?.queryable === true) { + return; + } + } catch (err) { + lastError = err; + await sleep(100); + } + } + + throw new Error( + `Index ${indexName} in ${database}.${collection} is not ready: +lastIndexStatus: ${JSON.stringify(lastIndexStatus)} +lastError: ${JSON.stringify(lastError)}` + ); +} From d2fa04c3bd306e0b817d81f4f70bc57ec7e791aa Mon Sep 17 00:00:00 2001 From: Kyle Lai <122811196+kylelai1@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:52:28 -0400 Subject: [PATCH 07/15] feat: Atlas list performance advisor tool (#609) --- scripts/accuracy/runAccuracyTests.sh | 5 + scripts/filter.ts | 4 + src/common/atlas/apiClient.ts | 48 ++ src/common/atlas/cluster.ts | 34 +- src/common/atlas/openapi.d.ts | 499 ++++++++++++++++++ src/common/atlas/performanceAdvisorUtils.ts | 153 ++++++ src/common/logger.ts | 5 + src/tools/atlas/atlasTool.ts | 17 +- src/tools/atlas/read/getPerformanceAdvisor.ts | 129 +++++ src/tools/atlas/tools.ts | 2 + tests/accuracy/getPerformanceAdvisor.test.ts | 165 ++++++ tests/accuracy/sdk/accuracyTestingClient.ts | 16 +- tests/accuracy/sdk/describeAccuracyTests.ts | 9 +- tests/integration/helpers.ts | 2 + tests/integration/tools/atlas/atlasHelpers.ts | 91 +++- .../integration/tools/atlas/clusters.test.ts | 71 +-- .../tools/atlas/performanceAdvisor.test.ts | 231 ++++++++ 17 files changed, 1413 insertions(+), 68 deletions(-) create mode 100644 src/common/atlas/performanceAdvisorUtils.ts create mode 100644 src/tools/atlas/read/getPerformanceAdvisor.ts create mode 100644 tests/accuracy/getPerformanceAdvisor.test.ts create mode 100644 tests/integration/tools/atlas/performanceAdvisor.test.ts diff --git a/scripts/accuracy/runAccuracyTests.sh b/scripts/accuracy/runAccuracyTests.sh index 180bd96fb..0b5aa015c 100644 --- a/scripts/accuracy/runAccuracyTests.sh +++ b/scripts/accuracy/runAccuracyTests.sh @@ -8,6 +8,11 @@ export MDB_ACCURACY_RUN_ID=$(npx uuid v4) # export MDB_AZURE_OPEN_AI_API_KEY="" # export MDB_AZURE_OPEN_AI_API_URL="" +# For providing Atlas API credentials (required for Atlas tools) +# Set dummy values for testing (allows Atlas tools to be registered for mocking) +export MDB_MCP_API_CLIENT_ID=${MDB_MCP_API_CLIENT_ID:-"test-atlas-client-id"} +export MDB_MCP_API_CLIENT_SECRET=${MDB_MCP_API_CLIENT_SECRET:-"test-atlas-client-secret"} + # For providing a mongodb based storage to store accuracy result # export MDB_ACCURACY_MDB_URL="" # export MDB_ACCURACY_MDB_DB="" diff --git a/scripts/filter.ts b/scripts/filter.ts index 714214904..d56fb5b0d 100755 --- a/scripts/filter.ts +++ b/scripts/filter.ts @@ -41,6 +41,10 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document { "deleteProjectIpAccessList", "listOrganizationProjects", "listAlerts", + "listDropIndexes", + "listClusterSuggestedIndexes", + "listSchemaAdvice", + "listSlowQueries", ]; const filteredPaths = {}; diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 671f0dd64..3d511d3c4 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -429,6 +429,42 @@ export class ApiClient { return data; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async listDropIndexes(options: FetchOptions) { + const { data, error, response } = await this.client.GET( + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/dropIndexSuggestions", + options + ); + if (error) { + throw ApiClientError.fromError(response, error); + } + return data; + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async listSchemaAdvice(options: FetchOptions) { + const { data, error, response } = await this.client.GET( + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/schemaAdvice", + options + ); + if (error) { + throw ApiClientError.fromError(response, error); + } + return data; + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async listClusterSuggestedIndexes(options: FetchOptions) { + const { data, error, response } = await this.client.GET( + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/suggestedIndexes", + options + ); + if (error) { + throw ApiClientError.fromError(response, error); + } + return data; + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async listDatabaseUsers(options: FetchOptions) { const { data, error, response } = await this.client.GET( @@ -508,6 +544,18 @@ export class ApiClient { return data; } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async listSlowQueries(options: FetchOptions) { + const { data, error, response } = await this.client.GET( + "/api/atlas/v2/groups/{groupId}/processes/{processId}/performanceAdvisor/slowQueryLogs", + options + ); + if (error) { + throw ApiClientError.fromError(response, error); + } + return data; + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type async listOrganizations(options?: FetchOptions) { const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options); diff --git a/src/common/atlas/cluster.ts b/src/common/atlas/cluster.ts index d0a7f6358..1ea30286b 100644 --- a/src/common/atlas/cluster.ts +++ b/src/common/atlas/cluster.ts @@ -1,7 +1,17 @@ import type { ClusterDescription20240805, FlexClusterDescription20241113 } from "./openapi.js"; import type { ApiClient } from "./apiClient.js"; import { LogId } from "../logger.js"; +import { ConnectionString } from "mongodb-connection-string-url"; +type AtlasProcessId = `${string}:${number}`; + +function extractProcessIds(connectionString: string): Array { + if (!connectionString) { + return []; + } + const connectionStringUrl = new ConnectionString(connectionString); + return connectionStringUrl.hosts as Array; +} export interface Cluster { name?: string; instanceType: "FREE" | "DEDICATED" | "FLEX"; @@ -9,16 +19,19 @@ export interface Cluster { state?: "IDLE" | "CREATING" | "UPDATING" | "DELETING" | "REPAIRING"; mongoDBVersion?: string; connectionString?: string; + processIds?: Array; } export function formatFlexCluster(cluster: FlexClusterDescription20241113): Cluster { + const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard; return { name: cluster.name, instanceType: "FLEX", instanceSize: undefined, state: cluster.stateName, mongoDBVersion: cluster.mongoDBVersion, - connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard, + connectionString, + processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""), }; } @@ -52,6 +65,7 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster { const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN"; const clusterInstanceType = instanceSize === "M0" ? "FREE" : "DEDICATED"; + const connectionString = cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard; return { name: cluster.name, @@ -59,7 +73,8 @@ export function formatCluster(cluster: ClusterDescription20240805): Cluster { instanceSize: clusterInstanceType === "DEDICATED" ? instanceSize : undefined, state: cluster.stateName, mongoDBVersion: cluster.mongoDBVersion, - connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard, + connectionString, + processIds: extractProcessIds(cluster.connectionStrings?.standard ?? ""), }; } @@ -96,3 +111,18 @@ export async function inspectCluster(apiClient: ApiClient, projectId: string, cl } } } + +export async function getProcessIdsFromCluster( + apiClient: ApiClient, + projectId: string, + clusterName: string +): Promise> { + try { + const cluster = await inspectCluster(apiClient, projectId, clusterName); + return cluster.processIds || []; + } catch (error) { + throw new Error( + `Failed to get processIds from cluster: ${error instanceof Error ? error.message : String(error)}` + ); + } +} diff --git a/src/common/atlas/openapi.d.ts b/src/common/atlas/openapi.d.ts index 890c45c7c..686f45d48 100644 --- a/src/common/atlas/openapi.d.ts +++ b/src/common/atlas/openapi.d.ts @@ -194,6 +194,66 @@ export interface paths { patch?: never; trace?: never; }; + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/dropIndexSuggestions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return All Suggested Indexes to Drop + * @description Returns the indexes that the Performance Advisor suggests to drop. The Performance Advisor suggests dropping unused, redundant, and hidden indexes to improve write performance and increase storage space. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. + */ + get: operations["listDropIndexes"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/schemaAdvice": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return Schema Advice + * @description Returns the schema suggestions that the Performance Advisor detects. The Performance Advisor provides holistic schema recommendations for your cluster by sampling documents in your most active collections and collections with slow-running queries. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. + */ + get: operations["listSchemaAdvice"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/suggestedIndexes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return All Suggested Indexes + * @description Returns the indexes that the Performance Advisor suggests. The Performance Advisor monitors queries that MongoDB considers slow and suggests new indexes to improve query performance. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. + */ + get: operations["listClusterSuggestedIndexes"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/atlas/v2/groups/{groupId}/databaseUsers": { parameters: { query?: never; @@ -286,6 +346,26 @@ export interface paths { patch?: never; trace?: never; }; + "/api/atlas/v2/groups/{groupId}/processes/{processId}/performanceAdvisor/slowQueryLogs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return Slow Queries + * @description Returns log lines for slow queries that the Performance Advisor and Query Profiler identified. The Performance Advisor monitors queries that MongoDB considers slow and suggests new indexes to improve query performance. MongoDB Cloud bases the threshold for slow queries on the average time of operations on your cluster. This enables workload-relevant recommendations. To use this resource, the requesting Service Account or API Key must have any Project Data Access role or the Project Observability Viewer role. + */ + get: operations["listSlowQueries"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/atlas/v2/orgs": { parameters: { query?: never; @@ -1735,6 +1815,11 @@ export interface components { * @example 32b6e34b3d91647abb20e7b8 */ readonly roleId?: string; + /** + * @description Provision status of the service account. + * @enum {string} + */ + readonly status?: "IN_PROGRESS" | "COMPLETE" | "FAILED" | "NOT_INITIATED"; } & { /** * @description discriminator enum property added by openapi-typescript @@ -3113,6 +3198,39 @@ export interface components { /** @description Flag that indicates whether this cluster enables disk auto-scaling. The maximum memory allowed for the selected cluster tier and the oplog size can limit storage auto-scaling. */ enabled?: boolean; }; + DropIndexSuggestionsIndex: { + /** + * Format: int64 + * @description Usage count (since last restart) of index. + */ + accessCount?: number; + /** @description List that contains documents that specify a key in the index and its sort order. */ + index?: Record[]; + /** @description Name of index. */ + name?: string; + /** @description Human-readable label that identifies the namespace on the specified host. The resource expresses this parameter value as `.`. */ + namespace?: string; + /** @description List that contains strings that specifies the shards where the index is found. */ + shards?: string[]; + /** + * Format: date-time + * @description Date of most recent usage of index. This parameter expresses its value in the ISO 8601 timestamp format in UTC. + */ + since?: string; + /** + * Format: int64 + * @description Size of index. + */ + sizeBytes?: number; + }; + DropIndexSuggestionsResponse: { + /** @description List that contains the documents with information about the hidden indexes that the Performance Advisor suggests to remove. */ + readonly hiddenIndexes?: components["schemas"]["DropIndexSuggestionsIndex"][]; + /** @description List that contains the documents with information about the redundant indexes that the Performance Advisor suggests to remove. */ + readonly redundantIndexes?: components["schemas"]["DropIndexSuggestionsIndex"][]; + /** @description List that contains the documents with information about the unused indexes that the Performance Advisor suggests to remove. */ + readonly unusedIndexes?: components["schemas"]["DropIndexSuggestionsIndex"][]; + }; /** @description MongoDB employee granted access level and expiration for a cluster. */ EmployeeAccessGrantView: { /** @@ -4379,6 +4497,156 @@ export interface components { */ readonly totalCount?: number; }; + PerformanceAdvisorIndex: { + /** + * Format: double + * @description The average size of an object in the collection of this index. + */ + readonly avgObjSize?: number; + /** + * @description Unique 24-hexadecimal digit string that identifies this index. + * @example 32b6e34b3d91647abb20e7b8 + */ + readonly id?: string; + /** @description List that contains unique 24-hexadecimal character string that identifies the query shapes in this response that the Performance Advisor suggests. */ + readonly impact?: string[]; + /** @description List that contains documents that specify a key in the index and its sort order. */ + readonly index?: { + [key: string]: 1 | -1; + }[]; + /** @description Human-readable label that identifies the namespace on the specified host. The resource expresses this parameter value as `.`. */ + readonly namespace?: string; + /** + * Format: double + * @description Estimated performance improvement that the suggested index provides. This value corresponds to **Impact** in the Performance Advisor user interface. + */ + readonly weight?: number; + }; + /** @description Details that this resource returned about the specified query. */ + PerformanceAdvisorOpStats: { + /** + * Format: int64 + * @description Length of time expressed during which the query finds suggested indexes among the managed namespaces in the cluster. This parameter expresses its value in milliseconds. This parameter relates to the **duration** query parameter. + */ + readonly ms?: number; + /** + * Format: int64 + * @description Number of results that the query returns. + */ + readonly nReturned?: number; + /** + * Format: int64 + * @description Number of documents that the query read. + */ + readonly nScanned?: number; + /** + * Format: int64 + * @description Date and time from which the query retrieves the suggested indexes. This parameter expresses its value in the number of seconds that have elapsed since the UNIX epoch. This parameter relates to the **since** query parameter. + */ + readonly ts?: number; + }; + PerformanceAdvisorOperationView: { + /** @description List that contains the search criteria that the query uses. To use the values in key-value pairs in these predicates requires **Project Data Access Read Only** permissions or greater. Otherwise, MongoDB Cloud redacts these values. */ + readonly predicates?: Record[]; + stats?: components["schemas"]["PerformanceAdvisorOpStats"]; + }; + PerformanceAdvisorResponse: { + /** @description List of query predicates, sorts, and projections that the Performance Advisor suggests. */ + readonly shapes?: components["schemas"]["PerformanceAdvisorShape"][]; + /** @description List that contains the documents with information about the indexes that the Performance Advisor suggests. */ + readonly suggestedIndexes?: components["schemas"]["PerformanceAdvisorIndex"][]; + }; + PerformanceAdvisorShape: { + /** + * Format: int64 + * @description Average duration in milliseconds for the queries examined that match this shape. + */ + readonly avgMs?: number; + /** + * Format: int64 + * @description Number of queries examined that match this shape. + */ + readonly count?: number; + /** + * @description Unique 24-hexadecimal digit string that identifies this shape. This string exists only for the duration of this API request. + * @example 32b6e34b3d91647abb20e7b8 + */ + readonly id?: string; + /** + * Format: int64 + * @description Average number of documents read for every document that the query returns. + */ + readonly inefficiencyScore?: number; + /** @description Human-readable label that identifies the namespace on the specified host. The resource expresses this parameter value as `.`. */ + readonly namespace?: string; + /** @description List that contains specific about individual queries. */ + readonly operations?: components["schemas"]["PerformanceAdvisorOperationView"][]; + }; + /** @description Details of one slow query that the Performance Advisor detected. */ + PerformanceAdvisorSlowQuery: { + /** @description Text of the MongoDB log related to this slow query. */ + readonly line?: string; + metrics?: components["schemas"]["PerformanceAdvisorSlowQueryMetrics"]; + /** @description Human-readable label that identifies the namespace on the specified host. The resource expresses this parameter value as `.`. */ + readonly namespace?: string; + /** @description Operation type (read/write/command) associated with this slow query log. */ + readonly opType?: string; + /** @description Replica state associated with this slow query log. */ + readonly replicaState?: string; + }; + PerformanceAdvisorSlowQueryList: { + /** @description List of operations that the Performance Advisor detected that took longer to execute than a specified threshold. */ + readonly slowQueries?: components["schemas"]["PerformanceAdvisorSlowQuery"][]; + }; + /** @description Metrics from a slow query log. */ + PerformanceAdvisorSlowQueryMetrics: { + /** + * Format: int64 + * @description The number of documents in the collection that MongoDB scanned in order to carry out the operation. + */ + readonly docsExamined?: number; + /** + * Format: double + * @description Ratio of documents examined to documents returned. + */ + readonly docsExaminedReturnedRatio?: number; + /** + * Format: int64 + * @description The number of documents returned by the operation. + */ + readonly docsReturned?: number; + /** @description This boolean will be true when the server can identfiy the query source as non-server. This field is only available for MDB 8.0+. */ + readonly fromUserConnection?: boolean; + /** @description Indicates if the query has index coverage. */ + readonly hasIndexCoverage?: boolean; + /** @description This boolean will be true when a query cannot use the ordering in the index to return the requested sorted results; i.e. MongoDB must sort the documents after it receives the documents from a cursor. */ + readonly hasSort?: boolean; + /** + * Format: int64 + * @description The number of index keys that MongoDB scanned in order to carry out the operation. + */ + readonly keysExamined?: number; + /** + * Format: double + * @description Ratio of keys examined to documents returned. + */ + readonly keysExaminedReturnedRatio?: number; + /** + * Format: int64 + * @description The number of times the operation yielded to allow other operations to complete. + */ + readonly numYields?: number; + /** + * Format: int64 + * @description Total execution time of a query in milliseconds. + */ + readonly operationExecutionTime?: number; + /** + * Format: int64 + * @description The length in bytes of the operation's result document. + */ + readonly responseLength?: number; + }; /** * Periodic Cloud Provider Snapshot Source * @description Scheduled Cloud Provider Snapshot as Source for a Data Lake Pipeline. @@ -4658,6 +4926,36 @@ export interface components { /** @description Variable that belongs to the set of the tag. For example, `production` in the `environment : production` tag. */ value: string; }; + SchemaAdvisorItemRecommendation: { + /** @description List that contains the namespaces and information on why those namespaces triggered the recommendation. */ + readonly affectedNamespaces?: components["schemas"]["SchemaAdvisorNamespaceTriggers"][]; + /** @description Description of the specified recommendation. */ + readonly description?: string; + /** + * @description Type of recommendation. + * @enum {string} + */ + readonly recommendation?: "REDUCE_LOOKUP_OPS" | "AVOID_UNBOUNDED_ARRAY" | "REDUCE_DOCUMENT_SIZE" | "REMOVE_UNNECESSARY_INDEXES" | "REDUCE_NUMBER_OF_NAMESPACES" | "OPTIMIZE_CASE_INSENSITIVE_REGEX_QUERIES" | "OPTIMIZE_TEXT_QUERIES"; + }; + SchemaAdvisorNamespaceTriggers: { + /** @description Namespace of the affected collection. Will be null for REDUCE_NUMBER_OF_NAMESPACE recommendation. */ + readonly namespace?: string | null; + /** @description List of triggers that specify why the collection activated the recommendation. */ + readonly triggers?: components["schemas"]["SchemaAdvisorTriggerDetails"][]; + }; + SchemaAdvisorResponse: { + /** @description List that contains the documents with information about the schema advice that Performance Advisor suggests. */ + readonly recommendations?: components["schemas"]["SchemaAdvisorItemRecommendation"][]; + }; + SchemaAdvisorTriggerDetails: { + /** @description Description of the trigger type. */ + readonly description?: string; + /** + * @description Type of trigger. + * @enum {string} + */ + readonly triggerType?: "PERCENT_QUERIES_USE_LOOKUP" | "NUMBER_OF_QUERIES_USE_LOOKUP" | "DOCS_CONTAIN_UNBOUNDED_ARRAY" | "NUMBER_OF_NAMESPACES" | "DOC_SIZE_TOO_LARGE" | "NUM_INDEXES" | "QUERIES_CONTAIN_CASE_INSENSITIVE_REGEX"; + }; /** Search Host Status Detail */ SearchHostStatusDetail: { /** @description Hostname that corresponds to the status detail. */ @@ -6368,6 +6666,21 @@ export interface components { "application/json": components["schemas"]["ApiError"]; }; }; + /** @description Too Many Requests. */ + tooManyRequests: { + headers: { + [name: string]: unknown; + }; + content: { + /** @example { + * "detail": "(This is just an example, the exception may not be related to this endpoint)", + * "error": 429, + * "errorCode": "RATE_LIMITED", + * "reason": "Too Many Requests" + * } */ + "application/json": components["schemas"]["ApiError"]; + }; + }; /** @description Unauthorized. */ unauthorized: { headers: { @@ -6524,6 +6837,8 @@ export type DiskBackupSnapshotExportBucketResponse = components['schemas']['Disk export type DiskBackupSnapshotGcpExportBucketRequest = components['schemas']['DiskBackupSnapshotGCPExportBucketRequest']; export type DiskBackupSnapshotGcpExportBucketResponse = components['schemas']['DiskBackupSnapshotGCPExportBucketResponse']; export type DiskGbAutoScaling = components['schemas']['DiskGBAutoScaling']; +export type DropIndexSuggestionsIndex = components['schemas']['DropIndexSuggestionsIndex']; +export type DropIndexSuggestionsResponse = components['schemas']['DropIndexSuggestionsResponse']; export type EmployeeAccessGrantView = components['schemas']['EmployeeAccessGrantView']; export type FieldViolation = components['schemas']['FieldViolation']; export type Fields = components['schemas']['Fields']; @@ -6578,6 +6893,14 @@ export type PaginatedFlexClusters20241113 = components['schemas']['PaginatedFlex export type PaginatedNetworkAccessView = components['schemas']['PaginatedNetworkAccessView']; export type PaginatedOrgGroupView = components['schemas']['PaginatedOrgGroupView']; export type PaginatedOrganizationView = components['schemas']['PaginatedOrganizationView']; +export type PerformanceAdvisorIndex = components['schemas']['PerformanceAdvisorIndex']; +export type PerformanceAdvisorOpStats = components['schemas']['PerformanceAdvisorOpStats']; +export type PerformanceAdvisorOperationView = components['schemas']['PerformanceAdvisorOperationView']; +export type PerformanceAdvisorResponse = components['schemas']['PerformanceAdvisorResponse']; +export type PerformanceAdvisorShape = components['schemas']['PerformanceAdvisorShape']; +export type PerformanceAdvisorSlowQuery = components['schemas']['PerformanceAdvisorSlowQuery']; +export type PerformanceAdvisorSlowQueryList = components['schemas']['PerformanceAdvisorSlowQueryList']; +export type PerformanceAdvisorSlowQueryMetrics = components['schemas']['PerformanceAdvisorSlowQueryMetrics']; export type PeriodicCpsSnapshotSource = components['schemas']['PeriodicCpsSnapshotSource']; export type RawMetricAlertView = components['schemas']['RawMetricAlertView']; export type RawMetricUnits = components['schemas']['RawMetricUnits']; @@ -6586,6 +6909,10 @@ export type ReplicaSetAlertViewForNdsGroup = components['schemas']['ReplicaSetAl export type ReplicaSetEventTypeViewForNdsGroupAlertable = components['schemas']['ReplicaSetEventTypeViewForNdsGroupAlertable']; export type ReplicationSpec20240805 = components['schemas']['ReplicationSpec20240805']; export type ResourceTag = components['schemas']['ResourceTag']; +export type SchemaAdvisorItemRecommendation = components['schemas']['SchemaAdvisorItemRecommendation']; +export type SchemaAdvisorNamespaceTriggers = components['schemas']['SchemaAdvisorNamespaceTriggers']; +export type SchemaAdvisorResponse = components['schemas']['SchemaAdvisorResponse']; +export type SchemaAdvisorTriggerDetails = components['schemas']['SchemaAdvisorTriggerDetails']; export type SearchHostStatusDetail = components['schemas']['SearchHostStatusDetail']; export type SearchIndex = components['schemas']['SearchIndex']; export type SearchIndexCreateRequest = components['schemas']['SearchIndexCreateRequest']; @@ -6675,6 +7002,7 @@ export type ResponseForbidden = components['responses']['forbidden']; export type ResponseInternalServerError = components['responses']['internalServerError']; export type ResponseNotFound = components['responses']['notFound']; export type ResponsePaymentRequired = components['responses']['paymentRequired']; +export type ResponseTooManyRequests = components['responses']['tooManyRequests']; export type ResponseUnauthorized = components['responses']['unauthorized']; export type ParameterEnvelope = components['parameters']['envelope']; export type ParameterGroupId = components['parameters']['groupId']; @@ -7194,6 +7522,120 @@ export interface operations { 500: components["responses"]["internalServerError"]; }; }; + listDropIndexes: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + /** @description Human-readable label that identifies the cluster. */ + clusterName: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2024-08-05+json": components["schemas"]["DropIndexSuggestionsResponse"]; + }; + }; + 400: components["responses"]["badRequest"]; + 401: components["responses"]["unauthorized"]; + 403: components["responses"]["forbidden"]; + 404: components["responses"]["notFound"]; + 429: components["responses"]["tooManyRequests"]; + 500: components["responses"]["internalServerError"]; + }; + }; + listSchemaAdvice: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + /** @description Human-readable label that identifies the cluster. */ + clusterName: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2024-08-05+json": components["schemas"]["SchemaAdvisorResponse"]; + }; + }; + 400: components["responses"]["badRequest"]; + 401: components["responses"]["unauthorized"]; + 403: components["responses"]["forbidden"]; + 404: components["responses"]["notFound"]; + 429: components["responses"]["tooManyRequests"]; + 500: components["responses"]["internalServerError"]; + }; + }; + listClusterSuggestedIndexes: { + parameters: { + query?: { + /** @description ProcessIds from which to retrieve suggested indexes. A processId is a combination of host and port that serves the MongoDB process. The host must be the hostname, FQDN, IPv4 address, or IPv6 address of the host that runs the MongoDB process (`mongod` or `mongos`). The port must be the IANA port on which the MongoDB process listens for requests. To include multiple processIds, pass the parameter multiple times delimited with an ampersand (`&`) between each processId. */ + processIds?: string[]; + /** @description Namespaces from which to retrieve suggested indexes. A namespace consists of one database and one collection resource written as `.`: `.`. To include multiple namespaces, pass the parameter multiple times delimited with an ampersand (`&`) between each namespace. Omit this parameter to return results for all namespaces. */ + namespaces?: string[]; + /** @description Date and time from which the query retrieves the suggested indexes. This parameter expresses its value in the number of milliseconds that have elapsed since the [UNIX epoch](https://en.wikipedia.org/wiki/Unix_time). + * + * - If you don't specify the **until** parameter, the endpoint returns data covering from the **since** value and the current time. + * - If you specify neither the **since** nor the **until** parameters, the endpoint returns data from the previous 24 hours. */ + since?: number; + /** @description Date and time up until which the query retrieves the suggested indexes. This parameter expresses its value in the number of milliseconds that have elapsed since the [UNIX epoch](https://en.wikipedia.org/wiki/Unix_time). + * + * - If you specify the **until** parameter, you must specify the **since** parameter. + * - If you specify neither the **since** nor the **until** parameters, the endpoint returns data from the previous 24 hours. */ + until?: number; + }; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + /** @description Human-readable label that identifies the cluster. */ + clusterName: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2024-08-05+json": components["schemas"]["PerformanceAdvisorResponse"]; + }; + }; + 400: components["responses"]["badRequest"]; + 401: components["responses"]["unauthorized"]; + 403: components["responses"]["forbidden"]; + 404: components["responses"]["notFound"]; + 429: components["responses"]["tooManyRequests"]; + 500: components["responses"]["internalServerError"]; + }; + }; listDatabaseUsers: { parameters: { query?: { @@ -7485,6 +7927,63 @@ export interface operations { 500: components["responses"]["internalServerError"]; }; }; + listSlowQueries: { + parameters: { + query?: { + /** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */ + envelope?: components["parameters"]["envelope"]; + /** @description Flag that indicates whether the response body should be in the prettyprint format. */ + pretty?: components["parameters"]["pretty"]; + /** @description Length of time expressed during which the query finds slow queries among the managed namespaces in the cluster. This parameter expresses its value in milliseconds. + * + * - If you don't specify the **since** parameter, the endpoint returns data covering the duration before the current time. + * - If you specify neither the **duration** nor **since** parameters, the endpoint returns data from the previous 24 hours. */ + duration?: number; + /** @description Namespaces from which to retrieve slow queries. A namespace consists of one database and one collection resource written as `.`: `.`. To include multiple namespaces, pass the parameter multiple times delimited with an ampersand (`&`) between each namespace. Omit this parameter to return results for all namespaces. */ + namespaces?: string[]; + /** @description Maximum number of lines from the log to return. */ + nLogs?: number; + /** @description Date and time from which the query retrieves the slow queries. This parameter expresses its value in the number of milliseconds that have elapsed since the [UNIX epoch](https://en.wikipedia.org/wiki/Unix_time). + * + * - If you don't specify the **duration** parameter, the endpoint returns data covering from the **since** value and the current time. + * - If you specify neither the **duration** nor the **since** parameters, the endpoint returns data from the previous 24 hours. */ + since?: number; + /** @description Whether or not to include metrics extracted from the slow query log as separate fields. */ + includeMetrics?: boolean; + /** @description Whether or not to include the replica state of the host when the slow query log was generated as a separate field. */ + includeReplicaState?: boolean; + /** @description Whether or not to include the operation type (read/write/command) extracted from the slow query log as a separate field. */ + includeOpType?: boolean; + }; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + /** @description Combination of host and port that serves the MongoDB process. The host must be the hostname, FQDN, IPv4 address, or IPv6 address of the host that runs the MongoDB process (`mongod` or `mongos`). The port must be the IANA port on which the MongoDB process listens for requests. */ + processId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2023-01-01+json": components["schemas"]["PerformanceAdvisorSlowQueryList"]; + }; + }; + 401: components["responses"]["unauthorized"]; + 403: components["responses"]["forbidden"]; + 404: components["responses"]["notFound"]; + 429: components["responses"]["tooManyRequests"]; + 500: components["responses"]["internalServerError"]; + }; + }; listOrganizations: { parameters: { query?: { diff --git a/src/common/atlas/performanceAdvisorUtils.ts b/src/common/atlas/performanceAdvisorUtils.ts new file mode 100644 index 000000000..11549039c --- /dev/null +++ b/src/common/atlas/performanceAdvisorUtils.ts @@ -0,0 +1,153 @@ +import { LogId } from "../logger.js"; +import type { ApiClient } from "./apiClient.js"; +import { getProcessIdsFromCluster } from "./cluster.js"; +import type { components } from "./openapi.js"; + +export type SuggestedIndex = components["schemas"]["PerformanceAdvisorIndex"]; +export type DropIndexSuggestion = components["schemas"]["DropIndexSuggestionsIndex"]; +export type SlowQueryLog = components["schemas"]["PerformanceAdvisorSlowQuery"]; + +export const DEFAULT_SLOW_QUERY_LOGS_LIMIT = 50; + +interface SuggestedIndexesResponse { + content: components["schemas"]["PerformanceAdvisorResponse"]; +} +interface DropIndexesResponse { + content: components["schemas"]["DropIndexSuggestionsResponse"]; +} +interface SchemaAdviceResponse { + content: components["schemas"]["SchemaAdvisorResponse"]; +} +export type SchemaRecommendation = components["schemas"]["SchemaAdvisorItemRecommendation"]; + +export async function getSuggestedIndexes( + apiClient: ApiClient, + projectId: string, + clusterName: string +): Promise<{ suggestedIndexes: Array }> { + try { + const response = await apiClient.listClusterSuggestedIndexes({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + return { + suggestedIndexes: (response as SuggestedIndexesResponse).content.suggestedIndexes ?? [], + }; + } catch (err) { + apiClient.logger.debug({ + id: LogId.atlasPaSuggestedIndexesFailure, + context: "performanceAdvisorUtils", + message: `Failed to list suggested indexes: ${err instanceof Error ? err.message : String(err)}`, + }); + throw new Error(`Failed to list suggested indexes: ${err instanceof Error ? err.message : String(err)}`); + } +} + +export async function getDropIndexSuggestions( + apiClient: ApiClient, + projectId: string, + clusterName: string +): Promise<{ + hiddenIndexes: Array; + redundantIndexes: Array; + unusedIndexes: Array; +}> { + try { + const response = await apiClient.listDropIndexes({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + return { + hiddenIndexes: (response as DropIndexesResponse).content.hiddenIndexes ?? [], + redundantIndexes: (response as DropIndexesResponse).content.redundantIndexes ?? [], + unusedIndexes: (response as DropIndexesResponse).content.unusedIndexes ?? [], + }; + } catch (err) { + apiClient.logger.debug({ + id: LogId.atlasPaDropIndexSuggestionsFailure, + context: "performanceAdvisorUtils", + message: `Failed to list drop index suggestions: ${err instanceof Error ? err.message : String(err)}`, + }); + throw new Error(`Failed to list drop index suggestions: ${err instanceof Error ? err.message : String(err)}`); + } +} + +export async function getSchemaAdvice( + apiClient: ApiClient, + projectId: string, + clusterName: string +): Promise<{ recommendations: Array }> { + try { + const response = await apiClient.listSchemaAdvice({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + return { recommendations: (response as SchemaAdviceResponse).content.recommendations ?? [] }; + } catch (err) { + apiClient.logger.debug({ + id: LogId.atlasPaSchemaAdviceFailure, + context: "performanceAdvisorUtils", + message: `Failed to list schema advice: ${err instanceof Error ? err.message : String(err)}`, + }); + throw new Error(`Failed to list schema advice: ${err instanceof Error ? err.message : String(err)}`); + } +} + +export async function getSlowQueries( + apiClient: ApiClient, + projectId: string, + clusterName: string, + since?: Date, + namespaces?: Array +): Promise<{ slowQueryLogs: Array }> { + try { + const processIds = await getProcessIdsFromCluster(apiClient, projectId, clusterName); + + if (processIds.length === 0) { + return { slowQueryLogs: [] }; + } + + const slowQueryPromises = processIds.map((processId) => + apiClient.listSlowQueries({ + params: { + path: { + groupId: projectId, + processId, + }, + query: { + ...(since && { since: since.getTime() }), + ...(namespaces && { namespaces: namespaces }), + nLogs: DEFAULT_SLOW_QUERY_LOGS_LIMIT, + }, + }, + }) + ); + + const responses = await Promise.allSettled(slowQueryPromises); + + const allSlowQueryLogs = responses.reduce((acc, response) => { + return acc.concat(response.status === "fulfilled" ? (response.value.slowQueries ?? []) : []); + }, [] as Array); + + return { slowQueryLogs: allSlowQueryLogs }; + } catch (err) { + apiClient.logger.debug({ + id: LogId.atlasPaSlowQueryLogsFailure, + context: "performanceAdvisorUtils", + message: `Failed to list slow query logs: ${err instanceof Error ? err.message : String(err)}`, + }); + throw new Error(`Failed to list slow query logs: ${err instanceof Error ? err.message : String(err)}`); + } +} diff --git a/src/common/logger.ts b/src/common/logger.ts index 07b126aa4..100191f91 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -70,6 +70,11 @@ export const LogId = { exportLockError: mongoLogId(1_007_008), oidcFlow: mongoLogId(1_008_001), + + atlasPaSuggestedIndexesFailure: mongoLogId(1_009_001), + atlasPaDropIndexSuggestionsFailure: mongoLogId(1_009_002), + atlasPaSchemaAdviceFailure: mongoLogId(1_009_003), + atlasPaSlowQueryLogsFailure: mongoLogId(1_009_004), } as const; export interface LogPayload { diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index b68eeafde..8d8914d67 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -38,6 +38,21 @@ For more information on setting up API keys, visit: https://www.mongodb.com/docs }; } + if (statusCode === 402) { + return { + content: [ + { + type: "text", + text: `Received a Payment Required API Error: ${error.message} + +Payment setup is required to perform this action in MongoDB Atlas. +Please ensure that your payment method for your organization has been set up and is active. +For more information on setting up payment, visit: https://www.mongodb.com/docs/atlas/billing/`, + }, + ], + }; + } + if (statusCode === 403) { return { content: [ @@ -45,7 +60,7 @@ For more information on setting up API keys, visit: https://www.mongodb.com/docs type: "text", text: `Received a Forbidden API Error: ${error.message} -You don't have sufficient permissions to perform this action in MongoDB Atlas +You don't have sufficient permissions to perform this action in MongoDB Atlas. Please ensure your API key has the necessary roles assigned. For more information on Atlas API access roles, visit: https://www.mongodb.com/docs/atlas/api/service-accounts-overview/`, }, diff --git a/src/tools/atlas/read/getPerformanceAdvisor.ts b/src/tools/atlas/read/getPerformanceAdvisor.ts new file mode 100644 index 000000000..120c765af --- /dev/null +++ b/src/tools/atlas/read/getPerformanceAdvisor.ts @@ -0,0 +1,129 @@ +import { z } from "zod"; +import { AtlasToolBase } from "../atlasTool.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { OperationType, ToolArgs } from "../../tool.js"; +import { formatUntrustedData } from "../../tool.js"; +import { + getSuggestedIndexes, + getDropIndexSuggestions, + getSchemaAdvice, + getSlowQueries, + DEFAULT_SLOW_QUERY_LOGS_LIMIT, +} from "../../../common/atlas/performanceAdvisorUtils.js"; +import { AtlasArgs } from "../../args.js"; + +const PerformanceAdvisorOperationType = z.enum([ + "suggestedIndexes", + "dropIndexSuggestions", + "slowQueryLogs", + "schemaSuggestions", +]); + +export class GetPerformanceAdvisorTool extends AtlasToolBase { + public name = "atlas-get-performance-advisor"; + protected description = `Get MongoDB Atlas performance advisor recommendations, which includes the operations: suggested indexes, drop index suggestions, schema suggestions, and a sample of the most recent (max ${DEFAULT_SLOW_QUERY_LOGS_LIMIT}) slow query logs`; + public operationType: OperationType = "read"; + protected argsShape = { + projectId: AtlasArgs.projectId().describe("Atlas project ID to get performance advisor recommendations"), + clusterName: AtlasArgs.clusterName().describe("Atlas cluster name to get performance advisor recommendations"), + operations: z + .array(PerformanceAdvisorOperationType) + .default(PerformanceAdvisorOperationType.options) + .describe("Operations to get performance advisor recommendations"), + since: z + .string() + .datetime() + .describe( + "Date to get slow query logs since. Must be a string in ISO 8601 format. Only relevant for the slowQueryLogs operation." + ) + .optional(), + namespaces: z + .array(z.string()) + .describe("Namespaces to get slow query logs. Only relevant for the slowQueryLogs operation.") + .optional(), + }; + + protected async execute({ + projectId, + clusterName, + operations, + since, + namespaces, + }: ToolArgs): Promise { + try { + const [suggestedIndexesResult, dropIndexSuggestionsResult, slowQueryLogsResult, schemaSuggestionsResult] = + await Promise.allSettled([ + operations.includes("suggestedIndexes") + ? getSuggestedIndexes(this.session.apiClient, projectId, clusterName) + : Promise.resolve(undefined), + operations.includes("dropIndexSuggestions") + ? getDropIndexSuggestions(this.session.apiClient, projectId, clusterName) + : Promise.resolve(undefined), + operations.includes("slowQueryLogs") + ? getSlowQueries( + this.session.apiClient, + projectId, + clusterName, + since ? new Date(since) : undefined, + namespaces + ) + : Promise.resolve(undefined), + operations.includes("schemaSuggestions") + ? getSchemaAdvice(this.session.apiClient, projectId, clusterName) + : Promise.resolve(undefined), + ]); + + const hasSuggestedIndexes = + suggestedIndexesResult.status === "fulfilled" && + suggestedIndexesResult.value?.suggestedIndexes && + suggestedIndexesResult.value.suggestedIndexes.length > 0; + const hasDropIndexSuggestions = + dropIndexSuggestionsResult.status === "fulfilled" && + dropIndexSuggestionsResult.value?.hiddenIndexes && + dropIndexSuggestionsResult.value?.redundantIndexes && + dropIndexSuggestionsResult.value?.unusedIndexes && + (dropIndexSuggestionsResult.value.hiddenIndexes.length > 0 || + dropIndexSuggestionsResult.value.redundantIndexes.length > 0 || + dropIndexSuggestionsResult.value.unusedIndexes.length > 0); + const hasSlowQueryLogs = + slowQueryLogsResult.status === "fulfilled" && + slowQueryLogsResult.value?.slowQueryLogs && + slowQueryLogsResult.value.slowQueryLogs.length > 0; + const hasSchemaSuggestions = + schemaSuggestionsResult.status === "fulfilled" && + schemaSuggestionsResult.value?.recommendations && + schemaSuggestionsResult.value.recommendations.length > 0; + + // Inserts the performance advisor data with the relevant section header if it exists + const performanceAdvisorData = [ + `## Suggested Indexes\n${ + hasSuggestedIndexes + ? `Note: The "Weight" field is measured in bytes, and represents the estimated number of bytes saved in disk reads per executed read query that would be saved by implementing an index suggestion. Please convert this to MB or GB for easier readability.\n${JSON.stringify(suggestedIndexesResult.value?.suggestedIndexes)}` + : "No suggested indexes found." + }`, + `## Drop Index Suggestions\n${hasDropIndexSuggestions ? JSON.stringify(dropIndexSuggestionsResult.value) : "No drop index suggestions found."}`, + `## Slow Query Logs\n${hasSlowQueryLogs ? JSON.stringify(slowQueryLogsResult.value?.slowQueryLogs) : "No slow query logs found."}`, + `## Schema Suggestions\n${hasSchemaSuggestions ? JSON.stringify(schemaSuggestionsResult.value?.recommendations) : "No schema suggestions found."}`, + ]; + + if (performanceAdvisorData.length === 0) { + return { + content: [{ type: "text", text: "No performance advisor recommendations found." }], + }; + } + + return { + content: formatUntrustedData("Performance advisor data", performanceAdvisorData.join("\n\n")), + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error retrieving performance advisor data: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + } +} diff --git a/src/tools/atlas/tools.ts b/src/tools/atlas/tools.ts index c43b88ef7..c2822ec55 100644 --- a/src/tools/atlas/tools.ts +++ b/src/tools/atlas/tools.ts @@ -10,6 +10,7 @@ import { CreateProjectTool } from "./create/createProject.js"; import { ListOrganizationsTool } from "./read/listOrgs.js"; import { ConnectClusterTool } from "./connect/connectCluster.js"; import { ListAlertsTool } from "./read/listAlerts.js"; +import { GetPerformanceAdvisorTool } from "./read/getPerformanceAdvisor.js"; export const AtlasTools = [ ListClustersTool, @@ -24,4 +25,5 @@ export const AtlasTools = [ ListOrganizationsTool, ConnectClusterTool, ListAlertsTool, + GetPerformanceAdvisorTool, ]; diff --git a/tests/accuracy/getPerformanceAdvisor.test.ts b/tests/accuracy/getPerformanceAdvisor.test.ts new file mode 100644 index 000000000..62b570c12 --- /dev/null +++ b/tests/accuracy/getPerformanceAdvisor.test.ts @@ -0,0 +1,165 @@ +import { describeAccuracyTests } from "./sdk/describeAccuracyTests.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +// Shared mock tool implementations +const mockedTools = { + "atlas-list-projects": (): CallToolResult => { + return { + content: [ + { + type: "text", + text: "Found 1 project\n\n# | Name | ID\n---|------|----\n1 | mflix | mflix", + }, + ], + }; + }, + "atlas-list-clusters": (): CallToolResult => { + return { + content: [ + { + type: "text", + text: "Found 1 cluster\n\n# | Name | Type | State\n---|------|------|-----\n1 | mflix-cluster | REPLICASET | IDLE", + }, + ], + }; + }, + "atlas-get-performance-advisor": (): CallToolResult => { + return { + content: [ + { + type: "text", + text: "Found 2 performance advisor recommendations\n\n## Suggested Indexes\n# | Namespace | Weight | Avg Obj Size | Index Keys\n---|-----------|--------|--------------|------------\n1 | mflix.movies | 0.8 | 1024 | title, year\n2 | mflix.shows | 0.6 | 512 | genre, rating", + }, + ], + }; + }, +}; + +describeAccuracyTests([ + // Test for Suggested Indexes operation + { + prompt: "Can you give me index suggestions for the database 'mflix' in the project 'mflix' and cluster 'mflix-cluster'?", + expectedToolCalls: [ + { + toolName: "atlas-list-projects", + parameters: {}, + }, + { + toolName: "atlas-list-clusters", + parameters: { + projectId: "mflix", + }, + }, + { + toolName: "atlas-get-performance-advisor", + parameters: { + projectId: "mflix", + clusterName: "mflix-cluster", + operations: ["suggestedIndexes"], + }, + }, + ], + mockedTools, + }, + // Test for Drop Index Suggestions operation + { + prompt: "Show me drop index suggestions for the 'mflix' project and 'mflix-cluster' cluster", + expectedToolCalls: [ + { + toolName: "atlas-list-projects", + parameters: {}, + }, + { + toolName: "atlas-list-clusters", + parameters: { + projectId: "mflix", + }, + }, + { + toolName: "atlas-get-performance-advisor", + parameters: { + projectId: "mflix", + clusterName: "mflix-cluster", + operations: ["dropIndexSuggestions"], + }, + }, + ], + mockedTools, + }, + // Test for Slow Query Logs operation + { + prompt: "Show me the slow query logs for the 'mflix' project and 'mflix-cluster' cluster for the namespaces 'mflix.movies' and 'mflix.shows' since January 1st, 2025.", + expectedToolCalls: [ + { + toolName: "atlas-list-projects", + parameters: {}, + }, + { + toolName: "atlas-list-clusters", + parameters: { + projectId: "mflix", + }, + }, + { + toolName: "atlas-get-performance-advisor", + parameters: { + projectId: "mflix", + clusterName: "mflix-cluster", + operations: ["slowQueryLogs"], + namespaces: ["mflix.movies", "mflix.shows"], + since: "2025-01-01T00:00:00Z", + }, + }, + ], + mockedTools, + }, + // Test for Schema Suggestions operation + { + prompt: "Give me schema suggestions for the 'mflix' project and 'mflix-cluster' cluster", + expectedToolCalls: [ + { + toolName: "atlas-list-projects", + parameters: {}, + }, + { + toolName: "atlas-list-clusters", + parameters: { + projectId: "mflix", + }, + }, + { + toolName: "atlas-get-performance-advisor", + parameters: { + projectId: "mflix", + clusterName: "mflix-cluster", + operations: ["schemaSuggestions"], + }, + }, + ], + mockedTools, + }, + // Test for all operations + { + prompt: "Show me all performance advisor recommendations for the 'mflix' project and 'mflix-cluster' cluster", + expectedToolCalls: [ + { + toolName: "atlas-list-projects", + parameters: {}, + }, + { + toolName: "atlas-list-clusters", + parameters: { + projectId: "mflix", + }, + }, + { + toolName: "atlas-get-performance-advisor", + parameters: { + projectId: "mflix", + clusterName: "mflix-cluster", + }, + }, + ], + mockedTools, + }, +]); diff --git a/tests/accuracy/sdk/accuracyTestingClient.ts b/tests/accuracy/sdk/accuracyTestingClient.ts index 692694aa7..130e8fb05 100644 --- a/tests/accuracy/sdk/accuracyTestingClient.ts +++ b/tests/accuracy/sdk/accuracyTestingClient.ts @@ -79,10 +79,22 @@ export class AccuracyTestingClient { this.llmToolCalls = []; } - static async initializeClient(mdbConnectionString: string): Promise { + static async initializeClient( + mdbConnectionString: string, + atlasApiClientId?: string, + atlasApiClientSecret?: string + ): Promise { + const args = [ + MCP_SERVER_CLI_SCRIPT, + "--connectionString", + mdbConnectionString, + ...(atlasApiClientId ? ["--apiClientId", atlasApiClientId] : []), + ...(atlasApiClientSecret ? ["--apiClientSecret", atlasApiClientSecret] : []), + ]; + const clientTransport = new StdioClientTransport({ command: process.execPath, - args: [MCP_SERVER_CLI_SCRIPT, "--connectionString", mdbConnectionString], + args, }); const client = await createMCPClient({ diff --git a/tests/accuracy/sdk/describeAccuracyTests.ts b/tests/accuracy/sdk/describeAccuracyTests.ts index 8073518b4..df35e3a03 100644 --- a/tests/accuracy/sdk/describeAccuracyTests.ts +++ b/tests/accuracy/sdk/describeAccuracyTests.ts @@ -66,6 +66,9 @@ export function describeAccuracyTests(accuracyTestConfigs: AccuracyTestConfig[]) const mdbIntegration = setupMongoDBIntegrationTest(); const { populateTestData, cleanupTestDatabases } = prepareTestData(mdbIntegration); + const atlasApiClientId = process.env.MDB_MCP_API_CLIENT_ID; + const atlasApiClientSecret = process.env.MDB_MCP_API_CLIENT_SECRET; + let commitSHA: string; let accuracyResultStorage: AccuracyResultStorage; let testMCPClient: AccuracyTestingClient; @@ -79,7 +82,11 @@ export function describeAccuracyTests(accuracyTestConfigs: AccuracyTestConfig[]) commitSHA = retrievedCommitSHA; accuracyResultStorage = getAccuracyResultStorage(); - testMCPClient = await AccuracyTestingClient.initializeClient(mdbIntegration.connectionString()); + testMCPClient = await AccuracyTestingClient.initializeClient( + mdbIntegration.connectionString(), + atlasApiClientId, + atlasApiClientSecret + ); agent = getVercelToolCallingAgent(); }); diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 6282851cf..0f510beca 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -48,6 +48,8 @@ export const defaultTestConfig: UserConfig = { loggers: ["stderr"], }; +export const DEFAULT_LONG_RUNNING_TEST_WAIT_TIMEOUT_MS = 1_200_000; + export function setupIntegrationTest( getUserConfig: () => UserConfig, getDriverOptions: () => DriverOptions, diff --git a/tests/integration/tools/atlas/atlasHelpers.ts b/tests/integration/tools/atlas/atlasHelpers.ts index 13f160c79..a9f24cb49 100644 --- a/tests/integration/tools/atlas/atlasHelpers.ts +++ b/tests/integration/tools/atlas/atlasHelpers.ts @@ -1,10 +1,11 @@ import { ObjectId } from "mongodb"; -import type { Group } from "../../../../src/common/atlas/openapi.js"; +import type { ClusterDescription20240805, Group } from "../../../../src/common/atlas/openapi.js"; import type { ApiClient } from "../../../../src/common/atlas/apiClient.js"; import type { IntegrationTest } from "../../helpers.js"; import { setupIntegrationTest, defaultTestConfig, defaultDriverOptions } from "../../helpers.js"; import type { SuiteCollector } from "vitest"; import { afterAll, beforeAll, describe } from "vitest"; +import type { Session } from "../../../../src/common/session.js"; export type IntegrationTestFunction = (integration: IntegrationTest) => void; @@ -150,3 +151,91 @@ async function createProject(apiClient: ApiClient): Promise>; } + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function assertClusterIsAvailable( + session: Session, + projectId: string, + clusterName: string +): Promise { + try { + await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + return true; + } catch { + return false; + } +} + +export async function deleteCluster( + session: Session, + projectId: string, + clusterName: string, + shouldWaitTillClusterIsDeleted: boolean = false +): Promise { + await session.apiClient.deleteCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + + if (!shouldWaitTillClusterIsDeleted) { + return; + } + + while (true) { + try { + await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + await sleep(1000); + } catch { + break; + } + } +} + +export async function waitCluster( + session: Session, + projectId: string, + clusterName: string, + check: (cluster: ClusterDescription20240805) => boolean | Promise, + pollingInterval: number = 1000, + maxPollingIterations: number = 300 +): Promise { + for (let i = 0; i < maxPollingIterations; i++) { + const cluster = await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + if (await check(cluster)) { + return; + } + await sleep(pollingInterval); + } + + throw new Error( + `Cluster wait timeout: ${clusterName} did not meet condition within ${maxPollingIterations} iterations` + ); +} diff --git a/tests/integration/tools/atlas/clusters.test.ts b/tests/integration/tools/atlas/clusters.test.ts index 0c7b75596..543988c47 100644 --- a/tests/integration/tools/atlas/clusters.test.ts +++ b/tests/integration/tools/atlas/clusters.test.ts @@ -1,67 +1,16 @@ import type { Session } from "../../../../src/common/session.js"; -import { expectDefined, getDataFromUntrustedContent, getResponseElements, sleep } from "../../helpers.js"; -import { describeWithAtlas, withProject, randomId, parseTable } from "./atlasHelpers.js"; -import type { ClusterDescription20240805 } from "../../../../src/common/atlas/openapi.js"; +import { expectDefined, getDataFromUntrustedContent, getResponseElements } from "../../helpers.js"; +import { + describeWithAtlas, + withProject, + randomId, + parseTable, + deleteCluster, + waitCluster, + sleep, +} from "./atlasHelpers.js"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -async function deleteCluster( - session: Session, - projectId: string, - clusterName: string, - wait: boolean = false -): Promise { - await session.apiClient.deleteCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - - if (!wait) { - return; - } - - while (true) { - try { - await session.apiClient.getCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - await sleep(1000); - } catch { - break; - } - } -} - -async function waitCluster( - session: Session, - projectId: string, - clusterName: string, - check: (cluster: ClusterDescription20240805) => boolean | Promise -): Promise { - while (true) { - const cluster = await session.apiClient.getCluster({ - params: { - path: { - groupId: projectId, - clusterName, - }, - }, - }); - if (await check(cluster)) { - return; - } - await sleep(1000); - } -} - describeWithAtlas("clusters", (integration) => { withProject(integration, ({ getProjectId, getIpAddress }) => { const clusterName = "ClusterTest-" + randomId; diff --git a/tests/integration/tools/atlas/performanceAdvisor.test.ts b/tests/integration/tools/atlas/performanceAdvisor.test.ts new file mode 100644 index 000000000..bb3753485 --- /dev/null +++ b/tests/integration/tools/atlas/performanceAdvisor.test.ts @@ -0,0 +1,231 @@ +// This test file includes long running tests (>10 minutes) because we provision a real M10 cluster, which can take up to 10 minutes to provision. +// The timeouts for the beforeAll/afterAll hooks have been modified to account for longer running tests. + +import type { Session } from "../../../../src/common/session.js"; +import { DEFAULT_LONG_RUNNING_TEST_WAIT_TIMEOUT_MS, expectDefined, getResponseElements } from "../../helpers.js"; +import { describeWithAtlas, withProject, randomId, waitCluster, deleteCluster } from "./atlasHelpers.js"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +describeWithAtlas("performanceAdvisor", (integration) => { + withProject(integration, ({ getProjectId }) => { + const clusterName = "ClusterTest-" + randomId; + + afterAll(async () => { + const projectId = getProjectId(); + if (projectId) { + const session: Session = integration.mcpServer().session; + await deleteCluster(session, projectId, clusterName); + } + }, DEFAULT_LONG_RUNNING_TEST_WAIT_TIMEOUT_MS); + + describe("atlas-get-performance-advisor", () => { + beforeAll(async () => { + const projectId = getProjectId(); + const session = integration.mcpServer().session; + + await session.apiClient.createCluster({ + params: { + path: { + groupId: projectId, + }, + }, + body: { + name: clusterName, + clusterType: "REPLICASET", + backupEnabled: true, + configServerManagementMode: "ATLAS_MANAGED", + diskWarmingMode: "FULLY_WARMED", + replicaSetScalingStrategy: "WORKLOAD_TYPE", + rootCertType: "ISRGROOTX1", + terminationProtectionEnabled: false, + versionReleaseSystem: "LTS", + replicationSpecs: [ + { + zoneName: "Zone 1", + regionConfigs: [ + { + providerName: "AWS", + regionName: "US_EAST_1", + electableSpecs: { instanceSize: "M10", nodeCount: 3 }, + priority: 7, + }, + ], + }, + ], + }, + }); + + await waitCluster( + session, + projectId, + clusterName, + (cluster) => { + return cluster.stateName === "IDLE"; + }, + 10000, + 120 + ); + }, DEFAULT_LONG_RUNNING_TEST_WAIT_TIMEOUT_MS); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should have correct metadata", async () => { + const { tools } = await integration.mcpClient().listTools(); + const getPerformanceAdvisor = tools.find((tool) => tool.name === "atlas-get-performance-advisor"); + expectDefined(getPerformanceAdvisor); + expect(getPerformanceAdvisor.inputSchema.type).toBe("object"); + expectDefined(getPerformanceAdvisor.inputSchema.properties); + expect(getPerformanceAdvisor.inputSchema.properties).toHaveProperty("projectId"); + expect(getPerformanceAdvisor.inputSchema.properties).toHaveProperty("clusterName"); + expect(getPerformanceAdvisor.inputSchema.properties).toHaveProperty("operations"); + expect(getPerformanceAdvisor.inputSchema.properties).toHaveProperty("since"); + expect(getPerformanceAdvisor.inputSchema.properties).toHaveProperty("namespaces"); + }); + + it("returns performance advisor data from a paid tier cluster", async () => { + const projectId = getProjectId(); + const session = integration.mcpServer().session; + + await session.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); + + const response = await integration.mcpClient().callTool({ + name: "atlas-get-performance-advisor", + arguments: { + projectId, + clusterName, + operations: ["suggestedIndexes", "dropIndexSuggestions", "schemaSuggestions"], + }, + }); + + const elements = getResponseElements(response.content); + expect(elements).toHaveLength(2); + + expect(elements[0]?.text).toContain("Performance advisor data"); + expect(elements[1]?.text).toContain(" { + const projectId = getProjectId(); + const session = integration.mcpServer().session; + + // Mock the API client methods since we can't guarantee performance advisor data + const mockSuggestedIndexes = vi.fn().mockResolvedValue({ + content: { + suggestedIndexes: [ + { + namespace: "testdb.testcollection", + index: { field: 1 }, + impact: ["queryShapeString"], + }, + ], + }, + }); + + const mockDropIndexSuggestions = vi.fn().mockResolvedValue({ + content: { + hiddenIndexes: [], + redundantIndexes: [ + { + accessCount: 100, + namespace: "testdb.testcollection", + index: { field: 1 }, + reason: "Redundant with compound index", + }, + ], + unusedIndexes: [], + }, + }); + + const mockSchemaAdvice = vi.fn().mockResolvedValue({ + content: { + recommendations: [ + { + description: "Consider adding an index on 'status' field", + recommendation: "REDUCE_LOOKUP_OPS", + affectedNamespaces: [ + { + namespace: "testdb.testcollection", + triggers: [ + { + triggerType: "PERCENT_QUERIES_USE_LOOKUP", + details: + "Queries filtering by status field are causing collection scans", + }, + ], + }, + ], + }, + ], + }, + }); + + const mockSlowQueries = vi.fn().mockResolvedValue({ + slowQueries: [ + { + namespace: "testdb.testcollection", + query: { find: "testcollection", filter: { status: "active" } }, + duration: 1500, + timestamp: "2024-01-15T10:30:00Z", + }, + ], + }); + + const mockGetCluster = vi.fn().mockResolvedValue({ + connectionStrings: { + standard: "mongodb://test-cluster.mongodb.net:27017", + }, + }); + + session.apiClient.listClusterSuggestedIndexes = mockSuggestedIndexes; + session.apiClient.listDropIndexes = mockDropIndexSuggestions; + session.apiClient.listSchemaAdvice = mockSchemaAdvice; + session.apiClient.listSlowQueries = mockSlowQueries; + session.apiClient.getCluster = mockGetCluster; + + const response = await integration.mcpClient().callTool({ + name: "atlas-get-performance-advisor", + arguments: { + projectId, + clusterName: "mockClusterName", + operations: ["suggestedIndexes", "dropIndexSuggestions", "slowQueryLogs", "schemaSuggestions"], + }, + }); + + if (response.isError) { + console.error("Performance advisor call failed:", response.content); + throw new Error("Performance advisor call failed - see console for details"); + } + + const elements = getResponseElements(response.content); + expect(elements).toHaveLength(2); + + expect(elements[0]?.text).toContain("Performance advisor data"); + expect(elements[1]?.text).toContain(" Date: Thu, 9 Oct 2025 09:07:08 +0100 Subject: [PATCH 08/15] chore: use redact by default (#624) --- src/server.ts | 2 +- src/telemetry/telemetry.ts | 12 +++-- src/telemetry/types.ts | 4 +- tests/unit/telemetry.test.ts | 89 ++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/server.ts b/src/server.ts index 794fb9867..f8aa3226b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -201,7 +201,7 @@ export class Server { if (command === "start") { event.properties.startup_time_ms = commandDuration; - event.properties.read_only_mode = this.userConfig.readOnly || false; + event.properties.read_only_mode = this.userConfig.readOnly ? "true" : "false"; event.properties.disabled_tools = this.userConfig.disabledTools || []; event.properties.confirmation_required_tools = this.userConfig.confirmationRequiredTools || []; } diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index bdba51a51..6a9db5c1b 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -8,6 +8,7 @@ import { EventCache } from "./eventCache.js"; import { detectContainerEnv } from "../helpers/container.js"; import type { DeviceId } from "../helpers/deviceId.js"; import { EventEmitter } from "events"; +import { redact } from "mongodb-redact"; type EventResult = { success: boolean; @@ -79,7 +80,7 @@ export class Telemetry { const [deviceIdValue, containerEnv] = await this.setupPromise; this.commonProperties.device_id = deviceIdValue; - this.commonProperties.is_container_env = containerEnv; + this.commonProperties.is_container_env = containerEnv ? "true" : "false"; this.isBufferingEvents = false; } @@ -123,7 +124,6 @@ export class Telemetry { this.events.emit("events-skipped"); return; } - // Don't wait for events to be sent - we should not block regular server // operations on telemetry void this.emit(events); @@ -213,14 +213,18 @@ export class Telemetry { } /** - * Attempts to send events through the provided API client + * Attempts to send events through the provided API client. + * Events are redacted before being sent to ensure no sensitive data is transmitted */ private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise { try { await client.sendEvents( events.map((event) => ({ ...event, - properties: { ...this.getCommonProperties(), ...event.properties }, + properties: { + ...redact(this.getCommonProperties(), this.session.keychain.allSecrets), + ...redact(event.properties, this.session.keychain.allSecrets), + }, })) ); return { success: true }; diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index c1eced5af..0c32799a3 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -43,7 +43,7 @@ export type ServerEventProperties = { reason?: string; startup_time_ms?: number; runtime_duration_ms?: number; - read_only_mode?: boolean; + read_only_mode?: TelemetryBoolSet; disabled_tools?: string[]; confirmation_required_tools?: string[]; }; @@ -97,7 +97,7 @@ export type CommonProperties = { /** * A boolean indicating whether the server is running in a container environment. */ - is_container_env?: boolean; + is_container_env?: TelemetryBoolSet; /** * The version of the MCP client as reported by the client on session establishment. diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index 0dd759314..8b4a0c9c1 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -9,6 +9,7 @@ import { NullLogger } from "../../tests/utils/index.js"; import type { MockedFunction } from "vitest"; import type { DeviceId } from "../../src/helpers/deviceId.js"; import { expectDefined } from "../integration/helpers.js"; +import { Keychain } from "../../src/common/keychain.js"; // Mock the ApiClient to avoid real API calls vi.mock("../../src/common/atlas/apiClient.js"); @@ -140,6 +141,7 @@ describe("Telemetry", () => { close: vi.fn().mockResolvedValue(undefined), setAgentRunner: vi.fn().mockResolvedValue(undefined), logger: new NullLogger(), + keychain: new Keychain(), } as unknown as Session; telemetry = Telemetry.create(session, config, mockDeviceId, { @@ -345,4 +347,91 @@ describe("Telemetry", () => { verifyMockCalls(); }); }); + + describe("when secrets are registered", () => { + describe("comprehensive redaction coverage", () => { + it("should redact sensitive data from CommonStaticProperties", async () => { + session.keychain.register("secret-server-version", "password"); + session.keychain.register("secret-server-name", "password"); + session.keychain.register("secret-password", "password"); + session.keychain.register("secret-key", "password"); + session.keychain.register("secret-token", "password"); + session.keychain.register("secret-password-version", "password"); + + // Simulates sensitive data across random properties + const sensitiveStaticProps = { + mcp_server_version: "secret-server-version", + mcp_server_name: "secret-server-name", + platform: "linux-secret-password", + arch: "x64-secret-key", + os_type: "linux-secret-token", + os_version: "secret-password-version", + }; + + telemetry = Telemetry.create(session, config, mockDeviceId, { + eventCache: mockEventCache as unknown as EventCache, + commonProperties: sensitiveStaticProps, + }); + + await telemetry.setupPromise; + + telemetry.emitEvents([createTestEvent()]); + + const calls = mockApiClient.sendEvents.mock.calls; + expect(calls).toHaveLength(1); + + // get event properties + const sentEvent = calls[0]?.[0][0] as { properties: Record }; + expectDefined(sentEvent); + + const eventProps = sentEvent.properties; + expect(eventProps.mcp_server_version).toBe(""); + expect(eventProps.mcp_server_name).toBe(""); + expect(eventProps.platform).toBe("linux-"); + expect(eventProps.arch).toBe("x64-"); + expect(eventProps.os_type).toBe("linux-"); + expect(eventProps.os_version).toBe("-version"); + }); + + it("should redact sensitive data from CommonProperties", () => { + // register the common properties as sensitive data + session.keychain.register("test-device-id", "password"); + session.keychain.register(session.sessionId, "password"); + + telemetry.emitEvents([createTestEvent()]); + + const calls = mockApiClient.sendEvents.mock.calls; + expect(calls).toHaveLength(1); + + // get event properties + const sentEvent = calls[0]?.[0][0] as { properties: Record }; + expectDefined(sentEvent); + + const eventProps = sentEvent.properties; + + expect(eventProps.device_id).toBe(""); + expect(eventProps.session_id).toBe(""); + }); + + it("should redact sensitive data that is added to events", () => { + session.keychain.register("test-device-id", "password"); + session.keychain.register(session.sessionId, "password"); + session.keychain.register("test-component", "password"); + + telemetry.emitEvents([createTestEvent()]); + + const calls = mockApiClient.sendEvents.mock.calls; + expect(calls).toHaveLength(1); + + // get event properties + const sentEvent = calls[0]?.[0][0] as { properties: Record }; + expectDefined(sentEvent); + + const eventProps = sentEvent.properties; + expect(eventProps.device_id).toBe(""); + expect(eventProps.session_id).toBe(""); + expect(eventProps.component).toBe(""); + }); + }); + }); }); From bebfd338617a0a6b62082331cc79e9ba4d01313f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 9 Oct 2025 15:31:44 +0200 Subject: [PATCH 09/15] fix: update tool category for collection-indexes (#630) --- README.md | 2 +- .../{read => metadata}/collectionIndexes.ts | 2 +- src/tools/mongodb/tools.ts | 2 +- src/tools/tool.ts | 34 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) rename src/tools/mongodb/{read => metadata}/collectionIndexes.ts (97%) diff --git a/README.md b/README.md index 469e33754..092e5f276 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ Operation types: - `update` - Tools that update resources, such as update document, rename collection, etc. - `delete` - Tools that delete resources, such as delete document, drop collection, etc. - `read` - Tools that read resources, such as find, aggregate, list clusters, etc. -- `metadata` - Tools that read metadata, such as list databases, list collections, collection schema, etc. +- `metadata` - Tools that read metadata, such as list databases/collections/indexes, infer collection schema, etc. - `connect` - Tools that allow you to connect or switch the connection to a MongoDB instance. If this is disabled, you will need to provide a connection string through the config when starting the server. #### Require Confirmation diff --git a/src/tools/mongodb/read/collectionIndexes.ts b/src/tools/mongodb/metadata/collectionIndexes.ts similarity index 97% rename from src/tools/mongodb/read/collectionIndexes.ts rename to src/tools/mongodb/metadata/collectionIndexes.ts index 84b8b1dbb..6da2c7886 100644 --- a/src/tools/mongodb/read/collectionIndexes.ts +++ b/src/tools/mongodb/metadata/collectionIndexes.ts @@ -7,7 +7,7 @@ export class CollectionIndexesTool extends MongoDBToolBase { public name = "collection-indexes"; protected description = "Describe the indexes for a collection"; protected argsShape = DbOperationArgs; - public operationType: OperationType = "read"; + public operationType: OperationType = "metadata"; protected async execute({ database, collection }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index 1567fd4f8..4c705fa69 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -1,6 +1,6 @@ import { ConnectTool } from "./connect/connect.js"; import { ListCollectionsTool } from "./metadata/listCollections.js"; -import { CollectionIndexesTool } from "./read/collectionIndexes.js"; +import { CollectionIndexesTool } from "./metadata/collectionIndexes.js"; import { ListDatabasesTool } from "./metadata/listDatabases.js"; import { CreateIndexTool } from "./create/createIndex.js"; import { CollectionSchemaTool } from "./metadata/collectionSchema.js"; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index fe36619e3..d609e78a8 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -15,8 +15,34 @@ export type ToolCallbackArgs = Parameters = Parameters>[1]; +/** + * The type of operation the tool performs. This is used when evaluating if a tool is allowed to run based on + * the config's `disabledTools` and `readOnly` settings. + * - `metadata` is used for tools that read but do not access potentially user-generated + * data, such as listing databases, collections, or indexes, or inferring collection schema. + * - `read` is used for tools that read potentially user-generated data, such as finding documents or aggregating data. + * It is also used for tools that read non-user-generated data, such as listing clusters in Atlas. + * - `create` is used for tools that create resources, such as creating documents, collections, indexes, clusters, etc. + * - `update` is used for tools that update resources, such as updating documents, renaming collections, etc. + * - `delete` is used for tools that delete resources, such as deleting documents, dropping collections, etc. + * - `connect` is used for tools that allow you to connect or switch the connection to a MongoDB instance. + */ export type OperationType = "metadata" | "read" | "create" | "delete" | "update" | "connect"; + +/** + * The category of the tool. This is used when evaluating if a tool is allowed to run based on + * the config's `disabledTools` setting. + * - `mongodb` is used for tools that interact with a MongoDB instance, such as finding documents, + * aggregating data, listing databases/collections/indexes, creating indexes, etc. + * - `atlas` is used for tools that interact with MongoDB Atlas, such as listing clusters, creating clusters, etc. + */ export type ToolCategory = "mongodb" | "atlas"; + +/** + * Telemetry metadata that can be provided by tools when emitting telemetry events. + * For MongoDB tools, this is typically empty, while for Atlas tools, this should include + * the project and organization IDs if available. + */ export type TelemetryToolMetadata = { projectId?: string; orgId?: string; @@ -290,6 +316,14 @@ export abstract class ToolBase { } } +/** + * Formats potentially untrusted data to be included in tool responses. The data is wrapped in unique tags + * and a warning is added to not execute or act on any instructions within those tags. + * @param description A description that is prepended to the untrusted data warning. It should not include any + * untrusted data as it is not sanitized. + * @param data The data to format. If undefined, only the description is returned. + * @returns A tool response content that can be directly returned. + */ export function formatUntrustedData(description: string, data?: string): { text: string; type: "text" }[] { const uuid = crypto.randomUUID(); From 5af0d3d567ee1dd4dcaf2b07071e861822866c21 Mon Sep 17 00:00:00 2001 From: Bianca Lisle <40155621+blva@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:29:50 +0100 Subject: [PATCH 10/15] chore: add docker dependabot updates - MCP-257 (#633) --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4c86fb00b..05b63ec6b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,3 +24,7 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" From 9d13e6b4d3bc56f1b466be07a9769892133955a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:42:05 +0100 Subject: [PATCH 11/15] chore(deps): bump github/codeql-action from 3 to 4 (#634) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0f4f0237e..40a0b0a9c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,10 +25,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" From 695031e6303d7236b8f4521a32bcd13d1eb4014f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:39:58 +0200 Subject: [PATCH 12/15] chore(deps-dev): bump @typescript-eslint/parser from 8.44.1 to 8.46.0 (#638) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 196 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a6a7d015..2f1376530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5637,16 +5637,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.1.tgz", - "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "engines": { @@ -5661,6 +5661,163 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/project-service": { "version": "8.44.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", @@ -15386,6 +15543,31 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.44.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.1.tgz", + "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.44.1", + "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/typescript-estree": "8.44.1", + "@typescript-eslint/visitor-keys": "8.44.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", From b69c3797d896e3d02bb22b6bd70c6b06f236fc60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:40:15 +0200 Subject: [PATCH 13/15] chore(deps-dev): bump @types/node from 24.5.2 to 24.7.0 (#637) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f1376530..a8c87c32c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5444,13 +5444,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/proper-lockfile": { @@ -15647,9 +15647,9 @@ } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true, "license": "MIT" }, From be54b362c49aba85aca9215efcae16241447e876 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:42:10 +0200 Subject: [PATCH 14/15] chore(deps): bump node from 22-alpine to 24-alpine (#636) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d842f6333..6d692a4dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine +FROM node:24-alpine ARG VERSION=latest RUN addgroup -S mcp && adduser -S mcp -G mcp RUN npm install -g mongodb-mcp-server@${VERSION} From 83f34b1ad4973f46673857f8b9746cc928debcbe Mon Sep 17 00:00:00 2001 From: "mongodb-devtools-bot[bot]" <189715634+mongodb-devtools-bot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:51:42 +0000 Subject: [PATCH 15/15] chore: release v1.1.0-prerelease.1 (#643) Co-authored-by: mongodb-devtools-bot[bot] <189715634+mongodb-devtools-bot[bot]@users.noreply.github.com> --- package-lock.json | 4 ++-- package.json | 2 +- src/common/packageInfo.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8c87c32c..811b26e46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mongodb-mcp-server", - "version": "1.0.3-prerelease.1", + "version": "1.1.0-prerelease.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mongodb-mcp-server", - "version": "1.0.3-prerelease.1", + "version": "1.1.0-prerelease.1", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", diff --git a/package.json b/package.json index 2801da8de..8b2e7b8be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongodb-mcp-server", "description": "MongoDB Model Context Protocol Server", - "version": "1.0.3-prerelease.1", + "version": "1.1.0-prerelease.1", "type": "module", "exports": { ".": { diff --git a/src/common/packageInfo.ts b/src/common/packageInfo.ts index b65b24839..37d426ec5 100644 --- a/src/common/packageInfo.ts +++ b/src/common/packageInfo.ts @@ -1,5 +1,5 @@ // This file was generated by scripts/updatePackageVersion.ts - Do not edit it manually. export const packageInfo = { - version: "1.0.3-prerelease.1", + version: "1.1.0-prerelease.1", mcpServerName: "MongoDB MCP Server", };