Skip to main content
Glama
README.md
# cloud-mcp

A Node.js skeleton for wrapping multiple cloud CLIs behind one command surface.

## What this gives you

- Unified CLI entrypoint (`cloud-wrap`)
- Provider pass-through commands for AWS, GCP, Azure, OCI, Alibaba, DigitalOcean, IBM Cloud, Tencent Cloud, and Huawei Cloud
- Config file support to override command paths and inject environment variables
- Vault abstraction for storing provider attributes with optional external replacement
- MCP stdio server that registers provider tools and command runners
- Structured logging and safe command execution with inherited stdio

## Quick start

```bash
npm install
npm run bootstrap:clis
npm start -- list
npm start -- aws sts get-caller-identity
npm start -- oci iam region list
npm start -- alibaba ecs DescribeInstances
npm run mcp
```

## Repository-local CLI layout

This project can keep provider CLI entrypoints under `mcp/<provider>/bin`.

- `mcp/aws/bin/aws`
- `mcp/gcp/bin/gcloud`
- `mcp/azure/bin/az`
- `mcp/oci/bin/oci`
- `mcp/alibaba/bin/aliyun`
- `mcp/digitalocean/bin/doctl`
- `mcp/ibmcloud/bin/ibmcloud`
- `mcp/tencent/bin/tccli`
- `mcp/huawei/bin/hcloud`

Run the bootstrap command to create links from your installed CLIs into this structure:

```bash
npm run bootstrap:clis
```

Or pull and install all CLIs directly into the structure:

```bash
npm run install:clis
```

This installer covers AWS, GCP, Azure, OCI, Alibaba, DigitalOcean, IBM Cloud, and Tencent Cloud directly. Huawei Cloud is wired through the same provider interface, but the public `huaweicloudcli` Python package referenced by older installer versions is not available; set `HUAWEI_CLI_BIN` to a supported `hcloud`-compatible binary to enable Huawei command execution.

At runtime, provider resolution order is:

1. `mcp/<provider>/bin/<cli>` when present
2. `<PROVIDER>_CLI_BIN` environment override
3. CLI from `PATH`

If neither `HUAWEI_CLI_BIN` nor `hcloud` on `PATH` is available during `npm run install:clis`, a placeholder `mcp/huawei/bin/hcloud` is created that fails with an explicit setup message instead of breaking the image build.

Shared command limits live in [mcp/cloud-command-limits.json](/Users/lesterjohn/Documents/GitHub/cloud-mcp/mcp/cloud-command-limits.json).

Current repository default (`mcp/cloud-command-limits.json`) is permissive for all providers and intentionally includes CLI-style aliases for two sections:

```json
{
  "alibaba.*": [],
  "aws.*": [],
  "az.*": [],
  "digitalocean.*": [],
  "gcloud.*": [],
  "huawei.*": [],
  "ibmcloud.*": [],
  "oci.*": [],
  "tencent.*": []
}
```

At load time this is normalized to canonical provider sections, so `az.*` becomes `azure.*` and `gcloud.*` becomes `gcp.*` in the effective runtime policy.

External command-limit loading:

- `CLOUD_COMMAND_LIMITS_SOURCE` (optional): load command limits from an external source at startup.
  - Supported values: file path, `file://` URL, `http://` URL, `https://` URL.
- `CLOUD_COMMAND_LIMITS_REFRESH_INTERVAL_SECONDS` (optional): when `CLOUD_COMMAND_LIMITS_SOURCE` is set and this value is `> 0`, command limits are reloaded on that interval.
- If refresh fails, the last successfully loaded limits remain active.

PostgreSQL-backed command limits:

- Command limits are persisted in PostgreSQL table `cloud_mcp.command_limits`.
- Runtime command validation reads limits from the database before each provider command execution.
- On startup, limits are loaded from `mcp/cloud-command-limits.json` (or `CLOUD_COMMAND_LIMITS_SOURCE`) and synced into PostgreSQL.
- When refresh is enabled, each refresh cycle updates PostgreSQL records from the external source.

Database environment variables:

- `COMMAND_LIMITS_DATABASE_URL` (preferred), or
- `DATABASE_URL`
- `COMMAND_LIMITS_LOCAL_POSTGRES_ENABLED` (optional): when `true|1|yes` and no external DB URL is set, auto-uses local postgres URL.
- `COMMAND_LIMITS_LOCAL_POSTGRES_PORT` (required when local postgres auto-mode is enabled): local postgres port used to build the DB URL.

If neither database variable is set, command limits run in in-memory mode.

Database resolution order:

1. Use `COMMAND_LIMITS_DATABASE_URL` when set.
2. Else use `DATABASE_URL` when set.
3. Else if `COMMAND_LIMITS_LOCAL_POSTGRES_ENABLED=true|1|yes`, require `COMMAND_LIMITS_LOCAL_POSTGRES_PORT` and use `postgres://cloud_mcp:cloud_mcp@127.0.0.1:<port>/cloud_mcp`.
4. Else run in-memory mode.

Start local PostgreSQL from repository assets:

```bash
export COMMAND_LIMITS_LOCAL_POSTGRES_PORT=5432
docker compose -f docker-compose.postgres.yml up -d
export COMMAND_LIMITS_DATABASE_URL="postgres://cloud_mcp:cloud_mcp@127.0.0.1:5432/cloud_mcp"
```

Standalone migration for existing databases:

```bash
psql "$COMMAND_LIMITS_DATABASE_URL" -f db/migrations/002_command_limits_namespace_migration.sql
```

This migration creates `cloud_mcp.command_limits`, copies legacy rows from `public.command_limits` when present, and ensures default provider-prefix records exist.

- Enforced sections are keyed by provider prefix: `aws.*`, `gcp.*`, `azure.*`, `oci.*`, `alibaba.*`, `digitalocean.*`, `ibmcloud.*`, `tencent.*`, `huawei.*`
- If a section is an empty array, all commands for that provider are allowed
- If a section contains entries, only matching prefixes are allowed
- Entries may be written as full prefixes like `aws.s3` or shorthand like `s3` within the `aws.*` section

Prefix naming note:

- Runtime enforcement uses provider names (`aws`, `gcp`, `azure`, `oci`, `alibaba`, `digitalocean`, `ibmcloud`, `tencent`, `huawei`), not binary names.
- CLI-style aliases are supported and normalized during load:
  - `gcloud.*` maps to `gcp.*`
  - `az.*` maps to `azure.*`
  - `aliyun.*` maps to `alibaba.*`
  - `doctl.*` maps to `digitalocean.*`
  - `tccli.*` maps to `tencent.*`
  - `hcloud.*` maps to `huawei.*`
- If both canonical and alias keys are provided for the same provider, canonical keys win (`gcp.*` over `gcloud.*`, `azure.*` over `az.*`, `alibaba.*` over `aliyun.*`, `digitalocean.*` over `doctl.*`, `tencent.*` over `tccli.*`, `huawei.*` over `hcloud.*`).
- Recommended mapping is:
  - `aws.*` for `aws`
  - `gcp.*` for `gcloud`
  - `azure.*` for `az`
  - `oci.*` for `oci`
  - `alibaba.*` for `aliyun`
  - `digitalocean.*` for `doctl`
  - `ibmcloud.*` for `ibmcloud`
  - `tencent.*` for `tccli`
  - `huawei.*` for `hcloud`

How to fill out the file:

1. Allow everything for every cloud:

```json
{
  "alibaba.*": [],
  "aws.*": [],
  "digitalocean.*": [],
  "gcp.*": [],
  "azure.*": [],
  "oci.*": [],
  "ibmcloud.*": [],
  "tencent.*": [],
  "huawei.*": []
}
```

2. Restrict AWS and GCP, leave Azure and OCI open:

