{"_id":"@aidalinfo/scaleway-mcp","_rev":"3-3efa1a09a7a4932bf3f597d63981428b","name":"@aidalinfo/scaleway-mcp","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@aidalinfo/scaleway-mcp","version":"1.0.0","keywords":["mcp","mcp-server","scaleway","scw","instances","rdb","dns","vpc","kubernetes","typescript"],"author":{"name":"Killian Stein / Aidalinfo"},"license":"MIT","_id":"@aidalinfo/scaleway-mcp@1.0.0","maintainers":[{"name":"aidalinfo","email":"dev@pulsemyit.fr"}],"homepage":"https://github.com/aidalinfo/cloud-mcp#readme","bugs":{"url":"https://github.com/aidalinfo/cloud-mcp/issues"},"bin":{"scaleway-mcp":"dist/index.js"},"dist":{"shasum":"80e26bfdf5205ebbc264d73783b7a6cd8d40e61c","tarball":"https://registry.npmjs.org/@aidalinfo/scaleway-mcp/-/scaleway-mcp-1.0.0.tgz","fileCount":13,"integrity":"sha512-FmB8j+tvMLMjWBBaGjB4p8JkuY1maJfZ91UyEdEusuyQW3tMdlG6iRnrQwx9mdYFmrMeP0SSGG/2fgbBLWWrMg==","signatures":[{"sig":"MEYCIQCrZAa/O9YiPtZyVR2TguKsqsPFoTbOI6RQrilHVpd6+gIhAKru1IujzKh62tv7byHtM5T06UZzb8rSycEt/pukAWQv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36913},"main":"dist/index.js","type":"module","engines":{"node":">=20"},"gitHead":"4f3981a565ec12ae934a7950b10b72fbbcca533b","scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests","build":"tsc","start":"node dist/index.js","prepack":"npm run build"},"_npmUser":{"name":"aidalinfo","email":"dev@pulsemyit.fr"},"repository":{"url":"git+https://github.com/aidalinfo/cloud-mcp.git","type":"git","directory":"packages/scaleway-mcp"},"_npmVersion":"10.9.8","description":"Scaleway MCP server (read-only) — projects, instances, managed databases, DNS, transactional email, VPC/network, Kubernetes","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","@modelcontextprotocol/sdk":"^1.27.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.3.0","ts-jest":"^29.4.6","typescript":"^5.7.0","@types/node":"^22.0.0","@jest/globals":"^30.3.0"},"_npmOperationalInternal":{"tmp":"tmp/scaleway-mcp_1.0.0_1781094462527_0.5907530399186702","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@aidalinfo/scaleway-mcp","version":"1.0.1","keywords":["mcp","mcp-server","scaleway","scw","instances","rdb","dns","vpc","kubernetes","typescript"],"author":{"name":"Killian Stein / Aidalinfo"},"license":"MIT","_id":"@aidalinfo/scaleway-mcp@1.0.1","maintainers":[{"name":"aidalinfo","email":"dev@pulsemyit.fr"}],"homepage":"https://github.com/aidalinfo/cloud-mcp#readme","bugs":{"url":"https://github.com/aidalinfo/cloud-mcp/issues"},"bin":{"scaleway-mcp":"dist/index.js"},"dist":{"shasum":"8c9b06fe4240d31ae99a27b0cdedde440f628878","tarball":"https://registry.npmjs.org/@aidalinfo/scaleway-mcp/-/scaleway-mcp-1.0.1.tgz","fileCount":13,"integrity":"sha512-35EPh+rDMRf6EdHqwQ8ACBACKN2HVxZr3GLjRfb8lCWbbYfV26rppWmqSolVMpD+Fht+rqB7wJlmGSHdy9C0aQ==","signatures":[{"sig":"MEYCIQDJM7uTEOlzivoc5+GLwaRtjFiabiJrYLXPU5X3IWxTyQIhAK4vJEOvT68StEG5QQbpKGOl1hvkHbX5tQULpiwgxLFR","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":36962},"main":"dist/index.js","type":"module","engines":{"node":">=20"},"gitHead":"516f51b876e35950de7d916eb20abce92756700a","scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests","build":"tsc","start":"node dist/index.js","prepack":"npm run build"},"_npmUser":{"name":"aidalinfo","email":"dev@pulsemyit.fr"},"repository":{"url":"git+https://github.com/aidalinfo/cloud-mcp.git","type":"git","directory":"packages/scaleway-mcp"},"_npmVersion":"10.9.8","description":"Scaleway MCP server (read-only) — projects, instances, managed databases, DNS, transactional email, VPC/network, Kubernetes","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","@modelcontextprotocol/sdk":"^1.27.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.3.0","ts-jest":"^29.4.6","typescript":"^5.7.0","@types/node":"^22.0.0","@jest/globals":"^30.3.0"},"_npmOperationalInternal":{"tmp":"tmp/scaleway-mcp_1.0.1_1781123822936_0.3970348831447028","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@aidalinfo/scaleway-mcp@1.1.0","bin":{"scaleway-mcp":"dist/index.js"},"bugs":{"url":"https://github.com/aidalinfo/cloud-mcp/issues"},"dist":{"shasum":"191ee53a200496875111fdeea6571ec865cf8cff","tarball":"https://registry.npmjs.org/@aidalinfo/scaleway-mcp/-/scaleway-mcp-1.1.0.tgz","fileCount":22,"integrity":"sha512-PzbudhH+/Kl+mU3aLZfJJKzR2ejNKBL1BH7zRo8C7Fy0E/pgMfTpPVUm1pHfP6dw3THFfo8l8qRH9zxAzLXo7g==","signatures":[{"sig":"MEUCIE84cWvZ/2gl1hmFLNQj781mNDsZmFmjTZ3qLDSAuucgAiEAvUNA9YBHRcy71mgn2xntYVrwg97DNYvGWXaJ778bjFM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDMtAu6elFN8LipyW75axP9QmP+lWk4uv1ABHJqH8iBrwIhAPuoEKBicvwlUAv5ZbOq8tecv+xXw4bTp5fSet88A9MQ"}],"unpackedSize":185057},"main":"dist/index.js","name":"@aidalinfo/scaleway-mcp","type":"module","author":{"name":"Killian Stein / Aidalinfo"},"engines":{"node":">=20"},"gitHead":"7ec638a5bb16eaffe1c61e308e0cafd9ab8a9773","license":"MIT","scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests","build":"tsc","start":"node dist/index.js","prepack":"npm run build"},"version":"1.1.0","_npmUser":{"name":"aidalinfo","email":"dev@pulsemyit.fr"},"homepage":"https://github.com/aidalinfo/cloud-mcp#readme","keywords":["mcp","mcp-server","scaleway","scw","instances","rdb","dns","vpc","kubernetes","typescript"],"repository":{"url":"git+https://github.com/aidalinfo/cloud-mcp.git","type":"git","directory":"packages/scaleway-mcp"},"_npmVersion":"10.9.8","description":"Scaleway MCP server (read-only by default, optional restricted-write mode) — projects, instances, managed databases, DNS, transactional email, VPC/network, Kubernetes, scoped Object Storage access provisioning","directories":{},"maintainers":[{"name":"aidalinfo","email":"dev@pulsemyit.fr"}],"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","fast-xml-parser":"^5.11.2","@modelcontextprotocol/sdk":"^1.27.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.3.0","ts-jest":"^29.4.6","typescript":"^5.7.0","@types/node":"^22.0.0","@jest/globals":"^30.3.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/scaleway-mcp_1.1.0_1791040209885_0.5757795477914569"}}},"time":{"created":"2026-06-10T12:27:42.400Z","modified":"2026-10-03T15:10:10.358Z","1.0.0":"2026-06-10T12:27:42.678Z","1.0.1":"2026-06-10T20:37:03.121Z","1.1.0":"2026-10-03T15:10:09.980Z"},"bugs":{"url":"https://github.com/aidalinfo/cloud-mcp/issues"},"author":{"name":"Killian Stein / Aidalinfo"},"license":"MIT","homepage":"https://github.com/aidalinfo/cloud-mcp#readme","keywords":["mcp","mcp-server","scaleway","scw","instances","rdb","dns","vpc","kubernetes","typescript"],"repository":{"url":"git+https://github.com/aidalinfo/cloud-mcp.git","type":"git","directory":"packages/scaleway-mcp"},"description":"Scaleway MCP server (read-only by default, optional restricted-write mode) — projects, instances, managed databases, DNS, transactional email, VPC/network, Kubernetes, scoped Object Storage access provisioning","maintainers":[{"name":"aidalinfo","email":"dev@pulsemyit.fr"}],"readme":"# scaleway-mcp\n\nMCP server pour explorer l'infrastructure Scaleway : projets, instances (VMs),\nbases de données managées (RDB), DNS, email transactionnel (TEM), réseau (VPC / private\nnetworks / load balancers) et Kubernetes (Kapsule).\n\n**Par défaut le serveur est en lecture seule** : tous les appels sont des `GET` sur\n`https://api.scaleway.com` authentifiés via le header `X-Auth-Token` (clé secrète), et aucun\ntool ne crée, modifie ou supprime quoi que ce soit. Un mode `restricted-write` optionnel\najoute un petit lot d'outils d'écriture ciblés (voir [Mode restricted-write](#mode-restricted-write)).\n\n## Configuration\n\nVariables d'environnement (voir `.env`) :\n\n| Variable | Requis | Description |\n|---|---|---|\n| `SCW_SECRET_KEY` | oui | Clé secrète Scaleway (envoyée en `X-Auth-Token`) |\n| `SCW_ACCESS_KEY` | recommandé | Access key — sert à auto-découvrir l'organization ID |\n| `SCW_DEFAULT_ORGANIZATION_ID` | optionnel | Court-circuite la découverte auto |\n| `SCW_MODE` | optionnel | `readonly` (défaut) ou `restricted-write`. Toute autre valeur = `readonly` |\n| `SCW_ALLOWED_PROJECT_IDS` | en écriture | Projets où les mutations sont permises (séparés par des virgules) |\n| `SCW_ALLOWED_TOOL_GROUPS` | en écriture | Groupes d'outils d'écriture à enregistrer (voir plus bas) |\n| `SCW_OSA_HANDOFF_DIR` | groupe `object-storage-access` | Répertoire absolu (0700) où sont déposés les secrets des clés créées |\n| `SCW_OSA_ALLOWED_BUCKETS` | groupe `object-storage-access` | Buckets autorisés (obligatoire, sinon groupe désactivé) |\n| `SCW_OSA_ALLOWED_S3_PREFIXES` | groupe `object-storage-access` | Préfixes S3 autorisés ; un préfixe demandé doit être dessous (obligatoire) |\n| `SCW_OSA_APPLICATION_PREFIXES` | groupe `object-storage-access` | Préfixes de noms d'applications IAM autorisés (obligatoire) |\n| `SCW_OSA_POLICY_SINGLE_WRITER` | groupe `object-storage-access` | `acknowledged` obligatoire : l'opérateur atteste que ce MCP est le **seul** à écrire les politiques des buckets autorisés (sinon groupe désactivé) |\n| `SCW_OSA_MAX_KEY_DAYS` | optionnel | Durée de vie maximale d'une clé (1–365, défaut 365 ; valeur invalide = groupe désactivé) |\n\nSi l'org n'est pas fournie, elle est découverte : API key → user/application → organization_id.\n\n## Build & lancement\n\n```bash\nnpm install\nnpm run build\nSCW_SECRET_KEY=... SCW_ACCESS_KEY=... npm start\n```\n\nTest de fumée bout-en-bout en lecture seule (lit `.env`) : `node smoke.mjs`\n\nTests unitaires (aucun appel réseau, `fetch` simulé) : `npm test`\n\n## Enregistrement dans Claude Code\n\n```bash\nclaude mcp add scaleway --scope user \\\n  -e SCW_ACCESS_KEY=... -e SCW_SECRET_KEY=... \\\n  -- npx -y @aidalinfo/scaleway-mcp\n```\n\n## Tools\n\n| Tool | Description |\n|---|---|\n| `scw_organization` | Vue d'ensemble org (projets, users, apps, API keys) |\n| `scw_projects` | Liste des projets |\n| `scw_instances` | VMs sur toutes les zones (filtre zone/projet) |\n| `scw_instance_info` | Détail d'une VM |\n| `scw_flexible_ips` | IPs publiques flottantes et leur rattachement |\n| `scw_volumes` | Volumes de stockage Instance |\n| `scw_databases` | Bases managées RDB (PostgreSQL/MySQL) toutes régions |\n| `scw_database_info` | Détail d'une base + bases logiques + users |\n| `scw_dns_zones` | Zones DNS |\n| `scw_dns_records` | Enregistrements d'une zone |\n| `scw_domains` | Noms de domaine enregistrés (registrar) |\n| `scw_email_domains` | Domaines d'envoi TEM + statut/réputation |\n| `scw_email_domain_info` | Détail TEM (SPF/DKIM) |\n| `scw_vpcs` | VPCs toutes régions |\n| `scw_private_networks` | Réseaux privés toutes régions |\n| `scw_load_balancers` | Load balancers toutes zones |\n| `scw_k8s_clusters` | Clusters Kubernetes |\n| `scw_k8s_cluster_info` | Détail cluster + pools + nodes |\n| `scw_raw_get` | Échappatoire GET arbitraire (Redis, Registry, Secret Manager, etc.) |\n| `scw_regions` | Référence régions/zones |\n\nRégions : `fr-par`, `nl-ams`, `pl-waw`. Zones : `*-1`, `*-2`, `*-3`.\n\n## Mode restricted-write\n\n```bash\nSCW_MODE=restricted-write\nSCW_ALLOWED_PROJECT_IDS=<project-id>[,<project-id>...]\nSCW_ALLOWED_TOOL_GROUPS=dns,instance-actions,snapshots,flexible-ips,rdb-backups,object-storage-access\n```\n\nLe mode est **fail-closed** : valeur de `SCW_MODE` inconnue, `SCW_ALLOWED_PROJECT_IDS` vide ou\naucun groupe valide → le serveur démarre en lecture seule (avertissement sur stderr). Les\ngroupes inconnus sont ignorés. Seuls les groupes listés sont enregistrés.\n\n| Groupe | Tool | Action |\n|---|---|---|\n| `dns` | `scw_dns_record_create` | Ajoute un enregistrement (A, AAAA, CNAME, TXT, MX, SRV, CAA) dans une zone **existante** |\n| `dns` | `scw_dns_record_update` | Change valeur/TTL/priorité d'un enregistrement identifié par son id |\n| `dns` | `scw_dns_record_delete` | Supprime un enregistrement par id, si `expectedName` + `expectedType` correspondent |\n| `instance-actions` | `scw_instance_power` | `poweron` / `poweroff` |\n| `instance-actions` | `scw_instance_sensitive_action` | `reboot` / `stop_in_place` (instance `running` uniquement) |\n| `snapshots` | `scw_volume_snapshot_create` | Snapshot d'un volume Instance ou Block Storage (SBS) |\n| `snapshots` | `scw_instance_backup` | Image de sauvegarde d'une instance (action `backup`) |\n| `flexible-ips` | `scw_flexible_ip_attach` | Attache une IP flexible libre à une instance de la même zone |\n| `flexible-ips` | `scw_flexible_ip_detach` | Détache une IP de l'instance attendue (`serverId`) |\n| `rdb-backups` | `scw_rdb_backup_create` | Sauvegarde manuelle d'une base logique RDB (expiration 1–365 j, 30 par défaut) |\n| `object-storage-access` | `scw_object_storage_access_provision` | Application IAM dédiée + clé expirante + accès limité à des préfixes d'un bucket existant, prouvé par canari (voir plus bas) |\n| `object-storage-access` | `scw_object_storage_access_verify` | Rejoue le canari pour une clé existante |\n| `object-storage-access` | `scw_object_storage_access_key_revoke` | Révoque **une** clé par access key exacte, si `expectedApplicationId` correspond |\n| `object-storage-access` | `scw_object_storage_access_status` | (lecture) Clés d'une application gérée et préfixes accordés sur un bucket |\n\n### Garde-fous\n\n- Chaque outil mutatif est marqué `[WRITE]` dans sa description.\n- Il n'existe **aucun** outil d'écriture générique : le client refuse toute mutation dont\n  la méthode + le chemin ne figurent pas dans une liste fermée (`WRITE_ROUTES`), avec un\n  contrôle du corps (ex. action `terminate` impossible, zone DNS jamais créée).\n  `scw_raw_get` reste strictement en `GET`.\n- Région, zone, identifiants (UUID), nom de zone DNS et champs d'enregistrement sont validés\n  avant tout appel.\n- La ressource est lue **avant** la mutation : son projet doit figurer dans\n  `SCW_ALLOWED_PROJECT_IDS`, sinon refus sans appel d'écriture. Pour un attachement d'IP,\n  l'IP et l'instance doivent toutes deux être dans un projet autorisé.\n- La ressource exacte est **relue après** la mutation ; si l'état attendu n'apparaît pas,\n  l'outil renvoie une erreur `Verification FAILED`.\n- Idempotence : enregistrement identique déjà présent, instance déjà dans l'état cible,\n  snapshot/image/sauvegarde de même nom, IP déjà (dé)tachée → rien n'est envoyé.\n- Les erreurs Scaleway sont nettoyées : clé secrète, access key, header `X-Auth-Token` et\n  champs `secret_key` / `password` / `token` sont masqués.\n- Hors périmètre : suppression d'instances, volumes, bases ou clusters ; IAM en dehors du groupe\n  `object-storage-access` (qui ne crée qu'une politique Object Storage bornée à un projet) ;\n  mutations Kubernetes (la prod reste pilotée par GitOps) ; création de ressources coûteuses.\n\nLes confirmations humaines restent du ressort de l'agent consommateur ; le serveur reste sûr\nlorsqu'il est appelé directement.\n\n### Accès Object Storage dédiés (`object-storage-access`)\n\nProvisionne sans console un accès S3 dédié, par exemple pour une sauvegarde RustDesk limitée à\n`backup-global/backup/rustdesk/**` :\n\n```bash\nSCW_MODE=restricted-write\nSCW_ALLOWED_PROJECT_IDS=<projet du bucket>\nSCW_ALLOWED_TOOL_GROUPS=object-storage-access\nSCW_OSA_HANDOFF_DIR=/srv/scaleway-mcp/handoff        # mkdir -m 700\nSCW_OSA_ALLOWED_BUCKETS=backup-global                # obligatoire\nSCW_OSA_ALLOWED_S3_PREFIXES=backup/                  # obligatoire\nSCW_OSA_APPLICATION_PREFIXES=backup-                 # obligatoire\nSCW_OSA_POLICY_SINGLE_WRITER=acknowledged           # obligatoire (voir « Écriture de la politique »)\nSCW_OSA_MAX_KEY_DAYS=180                             # optionnel\n```\n\n```text\nscw_object_storage_access_provision\n  projectId=<projet> region=fr-par bucket=backup-global\n  prefixes=[\"backup/rustdesk/**\"] applicationName=backup-rustdesk keyDescription=initial expiresInDays=90\n```\n\nModèle d'accès Scaleway (bucket policy `2023-04-17`) : une application n'accède à Object Storage\nque si une **politique IAM** le lui permet sur le projet ; la **bucket policy** ne fait ensuite que\n**restreindre** cet accès dans le bucket. Les droits IAM couvrent tous les buckets du projet : un\nbucket sans politique leur est entièrement ouvert.\n\nDéroulé :\n\n1. **Contrôles avant toute mutation** :\n   - projet dans `SCW_ALLOWED_PROJECT_IDS` ; bucket, préfixes, nom d'application et durée dans les\n     allowlists `SCW_OSA_*` ;\n   - répertoire de handoff réel (pas de lien), à soi, en `0700` ;\n   - bucket **existant** dans le projet (jamais créé), avec une politique **déjà présente** (en créer\n     une couperait l'accès à tous les principaux non listés) ;\n   - **isolation du projet** : tous les autres buckets du projet, dans toutes les régions Object\n     Storage (`fr-par`, `nl-ams`, `pl-waw`, `it-mil`), doivent avoir une politique, sans principal\n     joker `*`. Sinon les droits IAM de l'application les atteindraient ;\n   - **déclarations gérées intactes** : celles qui portent un Sid `scaleway-mcp-osa-<application>-*`\n     (bucket cible et autres buckets du projet) doivent être exactement deux et identiques à ce que\n     le MCP écrit (principal, effet, actions, ressources, condition, préfixes valides). Toute\n     altération est refusée au lieu d'être reprise. Les préfixes déjà accordés doivent rester dans\n     `SCW_OSA_ALLOWED_S3_PREFIXES` ;\n   - **lectures strictes** : chaque page d'une liste IAM doit porter le tableau attendu et un\n     `total_count` entier stable, égal au nombre d'éléments reçus, sans troncature. **Chaque\n     élément** est validé avant tout filtre de sécurité : identifiants présents et bien typés, un\n     seul principal par politique, un seul porteur par clé, pas de doublon. Un élément incomplet\n     fait échouer l'appel ; il n'est jamais ignoré. Les réponses S3\n     sont validées par un **parseur XML** (`fast-xml-parser`) : document complet et bien formé,\n     racine unique fermée, rien hors racine. `ListBuckets` doit être non tronqué, avec un seul\n     conteneur `<Buckets>` et un nom valide par bucket. `ListObjectsV2` doit avoir un `<KeyCount>`\n     unique égal au nombre de `<Contents>`, et jamais « vide mais tronqué » ;\n   - **une application par bucket** : une application qui a déjà des déclarations gérées sur un autre\n     bucket est refusée ;\n   - **aucun droit parallèle** : ni la politique du bucket cible ni celle d'un autre bucket du projet\n     ne doivent contenir de déclaration `Allow` non gérée par le MCP qui vise l'application (son\n     principal ou un joker). Ces droits échapperaient au canari ;\n   - permission sets attendus connus d'IAM ;\n   - application existante gérée seulement si elle porte le tag `scaleway-mcp:object-storage-access`,\n     n'est membre d'**aucun groupe IAM** (pas de droits hérités), n'est pas celle du MCP et ne\n     possède au plus que **sa** politique gérée. Cette politique doit contenir exactement une règle :\n     les permission sets ci-dessous, sur ce seul projet, avec `permission_sets_scope_type=projects`,\n     sans `organization_id`, `account_root_user_id` ni condition. Le schéma de règle est strict :\n     un champ inconnu est refusé. Les politiques sont lues avec le filtre\n     `application_ids` et revérifiées côté client.\n2. **Application IAM** retrouvée par nom exact, ou créée avec le tag. Un identifiant renvoyé par un\n   `POST` (application ou politique) n'est rendu supprimable par le rollback qu'après relecture et\n   **preuve de causalité** : marqueur `[op:…]` de l'appel, nom, tag, organisation, et application\n   pour la politique. Sinon, il est signalé et jamais supprimé.\n3. **Politique IAM gérée** `scaleway-mcp-osa-<application>` : une seule règle,\n   `ObjectStorageBucketsRead`, `ObjectStorageObjectsRead`, `ObjectStorageObjectsWrite` et\n   `ObjectStorageObjectsDelete`, sur le **seul** projet autorisé. Jamais d'écriture ou de suppression\n   de bucket, jamais de droit sur les politiques de bucket. Le client refuse tout autre contenu.\n4. **Clé applicative expirante** (projet par défaut = projet du bucket), idempotente par\n   `keyDescription`. Les clés d'une application sont listées avec `bearer_id` +\n   `bearer_type=application`. Une clé réutilisée doit être la **seule** clé valide portant cette\n   description, être une clé de **cette** application (pas d'un utilisateur), sur le projet demandé,\n   et expirer dans la limite de `expiresInDays` et de `SCW_OSA_MAX_KEY_DAYS` (sinon refus : faire une\n   rotation). Son fichier de handoff doit décrire exactement la même clé,\n   la même application et le même projet. L'expiration renvoyée pour une clé créée\n   est vérifiée. Le secret est écrit **uniquement** dans\n   `$SCW_OSA_HANDOFF_DIR/<application>.<access key>.env` : création exclusive en `0600`, sans suivre\n   de lien, jamais écrasé. Variables `SCW_*`, `AWS_*` et `S3_*`. `S3_PREFIXES` y contient le **périmètre effectif**, c'est-à-dire l'union des\n   préfixes déjà accordés et demandés. Une clé n'est réutilisée que si son fichier décrit exactement\n   le bucket, la région et ce périmètre ; sinon il faut faire une rotation. À la réutilisation, le fichier n'est\n   lu que s'il est régulier, à soi et en `0600` exactement : contrôle par `lstat`, ouverture\n   `O_NOFOLLOW`, puis `fstat` sur le même inode. Le secret n'apparaît jamais dans la réponse, les\n   erreurs ou les logs.\n5. **Politique de bucket fusionnée** : deux déclarations `scaleway-mcp-osa-<app>-*` sont ajoutées ou\n   mises à jour : objets `Get/Put/Delete/multipart` sur `bucket/prefixe/*`, et `ListBucket`\n   conditionné à `s3:prefix`. Les autres déclarations restent intactes et dans l'ordre, et les\n   préfixes déjà accordés sont conservés. La politique est **relue juste avant l'écriture** : si elle\n   a changé depuis la lecture initiale, refus. Après l'écriture, elle est relue et comparée\n   **structurellement en entier** : déclarations, principaux, actions, ressources, conditions.\n6. **Canari avec la clé de l'application** :\n   - `list` / `put` / `head` / `delete` d'un objet `.mcp-canary-*` dans **chaque préfixe accordé**,\n     y compris ceux accordés lors d'appels précédents ;\n   - **refus (403)** hors préfixe, à la racine du bucket : `list`, `head`, `put` et `delete` ;\n   - **refus (403)** des quatre mêmes opérations sur **chacun des autres buckets** du projet.\n\n   Après le canari, **toutes les régions sont réinventoriées** : l'ensemble des buckets du projet\n   doit être identique à celui inspecté, sinon échec et rollback.\n\n   Un objet canari reste suivi tant que son **absence n'est pas prouvée par une relecture** :\n   liste de cette clé avec `KeyCount=0`, ou `HEAD` → 404. Une réponse `DELETE` 200/204 n'est pas\n   une preuve.\n\n   Tout objet canari éventuellement créé, y compris un `put` hors préfixe accepté à tort, est\n   supprimé dans un `finally`. Le client S3 n'a aucune opération de suppression de bucket, de\n   politique ou d'objet métier.\n7. **Rollback** sur toute erreur ou exception après la première mutation, y compris réseau. Il\n   annule exactement ce que l'appel a créé : clé, politique IAM, application (suppression possible\n   uniquement pour des ids créés par ce processus), fichier de handoff. Si le résultat d'une création\n   IAM est ambigu (erreur de transport ou réponse sans identifiant), la ressource est recherchée avec\n   des relectures bornées. Une erreur de relecture, ou une ressource encore invisible après la\n   dernière tentative, n'est **jamais** traitée comme une absence : l'état est déclaré indéterminé.\n   Une application ou une politique n'est adoptée par le rollback que si elle porte le **marqueur\n   unique de l'appel** (`[op:…]` dans sa description). Une clé ne peut porter de marqueur et l'API n'a\n   pas de jeton d'idempotence : une clé ambiguë n'est **jamais** révoquée automatiquement, les\n   nouvelles clés de l'application sont listées pour une révision explicite. Tout ce qui n'est pas\n   adopté est **signalé sans être supprimé**. Chaque `DELETE` est suivi d'une **relecture**, même\n   après une erreur de transport (404 attendu). L'absence du fichier de handoff est vérifiée (un\n   fichier partiellement écrit est supprimé). Un objet canari restant, non supprimé avec la clé de\n   l'application ni avec l'identité de gestion (clés canari uniquement), rend aussi le rollback\n   incomplet. Le résultat indique « rolled back and\n   verified » ou « ROLLBACK INCOMPLETE ». La révocation est vérifiée par lecture directe et par la\n   liste des clés de l'application. La politique de bucket n'est\n   restaurée que si elle est toujours **exactement** celle écrite par l'appel ; sinon le rollback la\n   laisse et le signale pour une vérification manuelle. Elle est relue après restauration.\n\n**Rotation** : relancer `scw_object_storage_access_provision` avec une nouvelle `keyDescription`\n(ex. `rotation-2026-10`). Une nouvelle clé est créée et prouvée, et les anciennes sont listées. Une\nfois les consommateurs basculés, lancer\n`scw_object_storage_access_key_revoke accessKey=SCW… expectedApplicationId=<id>`. C'est une\nopération distincte : clé exacte, application attendue et gérée, projet autorisé, jamais la clé du\nMCP. Elle est idempotente et relue après.\n\n`scw_object_storage_access_verify` refait les mêmes contrôles (application gérée, bucket dans le\nprojet autorisé de la clé, isolation du projet, déclarations gérées intactes) **avant** toute\nécriture canari. Il exige que `prefixes` soit **exactement** l'ensemble accordé par la politique et\nnettoie lui aussi avec l'identité de gestion. Un résidu fait échouer la vérification. Il prend le\nmême verrou que le provisionnement. Après le canari, il exige que la politique relue soit identique\nà celle inspectée et que l'inventaire des buckets n'ait pas changé.\n\nLimites :\n\n- le bucket est résolu via le **projet par défaut de la clé du MCP** ; `SCW_ACCESS_KEY` est\n  obligatoire et signe aussi les requêtes S3 ;\n- le MCP doit pouvoir lister les buckets et lire les politiques de tous les buckets du projet,\n  ainsi qu'écrire celle du bucket cible. Pour nettoyer et prouver l'absence des objets canari, il\n  doit aussi avoir `s3:ListBucket`, `s3:GetObject` et `s3:DeleteObject` dans les politiques de\n  ces buckets (le client ne supprime que des clés `.mcp-canary-*`) ;\n- un bucket créé **plus tard** dans le projet sans politique serait atteint par les droits IAM de\n  l'application : toujours poser une politique sur les nouveaux buckets, et relancer `_verify`, qui\n  le détecte ;\n- **Écriture de la politique** : S3 n'offre aucune écriture conditionnelle (ni ETag, ni version) pour\n  `PutBucketPolicy`. Le groupe exige donc `SCW_OSA_POLICY_SINGLE_WRITER=acknowledged`, par lequel\n  l'opérateur atteste que ce MCP est le seul à écrire les politiques des buckets autorisés. Un\n  **verrou local** (`$SCW_OSA_HANDOFF_DIR/.lock-<région>-<bucket>`), pris par `provision` comme par\n  `verify`, sérialise ses propres appels. C'est un **répertoire jamais vide tant qu'il est tenu** : il\n  contient `owner-<jeton>`. La prise se fait par `rename` atomique d'un répertoire de réclamation\n  privé ; elle échoue si le verrou est tenu. La libération supprime d'abord son propre fichier\n  `owner-<jeton>`, un nom que personne d'autre n'utilise, puis fait un `rmdir`. Comme `rmdir` ne\n  supprime qu'un répertoire vide, il est impossible de retirer le verrou d'un successeur ; il n'y a\n  pas de fenêtre « vérifier puis supprimer ». Un verrou perdu ou impossible à libérer fait passer le\n  résultat en erreur. Un verrou restant n'est jamais cassé automatiquement. Toutes les instances du MCP qui écrivent ces politiques doivent\n  partager le même `SCW_OSA_HANDOFF_DIR`, sur un système de fichiers où `O_EXCL` est atomique. La relecture juste avant l'écriture\n  et la restauration conditionnelle restent en place pour détecter un écrivain non déclaré, sans\n  pouvoir l'exclure.\n\n### Application IAM dédiée\n\nCréer une **application IAM** propre au MCP (pas une clé utilisateur) et lui attacher une\npolitique minimale : lecture sur l'organisation, écriture uniquement sur les projets de\nl'allowlist et uniquement pour les produits des groupes activés. Exemple (noms de\npermission sets à vérifier dans la console) :\n\n| Règle | Portée | Permission sets |\n|---|---|---|\n| Lecture | Organisation | `ProjectReadOnly`, `IAMReadOnly`, `InstancesReadOnly`, `RelationalDatabasesReadOnly`, `DomainsDNSReadOnly`, `VPCReadOnly`, `LoadBalancersReadOnly`, `KubernetesReadOnly`, `TransactionalEmailReadOnly`, `BlockStorageReadOnly` |\n| Écriture (`dns`) | Projets autorisés | `DomainsDNSFullAccess` |\n| Écriture (`instance-actions`, `snapshots`, `flexible-ips`) | Projets autorisés | `InstancesFullAccess`, `BlockStorageFullAccess` |\n| Écriture (`rdb-backups`) | Projets autorisés | `RelationalDatabasesFullAccess` |\n| Écriture (`object-storage-access`) | Organisation | `IAMManager` (applications, politiques, clés) |\n| Écriture (`object-storage-access`) | Projets autorisés | `ObjectStorageBucketsRead`, `ObjectStorageBucketPolicyFullAccess` — plus une déclaration pour l'application du MCP dans la politique de chaque bucket du projet (`s3:ListBucket`, lecture de politique ; écriture de politique sur les buckets cibles) |\n\nLes permission sets Scaleway sont plus larges que les outils exposés (ex. `InstancesFullAccess`\npermet la suppression) : c'est le MCP qui restreint aux seules actions listées. N'accordez que\nles règles des groupes réellement activés.\n\n### Canari d'écriture\n\nSur un projet **non critique** avec le groupe `dns` activé :\n\n```bash\nnode canary.mjs canary.example.com   # ajoute un TXT _mcp-canary, vérifie l'idempotence, le supprime\n```\n","readmeFilename":"README.md"}