From 8454881b3693cee3dfc3190f79754339d642fd94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:55:36 +0000 Subject: [PATCH 1/5] chore: bump @aws-sdk/client-s3 from 3.964.0 to 3.968.0 Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.964.0 to 3.968.0. - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.968.0/clients/client-s3) --- updated-dependencies: - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.968.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70bfbd3ca5..0cc2d29ed7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2697,7 +2697,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2709,7 +2708,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2720,7 +2718,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3669,7 +3666,6 @@ "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3717,7 +3713,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5097,7 +5092,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5451,7 +5445,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5465,7 +5458,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5479,7 +5471,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5493,7 +5484,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5507,7 +5497,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5521,7 +5510,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5535,7 +5523,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5549,7 +5536,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5563,7 +5549,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5577,7 +5562,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5591,7 +5575,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5605,7 +5588,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5619,7 +5601,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5633,7 +5614,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5647,7 +5627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5661,7 +5640,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5678,7 +5656,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5692,7 +5669,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5706,7 +5682,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ From a08ffb8755475b700849c9dfbab453df7a4a3f77 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 14 Jan 2026 10:22:44 +0000 Subject: [PATCH 2/5] fix: npm install --- package-lock.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0cc2d29ed7..70bfbd3ca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2697,6 +2697,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2708,6 +2709,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2718,6 +2720,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3666,6 +3669,7 @@ "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3713,6 +3717,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5092,6 +5097,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5445,6 +5451,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5458,6 +5465,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5471,6 +5479,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5484,6 +5493,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5497,6 +5507,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5510,6 +5521,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5523,6 +5535,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5536,6 +5549,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5549,6 +5563,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5562,6 +5577,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5575,6 +5591,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5588,6 +5605,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5601,6 +5619,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5614,6 +5633,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5627,6 +5647,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5640,6 +5661,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5656,6 +5678,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5669,6 +5692,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5682,6 +5706,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ From 392c96f5bdbe287e0eb3f65742ac77522ef3fe94 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Tue, 20 Jan 2026 12:21:50 +0000 Subject: [PATCH 3/5] feat: extract v2 parity endpoints with legacy api --- scripts/endpoint-parity-output.json | 1025 +++++++++++++++++++++++++++ scripts/endpoint-parity.js | 242 +++++++ 2 files changed, 1267 insertions(+) create mode 100644 scripts/endpoint-parity-output.json create mode 100644 scripts/endpoint-parity.js diff --git a/scripts/endpoint-parity-output.json b/scripts/endpoint-parity-output.json new file mode 100644 index 0000000000..ec77fd3cbf --- /dev/null +++ b/scripts/endpoint-parity-output.json @@ -0,0 +1,1025 @@ +{ + "v1.0": { + "CAP": { + "count": 1, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/alerts/{alertId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/alerts/{alertId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/alerts/{alertId}", + "sourceOperationId": "acknowledgeAlert", + "targetOperationId": "acknowledgeGroupAlert", + "sunset": "2026-11-30", + "deprecated": true + } + ] + }, + "Backup - Atlas": { + "count": 8, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/backup/exportBuckets", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets", + "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets", + "sourceOperationId": "listExportBuckets", + "targetOperationId": "listGroupBackupExportBuckets", + "sunset": "2026-05-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/backup/exportBuckets", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets", + "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets", + "sourceOperationId": "createExportBucket", + "targetOperationId": "createGroupBackupExportBucket", + "sunset": "2026-05-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/backup/exportBuckets/{exportBucketId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets/{exportBucketId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets/{exportBucketId}", + "sourceOperationId": "getExportBucket", + "targetOperationId": "getGroupBackupExportBucket", + "sunset": "2026-05-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/backupCompliancePolicy", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backupCompliancePolicy", + "targetPath": "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy", + "sourceOperationId": "getDataProtectionSettings", + "targetOperationId": "getGroupBackupCompliancePolicy", + "sunset": "2026-10-01", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PUT", + "normalizedPath": "/groups/{groupId}/backupCompliancePolicy", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backupCompliancePolicy", + "targetPath": "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy", + "sourceOperationId": "updateDataProtectionSettings", + "targetOperationId": "updateGroupBackupCompliancePolicy", + "sunset": "2026-10-01", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourceOperationId": "getBackupSchedule", + "targetOperationId": "getGroupClusterBackupSchedule", + "sunset": "2026-05-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourceOperationId": "updateBackupSchedule", + "targetOperationId": "updateGroupClusterBackupSchedule", + "sunset": "2026-05-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", + "sourceOperationId": "deleteAllBackupSchedules", + "targetOperationId": "deleteGroupClusterBackupSchedule", + "sunset": "2026-05-30", + "deprecated": true + } + ] + }, + "Atlas Dedicated": { + "count": 20, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", + "sourceOperationId": "listLegacyClusters", + "targetOperationId": "listGroupClusters", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", + "sourceOperationId": "createLegacyCluster", + "targetOperationId": "createGroupCluster", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "sourceOperationId": "getLegacyCluster", + "targetOperationId": "getGroupCluster", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "sourceOperationId": "updateClusterConfiguration", + "targetOperationId": "updateGroupCluster", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "sourceOperationId": "deleteLegacyCluster", + "targetOperationId": "deleteGroupCluster", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", + "sourceOperationId": "downloadSharedClusterBackup", + "targetOperationId": "downloadGroupClusterBackupTenant", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", + "sourceOperationId": "createSharedClusterBackupRestoreJob", + "targetOperationId": "createGroupClusterBackupTenantRestore", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", + "sourceOperationId": "listSharedClusterBackupRestoreJobs", + "targetOperationId": "listGroupClusterBackupTenantRestores", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", + "sourceOperationId": "getSharedClusterBackupRestoreJob", + "targetOperationId": "getGroupClusterBackupTenantRestore", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", + "sourceOperationId": "listSharedClusterBackups", + "targetOperationId": "listGroupClusterBackupTenantSnapshots", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", + "sourceOperationId": "getSharedClusterBackup", + "targetOperationId": "getGroupClusterBackupTenantSnapshot", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites", + "sourceOperationId": "getGeoSharding", + "targetOperationId": "getGroupClusterGlobalWrites", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourceOperationId": "addAllCustomZoneMappings", + "targetOperationId": "createGroupClusterGlobalWriteCustomZoneMapping", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourceOperationId": "deleteAllLegacyCustomZoneMappings", + "targetOperationId": "deleteGroupClusterGlobalWriteCustomZoneMapping", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourceOperationId": "createLegacyManagedNamespace", + "targetOperationId": "createGroupClusterGlobalWriteManagedNamespace", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourceOperationId": "deleteLegacyManagedNamespace", + "targetOperationId": "deleteGroupClusterGlobalWriteManagedNamespaces", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/processArgs", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/processArgs", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs", + "sourceOperationId": "getClusterAdvancedConfiguration", + "targetOperationId": "getGroupClusterProcessArgs", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/processArgs", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/processArgs", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs", + "sourceOperationId": "updateClusterAdvancedConfiguration", + "targetOperationId": "updateGroupClusterProcessArgs", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/restartPrimaries", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/restartPrimaries", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/restartPrimaries", + "sourceOperationId": "testLegacyFailover", + "targetOperationId": "restartGroupClusterPrimaries", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", + "sourceOperationId": "downloadHostLogs", + "targetOperationId": "downloadGroupClusterLog", + "sunset": "2026-11-30", + "deprecated": true + } + ] + }, + "Search Catalog & Deployments": { + "count": 5, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes", + "sourceOperationId": "createAtlasSearchIndexDeprecated", + "targetOperationId": "createGroupClusterFtsIndex", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", + "sourceOperationId": "listAtlasSearchIndexesDeprecated", + "targetOperationId": "listGroupClusterFtsIndex", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourceOperationId": "getAtlasSearchIndexDeprecated", + "targetOperationId": "getGroupClusterFtsIndex", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourceOperationId": "updateAtlasSearchIndexDeprecated", + "targetOperationId": "updateGroupClusterFtsIndex", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", + "sourceOperationId": "deleteAtlasSearchIndexDeprecated", + "targetOperationId": "deleteGroupClusterFtsIndex", + "sunset": "2026-11-30", + "deprecated": true + } + ] + }, + "IAM Authorization": { + "count": 19, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/invites", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites", + "sourceOperationId": "listProjectInvitations", + "targetOperationId": "listGroupInvites", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/invites", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites", + "sourceOperationId": "createProjectInvitation", + "targetOperationId": "createGroupInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/invites", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites", + "sourceOperationId": "updateProjectInvitation", + "targetOperationId": "updateGroupInvites", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", + "sourceOperationId": "getProjectInvitation", + "targetOperationId": "getGroupInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", + "sourceOperationId": "updateProjectInvitationById", + "targetOperationId": "updateGroupInviteById", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", + "sourceOperationId": "deleteProjectInvitation", + "targetOperationId": "deleteGroupInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/users", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/users", + "targetPath": "/api/atlas/v2/groups/{groupId}/users", + "sourceOperationId": "listProjectUsers", + "targetOperationId": "listGroupUsers", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/users/{userId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/users/{userId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/users/{userId}", + "sourceOperationId": "removeProjectUser", + "targetOperationId": "removeGroupUser", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/orgs/{orgId}/invites", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", + "sourceOperationId": "listOrganizationInvitations", + "targetOperationId": "listOrgInvites", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/orgs/{orgId}/invites", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", + "sourceOperationId": "createOrganizationInvitation", + "targetOperationId": "createOrgInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/orgs/{orgId}/invites", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", + "sourceOperationId": "updateOrganizationInvitation", + "targetOperationId": "updateOrgInvites", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", + "sourceOperationId": "getOrganizationInvitation", + "targetOperationId": "getOrgInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", + "sourceOperationId": "updateOrganizationInvitationById", + "targetOperationId": "updateOrgInviteById", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", + "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", + "sourceOperationId": "deleteOrganizationInvitation", + "targetOperationId": "deleteOrgInvite", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users", + "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users", + "sourceOperationId": "listTeamUsers", + "targetOperationId": "listOrgTeamUsers", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users", + "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users", + "sourceOperationId": "addTeamUser", + "targetOperationId": "addOrgTeamUsers", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users/{userId}", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users/{userId}", + "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users/{userId}", + "sourceOperationId": "removeTeamUser", + "targetOperationId": "removeOrgTeamUserFromTeam", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/orgs/{orgId}/users", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/users", + "targetPath": "/api/atlas/v2/orgs/{orgId}/users", + "sourceOperationId": "listOrganizationUsers", + "targetOperationId": "listOrgUsers", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/orgs/{orgId}/users/{userId}", + "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/users/{userId}", + "targetPath": "/api/atlas/v2/orgs/{orgId}/users/{userId}", + "sourceOperationId": "removeOrganizationUser", + "targetOperationId": "removeOrgUser", + "sunset": "2026-09-15", + "deprecated": true + } + ] + }, + "Atlas Migrations": { + "count": 2, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/liveMigrations", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/liveMigrations", + "targetPath": "/api/atlas/v2/groups/{groupId}/liveMigrations", + "sourceOperationId": "createPushMigration", + "targetOperationId": "createGroupLiveMigration", + "sunset": "2026-11-30", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/liveMigrations/validate", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/liveMigrations/validate", + "targetPath": "/api/atlas/v2/groups/{groupId}/liveMigrations/validate", + "sourceOperationId": "validateMigration", + "targetOperationId": "validateGroupLiveMigrations", + "sunset": "2026-11-30", + "deprecated": true + } + ] + }, + "Atlas Clusters Security III": { + "count": 10, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "sourceOperationId": "listServerlessPrivateEndpoints", + "targetOperationId": "listGroupPrivateEndpointServerlessInstanceEndpoint", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", + "sourceOperationId": "createServerlessPrivateEndpoint", + "targetOperationId": "createGroupPrivateEndpointServerlessInstanceEndpoint", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourceOperationId": "getServerlessPrivateEndpoint", + "targetOperationId": "getGroupPrivateEndpointServerlessInstanceEndpoint", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourceOperationId": "updateServerlessPrivateEndpoint", + "targetOperationId": "updateGroupPrivateEndpointServerlessInstanceEndpoint", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", + "sourceOperationId": "deleteServerlessPrivateEndpoint", + "targetOperationId": "deleteGroupPrivateEndpointServerlessInstanceEndpoint", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless", + "sourceOperationId": "listServerlessInstances", + "targetOperationId": "listGroupServerlessInstances", + "sunset": "2026-01-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/serverless", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless", + "sourceOperationId": "createServerlessInstance", + "targetOperationId": "createGroupServerlessInstance", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless/{name}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", + "sourceOperationId": "getServerlessInstance", + "targetOperationId": "getGroupServerlessInstance", + "sunset": "2026-01-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/serverless/{name}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", + "sourceOperationId": "updateServerlessInstance", + "targetOperationId": "updateGroupServerlessInstance", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/serverless/{name}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", + "sourceOperationId": "deleteServerlessInstance", + "targetOperationId": "deleteGroupServerlessInstance", + "sunset": "2026-01-22", + "deprecated": true + } + ] + }, + "Backup - Private Cloud": { + "count": 5, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "sourceOperationId": "listServerlessBackupRestoreJobs", + "targetOperationId": "listGroupServerlessBackupRestoreJobs", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", + "sourceOperationId": "createServerlessBackupRestoreJob", + "targetOperationId": "createGroupServerlessBackupRestoreJob", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", + "sourceOperationId": "getServerlessBackupRestoreJob", + "targetOperationId": "getGroupServerlessBackupRestoreJob", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/snapshots", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/snapshots", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/snapshots", + "sourceOperationId": "listServerlessBackups", + "targetOperationId": "listGroupServerlessBackupSnapshots", + "sunset": "2026-01-22", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", + "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", + "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", + "sourceOperationId": "getServerlessBackup", + "targetOperationId": "getGroupServerlessBackupSnapshot", + "sunset": "2026-01-22", + "deprecated": true + } + ] + }, + "IAM Identity Security": { + "count": 3, + "endpoints": [ + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "POST", + "normalizedPath": "/users", + "sourcePath": "/api/atlas/v1.0/users", + "targetPath": "/api/atlas/v2/users", + "sourceOperationId": "createUser", + "targetOperationId": "createUser", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/users/byName/{userName}", + "sourcePath": "/api/atlas/v1.0/users/byName/{userName}", + "targetPath": "/api/atlas/v2/users/byName/{userName}", + "sourceOperationId": "getUserByUsername", + "targetOperationId": "getUserByName", + "sunset": "2026-09-15", + "deprecated": true + }, + { + "sourceVersion": "v1.0", + "targetVersion": "2023-01-01", + "method": "GET", + "normalizedPath": "/users/{userId}", + "sourcePath": "/api/atlas/v1.0/users/{userId}", + "targetPath": "/api/atlas/v2/users/{userId}", + "sourceOperationId": "getUser", + "targetOperationId": "getUser", + "sunset": "2026-09-15", + "deprecated": true + } + ] + } + }, + "v1.5": { + "Atlas Dedicated": { + "count": 6, + "endpoints": [ + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", + "sourceOperationId": "listClusters", + "targetOperationId": "listGroupClusters", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "POST", + "normalizedPath": "/groups/{groupId}/clusters", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", + "sourceOperationId": "createCluster", + "targetOperationId": "createGroupCluster", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "GET", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "sourceOperationId": "getCluster", + "targetOperationId": "getGroupCluster", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "PATCH", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", + "sourceOperationId": "updateCluster", + "targetOperationId": "updateGroupCluster", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", + "sourceOperationId": "deleteAllCustomZoneMappings", + "targetOperationId": "deleteGroupClusterGlobalWriteCustomZoneMapping", + "sunset": "2026-03-01", + "deprecated": true + }, + { + "sourceVersion": "v1.5", + "targetVersion": "2023-02-01", + "method": "DELETE", + "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", + "sourceOperationId": "deleteManagedNamespace", + "targetOperationId": "deleteGroupClusterGlobalWriteManagedNamespaces", + "sunset": "2026-03-01", + "deprecated": true + } + ] + } + }, + "summary": { + "v1.0 count": 316, + "v1.5 count": 9, + "v2 2023-01-01 count": 338, + "v2 2023-01-01 endpoints with sunset": 97, + "v2 2023-02-01 count": 358, + "v2 2023-02-01 endpoints with sunset": 95, + "v1.0 pairings with sunset": 73, + "v1.5 pairings with sunset": 6, + "teams": [ + "CAP", + "Backup - Atlas", + "Atlas Dedicated", + "Search Catalog & Deployments", + "IAM Authorization", + "Atlas Migrations", + "Atlas Clusters Security III", + "Backup - Private Cloud", + "IAM Identity Security" + ] + } +} \ No newline at end of file diff --git a/scripts/endpoint-parity.js b/scripts/endpoint-parity.js new file mode 100644 index 0000000000..0cae34781e --- /dev/null +++ b/scripts/endpoint-parity.js @@ -0,0 +1,242 @@ +#!/usr/bin/env node + +/** + * Script to find endpoint parity between v1/v1.5 and v2 API versions. + * + * Parity mappings: + * - v1 (v1.0) endpoints map 1-to-1 to v2 endpoints for version 2023-01-01 + * - v1.5 endpoints map 1-to-1 to v2 endpoints for version 2023-02-01 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// File paths +const V1_SPEC_PATH = path.join(__dirname, '../openapi/v1-deprecated/v1.json'); +const V2_SPEC_PATH = path.join(__dirname, '../openapi/.raw/v2.json'); +const V2_2023_01_01_PATH = path.join(__dirname, '../openapi/v2/openapi-2023-01-01.json'); +const V2_2023_02_01_PATH = path.join(__dirname, '../openapi/v2/openapi-2023-02-01.json'); + +/** + * Extract endpoints from an OpenAPI spec + * @param {object} spec - The OpenAPI specification object + * @param {string} versionFilter - Optional filter for path version (e.g., 'v1.0', 'v1.5', 'v2') + * @param {boolean} onlyWithSunset - If true, only include endpoints with x-sunset set + * @returns {Map} - Map of normalized path -> endpoint details + */ +function extractEndpoints(spec, versionFilter = null, onlyWithSunset = false) { + const endpoints = new Map(); + + if (!spec.paths) { + return endpoints; + } + + for (const [pathKey, pathItem] of Object.entries(spec.paths)) { + // Apply version filter if specified + if (versionFilter) { + const versionPattern = `/api/atlas/${versionFilter}`; + if (!pathKey.startsWith(versionPattern)) { + continue; + } + } + + const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']; + + for (const method of methods) { + if (pathItem[method]) { + const operation = pathItem[method]; + const sunset = operation['x-sunset'] || null; + + // Skip if we only want sunset endpoints and this one doesn't have sunset + if (onlyWithSunset && !sunset) { + continue; + } + + const key = `${method.toUpperCase()} ${pathKey}`; + endpoints.set(key, { + path: pathKey, + method: method.toUpperCase(), + operationId: operation.operationId || 'N/A', + sunset: sunset, + deprecated: operation.deprecated || false + }); + } + } + } + + return endpoints; +} + +/** + * Find the owning team for a given path and method + * @param {string} pathStr - The API path + * @param {string} method - The HTTP method + * @param {object} spec - The OpenAPI specification object + * @returns {string|null} - The owning team or null if not found + */ +function findTeam(pathStr, method, spec) { + const pathItem = spec.paths[pathStr]; + if (!pathItem) return null; + const operation = pathItem[method]; + if (!operation) return null; + return operation['x-xgen-owner-team'] || null; +} + +/** + * Normalize a path by removing the version prefix + * @param {string} pathStr - The API path + * @returns {string} - The normalized path without version prefix + */ +function normalizePath(pathStr) { + // Remove /api/atlas/v1.0/, /api/atlas/v1.5/, or /api/atlas/v2/ prefix + return pathStr.replace(/^\/api\/atlas\/v[12](\.[05])?/, ''); +} + +/** + * Get versioned path + * @param {string} pathStr - The API path + * @param {string} version - The version to use + * @returns {string} - The versioned path + */ +function getVersionedPath(pathStr, version) { + return `/api/atlas/${version}${normalizePath(pathStr)}`; +} + +/** + * Find pairings between two sets of endpoints + * @param {Map} sourceEndpoints - Source endpoints (v1 or v1.5) + * @param {Map} targetEndpoints - Target endpoints (v2) + * @param {string} sourceVersion - Source version label + * @param {string} targetVersion - Target version label + * @returns {Array} - Array of pairing objects + */ +function findPairings(sourceEndpoints, targetEndpoints, sourceVersion, targetVersion) { + const pairings = []; + + for (const [, sourceEndpoint] of sourceEndpoints) { + const normalizedSourcePath = normalizePath(sourceEndpoint.path); + + // Find matching v2 endpoint + for (const [, targetEndpoint] of targetEndpoints) { + const normalizedTargetPath = normalizePath(targetEndpoint.path); + + if (normalizedSourcePath === normalizedTargetPath && + sourceEndpoint.method === targetEndpoint.method) { + pairings.push({ + sourceVersion, + targetVersion, + method: sourceEndpoint.method, + sourcePath: sourceEndpoint.path, + targetPath: targetEndpoint.path, + sourceOperationId: sourceEndpoint.operationId, + targetOperationId: targetEndpoint.operationId, + sunset: targetEndpoint.sunset, + deprecated: targetEndpoint.deprecated + }); + break; + } + } + } + + return pairings; +} + +function aggregateByTeam(pairings, spec) { + const teamAggregation = {}; + + for (const pairing of pairings) { + const team = findTeam(getVersionedPath(pairing.targetPath, 'v2'), pairing.method.toLowerCase(), spec) || 'Unknown'; + + if (!teamAggregation[team]) { + teamAggregation[team] = { count: 0, endpoints: [] }; + } + + teamAggregation[team].count += 1; + teamAggregation[team].endpoints.push(pairing); + } + + return teamAggregation; +} + +// Minimum sunset date filter - only show endpoints with sunset >= this date +const MIN_SUNSET_DATE = '2026-01-01'; + +/** + * Filter pairings to only include those with sunset >= minDate + */ +function filterBySunsetDate(pairings, minDate) { + return pairings.filter(p => p.sunset && p.sunset >= minDate); +} + +/** + * Main execution + */ +function main() { + console.log('='.repeat(80)); + console.log('Endpoint Parity Analysis: v1/v1.5 to v2 Mappings (with Sunset)'); + console.log('='.repeat(80)); + console.log(); + console.log(`NOTE: Only showing v2 endpoints with x-sunset >= ${MIN_SUNSET_DATE}`); + console.log(); + + const v1Spec = JSON.parse(fs.readFileSync(V1_SPEC_PATH, 'utf8')); + const v2Spec = JSON.parse(fs.readFileSync(V2_SPEC_PATH, 'utf8')); + const v2_2023_01_01_Spec = JSON.parse(fs.readFileSync(V2_2023_01_01_PATH, 'utf8')); + const v2_2023_02_01_Spec = JSON.parse(fs.readFileSync(V2_2023_02_01_PATH, 'utf8')); + + // Extract endpoints - v1.0 and v1.5 are both in v1.json + const v1_0_Endpoints = extractEndpoints(v1Spec, 'v1.0'); + const v1_5_Endpoints = extractEndpoints(v1Spec, 'v1.5'); + + // Extract ALL v2 endpoints for reference counts + const v2_2023_01_01_AllEndpoints = extractEndpoints(v2_2023_01_01_Spec, 'v2', false); + const v2_2023_02_01_AllEndpoints = extractEndpoints(v2_2023_02_01_Spec, 'v2', false); + + // Extract only v2 endpoints with sunset + const v2_2023_01_01_SunsetEndpoints = extractEndpoints(v2_2023_01_01_Spec, 'v2', true); + const v2_2023_02_01_SunsetEndpoints = extractEndpoints(v2_2023_02_01_Spec, 'v2', true); + + const v1_0_AllPairings = findPairings(v1_0_Endpoints, v2_2023_01_01_SunsetEndpoints, 'v1.0', '2023-01-01'); + const v1_0_Pairings = filterBySunsetDate(v1_0_AllPairings, MIN_SUNSET_DATE); + console.log(`Found ${v1_0_Pairings.length} paired endpoints with sunset >= ${MIN_SUNSET_DATE}`); + + const v1_5_AllPairings = findPairings(v1_5_Endpoints, v2_2023_02_01_SunsetEndpoints, 'v1.5', '2023-02-01'); + const v1_5_Pairings = filterBySunsetDate(v1_5_AllPairings, MIN_SUNSET_DATE); + console.log(`Found ${v1_5_Pairings.length} paired endpoints with sunset >= ${MIN_SUNSET_DATE}`); + + const v1_0_TeamAggregation = aggregateByTeam(v1_0_Pairings, v2Spec); + const v1_5_TeamAggregation = aggregateByTeam(v1_5_Pairings, v2Spec); + + const teams = new Set([ + ...Object.keys(v1_0_TeamAggregation), + ...Object.keys(v1_5_TeamAggregation) + ]); + + // Output JSON + const output = { + "v1.0" : v1_0_TeamAggregation, + "v1.5" : v1_5_TeamAggregation, + summary: { + "v1.0 count": v1_0_Endpoints.size, + "v1.5 count": v1_5_Endpoints.size, + "v2 2023-01-01 count": v2_2023_01_01_AllEndpoints.size, + "v2 2023-01-01 endpoints with sunset": v2_2023_01_01_SunsetEndpoints.size, + "v2 2023-02-01 count": v2_2023_02_01_AllEndpoints.size, + "v2 2023-02-01 endpoints with sunset": v2_2023_02_01_SunsetEndpoints.size, + "v1.0 pairings with sunset": v1_0_Pairings.length, + "v1.5 pairings with sunset": v1_5_Pairings.length, + teams: Array.from(teams) + } + }; + + const outputPath = path.join(__dirname, 'endpoint-parity-output.json'); + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); + console.log(`\nJSON output written to: ${outputPath}`); +} + +main(); + From c70839f8bf6cbec6c63b36320f424342d6a55247 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Tue, 20 Jan 2026 12:28:56 +0000 Subject: [PATCH 4/5] fix: regenerate parity report --- scripts/endpoint-parity-output.json | 83 +---------------------------- 1 file changed, 2 insertions(+), 81 deletions(-) diff --git a/scripts/endpoint-parity-output.json b/scripts/endpoint-parity-output.json index ec77fd3cbf..41a35dcac3 100644 --- a/scripts/endpoint-parity-output.json +++ b/scripts/endpoint-parity-output.json @@ -7,7 +7,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/alerts/{alertId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/alerts/{alertId}", "targetPath": "/api/atlas/v2/groups/{groupId}/alerts/{alertId}", "sourceOperationId": "acknowledgeAlert", @@ -24,7 +23,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/backup/exportBuckets", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets", "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets", "sourceOperationId": "listExportBuckets", @@ -36,7 +34,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/backup/exportBuckets", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets", "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets", "sourceOperationId": "createExportBucket", @@ -48,7 +45,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/backup/exportBuckets/{exportBucketId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backup/exportBuckets/{exportBucketId}", "targetPath": "/api/atlas/v2/groups/{groupId}/backup/exportBuckets/{exportBucketId}", "sourceOperationId": "getExportBucket", @@ -60,7 +56,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/backupCompliancePolicy", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backupCompliancePolicy", "targetPath": "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy", "sourceOperationId": "getDataProtectionSettings", @@ -72,7 +67,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PUT", - "normalizedPath": "/groups/{groupId}/backupCompliancePolicy", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/backupCompliancePolicy", "targetPath": "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy", "sourceOperationId": "updateDataProtectionSettings", @@ -84,7 +78,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourceOperationId": "getBackupSchedule", @@ -96,7 +89,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourceOperationId": "updateBackupSchedule", @@ -108,7 +100,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/schedule", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/schedule", "sourceOperationId": "deleteAllBackupSchedules", @@ -125,7 +116,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", "sourceOperationId": "listLegacyClusters", @@ -137,7 +127,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", "sourceOperationId": "createLegacyCluster", @@ -149,7 +138,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", "sourceOperationId": "getLegacyCluster", @@ -161,7 +149,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", "sourceOperationId": "updateClusterConfiguration", @@ -173,7 +160,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", "sourceOperationId": "deleteLegacyCluster", @@ -185,7 +171,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/download", "sourceOperationId": "downloadSharedClusterBackup", @@ -197,7 +182,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restore", "sourceOperationId": "createSharedClusterBackupRestoreJob", @@ -209,7 +193,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores", "sourceOperationId": "listSharedClusterBackupRestoreJobs", @@ -221,7 +204,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/restores/{restoreId}", "sourceOperationId": "getSharedClusterBackupRestoreJob", @@ -233,7 +215,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots", "sourceOperationId": "listSharedClusterBackups", @@ -245,7 +226,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backup/tenant/snapshots/{snapshotId}", "sourceOperationId": "getSharedClusterBackup", @@ -257,7 +237,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites", "sourceOperationId": "getGeoSharding", @@ -269,7 +248,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourceOperationId": "addAllCustomZoneMappings", @@ -281,7 +259,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourceOperationId": "deleteAllLegacyCustomZoneMappings", @@ -293,7 +270,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourceOperationId": "createLegacyManagedNamespace", @@ -305,7 +281,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourceOperationId": "deleteLegacyManagedNamespace", @@ -317,7 +292,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/processArgs", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/processArgs", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs", "sourceOperationId": "getClusterAdvancedConfiguration", @@ -329,7 +303,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/processArgs", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/processArgs", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs", "sourceOperationId": "updateClusterAdvancedConfiguration", @@ -341,7 +314,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/restartPrimaries", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/restartPrimaries", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/restartPrimaries", "sourceOperationId": "testLegacyFailover", @@ -353,7 +325,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz", "sourceOperationId": "downloadHostLogs", @@ -370,7 +341,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes", "sourceOperationId": "createAtlasSearchIndexDeprecated", @@ -382,7 +352,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}", "sourceOperationId": "listAtlasSearchIndexesDeprecated", @@ -394,7 +363,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourceOperationId": "getAtlasSearchIndexDeprecated", @@ -406,7 +374,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourceOperationId": "updateAtlasSearchIndexDeprecated", @@ -418,7 +385,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}", "sourceOperationId": "deleteAtlasSearchIndexDeprecated", @@ -435,7 +401,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/invites", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", "targetPath": "/api/atlas/v2/groups/{groupId}/invites", "sourceOperationId": "listProjectInvitations", @@ -447,7 +412,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/invites", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", "targetPath": "/api/atlas/v2/groups/{groupId}/invites", "sourceOperationId": "createProjectInvitation", @@ -459,7 +423,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/invites", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites", "targetPath": "/api/atlas/v2/groups/{groupId}/invites", "sourceOperationId": "updateProjectInvitation", @@ -471,7 +434,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", "sourceOperationId": "getProjectInvitation", @@ -483,7 +445,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", "sourceOperationId": "updateProjectInvitationById", @@ -495,7 +456,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", "sourceOperationId": "deleteProjectInvitation", @@ -507,7 +467,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/users", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/users", "targetPath": "/api/atlas/v2/groups/{groupId}/users", "sourceOperationId": "listProjectUsers", @@ -519,7 +478,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/users/{userId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/users/{userId}", "targetPath": "/api/atlas/v2/groups/{groupId}/users/{userId}", "sourceOperationId": "removeProjectUser", @@ -531,7 +489,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/orgs/{orgId}/invites", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", "sourceOperationId": "listOrganizationInvitations", @@ -543,7 +500,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/orgs/{orgId}/invites", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", "sourceOperationId": "createOrganizationInvitation", @@ -555,7 +511,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/orgs/{orgId}/invites", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites", "sourceOperationId": "updateOrganizationInvitation", @@ -567,7 +522,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", "sourceOperationId": "getOrganizationInvitation", @@ -579,7 +533,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", "sourceOperationId": "updateOrganizationInvitationById", @@ -591,7 +544,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/orgs/{orgId}/invites/{invitationId}", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/invites/{invitationId}", "targetPath": "/api/atlas/v2/orgs/{orgId}/invites/{invitationId}", "sourceOperationId": "deleteOrganizationInvitation", @@ -603,7 +555,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users", "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users", "sourceOperationId": "listTeamUsers", @@ -615,7 +566,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users", "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users", "sourceOperationId": "addTeamUser", @@ -627,7 +577,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/orgs/{orgId}/teams/{teamId}/users/{userId}", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/teams/{teamId}/users/{userId}", "targetPath": "/api/atlas/v2/orgs/{orgId}/teams/{teamId}/users/{userId}", "sourceOperationId": "removeTeamUser", @@ -639,7 +588,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/orgs/{orgId}/users", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/users", "targetPath": "/api/atlas/v2/orgs/{orgId}/users", "sourceOperationId": "listOrganizationUsers", @@ -651,7 +599,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/orgs/{orgId}/users/{userId}", "sourcePath": "/api/atlas/v1.0/orgs/{orgId}/users/{userId}", "targetPath": "/api/atlas/v2/orgs/{orgId}/users/{userId}", "sourceOperationId": "removeOrganizationUser", @@ -668,7 +615,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/liveMigrations", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/liveMigrations", "targetPath": "/api/atlas/v2/groups/{groupId}/liveMigrations", "sourceOperationId": "createPushMigration", @@ -680,7 +626,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/liveMigrations/validate", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/liveMigrations/validate", "targetPath": "/api/atlas/v2/groups/{groupId}/liveMigrations/validate", "sourceOperationId": "validateMigration", @@ -697,7 +642,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "sourceOperationId": "listServerlessPrivateEndpoints", @@ -709,7 +653,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint", "sourceOperationId": "createServerlessPrivateEndpoint", @@ -721,7 +664,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourceOperationId": "getServerlessPrivateEndpoint", @@ -733,7 +675,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourceOperationId": "updateServerlessPrivateEndpoint", @@ -745,7 +686,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "targetPath": "/api/atlas/v2/groups/{groupId}/privateEndpoint/serverless/instance/{instanceName}/endpoint/{endpointId}", "sourceOperationId": "deleteServerlessPrivateEndpoint", @@ -757,19 +697,17 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless", "sourceOperationId": "listServerlessInstances", "targetOperationId": "listGroupServerlessInstances", - "sunset": "2026-01-15", + "sunset": "2027-01-15", "deprecated": true }, { "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/serverless", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless", "sourceOperationId": "createServerlessInstance", @@ -781,19 +719,17 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless/{name}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", "sourceOperationId": "getServerlessInstance", "targetOperationId": "getGroupServerlessInstance", - "sunset": "2026-01-15", + "sunset": "2027-01-15", "deprecated": true }, { "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/serverless/{name}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", "sourceOperationId": "updateServerlessInstance", @@ -805,7 +741,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/serverless/{name}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{name}", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{name}", "sourceOperationId": "deleteServerlessInstance", @@ -822,7 +757,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "sourceOperationId": "listServerlessBackupRestoreJobs", @@ -834,7 +768,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs", "sourceOperationId": "createServerlessBackupRestoreJob", @@ -846,7 +779,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/restoreJobs/{restoreJobId}", "sourceOperationId": "getServerlessBackupRestoreJob", @@ -858,7 +790,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/snapshots", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/snapshots", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/snapshots", "sourceOperationId": "listServerlessBackups", @@ -870,7 +801,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", "sourcePath": "/api/atlas/v1.0/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", "targetPath": "/api/atlas/v2/groups/{groupId}/serverless/{clusterName}/backup/snapshots/{snapshotId}", "sourceOperationId": "getServerlessBackup", @@ -887,7 +817,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "POST", - "normalizedPath": "/users", "sourcePath": "/api/atlas/v1.0/users", "targetPath": "/api/atlas/v2/users", "sourceOperationId": "createUser", @@ -899,7 +828,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/users/byName/{userName}", "sourcePath": "/api/atlas/v1.0/users/byName/{userName}", "targetPath": "/api/atlas/v2/users/byName/{userName}", "sourceOperationId": "getUserByUsername", @@ -911,7 +839,6 @@ "sourceVersion": "v1.0", "targetVersion": "2023-01-01", "method": "GET", - "normalizedPath": "/users/{userId}", "sourcePath": "/api/atlas/v1.0/users/{userId}", "targetPath": "/api/atlas/v2/users/{userId}", "sourceOperationId": "getUser", @@ -930,7 +857,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", "sourceOperationId": "listClusters", @@ -942,7 +868,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "POST", - "normalizedPath": "/groups/{groupId}/clusters", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters", "sourceOperationId": "createCluster", @@ -954,7 +879,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "GET", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", "sourceOperationId": "getCluster", @@ -966,7 +890,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "PATCH", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", "sourceOperationId": "updateCluster", @@ -978,7 +901,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping", "sourceOperationId": "deleteAllCustomZoneMappings", @@ -990,7 +912,6 @@ "sourceVersion": "v1.5", "targetVersion": "2023-02-01", "method": "DELETE", - "normalizedPath": "/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourcePath": "/api/atlas/v1.5/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "targetPath": "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces", "sourceOperationId": "deleteManagedNamespace", From 0b3b1e54e368d68d8c6de35bbe69e379cd048163 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 28 Jan 2026 12:29:19 +0000 Subject: [PATCH 5/5] feat(mcp): Add OpenAPI MCP server POC Add a Model Context Protocol (MCP) server for the FOAS CLI that enables AI agents to load, filter, and explore OpenAPI specifications. Features: - Tools: load_spec, filter_spec, export_spec, unload_spec, list_specs - Resources: /operations, /tags, /paths, /schemas (list and detail) - In-memory registry for loaded specs (max 50) - Reuses existing filter/slice functionality Build with: make build-mcp Binary: ./bin/openapi-mcp --- tools/cli/Makefile | 13 +- tools/cli/cmd/mcp/main.go | 82 ++++ tools/cli/go.mod | 4 + tools/cli/go.sum | 12 + tools/cli/internal/mcp/README.md | 187 +++++++++ tools/cli/internal/mcp/registry/registry.go | 153 +++++++ .../internal/mcp/registry/registry_test.go | 139 +++++++ tools/cli/internal/mcp/resources/resources.go | 382 ++++++++++++++++++ .../internal/mcp/resources/resources_test.go | 260 ++++++++++++ tools/cli/internal/mcp/tools/tools.go | 248 ++++++++++++ tools/cli/internal/mcp/tools/tools_test.go | 153 +++++++ 11 files changed, 1632 insertions(+), 1 deletion(-) create mode 100644 tools/cli/cmd/mcp/main.go create mode 100644 tools/cli/internal/mcp/README.md create mode 100644 tools/cli/internal/mcp/registry/registry.go create mode 100644 tools/cli/internal/mcp/registry/registry_test.go create mode 100644 tools/cli/internal/mcp/resources/resources.go create mode 100644 tools/cli/internal/mcp/resources/resources_test.go create mode 100644 tools/cli/internal/mcp/tools/tools.go create mode 100644 tools/cli/internal/mcp/tools/tools_test.go diff --git a/tools/cli/Makefile b/tools/cli/Makefile index de5a1ae3ba..9ebb179fab 100644 --- a/tools/cli/Makefile +++ b/tools/cli/Makefile @@ -1,8 +1,11 @@ # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html GOLANGCI_VERSION=v2.1.0 -SOURCE_FILES?=./cmd +SOURCE_FILES?=./cmd/foascli.go BINARY_NAME=foascli +MCP_SOURCE_FILES?=./cmd/mcp +MCP_BINARY_NAME=openapi-mcp +MCP_DESTINATION=./bin/$(MCP_BINARY_NAME) VERSION=v0.0.1 GIT_SHA?=$(shell git rev-parse HEAD) DESTINATION=./bin/$(BINARY_NAME) @@ -56,6 +59,14 @@ build-debug: @echo "==> Building foascli binary for debugging" go build -gcflags="$(DEBUG_FLAGS)" -ldflags "$(LINKER_FLAGS)" -o $(DESTINATION) $(SOURCE_FILES) +.PHONY: build-mcp +build-mcp: ## Build the OpenAPI MCP server binary + @echo "==> Building openapi-mcp binary" + go build -ldflags "$(LINKER_FLAGS)" -o $(MCP_DESTINATION) $(MCP_SOURCE_FILES) + +.PHONY: build-all +build-all: build build-mcp ## Build all binaries + .PHONY: lint lint: ## Run linter golangci-lint run diff --git a/tools/cli/cmd/mcp/main.go b/tools/cli/cmd/mcp/main.go new file mode 100644 index 0000000000..2aae500a70 --- /dev/null +++ b/tools/cli/cmd/mcp/main.go @@ -0,0 +1,82 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main provides the entry point for the OpenAPI Filter MCP Server. +package main + +import ( + "context" + "log" + "os" + "os/signal" + "syscall" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mongodb/openapi/tools/cli/internal/mcp/registry" + "github.com/mongodb/openapi/tools/cli/internal/mcp/resources" + "github.com/mongodb/openapi/tools/cli/internal/mcp/tools" + "github.com/mongodb/openapi/tools/cli/internal/version" +) + +const ( + serverName = "openapi-filter-mcp" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + log.Println("Shutting down MCP server...") + cancel() + }() + + // Create the spec registry + reg := registry.New() + + // Create the MCP server + server := mcp.NewServer( + &mcp.Implementation{ + Name: serverName, + Version: version.Version, + }, + &mcp.ServerOptions{ + Capabilities: &mcp.ServerCapabilities{ + Tools: &mcp.ToolCapabilities{ + ListChanged: true, + }, + Resources: &mcp.ResourceCapabilities{ + ListChanged: true, + }, + }, + }, + ) + + // Register tools + tools.RegisterTools(server, reg) + + // Register resources + resources.RegisterResources(server, reg) + + log.Printf("Starting %s v%s", serverName, version.Version) + + // Run the server over stdio + if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil { + log.Fatalf("Server error: %v", err) + } +} diff --git a/tools/cli/go.mod b/tools/cli/go.mod index 23de362c99..78dd734a26 100644 --- a/tools/cli/go.mod +++ b/tools/cli/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.0 require ( github.com/getkin/kin-openapi v0.132.0 github.com/iancoleman/strcase v0.3.0 + github.com/modelcontextprotocol/go-sdk v1.2.0 github.com/oasdiff/oasdiff v1.11.4 github.com/spf13/afero v1.14.0 github.com/spf13/cobra v1.9.1 @@ -22,6 +23,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -37,6 +39,8 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/wI2L/jsondiff v0.6.1 // indirect github.com/yargevad/filepathx v1.0.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/goldmark v1.7.11 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/oauth2 v0.30.0 // indirect ) diff --git a/tools/cli/go.sum b/tools/cli/go.sum index f2bcafcabc..f62ebac245 100644 --- a/tools/cli/go.sum +++ b/tools/cli/go.sum @@ -13,8 +13,12 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -27,6 +31,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s= +github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oasdiff/oasdiff v1.11.4 h1:FgThY78WNwOhWCLIhMk7AsKoHpVZZggRpKEGfd+IOIs= @@ -66,14 +72,20 @@ github.com/wI2L/jsondiff v0.6.1 h1:ISZb9oNWbP64LHnu4AUhsMF5W0FIj5Ok3Krip9Shqpw= github.com/wI2L/jsondiff v0.6.1/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo= github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/tools/cli/internal/mcp/README.md b/tools/cli/internal/mcp/README.md new file mode 100644 index 0000000000..cea174e57b --- /dev/null +++ b/tools/cli/internal/mcp/README.md @@ -0,0 +1,187 @@ +# OpenAPI MCP Server + +The OpenAPI MCP (Model Context Protocol) server enables AI agents to load, filter, and explore OpenAPI specifications. It provides tools and resources that allow agents to work with API definitions programmatically. + +## Overview + +This MCP server exposes: +- **Tools**: Actions the agent can perform (load specs, filter, export) +- **Resources**: Read-only data the agent can access (manifests, operations, schemas) + +## Building + +```bash +cd tools/cli +make build-mcp +``` + +The binary will be created at `./bin/openapi-mcp`. + +## Configuration + +### Claude Desktop + +Add to your Claude Desktop configuration file: + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "openapi": { + "command": "/absolute/path/to/openapi-mcp" + } + } +} +``` + +### Cursor + +Add to your Cursor MCP settings (`.cursor/mcp.json` in your project or global settings): + +```json +{ + "mcpServers": { + "openapi": { + "command": "/absolute/path/to/openapi-mcp" + } + } +} +``` + +### VS Code with Continue + +Add to your Continue configuration (`~/.continue/config.json`): + +```json +{ + "experimental": { + "modelContextProtocolServers": [ + { + "name": "openapi", + "command": "/absolute/path/to/openapi-mcp" + } + ] + } +} +``` + +### Generic MCP Client + +The server communicates over stdio using JSON-RPC 2.0. Start it directly: + +```bash +./bin/openapi-mcp +``` + +## Available Tools + +### `load_spec` +Load an OpenAPI specification from disk. + +**Input:** +- `file_path` (required): Absolute path to the OpenAPI/Swagger file +- `alias` (required): Short name to refer to this spec + +**Example prompt:** "Load the API spec from /path/to/openapi.yaml as 'myapi'" + +### `filter_spec` +Filter a loaded spec by tags, operation IDs, or paths. + +**Input:** +- `source_alias` (required): The alias of the loaded spec to filter +- `save_as` (optional): Name to save the filtered view as +- `tags` (optional): List of tags to keep +- `operation_ids` (optional): List of operation IDs to keep +- `paths` (optional): List of path patterns to keep + +**Example prompt:** "Filter 'myapi' to only include operations tagged with 'users'" + +### `export_spec` +Export a spec to a file. + +**Input:** +- `alias` (required): The alias of the spec to export +- `file_path` (required): Path to save the spec to +- `format` (optional): Output format - `json` or `yaml` (default: json) + +**Example prompt:** "Export the filtered spec to /tmp/users-api.yaml" + +### `unload_spec` +Remove a spec from memory. + +**Input:** +- `alias` (required): The alias of the spec to unload + +### `list_specs` +List all currently loaded specs. + +## Available Resources + +Resources are accessed via URI patterns: + +### List Resources + +| Resource | URI Pattern | Description | +|----------|-------------|-------------| +| Operations | `openapi://{alias}/operations` | List all operations with IDs, methods, paths, summaries | +| Tags | `openapi://{alias}/tags` | List all tags with descriptions | +| Paths | `openapi://{alias}/paths` | List all paths with available methods | +| Schemas | `openapi://{alias}/schemas` | List all component schema names | + +### Detail Resources + +| Resource | URI Pattern | Description | +|----------|-------------|-------------| +| Operation | `openapi://{alias}/operations/{operationId}` | Full details for a specific operation | +| Tag | `openapi://{alias}/tags/{tagName}` | All operations for a specific tag | +| Path | `openapi://{alias}/paths/{path}` | All operations for a specific path | +| Schema | `openapi://{alias}/schemas/{schemaName}` | Full JSON Schema for a component | + +## Example Workflow + +1. **Load a spec:** + > "Load the OpenAPI spec from ./openapi/v2.yaml as 'atlas'" + +2. **Explore the manifest:** + > "Show me the manifest for 'atlas'" + +3. **Filter to specific operations:** + > "Filter 'atlas' to only include operations with tag 'Clusters' and save as 'clusters'" + +4. **Get operation details:** + > "Show me the details for operation 'createCluster' in 'clusters'" + +5. **Export the filtered spec:** + > "Export 'clusters' to ./clusters-api.json" + +## Architecture + +``` +cmd/mcp/ +└── main.go # Entry point, server setup + +internal/mcp/ +├── registry/ +│ └── registry.go # In-memory spec storage +├── tools/ +│ └── tools.go # MCP tool implementations +└── resources/ + └── resources.go # MCP resource implementations +``` + +## Development + +Run tests: +```bash +cd tools/cli +go test ./internal/mcp/... -v +``` + +## Limitations + +- Maximum 50 specs can be loaded simultaneously +- Specs are stored in memory (not persisted across restarts) +- File paths must be absolute or relative to the server's working directory + diff --git a/tools/cli/internal/mcp/registry/registry.go b/tools/cli/internal/mcp/registry/registry.go new file mode 100644 index 0000000000..f61605f3f9 --- /dev/null +++ b/tools/cli/internal/mcp/registry/registry.go @@ -0,0 +1,153 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package registry provides an in-memory store for loaded OpenAPI specifications. +package registry + +import ( + "encoding/json" + "errors" + "fmt" + "sync" + + "github.com/getkin/kin-openapi/openapi3" +) + +// MaxSpecs is the maximum number of specs that can be loaded at once. +const MaxSpecs = 50 + +var ( + ErrNotFound = errors.New("spec not found") + ErrRegistryFull = errors.New("registry full: please unload specs before adding new ones") + ErrAliasExists = errors.New("alias already exists") +) + +// SpecEntry represents a loaded OpenAPI specification with metadata. +type SpecEntry struct { + Spec *openapi3.T + SourcePath string // Original file path (empty for virtual specs) + IsVirtual bool // True if this is a filtered/derived spec +} + +// Registry is a thread-safe in-memory store for OpenAPI specifications. +type Registry struct { + mu sync.RWMutex + specs map[string]*SpecEntry +} + +// New creates a new empty Registry. +func New() *Registry { + return &Registry{ + specs: make(map[string]*SpecEntry), + } +} + +// Load adds a spec to the registry under the given alias. +func (r *Registry) Load(alias string, spec *openapi3.T, sourcePath string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.specs[alias]; exists { + return fmt.Errorf("%w: %s", ErrAliasExists, alias) + } + + if len(r.specs) >= MaxSpecs { + return ErrRegistryFull + } + + r.specs[alias] = &SpecEntry{ + Spec: spec, + SourcePath: sourcePath, + IsVirtual: false, + } + return nil +} + +// LoadVirtual adds a virtual (filtered) spec to the registry. +func (r *Registry) LoadVirtual(alias string, spec *openapi3.T) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.specs[alias]; exists { + return fmt.Errorf("%w: %s", ErrAliasExists, alias) + } + + if len(r.specs) >= MaxSpecs { + return ErrRegistryFull + } + + r.specs[alias] = &SpecEntry{ + Spec: spec, + IsVirtual: true, + } + return nil +} + +// Get retrieves a spec by alias. +func (r *Registry) Get(alias string) (*SpecEntry, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + entry, exists := r.specs[alias] + if !exists { + return nil, fmt.Errorf("%w: %s", ErrNotFound, alias) + } + return entry, nil +} + +// Unload removes a spec from the registry. +func (r *Registry) Unload(alias string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.specs[alias]; !exists { + return fmt.Errorf("%w: %s", ErrNotFound, alias) + } + delete(r.specs, alias) + return nil +} + +// List returns all loaded spec aliases. +func (r *Registry) List() []string { + r.mu.RLock() + defer r.mu.RUnlock() + + aliases := make([]string, 0, len(r.specs)) + for alias := range r.specs { + aliases = append(aliases, alias) + } + return aliases +} + +// Count returns the number of loaded specs. +func (r *Registry) Count() int { + r.mu.RLock() + defer r.mu.RUnlock() + return len(r.specs) +} + +// Duplicate creates a deep copy of a spec. +func Duplicate(spec *openapi3.T) (*openapi3.T, error) { + data, err := json.Marshal(spec) + if err != nil { + return nil, fmt.Errorf("failed to marshal spec: %w", err) + } + + var copy openapi3.T + if err := json.Unmarshal(data, ©); err != nil { + return nil, fmt.Errorf("failed to unmarshal spec: %w", err) + } + return ©, nil +} + diff --git a/tools/cli/internal/mcp/registry/registry_test.go b/tools/cli/internal/mcp/registry/registry_test.go new file mode 100644 index 0000000000..e4757efa63 --- /dev/null +++ b/tools/cli/internal/mcp/registry/registry_test.go @@ -0,0 +1,139 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "errors" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestSpec(title, version string) *openapi3.T { + return &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: title, + Version: version, + }, + } +} + +func TestNew(t *testing.T) { + reg := New() + assert.NotNil(t, reg) + assert.Equal(t, 0, reg.Count()) +} + +func TestLoad(t *testing.T) { + reg := New() + spec := createTestSpec("Test API", "1.0.0") + + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + assert.Equal(t, 1, reg.Count()) + + entry, err := reg.Get("test") + require.NoError(t, err) + assert.Equal(t, spec, entry.Spec) + assert.Equal(t, "/path/to/spec.yaml", entry.SourcePath) + assert.False(t, entry.IsVirtual) +} + +func TestLoad_AliasExists(t *testing.T) { + reg := New() + spec := createTestSpec("Test API", "1.0.0") + + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + err = reg.Load("test", spec, "/path/to/other.yaml") + assert.True(t, errors.Is(err, ErrAliasExists)) +} + +func TestLoadVirtual(t *testing.T) { + reg := New() + spec := createTestSpec("Virtual API", "1.0.0") + + err := reg.LoadVirtual("virtual", spec) + require.NoError(t, err) + + entry, err := reg.Get("virtual") + require.NoError(t, err) + assert.True(t, entry.IsVirtual) + assert.Empty(t, entry.SourcePath) +} + +func TestGet_NotFound(t *testing.T) { + reg := New() + + _, err := reg.Get("nonexistent") + assert.True(t, errors.Is(err, ErrNotFound)) +} + +func TestUnload(t *testing.T) { + reg := New() + spec := createTestSpec("Test API", "1.0.0") + + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + assert.Equal(t, 1, reg.Count()) + + err = reg.Unload("test") + require.NoError(t, err) + assert.Equal(t, 0, reg.Count()) +} + +func TestUnload_NotFound(t *testing.T) { + reg := New() + + err := reg.Unload("nonexistent") + assert.True(t, errors.Is(err, ErrNotFound)) +} + +func TestList(t *testing.T) { + reg := New() + + err := reg.Load("api1", createTestSpec("API 1", "1.0.0"), "/path/1.yaml") + require.NoError(t, err) + err = reg.Load("api2", createTestSpec("API 2", "2.0.0"), "/path/2.yaml") + require.NoError(t, err) + + aliases := reg.List() + assert.Len(t, aliases, 2) + assert.Contains(t, aliases, "api1") + assert.Contains(t, aliases, "api2") +} + +func TestDuplicate(t *testing.T) { + original := createTestSpec("Original API", "1.0.0") + original.Info.Description = "Original description" + + copy, err := Duplicate(original) + require.NoError(t, err) + + // Verify it's a deep copy + assert.Equal(t, original.Info.Title, copy.Info.Title) + assert.Equal(t, original.Info.Version, copy.Info.Version) + + // Modify the copy and verify original is unchanged + copy.Info.Title = "Modified API" + assert.Equal(t, "Original API", original.Info.Title) + assert.Equal(t, "Modified API", copy.Info.Title) +} + diff --git a/tools/cli/internal/mcp/resources/resources.go b/tools/cli/internal/mcp/resources/resources.go new file mode 100644 index 0000000000..f4cd40b5bf --- /dev/null +++ b/tools/cli/internal/mcp/resources/resources.go @@ -0,0 +1,382 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package resources provides MCP resource implementations for OpenAPI specs. +package resources + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mongodb/openapi/tools/cli/internal/mcp/registry" + "github.com/mongodb/openapi/tools/cli/internal/mcp/tools" +) + +// OperationSummary represents a summary of an operation. +type OperationSummary struct { + ID string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + Summary string `json:"summary,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// TagSummary represents a tag with its description. +type TagSummary struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` +} + +// PathSummary represents a path with its available methods. +type PathSummary struct { + Path string `json:"path"` + Methods []string `json:"methods"` +} + +// SchemaSummary represents a schema name. +type SchemaSummary struct { + Name string `json:"name"` + Type string `json:"type,omitempty"` +} + +// RegisterResources registers resource templates with the MCP server. +func RegisterResources(server *mcp.Server, reg *registry.Registry) { + // List resources + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/operations", + Name: "List Operations", + Description: "List all operations in the spec with their IDs, methods, paths, and summaries.", + MIMEType: "application/json", + }, OperationsListHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/tags", + Name: "List Tags", + Description: "List all tags defined in the spec.", + MIMEType: "application/json", + }, TagsListHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/paths", + Name: "List Paths", + Description: "List all paths in the spec with their available methods.", + MIMEType: "application/json", + }, PathsListHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/schemas", + Name: "List Schemas", + Description: "List all component schemas defined in the spec.", + MIMEType: "application/json", + }, SchemasListHandler(reg)) + + // Detail resources + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/operations/{operationId}", + Name: "Get Operation", + Description: "Get the full details for a specific operation by ID.", + MIMEType: "application/json", + }, OperationDetailHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/tags/{tagName}", + Name: "Get Tag Operations", + Description: "Get all operations for a specific tag.", + MIMEType: "application/json", + }, TagDetailHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/paths/{path}", + Name: "Get Path", + Description: "Get all operations for a specific path.", + MIMEType: "application/json", + }, PathDetailHandler(reg)) + + server.AddResourceTemplate(&mcp.ResourceTemplate{ + URITemplate: "openapi://{alias}/schemas/{schemaName}", + Name: "Get Schema", + Description: "Get the full JSON Schema for a specific component.", + MIMEType: "application/json", + }, SchemaDetailHandler(reg)) +} + +// OperationsListHandler returns all operations in the spec. +func OperationsListHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias := extractParam(req.Params.URI, "openapi://", "/operations") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + operations := make([]OperationSummary, 0) + if entry.Spec.Paths != nil { + for path, pathItem := range entry.Spec.Paths.Map() { + for method, op := range pathItem.Operations() { + if op != nil { + operations = append(operations, OperationSummary{ + ID: op.OperationID, + Method: method, + Path: path, + Summary: op.Summary, + Tags: op.Tags, + }) + } + } + } + } + + return jsonResponse(req.Params.URI, operations) + } +} + +// TagsListHandler returns all tags in the spec. +func TagsListHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias := extractParam(req.Params.URI, "openapi://", "/tags") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + tags := make([]TagSummary, 0) + for _, tag := range entry.Spec.Tags { + tags = append(tags, TagSummary{ + Name: tag.Name, + Description: tag.Description, + }) + } + + return jsonResponse(req.Params.URI, tags) + } +} + +// PathsListHandler returns all paths in the spec. +func PathsListHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias := extractParam(req.Params.URI, "openapi://", "/paths") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + paths := make([]PathSummary, 0) + if entry.Spec.Paths != nil { + for path, pathItem := range entry.Spec.Paths.Map() { + methods := make([]string, 0) + for method := range pathItem.Operations() { + methods = append(methods, method) + } + paths = append(paths, PathSummary{ + Path: path, + Methods: methods, + }) + } + } + + return jsonResponse(req.Params.URI, paths) + } +} + +// SchemasListHandler returns all schemas in the spec. +func SchemasListHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias := extractParam(req.Params.URI, "openapi://", "/schemas") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + schemas := make([]SchemaSummary, 0) + if entry.Spec.Components != nil && entry.Spec.Components.Schemas != nil { + for name, schemaRef := range entry.Spec.Components.Schemas { + schemaType := "" + if schemaRef.Value != nil && schemaRef.Value.Type != nil { + schemaType = strings.Join(*schemaRef.Value.Type, ",") + } + schemas = append(schemas, SchemaSummary{ + Name: name, + Type: schemaType, + }) + } + } + + return jsonResponse(req.Params.URI, schemas) + } +} + +// OperationDetailHandler returns details for a specific operation. +func OperationDetailHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias, operationID := extractTwoParams(req.Params.URI, "openapi://", "/operations/") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + path, method, op := tools.GetOperationByID(entry.Spec, operationID) + if op == nil { + return nil, fmt.Errorf("operation '%s' not found", operationID) + } + + result := map[string]any{ + "operationId": operationID, + "path": path, + "method": method, + "operation": op, + } + + return jsonResponse(req.Params.URI, result) + } +} + +// TagDetailHandler returns all operations for a specific tag. +func TagDetailHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias, tagName := extractTwoParams(req.Params.URI, "openapi://", "/tags/") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + operations := make([]OperationSummary, 0) + if entry.Spec.Paths != nil { + for path, pathItem := range entry.Spec.Paths.Map() { + for method, op := range pathItem.Operations() { + if op != nil && containsTag(op.Tags, tagName) { + operations = append(operations, OperationSummary{ + ID: op.OperationID, + Method: method, + Path: path, + Summary: op.Summary, + Tags: op.Tags, + }) + } + } + } + } + + if len(operations) == 0 { + return nil, fmt.Errorf("tag '%s' not found or has no operations", tagName) + } + + return jsonResponse(req.Params.URI, operations) + } +} + +// PathDetailHandler returns all operations for a specific path. +func PathDetailHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias, pathParam := extractTwoParams(req.Params.URI, "openapi://", "/paths/") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + if entry.Spec.Paths == nil { + return nil, fmt.Errorf("no paths found in spec '%s'", alias) + } + + // URL decode the path (replace %2F with /) + decodedPath := strings.ReplaceAll(pathParam, "%2F", "/") + if !strings.HasPrefix(decodedPath, "/") { + decodedPath = "/" + decodedPath + } + + pathItem := entry.Spec.Paths.Find(decodedPath) + if pathItem == nil { + return nil, fmt.Errorf("path '%s' not found", decodedPath) + } + + result := map[string]any{ + "path": decodedPath, + "operations": pathItem.Operations(), + } + + return jsonResponse(req.Params.URI, result) + } +} + +// SchemaDetailHandler returns a specific schema. +func SchemaDetailHandler(reg *registry.Registry) mcp.ResourceHandler { + return func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + alias, schemaName := extractTwoParams(req.Params.URI, "openapi://", "/schemas/") + entry, err := reg.Get(alias) + if err != nil { + return nil, fmt.Errorf("spec '%s' not found", alias) + } + + if entry.Spec.Components == nil || entry.Spec.Components.Schemas == nil { + return nil, fmt.Errorf("no schemas found in spec '%s'", alias) + } + + schemaRef, exists := entry.Spec.Components.Schemas[schemaName] + if !exists { + return nil, fmt.Errorf("schema '%s' not found", schemaName) + } + + return jsonResponse(req.Params.URI, schemaRef) + } +} + +// extractParam extracts a single parameter from a URI. +func extractParam(uri, prefix, suffix string) string { + // Remove prefix and suffix to get the parameter + s := uri[len(prefix):] + if idx := len(s) - len(suffix); idx > 0 { + return s[:idx] + } + return s +} + +// extractTwoParams extracts alias and second param from URI like openapi://{alias}/operation/{id}. +func extractTwoParams(uri, prefix, middle string) (string, string) { + s := uri[len(prefix):] + // Find the middle separator + for i := 0; i < len(s); i++ { + if i+len(middle) <= len(s) && s[i:i+len(middle)] == middle { + return s[:i], s[i+len(middle):] + } + } + return s, "" +} + +// jsonResponse creates a JSON response for a resource. +func jsonResponse(uri string, data any) (*mcp.ReadResourceResult, error) { + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return nil, err + } + + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{{ + URI: uri, + MIMEType: "application/json", + Text: string(jsonData), + }}, + }, nil +} + +// containsTag checks if a tag is in the list of tags. +func containsTag(tags []string, tag string) bool { + for _, t := range tags { + if strings.EqualFold(t, tag) { + return true + } + } + return false +} diff --git a/tools/cli/internal/mcp/resources/resources_test.go b/tools/cli/internal/mcp/resources/resources_test.go new file mode 100644 index 0000000000..3711f9060e --- /dev/null +++ b/tools/cli/internal/mcp/resources/resources_test.go @@ -0,0 +1,260 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resources + +import ( + "context" + "encoding/json" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mongodb/openapi/tools/cli/internal/mcp/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestSpec() *openapi3.T { + spec := &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "Test API", + Version: "1.0.0", + Description: "A test API", + }, + Tags: openapi3.Tags{ + {Name: "users", Description: "User operations"}, + {Name: "orders", Description: "Order operations"}, + }, + Paths: &openapi3.Paths{}, + } + spec.Paths.Set("/users", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "getUsers", + Summary: "Get all users", + Tags: []string{"users"}, + }, + Post: &openapi3.Operation{ + OperationID: "createUser", + Summary: "Create a user", + Tags: []string{"users"}, + }, + }) + spec.Components = &openapi3.Components{ + Schemas: openapi3.Schemas{ + "User": &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"object"}, + Properties: openapi3.Schemas{ + "id": &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "name": &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + }, + }, + }, + }, + } + return spec +} + +func TestOperationsListHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := OperationsListHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/operations"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var operations []OperationSummary + err = json.Unmarshal([]byte(result.Contents[0].Text), &operations) + require.NoError(t, err) + + assert.Len(t, operations, 2) +} + +func TestOperationsListHandler_NotFound(t *testing.T) { + reg := registry.New() + handler := OperationsListHandler(reg) + + _, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://nonexistent/operations"}, + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestTagsListHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := TagsListHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/tags"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var tags []TagSummary + err = json.Unmarshal([]byte(result.Contents[0].Text), &tags) + require.NoError(t, err) + + assert.Len(t, tags, 2) + assert.Equal(t, "users", tags[0].Name) + assert.Equal(t, "User operations", tags[0].Description) +} + +func TestPathsListHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := PathsListHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/paths"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var paths []PathSummary + err = json.Unmarshal([]byte(result.Contents[0].Text), &paths) + require.NoError(t, err) + + assert.Len(t, paths, 1) + assert.Equal(t, "/users", paths[0].Path) + assert.Len(t, paths[0].Methods, 2) +} + +func TestSchemasListHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := SchemasListHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/schemas"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var schemas []SchemaSummary + err = json.Unmarshal([]byte(result.Contents[0].Text), &schemas) + require.NoError(t, err) + + assert.Len(t, schemas, 1) + assert.Equal(t, "User", schemas[0].Name) +} + +func TestOperationDetailHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := OperationDetailHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/operations/getUsers"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var opResult map[string]any + err = json.Unmarshal([]byte(result.Contents[0].Text), &opResult) + require.NoError(t, err) + + assert.Equal(t, "getUsers", opResult["operationId"]) + assert.Equal(t, "/users", opResult["path"]) + assert.Equal(t, "GET", opResult["method"]) +} + +func TestOperationDetailHandler_NotFound(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := OperationDetailHandler(reg) + _, err = handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/operations/nonexistent"}, + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestTagDetailHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := TagDetailHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/tags/users"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + var operations []OperationSummary + err = json.Unmarshal([]byte(result.Contents[0].Text), &operations) + require.NoError(t, err) + + assert.Len(t, operations, 2) +} + +func TestSchemaDetailHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := SchemaDetailHandler(reg) + result, err := handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/schemas/User"}, + }) + + require.NoError(t, err) + require.Len(t, result.Contents, 1) + assert.Contains(t, result.Contents[0].Text, "object") +} + +func TestSchemaDetailHandler_NotFound(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := SchemaDetailHandler(reg) + _, err = handler(context.Background(), &mcp.ReadResourceRequest{ + Params: &mcp.ReadResourceParams{URI: "openapi://test/schemas/NonExistent"}, + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} diff --git a/tools/cli/internal/mcp/tools/tools.go b/tools/cli/internal/mcp/tools/tools.go new file mode 100644 index 0000000000..43d30071e2 --- /dev/null +++ b/tools/cli/internal/mcp/tools/tools.go @@ -0,0 +1,248 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tools provides MCP tool implementations for OpenAPI operations. +package tools + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mongodb/openapi/tools/cli/internal/mcp/registry" + "github.com/mongodb/openapi/tools/cli/internal/openapi" + "github.com/mongodb/openapi/tools/cli/internal/openapi/slice" + "github.com/spf13/afero" +) + +// LoadSpecInput is the input for the load_spec tool. +type LoadSpecInput struct { + FilePath string `json:"file_path" jsonschema:"Absolute path to the OpenAPI/Swagger file on disk"` + Alias string `json:"alias" jsonschema:"Short name to refer to this spec (e.g. 'users' or 'main')"` +} + +// textResult creates a CallToolResult with text content. +func textResult(text string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: text}}, + } +} + +// errorResult creates a CallToolResult with an error. +func errorResult(text string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: text}}, + IsError: true, + } +} + +// LoadSpecHandler creates a handler for the load_spec tool. +func LoadSpecHandler(reg *registry.Registry) func(ctx context.Context, req *mcp.CallToolRequest, input LoadSpecInput) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, input LoadSpecInput) (*mcp.CallToolResult, any, error) { + // Check if file exists + if _, err := os.Stat(input.FilePath); os.IsNotExist(err) { + return errorResult(fmt.Sprintf("File at path %s does not exist", input.FilePath)), nil, nil + } + + // Load and parse the spec + loader := openapi.NewOpenAPI3() + specInfo, err := loader.CreateOpenAPISpecFromPath(input.FilePath) + if err != nil { + return errorResult(fmt.Sprintf("Failed to parse spec: %v", err)), nil, nil + } + + // Add to registry + if err := reg.Load(input.Alias, specInfo.Spec, input.FilePath); err != nil { + if errors.Is(err, registry.ErrRegistryFull) { + return errorResult("Registry full. Please unload specs before adding new ones."), nil, nil + } + if errors.Is(err, registry.ErrAliasExists) { + return errorResult(fmt.Sprintf("Alias '%s' already exists", input.Alias)), nil, nil + } + return errorResult(fmt.Sprintf("Failed to load spec: %v", err)), nil, nil + } + + // Build response with available resources + response := fmt.Sprintf("✅ Loaded '%s'.\n\nAvailable Resources:\n"+ + "- Manifest: openapi://%s/manifest\n"+ + "- Root Index: openapi://%s/root\n"+ + "- Info: openapi://%s/info", + input.Alias, input.Alias, input.Alias, input.Alias) + + return textResult(response), nil, nil + } +} + +// FilterSpecInput is the input for the filter_spec tool. +type FilterSpecInput struct { + SourceAlias string `json:"source_alias" jsonschema:"The alias of the loaded spec to filter"` + SaveAs string `json:"save_as,omitempty" jsonschema:"Optional name to save this filtered view as"` + Tags []string `json:"tags,omitempty" jsonschema:"List of tags to keep"` + OperationIDs []string `json:"operation_ids,omitempty" jsonschema:"List of operation IDs to keep"` + Paths []string `json:"paths,omitempty" jsonschema:"List of path patterns to keep"` +} + +// FilterSpecHandler creates a handler for the filter_spec tool. +func FilterSpecHandler(reg *registry.Registry) func(ctx context.Context, req *mcp.CallToolRequest, input FilterSpecInput) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, input FilterSpecInput) (*mcp.CallToolResult, any, error) { + // Get the source spec + entry, err := reg.Get(input.SourceAlias) + if err != nil { + return errorResult(fmt.Sprintf("Spec '%s' not found", input.SourceAlias)), nil, nil + } + + // Duplicate the spec to avoid modifying the original + specCopy, err := registry.Duplicate(entry.Spec) + if err != nil { + return errorResult(fmt.Sprintf("Failed to copy spec: %v", err)), nil, nil + } + + // Apply slice criteria + criteria := &slice.Criteria{ + Tags: input.Tags, + OperationIDs: input.OperationIDs, + Paths: input.Paths, + } + + if err := slice.Slice(specCopy, criteria); err != nil { + return errorResult(fmt.Sprintf("Failed to filter spec: %v", err)), nil, nil + } + + // If save_as is provided, save to registry + if input.SaveAs != "" { + if err := reg.LoadVirtual(input.SaveAs, specCopy); err != nil { + return errorResult(fmt.Sprintf("Failed to save filtered spec: %v", err)), nil, nil + } + } + + // Serialize the filtered spec + data, err := json.MarshalIndent(specCopy, "", " ") + if err != nil { + return errorResult(fmt.Sprintf("Failed to serialize spec: %v", err)), nil, nil + } + + return textResult(string(data)), nil, nil + } +} + +// ExportSpecInput is the input for the export_spec tool. +type ExportSpecInput struct { + Alias string `json:"alias" jsonschema:"The alias of the spec to export"` + FilePath string `json:"file_path" jsonschema:"Path to save the spec to"` + Format string `json:"format,omitempty" jsonschema:"Output format: json or yaml (default: json)"` +} + +// ExportSpecHandler creates a handler for the export_spec tool. +func ExportSpecHandler(reg *registry.Registry) func(ctx context.Context, req *mcp.CallToolRequest, input ExportSpecInput) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, input ExportSpecInput) (*mcp.CallToolResult, any, error) { + entry, err := reg.Get(input.Alias) + if err != nil { + return errorResult(fmt.Sprintf("Spec '%s' not found", input.Alias)), nil, nil + } + + format := input.Format + if format == "" { + format = openapi.JSON + } + + if err := openapi.Save(input.FilePath, entry.Spec, format, afero.NewOsFs()); err != nil { + return errorResult(fmt.Sprintf("Failed to save spec: %v", err)), nil, nil + } + + return textResult(fmt.Sprintf("✅ Exported '%s' to %s", input.Alias, input.FilePath)), nil, nil + } +} + +// UnloadSpecInput is the input for the unload_spec tool. +type UnloadSpecInput struct { + Alias string `json:"alias" jsonschema:"The alias of the spec to unload"` +} + +// UnloadSpecHandler creates a handler for the unload_spec tool. +func UnloadSpecHandler(reg *registry.Registry) func(ctx context.Context, req *mcp.CallToolRequest, input UnloadSpecInput) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, input UnloadSpecInput) (*mcp.CallToolResult, any, error) { + if err := reg.Unload(input.Alias); err != nil { + return errorResult(fmt.Sprintf("Spec '%s' not found", input.Alias)), nil, nil + } + return textResult(fmt.Sprintf("✅ Unloaded '%s'", input.Alias)), nil, nil + } +} + +// ListSpecsHandler creates a handler for the list_specs tool. +func ListSpecsHandler(reg *registry.Registry) func(ctx context.Context, req *mcp.CallToolRequest, input struct{}) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, input struct{}) (*mcp.CallToolResult, any, error) { + aliases := reg.List() + if len(aliases) == 0 { + return textResult("No specs loaded."), nil, nil + } + + result := fmt.Sprintf("Loaded specs (%d):\n", len(aliases)) + for _, alias := range aliases { + entry, _ := reg.Get(alias) + specType := "file" + if entry.IsVirtual { + specType = "virtual" + } + result += fmt.Sprintf("- %s (%s)\n", alias, specType) + } + return textResult(result), nil, nil + } +} + +// RegisterTools registers all MCP tools with the server. +func RegisterTools(server *mcp.Server, reg *registry.Registry) { + mcp.AddTool(server, &mcp.Tool{ + Name: "load_spec", + Description: "Load an OpenAPI spec from disk into memory. Assign it an alias for future use.", + }, LoadSpecHandler(reg)) + + mcp.AddTool(server, &mcp.Tool{ + Name: "filter_spec", + Description: "Filter a loaded spec to keep only specific operations by tags, operation IDs, or paths. Can optionally save the result as a new 'virtual' spec for reuse.", + }, FilterSpecHandler(reg)) + + mcp.AddTool(server, &mcp.Tool{ + Name: "export_spec", + Description: "Save a loaded or filtered spec to a physical file. Use for external tools (e.g. code generation).", + }, ExportSpecHandler(reg)) + + mcp.AddTool(server, &mcp.Tool{ + Name: "unload_spec", + Description: "Remove a spec from memory to free up space.", + }, UnloadSpecHandler(reg)) + + mcp.AddTool(server, &mcp.Tool{ + Name: "list_specs", + Description: "List all currently loaded specs.", + }, ListSpecsHandler(reg)) +} + +// GetOperationByID finds an operation by its ID in a spec. +func GetOperationByID(spec *openapi3.T, operationID string) (path, method string, op *openapi3.Operation) { + if spec.Paths == nil { + return "", "", nil + } + for p, pathItem := range spec.Paths.Map() { + for m, operation := range pathItem.Operations() { + if operation != nil && operation.OperationID == operationID { + return p, m, operation + } + } + } + return "", "", nil +} diff --git a/tools/cli/internal/mcp/tools/tools_test.go b/tools/cli/internal/mcp/tools/tools_test.go new file mode 100644 index 0000000000..33022fbb96 --- /dev/null +++ b/tools/cli/internal/mcp/tools/tools_test.go @@ -0,0 +1,153 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tools + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/mongodb/openapi/tools/cli/internal/mcp/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestSpec() *openapi3.T { + return &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "Test API", + Version: "1.0.0", + }, + Paths: &openapi3.Paths{}, + } +} + +func TestLoadSpecHandler_FileNotFound(t *testing.T) { + reg := registry.New() + handler := LoadSpecHandler(reg) + + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, LoadSpecInput{ + FilePath: "/nonexistent/path/spec.yaml", + Alias: "test", + }) + + require.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "does not exist") +} + +func TestLoadSpecHandler_Success(t *testing.T) { + // Create a temporary spec file + tmpDir := t.TempDir() + specPath := filepath.Join(tmpDir, "spec.yaml") + specContent := `openapi: "3.0.0" +info: + title: Test API + version: "1.0.0" +paths: {}` + err := os.WriteFile(specPath, []byte(specContent), 0644) + require.NoError(t, err) + + reg := registry.New() + handler := LoadSpecHandler(reg) + + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, LoadSpecInput{ + FilePath: specPath, + Alias: "test", + }) + + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "Loaded 'test'") + assert.Equal(t, 1, reg.Count()) +} + +func TestLoadSpecHandler_AliasExists(t *testing.T) { + tmpDir := t.TempDir() + specPath := filepath.Join(tmpDir, "spec.yaml") + specContent := `openapi: "3.0.0" +info: + title: Test API + version: "1.0.0" +paths: {}` + err := os.WriteFile(specPath, []byte(specContent), 0644) + require.NoError(t, err) + + reg := registry.New() + handler := LoadSpecHandler(reg) + + // Load first time + _, _, err = handler(context.Background(), &mcp.CallToolRequest{}, LoadSpecInput{ + FilePath: specPath, + Alias: "test", + }) + require.NoError(t, err) + + // Try to load again with same alias + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, LoadSpecInput{ + FilePath: specPath, + Alias: "test", + }) + + require.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "already exists") +} + +func TestFilterSpecHandler_SpecNotFound(t *testing.T) { + reg := registry.New() + handler := FilterSpecHandler(reg) + + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, FilterSpecInput{ + SourceAlias: "nonexistent", + }) + + require.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "not found") +} + +func TestUnloadSpecHandler_Success(t *testing.T) { + reg := registry.New() + spec := createTestSpec() + err := reg.Load("test", spec, "/path/to/spec.yaml") + require.NoError(t, err) + + handler := UnloadSpecHandler(reg) + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, UnloadSpecInput{ + Alias: "test", + }) + + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "Unloaded 'test'") + assert.Equal(t, 0, reg.Count()) +} + +func TestListSpecsHandler_Empty(t *testing.T) { + reg := registry.New() + handler := ListSpecsHandler(reg) + + result, _, err := handler(context.Background(), &mcp.CallToolRequest{}, struct{}{}) + + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, result.Content[0].(*mcp.TextContent).Text, "No specs loaded") +} +