```json
{
  "alibaba.*": ["ecs"],
  "aws.*": ["s3", "sts.get-caller-identity"],
  "gcp.*": ["projects", "compute.instances.list"],
  "azure.*": [],
  "oci.*": [],
  "digitalocean.*": ["compute"],
  "ibmcloud.*": [],
  "tencent.*": ["cvm"],
  "huawei.*": ["ecs"]
}
```

3. Use full provider-prefixed entries explicitly:

```json
{
  "aws.*": ["aws.s3", "aws.sts.get-caller-identity"],
  "gcp.*": ["projects", "compute.instances.list"],
  "azure.*": [],
  "oci.*": ["oci.iam"]
}
```

4. Lock each provider to a narrow subset:

```json
{
  "aws.*": ["ec2.describe-instances", "s3.ls"],
  "gcp.*": ["projects.list"],
  "azure.*": ["vm", "account.show"],
  "oci.*": ["iam.region.list"]
}
```

What the entries mean:

- `"s3"` inside `aws.*` means any AWS command starting with `aws.s3...`
- `"sts.get-caller-identity"` inside `aws.*` means only `aws sts get-caller-identity`
- `"projects"` inside `gcp.*` means any GCP command starting with `gcp.projects...`
- `"oci.iam"` inside `oci.*` means any OCI command starting with `oci.iam...`

With this file:

- `aws s3 ls` is allowed
- `aws ec2 describe-instances` is denied
- all Azure commands are allowed
- `gcloud projects list` is allowed
- `oci iam region list` is allowed

## Usage

### Generic form

```bash
npm start -- run <provider> [args...]
```

Examples:

```bash
npm start -- run aws s3 ls
npm start -- run gcp projects list
npm start -- run azure account show
npm start -- run oci iam region list
npm start -- run digitalocean compute droplet list
```

### Provider shorthands

```bash
npm start -- aws s3 ls
npm start -- gcp projects list
npm start -- azure account show
npm start -- oci iam region list
npm start -- tencent cvm DescribeInstances
```

## MCP server

Start MCP with both stdio and HTTP transports (default):

```bash
npm run mcp -- --config cloud-wrap.config.json
```

Run HTTP-only or stdio-only:

```bash
npm run mcp:http -- --config cloud-wrap.config.json
npm run mcp -- --transport stdio --config cloud-wrap.config.json
```

HTTP defaults:

- `MCP_HTTP_HOST=127.0.0.1`
- `MCP_HTTP_PORT=3000`
- `MCP_HTTP_PATH=/mcp`
- `MCP_HTTP_HEALTH_PATH=/healthz`

Transport mode:

- `MCP_TRANSPORT_MODE=stdio|http|both` (default: `both`)

HTTP authentication framework:

- `MCP_HTTP_AUTH_MODE=none|token|oauth2|both` (default: `none`)
- Bearer token mode (`token` or `both`):
  - `MCP_HTTP_TOKEN_SOURCE=env|vault` (default: `env`)
  - `MCP_HTTP_AUTH_TOKENS` (comma-separated bearer tokens)
  - Vault source settings (when `MCP_HTTP_TOKEN_SOURCE=vault`):
    - `MCP_HTTP_VAULT_TOKEN_INDEX_PATH` (default: `cloud-mcp/http/auth/token-index`)
    - `MCP_HTTP_VAULT_TOKEN_DEFAULT_USER_ID` (default: `default`)
    - `MCP_HTTP_VAULT_TOKEN_REQUIRED_SCOPES` (comma-separated, optional)
    - `MCP_HTTP_VAULT_TOKEN_REQUIRED_AUDIENCE` (comma-separated, optional)
    - `MCP_HTTP_VAULT_TOKEN_CACHE_TTL_MS` (optional)
