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

An MCP server that gives an LLM agent full API access to a VMware Cloud
Foundation 9.1 estate.

It connects to seven appliances — SDDC Manager, VCF Installer, vCenter, NSX,
Avi Load Balancer, VCF Operations and vSAN Data Protection — and exposes their
**8,931 API operations through a small generic set plus four
domain jobs**. Each domain job is a set of tools, not one
tool per job. It handles authentication for
each appliance, resolves request paths, follows async tasks, and records every
mutating call.

It speaks MCP over stdio, so it works with any MCP client: Claude Code, Claude
Desktop, Cursor, Windsurf, Zed, Continue, or your own agent built on an MCP
SDK. There is nothing to install — point a client at:

```
uvx --from git+https://github.com/NiranEC77/vcf-mcp vcf-mcp
```

---

## Contents

- [How it works](#how-it-works) · [Install](#install) · [Configure](#configure) ·
  [Connect an agent](#connect-an-agent) · [HTTP mode](#http-mode) · [Tools](#tools) ·
  [Targets](#targets) · [Environment variables](#environment-variables) ·
  [Write safety](#write-safety) · [Tests](#tests) ·
  [VCF 9.1 behaviour](#vcf-91-behaviour)

---

## How it works

**One tool per endpoint does not scale.** 8,931 tool schemas would exhaust the
context window before the agent asked its first question, and tool selection
degrades badly past a few dozen options.

Instead, the OpenAPI specs are indexed once at startup into a compact record
per operation (method, path, summary, operationId, tags). The agent then works
the way an engineer does — search, read the schema, dry-run, execute, follow
the task:

```
vcf_search_api("commission hosts")    ->  POST /v1/hosts  (commissionHosts)
vcf_describe_api(operation_id=...)    ->  required fields, types, responses
vcf_validate(target, path, body)      ->  dry-runs the spec, changes nothing
vcf_call(target, method, path, body)  ->  executes it, auth handled
vcf_task(target, task_id)             ->  follows the async result
```

Context cost stays fixed however many operations exist. Adding an appliance
means adding a spec file and a registry entry, not a new tool.

**Authentication** is per-appliance and automatic. Each target has its own
scheme (see [Targets](#targets)); the server mints a token on first use,
caches it in memory for the process lifetime, never writes it to disk, and
re-mints it automatically on a 401/403.

**Spec handling.** Both dialects are parsed: OpenAPI 3.x (SDDC Manager,
Installer, Operations, vCenter) and Swagger 2.0 (NSX). Base paths differ per
spec — `/suite-api` for Operations, `/api` for vCenter, `/policy/api/v1` for
NSX policy — and are resolved at index time, so paths returned by search are
real request paths you can pass straight to `vcf_call`.

---

## Install

Requires network access to the appliances. Nothing else — `uvx` fetches,
builds and runs the server in one step, and the API specs ship inside the
package, so there is no separate download:

```bash
uvx --from git+https://github.com/NiranEC77/vcf-mcp vcf-mcp check
```

That is also the command an MCP client should launch (see
[Connect an agent](#connect-an-agent)). `uvx` comes with
[uv](https://docs.astral.sh/uv/); install it with
`curl -LsSf https://astral.sh/uv/install.sh | sh`.

To install it as a normal command instead:

```bash
uv tool install git+https://github.com/NiranEC77/vcf-mcp    # then: vcf-mcp
pipx install git+https://github.com/NiranEC77/vcf-mcp       # same, via pipx
pip install git+https://github.com/NiranEC77/vcf-mcp        # into a venv
```

Or work from a clone (Python 3.10+):

```bash
git clone https://github.com/NiranEC77/vcf-mcp.git && cd vcf-mcp
uv venv --python 3.12 && uv pip install -e .
```

A clone keeps its config and logs in the repo directory; an installed copy
uses `~/.config/vcf-mcp/` and `~/.local/state/vcf-mcp/`. Either way the
environment variables below override both.

---

## Configure

### 1. Appliance addresses

No addresses are stored in this repo. Create a `hosts.json` — in
`~/.config/vcf-mcp/` for an installed copy, or the repo root for a clone
(where it is gitignored), or anywhere if you set `VCF_MCP_HOSTS_FILE`:

```json
{
  "hosts": {
    "sddc":      "sddc-manager.example.local",
    "installer": "vcf-installer.example.local",
    "vcenter":   "vcenter.example.local",
    "nsx":       "nsx-vip.example.local",
    "ops":       "vcf-ops.example.local",
    "avi":       "avi-controller.example.local",
    "vsan-dp":   "vcenter.example.local"
  }
}
```

Any target can instead be set with `VCF_MCP_<TARGET>_HOST`, which wins over the
file. `vsan-dp` is served by the vCenter appliance, so it takes the same
address as `vcenter`. Targets you leave out are reported as unconfigured by
`vcf_targets` rather than called.

### 2. Credentials

Passwords are read from a `.env` file — point `VCF_MCP_ENV_FILE` at whichever
file is already your rotation point, or create one next to `hosts.json`:

```bash
NSX_ADMIN_PASSWORD=...
SDDC_MANAGER_PASSWORD=...
VCF_INSTALLER_PASSWORD=...
NESTED_VCSA_PASSWORD=...
VCF_APPLIANCE_PASSWORD=...
```

Each target tries its own ordered subset of these keys; empty values and
anything containing `CHANGEME` are skipped. A single target can be overridden
with `VCF_MCP_<TARGET>_PASSWORD`. Nothing is copied into the repo, and no tool
ever returns a secret — failures name the *key* they looked for, never a value.

Authentication is capped at **3 attempts per target**
(`config.MAX_AUTH_ATTEMPTS`). vSphere SSO locks accounts after repeated
failures, so trying every password in the file is not a harmless fallback.

**Avi has no standing credential anywhere.** Its admin password is
VCF-generated and lives only in SDDC Manager's credential store. The server
fetches it at auth time (`GET /v1/credentials`, resourceType `NSX_ALB`), uses
it to log in, and never returns, logs or persists it. Set
`VCF_MCP_AVI_PASSWORD` to override this for a controller VCF does not manage.
Avi rejects HTTP Basic outright — only the session flow works.

### 3. Verify

```bash
vcf-mcp index    # index all operations (~8s, then cached to disk)
vcf-mcp check    # print every target and whether it answers
```

Prefix with `uvx --from git+https://github.com/NiranEC77/vcf-mcp` if you have
not installed it. `check` names any target whose address is still unset.

---

## Connect an agent

The server is a stdio process: run `vcf-mcp` with no arguments (equivalently,
`python -m vcf_mcp`) and it speaks MCP on stdin/stdout.

### Any MCP client

Most clients read the same JSON shape. Add this to the client's MCP config —
no prior install needed, `uvx` handles it:

```json
{
  "mcpServers": {
    "vcf": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/NiranEC77/vcf-mcp", "vcf-mcp"],
      "env": {
        "VCF_MCP_HOSTS_FILE": "/absolute/path/to/hosts.json",
        "VCF_MCP_ENV_FILE": "/absolute/path/to/your/.env"
      }
    }
  }
}
```

If you installed it already, replace those two fields with
`"command": "vcf-mcp"` (or the absolute path to the executable, which some
clients require because they do not inherit your shell's `PATH`).

`.mcp.example.json` in this repo is that file, ready to copy. Where each client
keeps its config:

| Client | Config location |
|---|---|
| Claude Code | `.mcp.json` in the project, or `claude mcp add` (below) |
| Claude Desktop | `claude_desktop_config.json` |
| Cursor | `.cursor/mcp.json` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| Zed | `settings.json`, under `context_servers` |
| Continue | `config.yaml`, under `mcpServers` |

### Claude Code

```bash
claude mcp add vcf \
  --env VCF_MCP_HOSTS_FILE=/absolute/path/to/hosts.json \
  --env VCF_MCP_ENV_FILE=/absolute/path/to/your/.env \
  -- uvx --from git+https://github.com/NiranEC77/vcf-mcp vcf-mcp
```

### Your own agent

Any MCP SDK can launch it as a subprocess. With the Python SDK:

```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

params = StdioServerParameters(
    command="uvx",
    args=["--from", "git+https://github.com/NiranEC77/vcf-mcp", "vcf-mcp"],
    env={"VCF_MCP_HOSTS_FILE": "/absolute/path/to/hosts.json"},
)

async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        result = await session.call_tool("vcf_search_api", {"query": "commission hosts"})
```

The server advertises `read_only` and `destructive` annotations per tool, so a
client that gates writes can do so without a hardcoded tool list.

---

## HTTP mode

Stdio is for a client that launches the server itself. A hosted copy — a
PaaS app, a container — serves **Streamable HTTP** at `/mcp` instead. It
switches on when `$PORT` is set (every PaaS sets it) or with
`vcf-mcp serve-http`. Install the extra:

```
pip install "vcf-mcp[http] @ git+https://github.com/NiranEC77/vcf-mcp"
```

HTTP mode refuses to start with no bearer configured, because it would
expose live VCF admin APIs to anyone who can reach the route. Two kinds of
bearer are accepted, alone or together:

**Static tokens.** `VCF_READ_TOKEN` allows GET/HEAD through `vcf_call`;
`VCF_ADMIN_TOKEN` allows everything. Both at least 16 characters, and
different from each other.

**OAuth 2.0 access tokens.** Set `VCF_MCP_OAUTH_ISSUER` and the server
becomes an OAuth resource server: it verifies the JWT against the issuer's
JWKS (discovered per RFC 8414, or `VCF_MCP_OAUTH_JWKS_URI`), checks expiry,
and requires an `aud` that names this server (`VCF_MCP_RESOURCE_URL` or one
of `VCF_MCP_OAUTH_AUDIENCES`). It serves RFC 9728 metadata at
`/.well-known/oauth-protected-resource` and points to it on every 401.

What the token's `scope` may carry, any one of which is enough:

| Scope | Capability | Meaning |
|---|---|---|
| a write tool name (`vcf_call`, `vcf_task`) | admin | the token was issued for that tool |
| a read tool name (`vcf_targets`, …) or `tools` | read | GET/HEAD only |
| an intent scope, `urn:iam:agent:intent:<job>` | admin | an agent gateway in front of this server granted the agent a **job**; the gateway has already mapped the tool to that job and asked its policy engine, and strips the tools the job does not cover. This server does not redo that decision by tool name. |

The intent rule is what lets this server sit behind an agent gateway the
same way any MCP server you do not control does: verify the token, then
trust the gateway for per-tool authorization. Change the prefix with
`VCF_MCP_OAUTH_INTENT_PREFIX`. When the backend route is reachable
without the gateway, restrict it at the network — a job scope opens the
whole server for a direct caller, exactly as it would for any other MCP
backend.

`VCF_MCP_AUDIT_LOG=/dev/stdout` keeps the mutation audit in the platform
log when the disk is ephemeral.

---

## Tools

| Tool | Arguments | Returns |
|---|---|---|
| `vcf_targets` | `check_reachability=true` | Every appliance: name, product, address, auth scheme, operation count, whether it answers |
| `vcf_search_api` | `query`, `target?`, `method?`, `limit=25`, `include_deprecated=false` | Ranked operations with method, full path, summary, operationId |
| `vcf_describe_api` | `operation_id?` or `method`+`path`, `target?`, `depth=3`, `max_properties=60` | Path/query parameters, resolved request body schema with required fields, response schemas |
| `vcf_validate` | `target`, `path`, `body?`, `wait_seconds=120` | `validated` true/false plus each failed check — executes nothing |
| `vcf_call` | `target`, `method`, `path`, `query?`, `body?`, `timeout?`, `max_response_chars=20000` | `count` + `summary` + slim `items` when the body is a list (full length, even if truncated); task id for async operations |
| `vcf_task` | `target`, `task_id`, `wait_seconds=0`, `poll_interval=5.0` | Task status and, on failure, which subtask failed and why |
| `vcf_inventory` | `targets?`, `per_section_limit=25` | Domains, clusters, hosts, gateways and alerts in one snapshot |
| `vcf_audit` | `limit=50` | Recent mutating calls made through this server |
| `vcf_list_vms` | | Every VM and the count. VM management. |
| `vcf_get_vm` | `vm` | One VM. VM management. |
| `vcf_vm_power_state` | `vm` | Current power state. VM management. |
| `vcf_start_vm` | `vm` | Power on. VM management. |
| `vcf_stop_vm` | `vm` | Power off. VM management. |
| `vcf_reset_vm` | `vm` | Reset. VM management. |
| `vcf_suspend_vm` | `vm` | Suspend. VM management. |
| `vcf_list_networks` | | vCenter networks. Network management. |
| `vcf_list_segments` | | NSX segments. Network management. |
| `vcf_list_gateways` | | NSX gateways. Network management. |
| `vcf_list_datastores` | | Datastores. Storage management. |
| `vcf_get_datastore` | `datastore` | One datastore. Storage management. |
| `vcf_datastore_policy` | `datastore` | Default storage policy. Storage management. |
| `vcf_list_alerts` | | Operations alerts. Metrics. |
| `vcf_ops_snapshot` | | Operations snapshot. Metrics. |

`vcf_call` and the four VM power tools are annotated as destructive; the rest are read-only. A grant names one domain job — a set of tools — or full access.

Typical sequence for a change: `vcf_search_api` → `vcf_describe_api` →
`vcf_validate` → `vcf_call` → `vcf_task`.

---

## Targets

| Target | Product | Authentication | Operations |
|---|---|---|---|
| `sddc` | SDDC Manager | `POST /v1/tokens` → Bearer | 500 |
| `installer` | VCF Installer | `POST /v1/tokens` → Bearer | 57 |
| `vcenter` | vCenter Server | `POST /api/session` → `vmware-api-session-id` | 1,367 |
| `nsx` | NSX Manager (VIP) | HTTP Basic | 5,182 |
| `avi` | Avi Load Balancer (NSX ALB) | `POST /login` → session cookies + `X-CSRFToken` | 1,233 |
| `ops` | VCF Operations | `POST /suite-api/api/auth/token/acquire` → `vRealizeOpsToken` | 527 |
| `vsan-dp` | vSAN Data Protection | vCenter session | 65 |

Every scheme above was verified against a live 9.1 estate.

---

## Environment variables

| Variable | Effect |
|---|---|
Defaults differ between a clone and an installed copy, as noted:

| Variable | Effect | Default (clone → installed) |
|---|---|---|
| `VCF_MCP_HOSTS_FILE` | Path to the addresses file | `./hosts.json` → `~/.config/vcf-mcp/hosts.json` |
| `VCF_MCP_<TARGET>_HOST` | Override one address, e.g. `VCF_MCP_NSX_HOST` | — |
| `VCF_MCP_ENV_FILE` | Path to the credentials `.env` | `./.env` → `~/.config/vcf-mcp/.env` |
| `VCF_MCP_<TARGET>_PASSWORD` | Override one target's password, e.g. `VCF_MCP_SDDC_PASSWORD` | — |
| `VCF_MCP_INSTALLER_CREDS` | Installer's generated credentials file | alongside the `.env` |
| `VCF_MCP_VERIFY_TLS=1` | Enforce TLS verification | off — appliances present self-signed certs |
| `VCF_MCP_AUDIT_LOG` | Where mutations are recorded | `./logs/vcf-mcp-audit.jsonl` → `~/.local/state/vcf-mcp/vcf-mcp-audit.jsonl` |
| `VCF_MCP_SPEC_DIR` | Spec source directory | `./specs` → the copy bundled in the package |
| `VCF_MCP_CACHE_DIR` | Index cache directory | `~/.cache/vcf-mcp` |

HTTP mode only ([HTTP mode](#http-mode)):

| Variable | Effect | Default |
|---|---|---|
| `PORT` / `VCF_MCP_HTTP=1` | Serve Streamable HTTP instead of stdio | stdio |
| `VCF_READ_TOKEN` / `VCF_ADMIN_TOKEN` | Static bearers (read = GET/HEAD; admin = everything) | — |
| `VCF_MCP_RESOURCE_URL` | This server's public `/mcp` URL (the `aud` it answers to; also sets the allowed host) | `http://127.0.0.1:8080/mcp` |
| `VCF_ALLOWED_HOSTS` | Comma list for DNS-rebinding protection | host of `VCF_MCP_RESOURCE_URL` + loopback |
| `VCF_MCP_OAUTH_ISSUER` | Authorization server that issues access tokens; unset = static tokens only | — |
| `VCF_MCP_OAUTH_JWKS_URI` | Key set URL when the issuer has no RFC 8414 discovery | discovered |
| `VCF_MCP_OAUTH_AUDIENCES` | Extra `aud` values to accept, e.g. an agent gateway's route | — |
| `VCF_MCP_OAUTH_INTENT_PREFIX` | Scope prefix a gateway uses for job grants | `urn:iam:agent:intent:` |
| `VCF_MCP_OAUTH_REQUIRED_SCOPES` | `scopes_supported` fallback in the RFC 9728 document | `tools` |
| `VCF_MCP_OAUTH_TLS_VERIFY=0` | Skip TLS verification when fetching issuer metadata / JWKS | verify |

---

## Write safety

**There is no write gate.** Any operation the API allows — including
`DELETE /v1/domains/{id}` and host decommission — executes immediately when the
agent calls it. This is deliberate: the server does not try to second-guess
which operations are safe.

What exists instead is a record. Every POST/PATCH/PUT/DELETE is appended to
`logs/vcf-mcp-audit.jsonl` with target, path, status, duration and a
**redacted** body — anything keyed like a password, token, secret or credential
is replaced before the line is written. `vcf_audit` reads it back, including
changes made by earlier sessions.

If you want a gate, `client.request()` is the single chokepoint that every call
in the server passes through.

---

## Tests

```bash
.venv/bin/python -m pytest tests/ -q
```

36 offline tests, no appliance required. Each pins a bug found during the
build: camelCase tokenisation, plural stemming, `$ref` cycle handling,
truncation across differently-named collections, secret redaction, task-id
detection, case-insensitive task states, and the rule that no appliance address
is ever hardcoded into the registry.

---

## Specs

Vendored from [vmware/vcf-api-specs](https://github.com/vmware/vcf-api-specs)
at commit `3949fc3` (2026-05-13), version `9.1.0.0`. Provenance in
`specs/SPECS-PROVENANCE.txt`.

The 170 Avi object specs in `specs/avi/` were downloaded from an Avi
controller's own swagger endpoint (`/swagger/<Object>.yaml`), so they are
version-matched to the deployed build by construction. Avi's per-object files
re-declare related objects' paths; the index deduplicates them and keeps the
declaration from the file named after the resource.

---

## VCF 9.1 behaviour

Discovered while building against a live estate, and encoded in the server:

- `POST /v1/system/prechecks` is gone; the replacement is
  `POST /v1/system/health-summary` (`startHealthCheck`).
- The whole `/v1/edge-clusters` family on SDDC Manager is deprecated, including
  `updateEdgeCluster` (`PATCH /v1/edge-clusters/{id}`).
- Deprecated operations are hidden from search unless `include_deprecated` is
  set. Well-scoring ones are still reported under `hidden_deprecated`, so a
  legacy path found in old documentation is identified as legacy rather than
  appearing not to exist.
- SDDC Manager returns task status as `"Successful"`, not `"SUCCESSFUL"`;
  `vcf_task` compares case-insensitively.
- vCenter (vAPI) specs declare enums as prose ("Possible values: ...").
  `vcf_describe_api` lifts them into a real `enum` list.
- NSX often has the strictest password complexity rules of the fleet, so an
  estate is frequently built with one password NSX accepts. `NSX_ADMIN_PASSWORD`
  is therefore tried first for several targets.

TDQS

A4.5/5.0

Scored across 8 tools

Disambiguation5/5

Each tool addresses a distinct stage in the VCF API workflow: target enumeration, operation search, schema inspection, estate snapshot, direct invocation, task tracking, validation, and auditing. No two tools overlap in core purpose; even search vs. describe vs. call are clearly separated.

Naming Consistency4/5

All tools share the consistent 'vcf_' prefix and use lowercase snake_case, making them recognizable as a family. Naming style is slightly mixed—some are verb_noun (search_api, describe_api), some are bare nouns (targets, inventory, task), and some are bare verbs (call, validate)—but the pattern is still predictable and readable.

Tool Count5/5

Eight tools is a well-scoped size for a VCF API gateway. Each tool serves a necessary function without redundancy, covering discovery, exploration, execution, validation, monitoring, and auditing in a focused set that is neither sparse nor overwhelming.

Completeness5/5

The tool surface forms a complete lifecycle for VCF API interaction: identify targets, find operations, inspect schemas, call safely, validate beforehand, track long-running tasks, and audit changes. There are no obvious dead ends or missing essential capabilities for the stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues