From a1c2b82574965245646e4071d4b45a79941965e0 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Mon, 6 Oct 2025 10:45:02 +0200 Subject: [PATCH 1/8] feat(search): Add a new tool to list search and vector search indexes It detects if the cluster supports search indexes the same way we do in all other tooling: by catching the exception. In some old clusters it might still return an empty list for unsupported custers, but we can't really know for sure. As we depend on mongot, we need to use the Atlas local image, which is not supported by mongodb-runner. Instead, we use testcontainers, that allows to run any docker image, which is enough for our use case. For now only run docker tests in Ubuntu * macos-latest does not support nested virtualisation yet * we don't have windows containers for atlas images --- .github/workflows/code-health-fork.yml | 5 + .github/workflows/code-health.yml | 5 + package-lock.json | 927 +++++++++++++++++- package.json | 3 +- src/tools/mongodb/search/listSearchIndexes.ts | 82 ++ src/tools/mongodb/tools.ts | 2 + tests/accuracy/listSearchIndexes.test.ts | 28 + tests/integration/helpers.ts | 4 + .../integration/tools/atlas/clusters.test.ts | 6 +- .../tools/mongodb/mongodbHelpers.ts | 62 +- .../mongodb/search/listSearchIndexes.test.ts | 174 ++++ tests/integration/transports/stdio.test.ts | 2 +- 12 files changed, 1257 insertions(+), 43 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/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 c52a96fc9..fa8939b5d 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", @@ -11916,6 +12566,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", @@ -11958,8 +12625,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" @@ -12234,6 +12901,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", @@ -13340,12 +14040,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", @@ -13418,6 +14147,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", @@ -13847,11 +14588,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", @@ -13863,8 +14604,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", @@ -13875,6 +14616,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", @@ -13890,7 +14632,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -13900,15 +14641,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", @@ -13922,8 +14663,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", @@ -14106,6 +14847,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", @@ -14218,6 +15060,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", @@ -15365,8 +16217,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -15498,6 +16348,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 72981fc6f..2dac53a36 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@types/yargs-parser": "^21.0.3", "@typescript-eslint/parser": "^8.44.0", "@vitest/coverage-v8": "^3.2.4", + "@vitest/eslint-plugin": "^1.3.4", "ai": "^4.3.17", "duplexpair": "^1.0.2", "eslint": "^9.34.0", @@ -90,10 +91,10 @@ "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", - "@vitest/eslint-plugin": "^1.3.4", "uuid": "^13.0.0", "vitest": "^3.2.4" }, diff --git a/src/tools/mongodb/search/listSearchIndexes.ts b/src/tools/mongodb/search/listSearchIndexes.ts new file mode 100644 index 000000000..93f76889b --- /dev/null +++ b/src/tools/mongodb/search/listSearchIndexes.ts @@ -0,0 +1,82 @@ +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( + `There are no search or vector search indexes in ${database}.${collection}` + ), + }; + } + } + + /** + * 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, + { database, collection }: 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", + }, + ], + }; + } else { + return { + content: [ + { + text: `Could not retrieve indexes in ${database}.${collection}: ${EJSON.stringify(error)}.`, + type: "text", + }, + ], + }; + } + } +} 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/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/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index 60961df32..7683b8b93 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -16,6 +16,9 @@ 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 { GenericContainer } from "testcontainers"; +import type { StartedTestContainer } from "testcontainers"; +import { ShellWaitStrategy } from "testcontainers/build/wait-strategies/shell-wait-strategy.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -58,15 +61,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 }, + downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration = { enterprise: false }, serverArgs: string[] = [] ): void { - describe(name, () => { + describe.skipIf(!isMongoDBEnvSupported(downloadOptions))(name, () => { const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions, serverArgs); const integration = setupIntegrationTest( () => ({ @@ -93,11 +98,35 @@ export function describeWithMongoDB( }); } +function isSearchOptions( + opt: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration +): opt is MongoSearchConfiguration { + return (opt as MongoSearchConfiguration)?.search === true; +} + +function isTestContainersCluster( + cluster: MongoCluster | StartedTestContainer | undefined +): cluster is StartedTestContainer { + return !!(cluster as StartedTestContainer)?.stop; +} + +function isMongoDBEnvSupported( + downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration +): boolean { + // on GHA, OSX does not support nested virtualisation and we don't release + // windows containers, so for now we can only run these tests in Linux. + if (isSearchOptions(downloadOptions) && process.env.GITHUB_ACTIONS === "true") { + return process.platform === "linux"; + } + + return true; +} + export function setupMongoDBIntegrationTest( - downloadOptions: MongoClusterOptions["downloadOptions"], + downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration, serverArgs: string[] ): MongoDBIntegrationTest { - let mongoCluster: MongoCluster | undefined; + let mongoCluster: MongoCluster | StartedTestContainer | undefined; let mongoClient: MongoClient | undefined; let randomDbName: string; @@ -111,6 +140,15 @@ export function setupMongoDBIntegrationTest( }); beforeAll(async function () { + if (isSearchOptions(downloadOptions)) { + mongoCluster = await new GenericContainer(downloadOptions.image ?? "mongodb/mongodb-atlas-local:8") + .withExposedPorts(27017) + .withCommand(["/usr/local/bin/runner", "server"]) + .withWaitStrategy(new ShellWaitStrategy(`mongosh --eval 'db.test.getSearchIndexes()'`)) + .start(); + return; + } + // Downloading Windows executables in CI takes a long time because // they include debug symbols... const tmpDir = path.join(__dirname, "..", "..", "..", "tmp"); @@ -152,7 +190,11 @@ export function setupMongoDBIntegrationTest( }, 120_000); afterAll(async function () { - await mongoCluster?.close(); + if (isTestContainersCluster(mongoCluster)) { + await mongoCluster.stop(); + } else { + await mongoCluster?.close(); + } mongoCluster = undefined; }); @@ -161,6 +203,11 @@ export function setupMongoDBIntegrationTest( throw new Error("beforeAll() hook not ran yet"); } + if (isTestContainersCluster(mongoCluster)) { + const mappedPort = mongoCluster.getMappedPort(27017); + return `mongodb://${mongoCluster.getHost()}:${mappedPort}`; + } + return mongoCluster.connectionString; }; @@ -268,6 +315,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/search/listSearchIndexes.test.ts b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts new file mode 100644 index 000000000..7c3895842 --- /dev/null +++ b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts @@ -0,0 +1,174 @@ +import { describeWithMongoDB, getSingleDocFromUntrustedContent } from "../mongodbHelpers.js"; +import { describe, it, expect, beforeEach } from "vitest"; +import { + getResponseContent, + databaseCollectionParameters, + validateToolMetadata, + validateThrowsForInvalidArguments, + databaseCollectionInvalidArgs, + sleep, +} 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 content = getResponseContent(response.content); + 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 content = getResponseContent(response.content); + 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 success = false; + 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 } } }]); + success = true; + break; + } catch (err) { + lastError = err; + await sleep(100); + } + } + + if (!success) { + 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 success = false; + 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) { + success = true; + break; + } + } catch (err) { + lastError = err; + await sleep(100); + } + } + + if (!success) { + throw new Error( + `Index ${indexName} in ${database}.${collection} is not ready: +lastIndexStatus: ${JSON.stringify(lastIndexStatus)} +lastError: ${JSON.stringify(lastError)}` + ); + } +} diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index aaa61d638..b5ed80840 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -32,7 +32,7 @@ describeWithMongoDB("StdioRunner", (integration) => { const response = await client.listTools(); expect(response).toBeDefined(); expect(response.tools).toBeDefined(); - expect(response.tools).toHaveLength(21); + expect(response.tools).toHaveLength(22); const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedTools[0]?.name).toBe("aggregate"); From fd5bb034853be8809e65b6523b1624b3c50a69b6 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 10:25:00 +0200 Subject: [PATCH 2/8] chore: simplify wait methods (pr comments) --- .../mongodb/search/listSearchIndexes.test.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts index 7c3895842..3af48f5fd 100644 --- a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts +++ b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts @@ -118,24 +118,20 @@ describeWithMongoDB( ); async function waitUntilSearchIsReady(provider: NodeDriverServiceProvider, abortSignal: AbortSignal): Promise { - let success = false; 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 } } }]); - success = true; - break; + return; } catch (err) { lastError = err; await sleep(100); } } - if (!success) { - throw new Error(`Search Management Index is not ready.\nlastError: ${JSON.stringify(lastError)}`); - } + throw new Error(`Search Management Index is not ready.\nlastError: ${JSON.stringify(lastError)}`); } async function waitUntilIndexIsQueryable( @@ -145,7 +141,6 @@ async function waitUntilIndexIsQueryable( indexName: string, abortSignal: AbortSignal ): Promise { - let success = false; let lastIndexStatus: unknown = null; let lastError: unknown = null; @@ -155,8 +150,7 @@ async function waitUntilIndexIsQueryable( lastIndexStatus = indexStatus; if (indexStatus?.queryable === true) { - success = true; - break; + return; } } catch (err) { lastError = err; @@ -164,11 +158,9 @@ async function waitUntilIndexIsQueryable( } } - if (!success) { - throw new Error( - `Index ${indexName} in ${database}.${collection} is not ready: + throw new Error( + `Index ${indexName} in ${database}.${collection} is not ready: lastIndexStatus: ${JSON.stringify(lastIndexStatus)} lastError: ${JSON.stringify(lastError)}` - ); - } + ); } From 253e49c10a3b45f581e763580db608447b334164 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 11:14:50 +0200 Subject: [PATCH 3/8] chore: Refactor the integration test set up so it's easier to maintain Now, there is a new class, MongoDBClusterProcess, that knows how to start and stop a cluster based on the provided configuration. This decouples the test setup and simplifies any future approaches to consolidate running strategies into one. --- tests/accuracy/sdk/describeAccuracyTests.ts | 2 +- .../common/connectionManager.oidc.test.ts | 3 +- .../tools/mongodb/mongodbClusterProcess.ts | 104 ++++++++++++++++ .../tools/mongodb/mongodbHelpers.ts | 111 +++--------------- .../tools/mongodb/mongodbTool.test.ts | 2 +- 5 files changed, 123 insertions(+), 99 deletions(-) create mode 100644 tests/integration/tools/mongodb/mongodbClusterProcess.ts 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/tools/mongodb/mongodbClusterProcess.ts b/tests/integration/tools/mongodb/mongodbClusterProcess.ts new file mode 100644 index 000000000..8ed7a2a10 --- /dev/null +++ b/tests/integration/tools/mongodb/mongodbClusterProcess.ts @@ -0,0 +1,104 @@ +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(); + return; + } + + 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 7683b8b93..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,9 +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 { GenericContainer } from "testcontainers"; -import type { StartedTestContainer } from "testcontainers"; -import { ShellWaitStrategy } from "testcontainers/build/wait-strategies/shell-wait-strategy.js"; +import { MongoDBClusterProcess } from "./mongodbClusterProcess.js"; +import type { MongoClusterConfiguration } from "./mongodbClusterProcess.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -52,6 +49,12 @@ const testDataPaths = [ }, ]; +const DEFAULT_MONGODB_PROCESS_OPTIONS: MongoClusterConfiguration = { + runner: true, + downloadOptions: { enterprise: false }, + serverArgs: [], +}; + interface MongoDBIntegrationTest { mongoClient: () => MongoClient; connectionString: () => string; @@ -68,11 +71,10 @@ export function describeWithMongoDB( fn: (integration: MongoDBIntegrationTestCase) => void, getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig, getDriverOptions: (mdbIntegration: MongoDBIntegrationTest) => DriverOptions = () => defaultDriverOptions, - downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration = { enterprise: false }, - serverArgs: string[] = [] + downloadOptions: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS ): void { - describe.skipIf(!isMongoDBEnvSupported(downloadOptions))(name, () => { - const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions, serverArgs); + describe.skipIf(!MongoDBClusterProcess.isConfigurationSupportedInCurrentEnv(downloadOptions))(name, () => { + const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions); const integration = setupIntegrationTest( () => ({ ...getUserConfig(mdbIntegration), @@ -98,35 +100,10 @@ export function describeWithMongoDB( }); } -function isSearchOptions( - opt: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration -): opt is MongoSearchConfiguration { - return (opt as MongoSearchConfiguration)?.search === true; -} - -function isTestContainersCluster( - cluster: MongoCluster | StartedTestContainer | undefined -): cluster is StartedTestContainer { - return !!(cluster as StartedTestContainer)?.stop; -} - -function isMongoDBEnvSupported( - downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration -): boolean { - // on GHA, OSX does not support nested virtualisation and we don't release - // windows containers, so for now we can only run these tests in Linux. - if (isSearchOptions(downloadOptions) && process.env.GITHUB_ACTIONS === "true") { - return process.platform === "linux"; - } - - return true; -} - export function setupMongoDBIntegrationTest( - downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration, - serverArgs: string[] + configuration: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS ): MongoDBIntegrationTest { - let mongoCluster: MongoCluster | StartedTestContainer | undefined; + let mongoCluster: MongoDBClusterProcess | undefined; let mongoClient: MongoClient | undefined; let randomDbName: string; @@ -140,61 +117,11 @@ export function setupMongoDBIntegrationTest( }); beforeAll(async function () { - if (isSearchOptions(downloadOptions)) { - mongoCluster = await new GenericContainer(downloadOptions.image ?? "mongodb/mongodb-atlas-local:8") - .withExposedPorts(27017) - .withCommand(["/usr/local/bin/runner", "server"]) - .withWaitStrategy(new ShellWaitStrategy(`mongosh --eval 'db.test.getSearchIndexes()'`)) - .start(); - return; - } - - // 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 () { - if (isTestContainersCluster(mongoCluster)) { - await mongoCluster.stop(); - } else { - await mongoCluster?.close(); - } + await mongoCluster?.close(); mongoCluster = undefined; }); @@ -203,12 +130,7 @@ export function setupMongoDBIntegrationTest( throw new Error("beforeAll() hook not ran yet"); } - if (isTestContainersCluster(mongoCluster)) { - const mappedPort = mongoCluster.getMappedPort(27017); - return `mongodb://${mongoCluster.getHost()}:${mappedPort}`; - } - - return mongoCluster.connectionString; + return mongoCluster.connectionString(); }; return { @@ -219,7 +141,6 @@ export function setupMongoDBIntegrationTest( return mongoClient; }, connectionString: getConnectionString, - randomDbName: () => randomDbName, }; } diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index f2e4930a2..5853753e6 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -52,7 +52,7 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => { }; describe("MongoDBTool implementations", () => { - const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []); + const mdbIntegration = setupMongoDBIntegrationTest(); const executeStub: MockedFunction<() => Promise> = vi .fn() .mockResolvedValue({ content: [{ type: "text", text: "Something" }] }); From 4a33fa864ed19a89442315ab4a5754723f07b9a3 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 12:12:51 +0200 Subject: [PATCH 4/8] chore: make sure that db/coll are in the untrusted data wrapper --- src/tools/mongodb/search/listSearchIndexes.ts | 10 ++-------- .../tools/mongodb/search/listSearchIndexes.test.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/tools/mongodb/search/listSearchIndexes.ts b/src/tools/mongodb/search/listSearchIndexes.ts index 93f76889b..a761825a0 100644 --- a/src/tools/mongodb/search/listSearchIndexes.ts +++ b/src/tools/mongodb/search/listSearchIndexes.ts @@ -33,6 +33,7 @@ export class ListSearchIndexesTool extends MongoDBToolBase { } else { return { content: formatUntrustedData( + "Could not retrieve search indexes", `There are no search or vector search indexes in ${database}.${collection}` ), }; @@ -69,14 +70,7 @@ export class ListSearchIndexesTool extends MongoDBToolBase { ], }; } else { - return { - content: [ - { - text: `Could not retrieve indexes in ${database}.${collection}: ${EJSON.stringify(error)}.`, - type: "text", - }, - ], - }; + return { content: formatUntrustedData("Could not retrieve search indexes", EJSON.stringify(error)) }; } } } diff --git a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts index 3af48f5fd..97571c0a9 100644 --- a/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts +++ b/tests/integration/tools/mongodb/search/listSearchIndexes.test.ts @@ -7,6 +7,7 @@ import { 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"; @@ -54,7 +55,9 @@ describeWithMongoDB( name: "list-search-indexes", arguments: { database: "any", collection: "foo" }, }); - const content = getResponseContent(response.content); + 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"); }); }); @@ -65,7 +68,9 @@ describeWithMongoDB( name: "list-search-indexes", arguments: { database: "any", collection: "foo" }, }); - const content = getResponseContent(response.content); + 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"); }); }); From 32cd5508d61304d1da03f19d95f4561e50cb69ad Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 12:36:59 +0200 Subject: [PATCH 5/8] chore: use isError and delegate to super.handleError for generic error messages --- src/tools/mongodb/search/listSearchIndexes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/mongodb/search/listSearchIndexes.ts b/src/tools/mongodb/search/listSearchIndexes.ts index a761825a0..f706ab051 100644 --- a/src/tools/mongodb/search/listSearchIndexes.ts +++ b/src/tools/mongodb/search/listSearchIndexes.ts @@ -58,7 +58,7 @@ export class ListSearchIndexesTool extends MongoDBToolBase { protected handleError( error: unknown, - { database, collection }: ToolArgs + args: ToolArgs ): Promise | CallToolResult { if (error instanceof Error && "codeName" in error && error.codeName === "SearchNotEnabled") { return { @@ -66,11 +66,11 @@ export class ListSearchIndexesTool extends MongoDBToolBase { { 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, }, ], }; - } else { - return { content: formatUntrustedData("Could not retrieve search indexes", EJSON.stringify(error)) }; } + return super.handleError(error, args); } } From de4042249c866d32ea2bad516907a1b7ff0ca12c Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 14:32:09 +0200 Subject: [PATCH 6/8] chore: Only enable the tool in test environment for now --- src/tools/mongodb/search/listSearchIndexes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tools/mongodb/search/listSearchIndexes.ts b/src/tools/mongodb/search/listSearchIndexes.ts index f706ab051..1b520d523 100644 --- a/src/tools/mongodb/search/listSearchIndexes.ts +++ b/src/tools/mongodb/search/listSearchIndexes.ts @@ -40,6 +40,11 @@ export class ListSearchIndexesTool extends MongoDBToolBase { } } + 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 From b121c88a0ef2ebdf5fe6a05b24dcb8a86258f2e3 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 14:43:35 +0200 Subject: [PATCH 7/8] Update tests/integration/tools/mongodb/mongodbClusterProcess.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/integration/tools/mongodb/mongodbClusterProcess.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/tools/mongodb/mongodbClusterProcess.ts b/tests/integration/tools/mongodb/mongodbClusterProcess.ts index 8ed7a2a10..b0f7ee863 100644 --- a/tests/integration/tools/mongodb/mongodbClusterProcess.ts +++ b/tests/integration/tools/mongodb/mongodbClusterProcess.ts @@ -83,7 +83,6 @@ export class MongoDBClusterProcess { async close(): Promise { await this.tearDownFunction(); - return; } static isConfigurationSupportedInCurrentEnv(config: MongoClusterConfiguration): boolean { From c10490823e30994bd23c6c210c7a58fb744b7b86 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Wed, 8 Oct 2025 14:55:50 +0200 Subject: [PATCH 8/8] chore: now it's 21 tools --- tests/integration/transports/stdio.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/transports/stdio.test.ts b/tests/integration/transports/stdio.test.ts index b5ed80840..aaa61d638 100644 --- a/tests/integration/transports/stdio.test.ts +++ b/tests/integration/transports/stdio.test.ts @@ -32,7 +32,7 @@ describeWithMongoDB("StdioRunner", (integration) => { const response = await client.listTools(); expect(response).toBeDefined(); expect(response.tools).toBeDefined(); - expect(response.tools).toHaveLength(22); + expect(response.tools).toHaveLength(21); const sortedTools = response.tools.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedTools[0]?.name).toBe("aggregate");