- OAuth2 introspection mode (`oauth2` or `both`):
  - `MCP_HTTP_OAUTH2_INTROSPECTION_URL`
  - `MCP_HTTP_OAUTH2_CLIENT_ID` (optional)
  - `MCP_HTTP_OAUTH2_CLIENT_SECRET` (optional)
  - `MCP_HTTP_OAUTH2_REQUIRED_SCOPES` (comma-separated, optional)
  - `MCP_HTTP_OAUTH2_REQUIRED_AUDIENCE` (comma-separated, optional)
  - `MCP_HTTP_OAUTH2_TIMEOUT_MS` (optional)
  - `MCP_HTTP_OAUTH2_CACHE_TTL_MS` (optional)

HTTP hardening controls:

- `MCP_HTTP_TRUST_PROXY` (optional: `true|false`)
- `MCP_HTTP_ALLOWED_ORIGINS` (comma-separated allowlist)
- `MCP_HTTP_ALLOWED_IPS` (comma-separated allowlist)
- `MCP_HTTP_RATE_LIMIT_WINDOW_MS` (default: `60000`)
- `MCP_HTTP_RATE_LIMIT_MAX_REQUESTS` (default: `60`)
- `MCP_HTTP_MAX_BODY_BYTES` (default: `1048576`)

When allowlists are set:

- requests outside allowed IPs are rejected with `403`
- requests with disallowed origin/host are rejected with `403`
- per-IP rate limit violations return `429` with `Retry-After`

Example: bearer tokens only

```bash
export MCP_HTTP_AUTH_MODE=token
export MCP_HTTP_AUTH_TOKENS="dev-token-1,dev-token-2"
npm run mcp -- --config cloud-wrap.config.json
```

Example: Vault-backed bearer token index

```bash
export MCP_HTTP_AUTH_MODE=token
export MCP_HTTP_TOKEN_SOURCE=vault
export MCP_HTTP_VAULT_TOKEN_INDEX_PATH="cloud-mcp/http/auth/token-index"
npm run mcp -- --config cloud-wrap.config.json
```

Vault token index shape (tokens stored by SHA-256 hash, not plaintext):

```json
{
  "tokens": {
    "<sha256(token)>": {
      "userId": "default",
      "tokenId": "tok-123",
      "active": true,
      "scopes": ["mcp:invoke"],
      "audience": ["cloud-mcp"],
      "expiresAt": "2026-12-31T23:59:59Z"
    }
  }
}
```

Example: OAuth2 introspection only

```bash
export MCP_HTTP_AUTH_MODE=oauth2
export MCP_HTTP_OAUTH2_INTROSPECTION_URL="https://auth.example.com/oauth2/introspect"
export MCP_HTTP_OAUTH2_CLIENT_ID="cloud-mcp"
export MCP_HTTP_OAUTH2_CLIENT_SECRET="replace-me"
export MCP_HTTP_OAUTH2_REQUIRED_SCOPES="mcp:invoke"
npm run mcp -- --config cloud-wrap.config.json
```

### Register This MCP In Codex, VS Code, and Claude

Use stdio transport for client registration. Keep this repository path and config path as absolute paths.

1. Start from a known absolute repo path:

```bash
cd /Users/lesterjohn/Documents/GitHub/cloud-mcp
pwd
```

2. Confirm local launch command works before registering:

```bash
npm run mcp -- --transport stdio --config /Users/lesterjohn/Documents/GitHub/cloud-mcp/cloud-wrap.config.json
```

3. Register in Codex.
On macOS, edit `~/.codex/config.toml` and add:

```toml
[mcp_servers.cloud-mcp]
command = "npm"
args = [
  "run",
  "mcp",
  "--",
  "--transport",
  "stdio",
  "--config",
  "/Users/lesterjohn/Documents/GitHub/cloud-mcp/cloud-wrap.config.json"
]
cwd = "/Users/lesterjohn/Documents/GitHub/cloud-mcp"
```

4. Register in VS Code.
Create or update workspace file `.vscode/mcp.json` with:

