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 eight tools**. 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) · [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.

---

## 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` | Status, response body, and `count` 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_call` is annotated as destructive; every other tool is annotated read-only.

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` |

---

## 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