diff --git a/GEMINI.md b/GEMINI.md index 576986b..ad3f146 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -19,6 +19,7 @@ You are an expert helper for managing Google Cloud NetApp Volumes (GCNV) using t - Default transport is stdio; start HTTP with `npm run start:http` or `node build/index.js --transport http --port `. - If you change the port, adjust the URL accordingly. - Prefer stdio for CLI/editor use; use HTTP for browser/SSE integrations. +- **GCNV REST API (what MCP tools call):** This is separate from the MCP transport URL above. The server uses `@google-cloud/netapp` with `apiEndpoint` taken from the **`GCNV_API_ENDPOINT`** environment variable when it is set; if unset, the client uses Google’s default production NetApp Volumes API host. Request URLs are therefore `https:///...` (exact paths are chosen by the client library). When a user asks what URL or host the **API** uses, answer with **stdio vs HTTP MCP** only if they asked about MCP wiring; for **Google API** calls, state the effective **`GCNV_API_ENDPOINT`** value (or that it is unset and the library default applies)—do **not** reply with `localhost` unless they are explicitly asking about HTTP MCP mode. - **Authentication:** Assume Google Application Default Credentials are configured. - On authentication errors, remind the user to set `GOOGLE_APPLICATION_CREDENTIALS` or run: ``` @@ -50,7 +51,7 @@ Use this link to explain billing and estimate pricing (pair with the Google Clou - For **FLEX storage pool creation** only: if `location` is a **zone** (e.g., `us-central1-a`), that satisfies "zone in location" and you can omit `zone`/`replicaZone`. If `location` is a **region** (e.g., `us-central1`), the user must provide both `zone` and `replicaZone`. - **For list tools (`gcnv_*_list`), `location` is optional.** If the user does not specify a location (e.g. "list my storage pools", "list all volumes"), omit the `location` parameter or pass `-`; the API will return resources from all locations. Do not ask for a location when the user only wants a full list. -### Request construction +### Request constructionf - When building nested objects—export policies, protocol settings, replication configs—include **only fields the user specifies**. - Do not auto-populate defaults unless the official API mandates them and the user has not provided alternatives. @@ -120,8 +121,8 @@ Notes: - Users often type `flex` in lowercase; the server accepts `serviceLevel` case-insensitively for pool creation (for example `flex` or `FLEX`). - Minimum storage pool capacity (this project’s guidance): - `FLEX`: - - `FILE` / `UNIFIED`: **1024 GiB** - - `UNIFIED_LARGE_CAPACITY`: **6 TiB (6144 GiB)** + - `FILE` / `UNIFIED` (default scale): **1024 GiB** + - `UNIFIED` (large capacity / scale-out): **6 TiB (6144 GiB)** - `STANDARD`, `PREMIUM`, `EXTREME`: **2048 GiB** - Flex custom performance: users can optionally provide `totalThroughputMibps` (MiBps) when creating a **FLEX** pool. This is only supported in select regions; if the API rejects it, suggest using default performance or a supported region/zone. - Manual QoS: `qosType` can be `AUTO` or `MANUAL` for storage pools. Manual QoS is supported for Standard/Premium/Extreme and **isn't available for Flex**. See the Google Cloud docs: `https://docs.cloud.google.com/netapp/volumes/docs/performance/optimize-performance#set_up_manual_qos_limits`. @@ -129,8 +130,14 @@ Notes: - If `location` is a **zone** (e.g. `us-central1-a`), that satisfies “zone in location” for FLEX pool creation and the request body should omit `zone`/`replicaZone`. - If `location` is a **region** (e.g. `us-central1`), FLEX pool creation requires both `zone` and `replicaZone`. - StoragePoolType: - - Users can optionally provide `storagePoolType` (`FILE`, `UNIFIED`, `UNIFIED_LARGE_CAPACITY`). - - `UNIFIED` and `UNIFIED_LARGE_CAPACITY` are only supported for **FLEX** service level. + - Users can optionally provide `storagePoolType` (`FILE`, `UNIFIED`). + - `UNIFIED` is only supported for **FLEX** service level. +- ScaleType: + - `scaleType` is **only applicable to FLEX `UNIFIED` pools**. Do not send it for `FILE` pools or non-FLEX service levels (Standard/Premium/Extreme). + - Users can optionally provide `scaleType` (`SCALE_TYPE_DEFAULT`, `SCALE_TYPE_SCALEOUT`). + - `SCALE_TYPE_DEFAULT`: standard capacity and performance, suitable for general purpose workloads. Use for standard `UNIFIED` pools. + - `SCALE_TYPE_SCALEOUT`: higher capacity and performance, suitable for more demanding workloads. Use for large capacity `UNIFIED` pools. + - **`scaleType` must always be set explicitly by the user.** Do not infer it from capacity — a `UNIFIED` pool of any size can be either `SCALE_TYPE_DEFAULT` or `SCALE_TYPE_SCALEOUT`. Always ask the user which scale type they want when creating a FLEX `UNIFIED` pool if they have not specified it. - In simple terms: - **FLEX** is the newer service level focused on flexibility (smaller minimum sizes and, in some regions, more independent performance scaling). It is also available in many more regions. - **STANDARD / PREMIUM / EXTREME** are the classic tiers; Premium and Extreme are higher-performance tiers than Standard. @@ -159,7 +166,7 @@ Notes: - Pool: set `allowAutoTiering: true` when creating the storage pool. - Volume: set `tieringPolicy` on the volume (for example `tierAction: ENABLED`, optional `coolingThresholdDays`, optional `hotTierBypassModeEnabled`). - Hybrid replication: set `hybridReplicationParameters` on the volume (for example `replicationSchedule: HOURLY` and `hybridReplicationType: CONTINUOUS_REPLICATION`) along with peer details (cluster/SVM/IPs). -- Large capacity volumes: set `largeCapacity: true` (Premium/Extreme only; minimum 15 TiB) and optionally `multipleEndpoints: true` for multiple storage endpoints. See the volume limits and overview docs. +- Large capacity volumes: set `largeCapacity: true`. Required when the storage pool is a Unified scale-out / large-capacity pool — volumes in such pools must themselves be large-capacity. The MCP server sends `largeCapacityConfig: {}` on the wire (Volume field 46); the legacy `largeCapacity` boolean is for legacy FILE pools only. FLEX scale-out / PREMIUM / EXTREME; minimum **4916 GiB**. Optional `multipleEndpoints: true`. - SMB attributes (only when `protocols` includes `SMB`): - Boolean shortcuts on `gcnv_volume_create`: - `smbEncryptData: true` → SMB encryption (`ENCRYPT_DATA`) diff --git a/README.md b/README.md index f6aa4c1..fd46802 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,12 @@ HTTP endpoint: `http://localhost:/message` **Service level guidance:** -- **FLEX** -- Smaller minimums, broader region availability, independent performance scaling. Minimum: 1024 GiB (FILE/UNIFIED) or 6144 GiB (UNIFIED_LARGE_CAPACITY). +- **FLEX** -- Smaller minimums, broader region availability, independent performance scaling. Minimum: 1024 GiB (FILE/UNIFIED) or 6144 GiB (UNIFIED large capacity). - **STANDARD / PREMIUM / EXTREME** -- Classic tiers with fixed performance-to-capacity ratio. Minimum: 2048 GiB. - `serviceLevel` is accepted case-insensitively (e.g. `flex` or `FLEX`). - FLEX pools in a region-level location require both `zone` and `replicaZone`; zone-level locations satisfy this automatically. +- `storagePoolType` accepts `FILE` or `UNIFIED`; `UNIFIED` is only available for FLEX. +- `scaleType` accepts `SCALE_TYPE_DEFAULT` (standard capacity, general purpose) or `SCALE_TYPE_SCALEOUT` (higher capacity and performance, used for large capacity UNIFIED pools). ### Volume Tools @@ -130,7 +132,9 @@ HTTP endpoint: `http://localhost:/message` **iSCSI notes:** Protocols must be `["ISCSI"]` only (no mixing). Requires `hostGroup` or `hostGroups`. Optional `blockDevice` object with `identifier`, `osType` (`LINUX` / `WINDOWS` / `ESXI`), and `sizeGib`. -**Large capacity volumes:** Set `largeCapacity: true` (Premium/Extreme only, minimum 15 TiB). Optional `multipleEndpoints: true`. +**Large capacity volumes:** Set `largeCapacity: true` to create a large-capacity volume. Required when the storage pool is a Unified scale-out / large-capacity pool (volumes in such pools must themselves be large-capacity). The MCP server sends `largeCapacityConfig: {}` (Volume field 46) — the legacy `largeCapacity` boolean is reserved for legacy FILE pools and is mutually exclusive. FLEX scale-out / PREMIUM / EXTREME; minimum 4916 GiB. Optional `multipleEndpoints: true`. + +> Note: `LargeCapacityConfig` is not yet shipped in `@google-cloud/netapp@0.17.1`. This repo applies a small `patch-package` patch (`patches/@google-cloud+netapp+0.17.1.patch`) to add the message + Volume field 46 to the bundled proto so the gRPC client can serialize it. The patch is applied automatically via the `postinstall` script. **SMB attributes:** When `protocols` includes `SMB`, `gcnv_volume_create` accepts optional SMB feature flags that map to the `smbSettings` field on the volume: diff --git a/package-lock.json b/package-lock.json index a1c4b2b..3918ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@google-cloud/local-auth": "3.0.1", - "@google-cloud/netapp": "^0.16.0", + "@google-cloud/netapp": "^0.17.1", "@modelcontextprotocol/sdk": "^1.20.1", "axios": "^1.6.3", "pino": "^9.5.0", @@ -26,6 +26,7 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.1", + "patch-package": "^8.0.1", "prettier": "^3.4.1", "tsx": "^4.19.1", "typescript": "^5.8.3", @@ -825,9 +826,9 @@ } }, "node_modules/@google-cloud/netapp": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@google-cloud/netapp/-/netapp-0.16.0.tgz", - "integrity": "sha512-X+7mn/VE/TOmoJWFGN+lX7Wen8q8AYshCLBltFXe86XdlpO4WVYZjqvgTrkFryzpmZMK9Rj0tBOaPIOhYrcg8A==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@google-cloud/netapp/-/netapp-0.17.1.tgz", + "integrity": "sha512-igzgRNky0QI5lj2R9MuGL2UqpFsOsWs1E02KyiAklikTqIrud35TV9lve5HtddwcjZmE8izr3c607gjA9KmM5w==", "license": "Apache-2.0", "dependencies": { "google-gax": "^5.0.0" @@ -2050,6 +2051,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2298,6 +2306,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -2347,6 +2368,25 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2430,6 +2470,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2606,6 +2662,24 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3248,6 +3322,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3286,6 +3373,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -3410,6 +3507,21 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3652,6 +3764,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/gtoken": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", @@ -3675,6 +3794,19 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3939,6 +4071,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -3969,6 +4111,13 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4110,6 +4259,26 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4117,6 +4286,29 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -4148,6 +4340,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4278,6 +4480,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4443,6 +4672,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -4567,6 +4806,36 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5191,6 +5460,24 @@ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5309,6 +5596,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -5633,6 +5930,29 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -5752,6 +6072,16 @@ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6087,6 +6417,22 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 84e7c8e..109dac6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build", "GEMINI.md", "gemini-extension.json", + "patches", "README.md", "LICENSE" ], @@ -27,6 +28,7 @@ }, "scripts": { "githooks:install": "git config core.hooksPath .githooks", + "postinstall": "patch-package", "precommit": "npm run lint && npm test", "prepack": "npm run build", "build": "npm run lint && npm run format && tsc", @@ -60,6 +62,7 @@ "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.1", + "patch-package": "^8.0.1", "prettier": "^3.4.1", "tsx": "^4.19.1", "typescript": "^5.8.3", @@ -67,8 +70,8 @@ "vitest": "^3.2.4" }, "dependencies": { - "@google-cloud/netapp": "^0.16.0", "@google-cloud/local-auth": "3.0.1", + "@google-cloud/netapp": "^0.17.1", "@modelcontextprotocol/sdk": "^1.20.1", "axios": "^1.6.3", "pino": "^9.5.0", diff --git a/patches/@google-cloud+netapp+0.17.1.patch b/patches/@google-cloud+netapp+0.17.1.patch new file mode 100644 index 0000000..cdc647e --- /dev/null +++ b/patches/@google-cloud+netapp+0.17.1.patch @@ -0,0 +1,64 @@ +diff --git a/node_modules/@google-cloud/netapp/build/protos/protos.d.ts b/node_modules/@google-cloud/netapp/build/protos/protos.d.ts +index ebfa437..2b5fd97 100644 +--- a/node_modules/@google-cloud/netapp/build/protos/protos.d.ts ++++ b/node_modules/@google-cloud/netapp/build/protos/protos.d.ts +@@ -11621,6 +11621,9 @@ export namespace google { + /** Volume blockDevices */ + blockDevices?: (google.cloud.netapp.v1.IBlockDevice[]|null); + ++ /** Volume largeCapacityConfig */ ++ largeCapacityConfig?: (google.cloud.netapp.v1.ILargeCapacityConfig|null); ++ + /** Volume cloneDetails */ + cloneDetails?: (google.cloud.netapp.v1.Volume.ICloneDetails|null); + } +@@ -11967,6 +11970,13 @@ export namespace google { + } + } + ++ /** Properties of a LargeCapacityConfig. */ ++ interface ILargeCapacityConfig { ++ ++ /** LargeCapacityConfig constituentCount */ ++ constituentCount?: (number|null); ++ } ++ + /** Properties of an ExportPolicy. */ + interface IExportPolicy { + +diff --git a/node_modules/@google-cloud/netapp/build/protos/protos.json b/node_modules/@google-cloud/netapp/build/protos/protos.json +index f5efdb4..5906d74 100644 +--- a/node_modules/@google-cloud/netapp/build/protos/protos.json ++++ b/node_modules/@google-cloud/netapp/build/protos/protos.json +@@ -4757,6 +4757,13 @@ + "(google.api.field_behavior)": "OPTIONAL" + } + }, ++ "largeCapacityConfig": { ++ "type": "LargeCapacityConfig", ++ "id": 46, ++ "options": { ++ "(google.api.field_behavior)": "OPTIONAL" ++ } ++ }, + "cloneDetails": { + "type": "CloneDetails", + "id": 47, +@@ -4809,6 +4816,17 @@ + } + } + }, ++ "LargeCapacityConfig": { ++ "fields": { ++ "constituentCount": { ++ "type": "int32", ++ "id": 1, ++ "options": { ++ "(google.api.field_behavior)": "OPTIONAL" ++ } ++ } ++ } ++ }, + "ExportPolicy": { + "fields": { + "rules": { diff --git a/src/tools/handlers/storage-pool-handler.test.ts b/src/tools/handlers/storage-pool-handler.test.ts index d5f8f47..5dc8d56 100644 --- a/src/tools/handlers/storage-pool-handler.test.ts +++ b/src/tools/handlers/storage-pool-handler.test.ts @@ -109,7 +109,196 @@ describe('storage-pool-handler', () => { }); }); - it('createStoragePoolHandler rejects UNIFIED_* storagePoolType for non-FLEX service levels', async () => { + it('createStoragePoolHandler includes scaleType in request payload', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 6144, + serviceLevel: 'FLEX', + network: 'net1', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_SCALEOUT', + }); + + expect(createStoragePool).toHaveBeenCalledTimes(1); + expect(createStoragePool.mock.calls[0]?.[0]).toMatchObject({ + storagePool: expect.objectContaining({ + serviceLevel: 'FLEX', + type: 2, + scaleType: 'SCALE_TYPE_SCALEOUT', + }), + }); + }); + + it('createStoragePoolHandler accepts all valid scaleType values (case-insensitive)', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + const defaults = ['SCALE_TYPE_DEFAULT', 'scale_type_default', 'Scale_Type_Default']; + for (const scaleType of defaults) { + createStoragePool.mockClear(); + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 1024, + serviceLevel: 'FLEX', + network: 'net1', + scaleType, + }); + expect(createStoragePool.mock.calls[0]?.[0]).toMatchObject({ + storagePool: expect.objectContaining({ scaleType: 'SCALE_TYPE_DEFAULT' }), + }); + } + + createStoragePool.mockClear(); + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 1024, + serviceLevel: 'FLEX', + network: 'net1', + scaleType: 'SCALE_TYPE_UNSPECIFIED', + }); + expect(createStoragePool.mock.calls[0]?.[0]).toMatchObject({ + storagePool: expect.objectContaining({ scaleType: 'SCALE_TYPE_UNSPECIFIED' }), + }); + }); + + it('createStoragePoolHandler rejects invalid scaleType', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + const result = await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 1024, + serviceLevel: 'FLEX', + network: 'net1', + scaleType: 'INVALID_SCALE', + }); + + expect((result as any).isError).toBe(true); + expect((result as any).content?.[0]?.text).toContain('scaleType must be one of'); + expect(createStoragePool).not.toHaveBeenCalled(); + }); + + it('createStoragePoolHandler rejects non-string scaleType', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + const result = await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 1024, + serviceLevel: 'FLEX', + network: 'net1', + scaleType: 42, + }); + + expect((result as any).isError).toBe(true); + expect((result as any).content?.[0]?.text).toContain('scaleType must be a string enum name'); + expect(createStoragePool).not.toHaveBeenCalled(); + }); + + it('createStoragePoolHandler omits scaleType from payload when storagePoolType is not UNIFIED and scaleType not provided', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 2048, + serviceLevel: 'PREMIUM', + network: 'net1', + }); + + const payload = createStoragePool.mock.calls[0]?.[0]?.storagePool; + expect(payload).not.toHaveProperty('scaleType'); + }); + + it('createStoragePoolHandler omits scaleType from payload when UNIFIED pool created without scaleType', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + // No auto-inference — scaleType must be provided explicitly + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 6144, + serviceLevel: 'FLEX', + network: 'net1', + storagePoolType: 'UNIFIED', + }); + + const payload = createStoragePool.mock.calls[0]?.[0]?.storagePool; + expect(payload).not.toHaveProperty('scaleType'); + }); + + it('createStoragePoolHandler sends SCALE_TYPE_SCALEOUT when explicitly provided for large capacity UNIFIED pool', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 6144, + serviceLevel: 'FLEX', + network: 'net1', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_SCALEOUT', + }); + + expect(createStoragePool.mock.calls[0]?.[0]).toMatchObject({ + storagePool: expect.objectContaining({ + type: 2, + scaleType: 'SCALE_TYPE_SCALEOUT', + }), + }); + }); + + it('createStoragePoolHandler sends SCALE_TYPE_DEFAULT when explicitly provided for standard UNIFIED pool', async () => { + const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); + createClientMock.mockReturnValue({ createStoragePool }); + const { createStoragePoolHandler } = await import('./storage-pool-handler.js'); + + await createStoragePoolHandler({ + projectId: 'p1', + location: 'us-central1-a', + storagePoolId: 'sp1', + capacityGib: 1024, + serviceLevel: 'FLEX', + network: 'net1', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_DEFAULT', + }); + + expect(createStoragePool.mock.calls[0]?.[0]).toMatchObject({ + storagePool: expect.objectContaining({ + type: 2, + scaleType: 'SCALE_TYPE_DEFAULT', + }), + }); + }); + + it('createStoragePoolHandler rejects UNIFIED storagePoolType for non-FLEX service levels', async () => { const createStoragePool = vi.fn().mockResolvedValue([{ name: 'op-create' }]); createClientMock.mockReturnValue({ createStoragePool }); @@ -121,11 +310,11 @@ describe('storage-pool-handler', () => { capacityGib: 100, serviceLevel: 'STANDARD', network: 'net1', - storagePoolType: 'UNIFIED_LARGE_CAPACITY', + storagePoolType: 'UNIFIED', }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('storagePoolType UNIFIED and UNIFIED_LARGE_CAPACITY'); + expect(result.content[0].text).toContain('storagePoolType UNIFIED is only supported'); expect(createStoragePool).not.toHaveBeenCalled(); }); @@ -927,9 +1116,7 @@ describe('storage-pool-handler', () => { storagePoolType: 'UNIFIED', }); expect((nonFlex as any).isError).toBe(true); - expect((nonFlex as any).content?.[0]?.text).toContain( - 'UNIFIED and UNIFIED_LARGE_CAPACITY are only supported' - ); + expect((nonFlex as any).content?.[0]?.text).toContain('UNIFIED is only supported'); }); it('updateStoragePoolHandler supports updating zone and replicaZone', async () => { diff --git a/src/tools/handlers/storage-pool-handler.ts b/src/tools/handlers/storage-pool-handler.ts index 90f202e..a923e74 100644 --- a/src/tools/handlers/storage-pool-handler.ts +++ b/src/tools/handlers/storage-pool-handler.ts @@ -9,14 +9,13 @@ function parseStoragePoolType(input: any): { value?: number; error?: string } { STORAGE_POOL_TYPE_UNSPECIFIED: 0, FILE: 1, UNIFIED: 2, - UNIFIED_LARGE_CAPACITY: 3, }; if (input === undefined || input === null) return {}; if (typeof input === 'number') { if (Object.values(enumMap).includes(input)) return { value: input }; - return { error: 'storagePoolType must be a valid enum number (0-3)' }; + return { error: 'storagePoolType must be a valid enum number (0-2)' }; } if (typeof input === 'string') { @@ -24,13 +23,34 @@ function parseStoragePoolType(input: any): { value?: number; error?: string } { if (enumMap[trimmed] !== undefined) return { value: enumMap[trimmed] }; return { error: - 'storagePoolType must be one of STORAGE_POOL_TYPE_UNSPECIFIED, FILE, UNIFIED, UNIFIED_LARGE_CAPACITY, or the corresponding enum number', + 'storagePoolType must be one of STORAGE_POOL_TYPE_UNSPECIFIED, FILE, UNIFIED, or the corresponding enum number', }; } return { error: 'storagePoolType must be a string enum name or enum number' }; } +function parseScaleType(input: any): { value?: string; error?: string } { + const validValues = new Set([ + 'SCALE_TYPE_UNSPECIFIED', + 'SCALE_TYPE_DEFAULT', + 'SCALE_TYPE_SCALEOUT', + ]); + + if (input === undefined || input === null) return {}; + + if (typeof input === 'string') { + const trimmed = input.trim().toUpperCase(); + if (validValues.has(trimmed)) return { value: trimmed }; + return { + error: + 'scaleType must be one of SCALE_TYPE_UNSPECIFIED, SCALE_TYPE_DEFAULT, SCALE_TYPE_SCALEOUT', + }; + } + + return { error: 'scaleType must be a string enum name' }; +} + function normalizeStoragePoolState(state: any): string { return typeof state === 'string' ? state : 'UNKNOWN'; } @@ -61,6 +81,7 @@ export const createStoragePoolHandler: ToolHandler = async (args: { [key: string qosType, allowAutoTiering, storagePoolType, + scaleType, zone, replicaZone, } = args; @@ -91,18 +112,51 @@ export const createStoragePoolHandler: ToolHandler = async (args: { [key: string }; } - // New pool types (UNIFIED / UNIFIED_LARGE_CAPACITY) are only available for FLEX - if ( - parsedStoragePoolType !== undefined && - (parsedStoragePoolType === 2 || parsedStoragePoolType === 3) && - normalizedServiceLevel !== 'FLEX' - ) { + const { value: parsedScaleType, error: scaleTypeError } = parseScaleType(scaleType); + if (scaleTypeError) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `Error creating storage pool: ${scaleTypeError}`, + }, + ], + }; + } + + // scaleType is only applicable to FLEX UNIFIED pools + if (parsedScaleType !== undefined && normalizedServiceLevel !== 'FLEX') { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: 'Error creating storage pool: scaleType is only applicable to FLEX UNIFIED storage pools.', + }, + ], + }; + } + if (parsedScaleType !== undefined && parsedStoragePoolType === 1) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: 'Error creating storage pool: scaleType is only applicable to UNIFIED storage pools, not FILE pools.', + }, + ], + }; + } + + // UNIFIED is only available for FLEX + if (parsedStoragePoolType != undefined && parsedStoragePoolType === 2 && normalizedServiceLevel !== 'FLEX') { return { isError: true, content: [ { type: 'text' as const, - text: 'Error creating storage pool: storagePoolType UNIFIED and UNIFIED_LARGE_CAPACITY are only supported when serviceLevel is FLEX.', + text: 'Error creating storage pool: storagePoolType UNIFIED is only supported when serviceLevel is FLEX.', }, ], }; @@ -184,6 +238,7 @@ export const createStoragePoolHandler: ToolHandler = async (args: { [key: string if (normalizedQosType) storagePoolPayload.qosType = normalizedQosType; if (allowAutoTiering !== undefined) storagePoolPayload.allowAutoTiering = allowAutoTiering; if (parsedStoragePoolType !== undefined) storagePoolPayload.type = parsedStoragePoolType; + if (parsedScaleType !== undefined) storagePoolPayload.scaleType = parsedScaleType; if (normalizedServiceLevel === 'FLEX') { // IMPORTANT: For zonal pools, the zone is encoded in the URL/location already; @@ -540,8 +595,8 @@ export const updateStoragePoolHandler: ToolHandler = async (args: { [key: string }; } - // Only enforce FLEX for new types; FILE is allowed everywhere (and is the historical default) - if (parsedType === 2 || parsedType === 3) { + // Only enforce FLEX for UNIFIED; FILE is allowed everywhere (and is the historical default) + if (parsedType === 2) { const existing = await getExistingPool(); const existingServiceLevel = typeof existing?.serviceLevel === 'string' @@ -553,7 +608,7 @@ export const updateStoragePoolHandler: ToolHandler = async (args: { [key: string content: [ { type: 'text' as const, - text: 'Error updating storage pool: storagePoolType UNIFIED and UNIFIED_LARGE_CAPACITY are only supported when serviceLevel is FLEX.', + text: 'Error updating storage pool: storagePoolType UNIFIED is only supported when serviceLevel is FLEX.', }, ], }; diff --git a/src/tools/handlers/volume-handler.test.ts b/src/tools/handlers/volume-handler.test.ts index 1d0e88c..dbd2c3c 100644 --- a/src/tools/handlers/volume-handler.test.ts +++ b/src/tools/handlers/volume-handler.test.ts @@ -88,7 +88,7 @@ describe('volume-handler', () => { parent: 'projects/p1/locations/us-central1', volumeId: 'vol1', volume: { - storagePool: 'projects/p1/locations/us-central1/storagePools/sp1', + storagePool: 'sp1', capacityGib: 100, protocols: [1], description: 'd', @@ -381,7 +381,7 @@ describe('volume-handler', () => { ); }); - it('createVolumeHandler supports Large Capacity Volumes (Premium/Extreme only)', async () => { + it('createVolumeHandler supports Large Capacity Volumes (FLEX / Premium / Extreme)', async () => { const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]); const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'PREMIUM' }]); createClientMock.mockReturnValue({ createVolume, getStoragePool }); @@ -392,7 +392,7 @@ describe('volume-handler', () => { location: 'us-central1', storagePoolId: 'projects/p1/locations/us-central1/storagePools/sp1', volumeId: 'vol-big', - capacityGib: 15360, + capacityGib: 4916, protocols: ['NFSV3'], largeCapacity: true, multipleEndpoints: true, @@ -404,10 +404,51 @@ describe('volume-handler', () => { expect(createVolume.mock.calls[0]?.[0]).toMatchObject({ volumeId: 'vol-big', volume: expect.objectContaining({ - largeCapacity: true, + largeCapacityConfig: {}, multipleEndpoints: true, }), }); + expect(createVolume.mock.calls[0]?.[0]?.volume).not.toHaveProperty('largeCapacity'); + }); + + it('createVolumeHandler sends largeCapacityConfig {} on FLEX scale-out pool for SMB volume', async () => { + const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcc' }]); + const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'FLEX' }]); + createClientMock.mockReturnValue({ createVolume, getStoragePool }); + + const { createVolumeHandler } = await import('./volume-handler.js'); + await createVolumeHandler({ + projectId: 'netapp-gcnv-vsa-control-plane', + location: 'us-east4-a', + storagePoolId: 'pool-mcp-lv-ad', + volumeId: 'volsmb3', + capacityGib: 4916, + protocols: ['SMB'], + largeCapacity: true, + }); + + const vol = createVolume.mock.calls[0]?.[0]?.volume as Record; + expect(vol?.largeCapacityConfig).toEqual({}); + expect(vol).not.toHaveProperty('largeCapacity'); + }); + + it('createVolumeHandler omits largeCapacityConfig when the flag is not set', async () => { + const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-no-lc' }]); + createClientMock.mockReturnValue({ createVolume }); + + const { createVolumeHandler } = await import('./volume-handler.js'); + await createVolumeHandler({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + volumeId: 'vol-small', + capacityGib: 1024, + protocols: ['NFSV3'], + }); + + const vol = createVolume.mock.calls[0]?.[0]?.volume as Record; + expect(vol).not.toHaveProperty('largeCapacity'); + expect(vol).not.toHaveProperty('largeCapacityConfig'); }); it('createVolumeHandler supports ISCSI with hostGroup and creates blockDevices payload', async () => { @@ -689,9 +730,10 @@ describe('volume-handler', () => { expect(getStoragePool).not.toHaveBeenCalled(); expect(createVolume).not.toHaveBeenCalled(); expect((result as any).isError).toBe(true); + expect((result.content?.[0] as any)?.text).toMatch(/large-capacity/); }); - it('createVolumeHandler rejects largeCapacity when capacity is < 15 TiB', async () => { + it('createVolumeHandler rejects largeCapacity when capacity is below minimum GiB', async () => { const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]); const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'PREMIUM' }]); createClientMock.mockReturnValue({ createVolume, getStoragePool }); @@ -710,9 +752,53 @@ describe('volume-handler', () => { expect(getStoragePool).not.toHaveBeenCalled(); expect(createVolume).not.toHaveBeenCalled(); expect((result as any).isError).toBe(true); + expect((result.content?.[0] as any)?.text).toContain('4916'); + }); + + it('createVolumeHandler rejects largeCapacity when capacity is one GiB below minimum', async () => { + const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]); + const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'FLEX' }]); + createClientMock.mockReturnValue({ createVolume, getStoragePool }); + + const { createVolumeHandler } = await import('./volume-handler.js'); + const result = await createVolumeHandler({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + volumeId: 'vol-edge', + capacityGib: 4915, + protocols: ['NFSV3'], + largeCapacity: true, + }); + + expect(getStoragePool).not.toHaveBeenCalled(); + expect(createVolume).not.toHaveBeenCalled(); + expect((result as any).isError).toBe(true); + }); + + it('createVolumeHandler allows largeCapacity on FLEX pool at minimum GiB', async () => { + const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv-flex' }]); + const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'FLEX' }]); + createClientMock.mockReturnValue({ createVolume, getStoragePool }); + + const { createVolumeHandler } = await import('./volume-handler.js'); + await createVolumeHandler({ + projectId: 'p1', + location: 'us-east4-a', + storagePoolId: 'pool-mcp-lv-ad', + volumeId: 'volsmb1', + capacityGib: 4916, + protocols: ['SMB'], + largeCapacity: true, + }); + + expect(getStoragePool).toHaveBeenCalled(); + expect(createVolume).toHaveBeenCalled(); + expect(createVolume.mock.calls[0]?.[0]?.volume?.largeCapacityConfig).toEqual({}); + expect(createVolume.mock.calls[0]?.[0]?.volume).not.toHaveProperty('largeCapacity'); }); - it('createVolumeHandler rejects largeCapacity for non-PREMIUM/EXTREME pools', async () => { + it('createVolumeHandler rejects largeCapacity for pools outside FLEX/PREMIUM/EXTREME', async () => { const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]); const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'STANDARD' }]); createClientMock.mockReturnValue({ createVolume, getStoragePool }); @@ -723,7 +809,7 @@ describe('volume-handler', () => { location: 'us-central1', storagePoolId: 'sp1', // exercise "ID" form -> handler builds full name volumeId: 'vol-big', - capacityGib: 15360, + capacityGib: 4916, protocols: ['NFSV3'], largeCapacity: true, }); @@ -733,6 +819,7 @@ describe('volume-handler', () => { }); expect(createVolume).not.toHaveBeenCalled(); expect((result as any).isError).toBe(true); + expect((result.content?.[0] as any)?.text).toMatch(/FLEX|PREMIUM|EXTREME/); }); it('createVolumeHandler shows UNKNOWN in error when storage pool serviceLevel is missing (covers poolServiceLevel || \"UNKNOWN\")', async () => { @@ -746,7 +833,7 @@ describe('volume-handler', () => { location: 'us-central1', storagePoolId: 'projects/p1/locations/us-central1/storagePools/sp1', volumeId: 'vol-big', - capacityGib: 15360, + capacityGib: 4916, protocols: ['NFSV3'], largeCapacity: true, }); @@ -756,7 +843,7 @@ describe('volume-handler', () => { expect((result as any).content?.[0]?.text).toContain('UNKNOWN'); }); - it('createVolumeHandler computes storagePoolName when storagePoolId is missing (covers storagePoolId || \"\" branch)', async () => { + it('createVolumeHandler rejects create when neither storagePoolId nor storagePool is provided', async () => { const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-lcv' }]); const getStoragePool = vi.fn().mockResolvedValue([{ serviceLevel: 'STANDARD' }]); createClientMock.mockReturnValue({ createVolume, getStoragePool }); @@ -765,18 +852,38 @@ describe('volume-handler', () => { const result = await createVolumeHandler({ projectId: 'p1', location: 'us-central1', - // storagePoolId intentionally omitted to cover `storagePoolId || ''` volumeId: 'vol-big', - capacityGib: 15360, + capacityGib: 4916, protocols: ['NFSV3'], largeCapacity: true, }); - expect(getStoragePool).toHaveBeenCalledWith({ - name: 'projects/p1/locations/us-central1/storagePools/undefined', - }); + expect(getStoragePool).not.toHaveBeenCalled(); expect(createVolume).not.toHaveBeenCalled(); expect((result as any).isError).toBe(true); + expect((result.content?.[0] as any)?.text).toMatch(/storagePoolId or storagePool is required/); + }); + + it('createVolumeHandler accepts storagePool without storagePoolId', async () => { + const createVolume = vi.fn().mockResolvedValue([{ name: 'operations/op-pool-alias' }]); + createClientMock.mockReturnValue({ createVolume }); + + const { createVolumeHandler } = await import('./volume-handler.js'); + await createVolumeHandler({ + projectId: 'p1', + location: 'us-east4-a', + storagePool: 'pool-mcp-lv-ad', + volumeId: 'vol1', + capacityGib: 1024, + protocols: ['SMB'], + }); + + expect(createVolume).toHaveBeenCalledTimes(1); + expect(createVolume.mock.calls[0]?.[0]).toMatchObject({ + parent: 'projects/p1/locations/us-east4-a', + volumeId: 'vol1', + volume: expect.objectContaining({ storagePool: 'pool-mcp-lv-ad' }), + }); }); it('createVolumeHandler defaults protocols to NFSV3 and uses provided shareName', async () => { diff --git a/src/tools/handlers/volume-handler.ts b/src/tools/handlers/volume-handler.ts index cbefb52..a5966d5 100644 --- a/src/tools/handlers/volume-handler.ts +++ b/src/tools/handlers/volume-handler.ts @@ -4,6 +4,11 @@ import { logger } from '../../logger.js'; const log = logger.child({ module: 'volume-handler' }); +/** Minimum volume size (GiB) for GCNV large-capacity volumes (Unified scale-out pools). */ +const LARGE_CAPACITY_VOLUME_MIN_CAPACITY_GIB = 4916; + +const LARGE_CAPACITY_ALLOWED_POOL_SERVICE_LEVELS = new Set(['FLEX', 'PREMIUM', 'EXTREME']); + // Optional display hint when resource name matches a legacy pattern (used by some clients for UI) function normalizeResourceOutput(o: Record): void { if (!o || typeof o.name !== 'string') return; @@ -180,11 +185,43 @@ function buildSmbSettingsForCreate(args: { function resolveStoragePoolResourceName( projectId: string, location: string, - storagePoolId: string + storagePoolRef: string ): string { - return String(storagePoolId || '').includes('/') - ? storagePoolId - : `projects/${projectId}/locations/${location}/storagePools/${storagePoolId}`; + return String(storagePoolRef || '').includes('/') + ? storagePoolRef + : `projects/${projectId}/locations/${location}/storagePools/${storagePoolRef}`; +} + +/** + * Value for Volume.storagePool on create. The NetApp Volumes API expects the pool's short ID (final segment + * of the storage pool resource name), matching Volume.storagePool on GET — not a full + * projects/.../storagePools/... path. + */ +function storagePoolFieldForVolumeCreate(storagePoolRef: string): string { + const trimmed = String(storagePoolRef || '').trim(); + if (!trimmed) { + return trimmed; + } + const marker = '/storagePools/'; + const idx = trimmed.indexOf(marker); + if (idx >= 0) { + return trimmed.slice(idx + marker.length); + } + const parts = trimmed.split('/').filter(Boolean); + if (parts.length > 1) { + return parts[parts.length - 1] ?? trimmed; + } + return trimmed; +} + +function firstNonEmptyPoolRef(args: { [key: string]: unknown }): string | undefined { + const candidates = [args.storagePoolId, args.storagePool, args.storage_pool]; + for (const c of candidates) { + if (typeof c === 'string' && c.trim() !== '') { + return c.trim(); + } + } + return undefined; } // Helper to format volume data for responses @@ -267,7 +304,6 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an const { projectId, location, - storagePoolId, volumeId, capacityGib, protocols: rawProtocols, @@ -292,6 +328,19 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an smbContinuouslyAvailable, } = args; + const storagePoolRef = firstNonEmptyPoolRef(args); + if (!storagePoolRef) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: 'Error creating volume: storagePoolId or storagePool is required.', + }, + ], + }; + } + // Create a new NetApp client using the factory const netAppClient = NetAppClientFactory.createClient(); @@ -300,32 +349,33 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an const storagePoolResourceName = resolveStoragePoolResourceName( projectId, location, - String(storagePoolId) + storagePoolRef ); + const volumeStoragePool = storagePoolFieldForVolumeCreate(storagePoolRef); + + const isLargeCapacity = largeCapacity === true; - // Large Capacity Volumes guardrails: - // - Premium/Extreme only (enforced by checking the storage pool service level) - // - Minimum size 15 TiB => 15360 GiB - if (multipleEndpoints && !largeCapacity) { + // Large Capacity Volumes guardrails (pool capabilities are still enforced by the API). + if (multipleEndpoints && !isLargeCapacity) { return { isError: true, content: [ { type: 'text' as const, - text: 'Error creating volume: multipleEndpoints is only valid when largeCapacity is true.', + text: 'Error creating volume: multipleEndpoints is only valid for large-capacity volumes (set largeCapacity true).', }, ], }; } - if (largeCapacity) { - if (capacityGib < 15360) { + if (isLargeCapacity) { + if (capacityGib < LARGE_CAPACITY_VOLUME_MIN_CAPACITY_GIB) { return { isError: true, content: [ { type: 'text' as const, - text: 'Error creating volume: largeCapacity requires capacityGib >= 15360 (15 TiB).', + text: `Error creating volume: large-capacity volumes require capacityGib >= ${LARGE_CAPACITY_VOLUME_MIN_CAPACITY_GIB} GiB.`, }, ], }; @@ -333,13 +383,13 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an const [pool] = await netAppClient.getStoragePool({ name: storagePoolResourceName }); const poolServiceLevel = (pool?.serviceLevel || '').toString().toUpperCase(); - if (poolServiceLevel !== 'PREMIUM' && poolServiceLevel !== 'EXTREME') { + if (!LARGE_CAPACITY_ALLOWED_POOL_SERVICE_LEVELS.has(poolServiceLevel)) { return { isError: true, content: [ { type: 'text' as const, - text: `Error creating volume: largeCapacity volumes are only supported in PREMIUM or EXTREME pools (got ${poolServiceLevel || 'UNKNOWN'}).`, + text: `Error creating volume: large-capacity volumes require a FLEX, PREMIUM, or EXTREME storage pool (got ${poolServiceLevel || 'UNKNOWN'}).`, }, ], }; @@ -500,12 +550,14 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an const protocolEnums = normalizedProtocolNames.map((p) => protocolEnumMap[p]); const effectiveShareName = isIscsi ? undefined : shareName || volumeId; - // Create the volume request + // CreateVolume (CCFE): volumeId is a query parameter; the body is a Volume resource. The JSON field for + // the pool is always `storagePool` — there is no `storagePoolId` on Volume. MCP tools accept + // `storagePoolId` / `storagePool` as arguments only; they are mapped into `volume.storagePool` here. const request = { parent, volumeId, volume: { - storagePool: storagePoolId, + storagePool: volumeStoragePool, capacityGib, protocols: protocolEnums, description, @@ -518,7 +570,9 @@ export const createVolumeHandler: ToolHandler = async (args: { [key: string]: an exportPolicy, ...(blockDevicesPayload ? { blockDevices: blockDevicesPayload } : {}), ...(throughputMibps !== undefined ? { throughputMibps } : {}), - ...(largeCapacity !== undefined ? { largeCapacity } : {}), + // Unified scale-out pools require `largeCapacityConfig` (mutually exclusive with the legacy `largeCapacity` + // boolean). The `@google-cloud/netapp` proto is patched (see patches/) to recognize field 46. + ...(isLargeCapacity ? { largeCapacityConfig: {} } : {}), ...(multipleEndpoints !== undefined ? { multipleEndpoints } : {}), ...(smbSettingsList && smbSettingsList.length > 0 ? { smbSettings: smbSettingsList } : {}), }, diff --git a/src/tools/storage-pool-tools.test.ts b/src/tools/storage-pool-tools.test.ts index 7ce4d66..2a4b443 100644 --- a/src/tools/storage-pool-tools.test.ts +++ b/src/tools/storage-pool-tools.test.ts @@ -42,7 +42,7 @@ describe('storage-pool-tools', () => { ).not.toThrow(); }); - it('createStoragePoolTool accepts storagePoolType values (validation is enforced in handler)', () => { + it('createStoragePoolTool accepts valid storagePoolType values and rejects removed UNIFIED_LARGE_CAPACITY', () => { const schema = z.object(createStoragePoolTool.inputSchema); expect(() => @@ -52,7 +52,7 @@ describe('storage-pool-tools', () => { storagePoolId: 'sp1', capacityGib: 100, serviceLevel: 'FLEX', - storagePoolType: 'UNIFIED_LARGE_CAPACITY', + storagePoolType: 'UNIFIED', }) ).not.toThrow(); @@ -66,6 +66,70 @@ describe('storage-pool-tools', () => { storagePoolType: 'FILE', }) ).not.toThrow(); + + // Negative: UNIFIED_LARGE_CAPACITY was removed from the enum; schema must reject it + expect(() => + schema.parse({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 100, + serviceLevel: 'FLEX', + storagePoolType: 'UNIFIED_LARGE_CAPACITY', + }) + ).toThrow(); + }); + + it('createStoragePoolTool accepts scaleType values', () => { + const schema = z.object(createStoragePoolTool.inputSchema); + + expect(() => + schema.parse({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 100, + serviceLevel: 'FLEX', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_DEFAULT', + }) + ).not.toThrow(); + + expect(() => + schema.parse({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 100, + serviceLevel: 'FLEX', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_SCALEOUT', + }) + ).not.toThrow(); + + expect(() => + schema.parse({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 100, + serviceLevel: 'FLEX', + storagePoolType: 'UNIFIED', + scaleType: 'SCALE_TYPE_UNSPECIFIED', + }) + ).not.toThrow(); + + expect(() => + schema.parse({ + projectId: 'p1', + location: 'us-central1', + storagePoolId: 'sp1', + capacityGib: 100, + serviceLevel: 'FLEX', + storagePoolType: 'UNIFIED', + scaleType: 'INVALID_SCALE_TYPE', + }) + ).toThrow(); }); it('updateStoragePoolTool accepts totalThroughputMibps input', () => { diff --git a/src/tools/storage-pool-tools.ts b/src/tools/storage-pool-tools.ts index bbd5f67..d5c11b4 100644 --- a/src/tools/storage-pool-tools.ts +++ b/src/tools/storage-pool-tools.ts @@ -77,13 +77,22 @@ export const createStoragePoolTool: ToolConfig = { ), storagePoolType: z .union([ - z.enum(['STORAGE_POOL_TYPE_UNSPECIFIED', 'FILE', 'UNIFIED', 'UNIFIED_LARGE_CAPACITY']), - z.enum(['storage_pool_type_unspecified', 'file', 'unified', 'unified_large_capacity']), + z.enum(['STORAGE_POOL_TYPE_UNSPECIFIED', 'FILE', 'UNIFIED']), + z.enum(['storage_pool_type_unspecified', 'file', 'unified']), z.number(), ]) .optional() .describe( - 'Storage pool type (StoragePoolType). UNIFIED and UNIFIED_LARGE_CAPACITY are only available for FLEX service level.' + 'Storage pool type (StoragePoolType). UNIFIED is only available for FLEX service level.' + ), + scaleType: z + .union([ + z.enum(['SCALE_TYPE_UNSPECIFIED', 'SCALE_TYPE_DEFAULT', 'SCALE_TYPE_SCALEOUT']), + z.enum(['scale_type_unspecified', 'scale_type_default', 'scale_type_scaleout']), + ]) + .optional() + .describe( + 'Scale type for the UNIFIED storage pool. SCALE_TYPE_SCALEOUT: higher capacity and performance, suitable for more demanding workloads (large capacity pools). SCALE_TYPE_DEFAULT: standard capacity and performance, suitable for general purpose workloads. Must be set explicitly — the handler does not infer this from capacity.' ), }, outputSchema: { @@ -259,13 +268,13 @@ export const updateStoragePoolTool: ToolConfig = { ), storagePoolType: z .union([ - z.enum(['STORAGE_POOL_TYPE_UNSPECIFIED', 'FILE', 'UNIFIED', 'UNIFIED_LARGE_CAPACITY']), - z.enum(['storage_pool_type_unspecified', 'file', 'unified', 'unified_large_capacity']), + z.enum(['STORAGE_POOL_TYPE_UNSPECIFIED', 'FILE', 'UNIFIED']), + z.enum(['storage_pool_type_unspecified', 'file', 'unified']), z.number(), ]) .optional() .describe( - 'Storage pool type (StoragePoolType). UNIFIED and UNIFIED_LARGE_CAPACITY are only available for FLEX service level.' + 'Storage pool type (StoragePoolType). UNIFIED is only available for FLEX service level.' ), zone: z .string() diff --git a/src/tools/volume-tools.ts b/src/tools/volume-tools.ts index f0f4942..9c68a4f 100644 --- a/src/tools/volume-tools.ts +++ b/src/tools/volume-tools.ts @@ -9,7 +9,18 @@ export const createVolumeTool: ToolConfig = { inputSchema: { projectId: z.string().describe('The ID of the Google Cloud project'), location: z.string().describe('The location where the volume should be created'), - storagePoolId: z.string().describe('The ID of the storage pool to create the volume in'), + storagePoolId: z + .string() + .optional() + .describe( + 'The storage pool short ID or full resource name. Provide this and/or storagePool (same meaning as the API Volume.storagePool field).' + ), + storagePool: z + .string() + .optional() + .describe( + 'Alias for storagePoolId (matches API JSON field storagePool). At least one of storagePoolId or storagePool must be set.' + ), volumeId: z.string().describe('The ID to assign to the volume'), capacityGib: z.number().describe('The capacity of the volume in GiB'), protocols: z @@ -286,13 +297,13 @@ export const createVolumeTool: ToolConfig = { .boolean() .optional() .describe( - 'Enable Large Capacity Volume mode (Premium/Extreme only). Requires capacityGib >= 15360 (15 TiB).' + 'When true, creates the volume as a large-capacity volume by sending `largeCapacityConfig: {}` (mutually exclusive with the legacy `largeCapacity` boolean, which is for legacy FILE pools only). Required for volumes in Unified scale-out / large-capacity pools. Requires capacityGib >= 4916 with FLEX/PREMIUM/EXTREME pool.' ), multipleEndpoints: z .boolean() .optional() .describe( - 'Use multiple storage endpoints for Large Capacity Volumes (only valid when largeCapacity is true).' + 'Use multiple storage endpoints for large-capacity volumes (requires largeCapacity to be true).' ), smbEncryptData: z .boolean() diff --git a/src/utils/netapp-client-factory.ts b/src/utils/netapp-client-factory.ts index efcec15..5f13fc6 100644 --- a/src/utils/netapp-client-factory.ts +++ b/src/utils/netapp-client-factory.ts @@ -17,6 +17,9 @@ export class NetAppClientFactory { maxDelayMs: 30000, maxRetries: 5, }, + // Use REST/JSON transport instead of gRPC binary encoding. REST sends fields by name + // which is more robust and forward-compatible with API changes. + fallback: true, }; /**