```json
{
  "servers": {
    "cloud-mcp": {
      "command": "npm",
      "args": [
        "run",
        "mcp",
        "--",
        "--transport",
        "stdio",
        "--config",
        "/Users/lesterjohn/Documents/GitHub/cloud-mcp/cloud-wrap.config.json"
      ],
      "cwd": "/Users/lesterjohn/Documents/GitHub/cloud-mcp"
    }
  }
}
```

Then reload VS Code window and verify `cloud-mcp` appears in MCP server list.

5. Register in Claude Desktop.
On macOS, edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add:

```json
{
  "mcpServers": {
    "cloud-mcp": {
      "command": "npm",
      "args": [
        "run",
        "mcp",
        "--",
        "--transport",
        "stdio",
        "--config",
        "/Users/lesterjohn/Documents/GitHub/cloud-mcp/cloud-wrap.config.json"
      ],
      "cwd": "/Users/lesterjohn/Documents/GitHub/cloud-mcp"
    }
  }
}
```

6. Restart the client app after config changes.

7. Validate tool registration from the client by calling `discover_tools` first, then `list_providers` or `run_provider` based on the returned recommendation.

Notes:

- If your client already has config content, merge only the `cloud-mcp` server entry.
- If your environment needs auth, set env vars before launching the client (`MCP_HTTP_*`, `MCP_PROVIDER_AUTH_KEY`, Vault vars).
- For remote HTTP integration instead of stdio, run `npm run mcp:http` and register endpoint `http://127.0.0.1:3000/mcp` with the client that supports streamable HTTP MCP.

## MCP Tool Reference

The MCP server exposes the following tools. Read-only tools are safe to inspect state; mutating tools change vault, database, or token-index data and should be used carefully.

- `discover_tools` is read-only. Use it first when you need schema discovery, want a recommendation for which MCP tool fits a task, or need the input schema for a specific tool before calling it.
- `list_providers` is read-only. Use it to discover which provider names are currently available before calling `get_provider` or `run_provider`.
- `get_provider` is read-only. It returns the stored provider configuration or `null` if missing. If `MCP_PROVIDER_AUTH_KEY` is set, `authorizationKey` is required.
- `set_provider` mutates vault state and is high-risk because it changes what future CLI calls execute. Use it to register or replace a provider config. The required payload is `config.command`; `env` defaults to `{}`; `profiles.*.users` is optional and an empty list means any user may use the profile.
- `run_provider` is high-risk because it spawns the configured provider CLI and can reach external cloud APIs. `args` must be literal argv segments, not shell text. Use this when the provider name is dynamic.
- `run_<provider>` is the provider-specific version of `run_provider`. Use it when the provider is fixed and you want a narrower tool surface.
- `get_command_limits` is read-only. It returns the normalized command-limit policy currently loaded from the database.
- `set_command_limit_section` mutates the database and then force-pushes the current policy to the selected JSON target. Use supported provider aliases like `gcloud`, `az`, `aliyun`, `doctl`, `tccli`, or `hcloud`; the runtime normalizes them to canonical sections.
- `replace_command_limits` replaces the entire command-limit policy in the database and then force-pushes the JSON target. The payload must include the canonical sections `aws.*`, `gcp.*`, `azure.*`, `oci.*`, `alibaba.*`, `digitalocean.*`, `ibmcloud.*`, `tencent.*`, and `huawei.*`.
- `push_command_limits` does not change the database. It writes the current database-backed policy to the internal file or external source. `pushTarget=auto` prefers the external source when configured, otherwise the internal file.
- `vault_seed_http_token` generates a new bearer token, stores only its SHA-256 hash in the Vault token index, and returns the plaintext token once. Keep the returned token, because it cannot be recovered later.
- `vault_seed_oauth_token` stores a provided OAuth access token as a SHA-256 hash in the Vault token index and never returns the plaintext token.

Common prerequisites and constraints:

- `discover_tools` accepts optional `query`, `tool`, and `limit`. Use `tool` for exact schema lookup and `query` when you want ranked suggestions.
- Set `MCP_PROVIDER_AUTH_KEY` when you want provider vault and token-index admin tools to require `authorizationKey`.
- `scopes` and `audience` accept either a comma-separated string or an array of strings.
- `expiresAt` should be an ISO-8601 timestamp.
- `path` overrides the Vault token index path when you need a non-default location.
- Provider CLI resolution still follows the runtime order documented above: repository-local bin, `<PROVIDER>_CLI_BIN`, then `PATH`.

Example response shapes:

- `discover_tools` returns `totalTools`, `returnedTools`, and `tools[]`, where each tool entry includes `name`, `description`, `risk`, `recommendation`, and JSON `inputSchema`.
- `vault_seed_http_token` returns `token`, `tokenHash`, `tokenId`, `indexPath`, and related entry fields.
- `vault_seed_oauth_token` returns `tokenHash`, `tokenId`, `indexPath`, and related entry fields.

## Container note

This repository no longer ships a first-party `Dockerfile`, so it does not provide a built-in container image build path.

If your deployment requires containers, provide your own image definition around the Node entrypoints (`node src/mcp.js` for MCP mode, `node src/index.js` for CLI mode) and any cloud CLIs you want available in that runtime.

A sample container definition is available at `docker/Containerfile.sample` and can be used with either Docker or Podman:

```bash
docker build -f docker/Containerfile.sample -t cloud-mcp:local .
podman build -f docker/Containerfile.sample -t cloud-mcp:local .
```

## Kubernetes (Helm) sample

A sample Helm chart is available at `helm/cloud-mcp`.

1. Build and push an image (Docker or Podman):

```bash
docker build -f docker/Containerfile.sample -t ghcr.io/your-org/cloud-mcp:latest .
docker push ghcr.io/your-org/cloud-mcp:latest
```

2. Copy chart values and edit for your environment:

```bash
cp helm/cloud-mcp/values.yaml helm/cloud-mcp/values.local.yaml
```

Set at minimum:

- `image.repository`
- `image.tag`
- `env.VAULT_ADDR`
- `secrets.data.VAULT_TOKEN`
- `env.COMMAND_LIMITS_DATABASE_URL` (or local-postgres toggle values)

3. Install or upgrade:

```bash
helm upgrade --install cloud-mcp ./helm/cloud-mcp -f helm/cloud-mcp/values.local.yaml
```

4. Verify:

```bash
kubectl rollout status deployment/cloud-mcp-cloud-mcp
kubectl logs deployment/cloud-mcp-cloud-mcp --tail=200
```

Notes:

- The chart mounts `cloud-wrap.config.json` from a ConfigMap at `/etc/cloud-mcp/cloud-wrap.config.json`.
- `VAULT_TOKEN` and `MCP_PROVIDER_AUTH_KEY` are provided by Kubernetes Secret (`secrets.data`).
- Default container args run MCP mode (`mcp --config /etc/cloud-mcp/cloud-wrap.config.json`).

## Configuration

Create `cloud-wrap.config.json` using `cloud-wrap.config.example.json` as a template.

```json
{
  "vault": {
    "module": "./external-vault.js",
    "options": {}
  },
  "providers": {
    "aws": {
      "command": "aws",
      "env": {
        "AWS_PROFILE": "default"
      },
      "defaultProfile": "default",
      "profileSupport": {
        "mode": "env",
        "envVar": "AWS_PROFILE"
      },
      "profiles": {
        "default": {
          "env": {
            "AWS_PROFILE": "default"
          },
          "users": []
        }
      }
    }
  }
}
```

If `vault.module` is present, the runtime will try to load that module first. The module should expose either `createVault`, a default factory, or a vault object with the same `get`/`set`/`snapshot` methods as the built-in service. If loading fails, the local in-memory vault is used.

This repo also includes a built-in external HashiCorp Vault adapter at `src/core/hashicorpVault.js`, mirrored after the `akoya-mcp` external vault setup. It is auto-selected when either:

- `VAULT_PROVIDER=external`
- both `VAULT_ADDR` and `VAULT_TOKEN` are set

Fail-closed behavior: when `VAULT_PROVIDER=external` and both `VAULT_ADDR` and `VAULT_TOKEN` are set, startup fails if the external vault module cannot be loaded or initialized. In this explicit external mode, it does not fall back to local in-memory vault.

`CLOUD_WRAP_VAULT_MODULE` still takes precedence over all auto-selection logic.

For external vault integrations, these environment variables are forwarded into the external vault `options` object when set:

- `VAULT_PROVIDER`
- `VAULT_ADDR`
- `VAULT_TOKEN`
- `VAULT_NAMESPACE`
- `VAULT_KV_MOUNT`
- `VAULT_KV_VERSION`
- `VAULT_SECRET_PATH`
- `COMMAND_LIMITS_LOCAL_POSTGRES_ENABLED`
- `COMMAND_LIMITS_LOCAL_POSTGRES_PORT`

Required vault key contract:

- Required environment variables for explicit external mode:
  - `VAULT_PROVIDER=external`
  - `VAULT_ADDR`
  - `VAULT_TOKEN`
- Required secret key at each provider path:
  - key name: `provider`
  - required object fields: `command` (string), `env` (object)
  - optional profile fields: `defaultProfile` (string), `profiles` (map), `profileSupport` (`mode=arg|env`, with `flag` or `envVar`)
  - optional per-profile access field: `profiles.<name>.users` (string array)
- Provider authorization key (when enabled):
  - set `MCP_PROVIDER_AUTH_KEY` to seed vault path `mcp.authorization.providerKey`
  - `get_provider` and `set_provider` requests must include `authorizationKey` matching that value

When using the built-in external adapter, `VAULT_SECRET_PATH` is treated as a base path and each cloud CLI provider is stored separately:

- `${VAULT_SECRET_PATH}/aws`
- `${VAULT_SECRET_PATH}/gcp`
- `${VAULT_SECRET_PATH}/azure`
- `${VAULT_SECRET_PATH}/oci`
- `${VAULT_SECRET_PATH}/alibaba`
- `${VAULT_SECRET_PATH}/digitalocean`
- `${VAULT_SECRET_PATH}/ibmcloud`
- `${VAULT_SECRET_PATH}/tencent`
- `${VAULT_SECRET_PATH}/huawei`

Each provider secret stores one object at key `provider` containing `command`, `env`, and optional profile fields.

Multi-profile provider behavior:

- `run_provider` and `run_<provider>` accept optional `profile`.
- `run_provider` and `run_<provider>` accept optional `user` for profile access checks.
- If `profile` is provided, runtime applies `profileSupport` to inject profile context via args or env.
- `profiles.<name>.args` and `profiles.<name>.env` are merged into execution.
- `profiles.<name>.users` controls profile access:
  - empty or missing array means profile is available to all users
  - non-empty array restricts profile use to those users
- If `profile` is omitted and `defaultProfile` is configured, that profile is used.

`CLOUD_WRAP_VAULT_MODULE` can also be used to override `vault.module` from config.

For a ready-made external profile, use `cloud-wrap.config.external-vault.example.json`.

Then run:

```bash
npm start -- --config cloud-wrap.config.json aws sts get-caller-identity
```

## Extend with additional providers

Add a provider in your config file:

```json
{
  "providers": {
    "do": {
      "command": "doctl",
      "env": {
        "DIGITALOCEAN_ACCESS_TOKEN": "<token>"
      }
    }
  }
}
```

Then call:

```bash
npm start -- run do account get
```

## Project structure

```text
src/
  index.js            # entry point
  mcp.js              # MCP stdio entry point
  program.js          # command definitions
  core/
    context.js        # runtime context creation
    execute.js        # provider CLI spawning
    mcp.js            # MCP tool registration and server startup
  config/
    providers.js      # built-in provider defaults
    loadConfig.js     # config loading and validation
  utils/
    logger.js         # pino logger setup
```