使用说明
AriaOps MCP Server
A Model Context Protocol (MCP) server for VMware Aria Operations (on-prem). Exposes Aria Ops REST API capabilities as MCP tools so AI assistants can query infrastructure health, alerts, metrics, capacity, and reports — and optionally perform write operations when explicitly enabled.
Features
- Resources — list, search, query VMs/hosts/clusters and their properties
- Alerts — active alerts, definitions, notes, contributing symptoms
- Metrics — stats over time, latest values, bulk queries, Top-N
- Capacity — remaining capacity per cluster, time-to-full, policy thresholds
- Reports — list templates, generated reports, download (PDF/CSV)
- Discovery — version info, collectors, symptom definitions, recommendations
- Write operations (opt-in) — modify alerts, maintenance mode, schedules, report generation, resource lifecycle
Supports both stdio (local/testing) and streamable HTTP (production) transports.
Requirements
- Python 3.11+
- Access to an on-prem VMware Aria Operations 8.x instance
- Podman (optional, for containerized deployment)
Quick Start
Install
git clone https://github.com/asnd/ariaops-mcp.git
cd ariaops-mcp
pip install -e .
For development (includes linting and test tools):
pip install -e ".[dev]"
Configure
cp .env.example .env
# Edit .env with your Aria Ops credentials
ARIAOPS_HOST=vrops.example.com
ARIAOPS_USERNAME=admin
ARIAOPS_PASSWORD=secret
ARIAOPS_AUTH_SOURCE=local # or your LDAP source name
ARIAOPS_VERIFY_SSL=true
Run (stdio — for MCP clients)
python -m ariaops_mcp
Run (HTTP — production)
ARIAOPS_TRANSPORT=http ARIAOPS_PORT=8080 python -m ariaops_mcp
To require OAuth 2.x bearer tokens on the HTTP transport, enable the flag and provide the resource-server settings:
ARIAOPS_TRANSPORT=http \
ARIAOPS_HTTP_OAUTH_ENABLED=true \
ARIAOPS_HTTP_OAUTH_ISSUER_URL=https://issuer.example.com \
ARIAOPS_HTTP_OAUTH_RESOURCE_SERVER_URL=https://mcp.example.com \
ARIAOPS_HTTP_OAUTH_JWT_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" \
ARIAOPS_HTTP_OAUTH_REQUIRED_SCOPES=mcp:read \
python -m ariaops_mcp
OAuth knobs
| Variable | Required | Default | Notes |
|---|---|---|---|
ARIAOPS_HTTP_OAUTH_ENABLED |
no | false |
Turn enforcement on. Requires ARIAOPS_TRANSPORT=http. |
ARIAOPS_HTTP_OAUTH_PROVIDER |
no | generic |
Set to keycloak to derive the realm JWKS URL and default accepted algorithms to RS256 when no key material is supplied. |
ARIAOPS_HTTP_OAUTH_ISSUER_URL |
yes | — | Expected iss claim and the authorization server published via the discovery doc. |
ARIAOPS_HTTP_OAUTH_RESOURCE_SERVER_URL |
yes | — | This server's resource identifier — used as the default aud and exposed via /.well-known/oauth-protected-resource. |
ARIAOPS_HTTP_OAUTH_JWT_KEY |
one of | — | HS256/384/512 shared secret (≥32 bytes) or PEM public key for RS*/ES*/PS*. |
ARIAOPS_HTTP_OAUTH_JWKS_URL |
one of | — | JWKS endpoint URL (e.g. Keycloak's /realms/<realm>/protocol/openid-connect/certs). Mutually exclusive with JWT_KEY; required for IdPs that rotate keys. Optional when ARIAOPS_HTTP_OAUTH_PROVIDER=keycloak. |
ARIAOPS_HTTP_OAUTH_JWT_ALGORITHMS |
no | HS256 |
Comma-separated list. The verifier rejects every algorithm not on this list (so alg: none and algorithm confusion are blocked). |
ARIAOPS_HTTP_OAUTH_AUDIENCE |
no | resource URL | Override if your IdP issues a different aud. |
ARIAOPS_HTTP_OAUTH_REQUIRED_SCOPES |
no | [] |
Tokens must carry every listed scope or get a 403 insufficient_scope. |
ARIAOPS_HTTP_OAUTH_LEEWAY_SECONDS |
no | 30 |
Clock-skew tolerance for exp / nbf / iat. |
ARIAOPS_HTTP_OAUTH_JWKS_CACHE_TTL |
no | 300 |
How long (s) to cache JWKS keys before refetching. |
Discovery: With OAuth enabled, the server publishes
GET /.well-known/oauth-protected-resource per RFC 9728 so MCP clients
auto-discover the authorization server and required scopes.
Health: GET /health is intentionally unauthenticated so probes and load
balancers keep working with OAuth turned on.
Keycloak setup
Keycloak is a supported IdP. Use JWKS — Keycloak rotates RS256 keys.
-
Create a client in your realm (
Clients → Create client):- Client type: OpenID Connect
- Client authentication: on (confidential) for the MCP client
- Valid redirect URIs: whatever your MCP client uses
-
Add an Audience mapper (
Client scopes → <client>-dedicated → Add mapper → Audience):- Included Client Audience: the client ID you want as
aud(e.g.mcp-client). - This is required because Keycloak does not put the resource URL in
audby default.
- Included Client Audience: the client ID you want as
-
Add scope(s) (
Client scopes → Create) and assign to the client (DefaultorOptional). -
Configure ariaops-mcp:
ARIAOPS_TRANSPORT=http \ ARIAOPS_HTTP_OAUTH_ENABLED=true \ ARIAOPS_HTTP_OAUTH_PROVIDER=keycloak \ ARIAOPS_HTTP_OAUTH_ISSUER_URL=https://kc.example.com/realms/myrealm \ ARIAOPS_HTTP_OAUTH_RESOURCE_SERVER_URL=https://mcp.example.com \ ARIAOPS_HTTP_OAUTH_AUDIENCE=mcp-client \ ARIAOPS_HTTP_OAUTH_REQUIRED_SCOPES=mcp:read \ python -m ariaops_mcp -
Test from the CLI with a client-credentials grant:
TOKEN=$(curl -s -X POST \ "https://kc.example.com/realms/myrealm/protocol/openid-connect/token" \ -d grant_type=client_credentials \ -d client_id=mcp-client \ -d client_secret=$KC_SECRET \ -d scope="mcp:read" | jq -r .access_token) curl -H "Authorization: Bearer $TOKEN" https://mcp.example.com/
Notes:
issin Keycloak tokens is exactlyhttps://<host>/realms/<realm>(no trailing slash). The verifier strips trailing slashes on both sides, so either form works in config.ARIAOPS_HTTP_OAUTH_PROVIDER=keycloakderivesARIAOPS_HTTP_OAUTH_JWKS_URLas<issuer>/protocol/openid-connect/certsand defaultsARIAOPS_HTTP_OAUTH_JWT_ALGORITHMStoRS256unless you explicitly set algorithms orARIAOPS_HTTP_OAUTH_JWT_KEY.- Roles (
realm_access.roles,resource_access.<client>.roles) are not enforced — only OAuthscope. Map roles to scopes in Keycloak if you need RBAC. - The verifier wraps
PyJWKClientinasyncio.to_thread, so a JWKS cache miss does not block the event loop.
LDAP / Active Directory authentication
As an alternative to OAuth, the HTTP transport can authenticate MCP clients with
HTTP Basic credentials verified against LDAPS. The server binds directly with
the user's credentials (no service account), reads their memberOf groups, and
maps them to the same role/country/instance claims the OAuth path uses —
so role-based access works identically.
ARIAOPS_TRANSPORT=http \
ARIAOPS_HTTP_AUTH_MODE=ldap \
ARIAOPS_LDAP_SERVER_URI="ldaps://dc1.corp.example.com:636" \
ARIAOPS_LDAP_USER_DN_TEMPLATE="{username}@corp.example.com" \
ARIAOPS_LDAP_USER_SEARCH_BASE="dc=corp,dc=example,dc=com" \
ARIAOPS_LDAP_GROUP_ROLE_MAP='{"vrops-ops":{"role":"ops"},"vrops-se":{"role":"country","country":"SE"}}' \
python -m ariaops_mcp
| Variable | Required | Default | Notes |
|---|---|---|---|
ARIAOPS_HTTP_AUTH_MODE |
yes | none |
Set to ldap. Mutually exclusive with ARIAOPS_HTTP_OAUTH_ENABLED. |
ARIAOPS_LDAP_SERVER_URI |
yes | — | ldaps:// URI. Plain ldap:// only allowed with ARIAOPS_LDAP_VERIFY_TLS=false. |
ARIAOPS_LDAP_USER_DN_TEMPLATE |
yes | — | Bind DN with {username}. AD UPN {username}@corp.example.com or uid={username},ou=people,dc=corp,dc=com. |
ARIAOPS_LDAP_USER_SEARCH_BASE |
yes | — | Base DN for the memberOf lookup. |
ARIAOPS_LDAP_GROUP_ROLE_MAP |
no | {} |
JSON: AD group CN/DN → {"role":"ops"} or {"role":"country","country":"SE"}/{"role":"country","instance":"de"}. An ops group wins over country. A bare-CN key matches any group with that CN anywhere in the directory; a full-DN key matches only that exact DN. When empty, ARIAOPS_DEFAULT_ROLE must be set explicitly (config fails closed otherwise) and every authenticated user gets that role. A bound user matching no mapped group is denied. |
ARIAOPS_LDAP_CA_CERT_FILE |
no | system trust | PEM bundle for LDAPS verification. |
ARIAOPS_LDAP_VERIFY_TLS |
no | true |
Disable only for lab/testing. |
ARIAOPS_LDAP_CACHE_TTL |
no | 300 |
Seconds to cache a successful bind's claims. |
ARIAOPS_LDAP_BIND_TIMEOUT |
no | 10 |
LDAP connect timeout (seconds). |
/health stays unauthenticated, and instance authorization is enforced by the
same principal layer as OAuth — LDAP only decides the caller's role/instance.
Security notes:
- Group lookup reads
memberOf, which contains direct group memberships only — a user who is in a mapped group via a nested group will not receive its role. Map the groups users belong to directly (resolving nested membership via the AD matching-rule-in-chain OID1.2.840.113556.1.4.1941is a possible future extension). - Failed binds are never cached, so every bad-password request reaches the directory. Put a rate limit on the MCP endpoint at your reverse proxy to protect against password spraying and AD account-lockout abuse.
ARIAOPS_HTTP_OAUTH_REQUIRED_SCOPESis ignored in LDAP mode (Basic-auth requests carry no OAuth scopes); a warning is logged if it is set.
See AUTH_FLOW.md for a detailed message-flow walkthrough of both OAuth and LDAP modes, including middleware stack, token verification steps, cache behaviour, and principal resolution.
Run with Podman
podman build --format docker -t ariaops-mcp .
podman run --env-file .env -p 8080:8080 ariaops-mcp
Multiple Aria Operations instances
A single MCP server can front several Aria Operations instances. Define them
with ARIAOPS_INSTANCES (a JSON array); when set, the legacy single-host
variables (ARIAOPS_HOST/ARIAOPS_USERNAME/ARIAOPS_PASSWORD) are optional.
ARIAOPS_INSTANCES=[
{"id":"us","host":"us.vrops.example.com","username":"svc","password":"secret","country":"US"},
{"id":"de","host":"de.vrops.example.com","username":"svc","password":"secret","country":"DE"}
]
Each entry needs a unique id. Optional per-instance fields: auth_source
(default local), verify_ssl (default true), and country (used to pin
country-role users).
Role-based access
Access is scoped by the caller's role:
| Role | Access |
|---|---|
ops |
All configured instances. The target instance is chosen per call via the tool's instance argument (required only when more than one instance exists). |
country |
Exactly one instance — the one matching the user's country (or explicit instance) claim. Requests for any other instance are rejected. |
On the HTTP transport the role/country/instance are read from the validated
JWT claims (claim names are configurable). On stdio (local) they fall back to
the ARIAOPS_DEFAULT_* settings.
Every instance-bound tool accepts an optional instance argument, and the
list_instances tool returns the instances the current caller may use. Backward
compatibility is preserved: with only the legacy single-host variables set, a
single default instance is synthesized and existing behavior is unchanged.
| Variable | Default | Description |
|---|---|---|
ARIAOPS_INSTANCES |
— | JSON array of instance objects (id, host, username, password, optional auth_source/verify_ssl/country) |
ARIAOPS_ROLE_CLAIM |
ariaops_role |
JWT claim holding the caller's role |
ARIAOPS_COUNTRY_CLAIM |
ariaops_country |
JWT claim holding the country code for country-role users |
ARIAOPS_INSTANCE_CLAIM |
ariaops_instance |
JWT claim holding an explicit instance id for country-role users |
ARIAOPS_OPS_ROLE |
ops |
Role value that grants access to all instances |
ARIAOPS_COUNTRY_ROLE |
country |
Role value that pins a user to a single instance |
ARIAOPS_DEFAULT_ROLE |
ops |
Role assumed when no JWT role claim is present (e.g. stdio) |
ARIAOPS_DEFAULT_COUNTRY |
— | Country assumed for a country-role caller without a claim |
ARIAOPS_DEFAULT_INSTANCE |
— | Default instance id for ops callers and the synthesized single instance |
MCP Architecture
+---------------------------+ MCP (stdio / streamable HTTP) +---------------------------+
| MCP Client / AI Assistant | <-----------------------------------> | ariaops-mcp server |
| Claude Desktop, IDE, etc. | | |
+-------------+-------------+ | read tools: always on |
| | write tools: opt-in |
| +-------------+-------------+
| |
| | HTTPS REST
| v
| +---------------------------+
+---------------------------------------------------> | VMware Aria Operations |
| /suite-api/api/... |
+---------------------------+
+-----------------------+ chat / tool plans +-----------------------+
| Browser | <-------------------------> | test-ui/app.py |
| Gradio test UI | | Gradio + tool bridge |
+-----------+-----------+ +-----+-----------+-----+
| | |
| | |
| LLM Gateway | | in-process MCP tools
v v v
+-------------------+ +----------------+ +----------------------+
| Human operator | | OpenAI-compat | | ariaops_mcp modules |
+-------------------+ | model gateway | | + AriaOpsClient |
+----------------+ +----------+-----------+
|
v
+----------------------+
| VMware Aria Ops |
+----------------------+
MCP Client Integration
Add to your MCP config (e.g. ~/.claude/mcp_settings.json for AI code assistants):
{
"mcpServers": {
"ariaops": {
"command": "python",
"args": ["-m", "ariaops_mcp"],
"env": {
"ARIAOPS_HOST": "vrops.example.com",
"ARIAOPS_USERNAME": "admin",
"ARIAOPS_PASSWORD": "secret"
}
}
}
}
Available Tools
| Domain | Tools |
|---|---|
| Resources | list_resources, get_resource, query_resources, get_resource_properties, get_resource_relationships, list_resource_kinds, list_adapter_kinds, list_resource_groups, get_resource_group_members |
| Alerts | list_alerts, get_alert, query_alerts, get_alert_notes, list_alert_definitions, get_alert_definition, get_contributing_symptoms |
| Metrics | get_resource_stats, get_latest_stats, query_stats, query_latest_stats, get_stat_keys, get_top_n_stats, list_properties_latest |
| Capacity | get_capacity_remaining, get_capacity_overview, list_policies, get_capacity_forecast, get_trend_analysis |
| Reports | list_report_definitions, get_report_definition, list_reports, get_report, download_report, list_report_schedules |
| Discovery | get_version, list_collectors, list_symptoms, list_recommendations, list_supermetrics |
| Instances | list_instances (always available; lists the Aria Ops instances accessible to the caller) |
Write Tools (requires ARIAOPS_ENABLE_WRITE_OPERATIONS=true)
| Domain | Tools |
|---|---|
| Alert operations | modify_alerts, add_alert_note, delete_alert_note, delete_canceled_alerts |
| Resource maintenance | mark_resources_maintained, unmark_resources_maintained |
| Maint. schedules | create_maintenance_schedule, update_maintenance_schedule, delete_maintenance_schedule |
| Report operations | generate_report, delete_report, create_report_schedule, update_report_schedule, delete_report_schedule |
| Resource lifecycle | create_resource, update_resource, delete_resources |
Full tool catalog: TOOLS.md
Full spec: REQUIREMENTS.md
Test UI App
The repo includes a Gradio-based test UI in test-ui/ for exercising the same tool handlers in-process while routing chat through an LLM gateway.
Create a local env file first so no tenant IDs, client IDs, or tokens need to be hardcoded in the repo:
cp .env.example .env
Set LITELLM_BASE_URL for the gateway. If you want to use the Get Token via Azure SSO button, also set:
AZURE_TENANT_ID=your-azure-tenant-id
AZURE_CLIENT_ID=your-azure-client-id
python -m pip install -r test-ui/requirements.txt
python test-ui/app.py --port 7860
To run its automated tests:
cd test-ui
pytest tests
Chainlit auth test harness
test-ui/chainlit/ is a minimal Chainlit frontend that tests both auth modes end-to-end. It proves the LDAP (Basic) and OAuth (Bearer) wiring without coupling the demo UI to Gradio.
cp test-ui/chainlit/.env.example test-ui/chainlit/.env
# edit .env: set ARIAOPS_MCP_URL + optional LITELLM_* vars
pip install chainlit httpx python-dotenv
chainlit run test-ui/chainlit/app.py -w
See CHAINLIT_AUTH.md for full configuration details.
OAuth2 end-to-end scenario
test-e2e/oauth/ contains a self-contained Podman scenario that spins up a real Keycloak IdP with two simulated users (alice=ops, bob=country/SE) and verifies token validation, role-based instance access, and instance enforcement over the streamable HTTP transport.
./test-e2e/oauth/run.sh # build image, start stack, run 8 tests, leave running
./test-e2e/oauth/run.sh down # tear down
See test-e2e/oauth/README.md for topology, user table, and proxy notes.
Development
pip install -e ".[dev]"
pytest # run tests
ruff check src/ tests/ # lint
pyright # type-check
cd test-ui && pytest tests
Configuration Reference
| Variable | Required | Default | Description |
|---|---|---|---|
ARIAOPS_HOST |
Yes | — | Aria Ops hostname (no scheme) |
ARIAOPS_USERNAME |
Yes | — | API username |
ARIAOPS_PASSWORD |
Yes | — | API password |
ARIAOPS_AUTH_SOURCE |
No | local |
Auth source (local / LDAP name) |
ARIAOPS_INSTANCES |
No | — | JSON array of Aria Ops instances for multi-instance mode (see Multiple Aria Operations instances) |
ARIAOPS_ROLE_CLAIM |
No | ariaops_role |
JWT claim holding the caller's role (ops / country) |
ARIAOPS_COUNTRY_CLAIM |
No | ariaops_country |
JWT claim holding a country-role user's country code |
ARIAOPS_INSTANCE_CLAIM |
No | ariaops_instance |
JWT claim holding an explicit instance id for country-role users |
ARIAOPS_OPS_ROLE |
No | ops |
Role value granting access to all instances |
ARIAOPS_COUNTRY_ROLE |
No | country |
Role value pinning a user to a single instance |
ARIAOPS_DEFAULT_ROLE |
No | ops |
Role assumed when no JWT role claim is present |
ARIAOPS_DEFAULT_COUNTRY |
No | — | Country assumed for a country-role caller without a claim |
ARIAOPS_DEFAULT_INSTANCE |
No | — | Default instance id for ops callers / synthesized single instance |
ARIAOPS_VERIFY_SSL |
No | true |
TLS certificate verification |
ARIAOPS_TRANSPORT |
No | stdio |
stdio or http |
ARIAOPS_PORT |
No | 8080 |
HTTP listen port |
ARIAOPS_HTTP_OAUTH_ENABLED |
No | false |
Require OAuth 2.x bearer tokens on the HTTP MCP transport |
ARIAOPS_HTTP_OAUTH_PROVIDER |
No | generic |
Use keycloak for Keycloak realm JWKS/RS256 defaults |
ARIAOPS_HTTP_OAUTH_ISSUER_URL |
No | — | OAuth 2.x issuer URL advertised to MCP clients when HTTP auth is enabled |
ARIAOPS_HTTP_OAUTH_RESOURCE_SERVER_URL |
No | — | Public MCP HTTP endpoint URL used for OAuth protected-resource metadata |
ARIAOPS_HTTP_OAUTH_JWT_KEY |
One of | — | HS* shared secret (≥32 bytes) or PEM public key for RS*/ES*/PS* |
ARIAOPS_HTTP_OAUTH_JWKS_URL |
One of | — | JWKS endpoint (e.g. Keycloak certs URL); mutually exclusive with JWT_KEY |
ARIAOPS_HTTP_OAUTH_JWT_ALGORITHMS |
No | HS256 |
Comma-separated or JSON-array list of accepted JWT algorithms |
ARIAOPS_HTTP_OAUTH_AUDIENCE |
No | ARIAOPS_HTTP_OAUTH_RESOURCE_SERVER_URL |
Expected JWT audience for HTTP bearer tokens |
ARIAOPS_HTTP_OAUTH_REQUIRED_SCOPES |
No | — | Comma-separated or JSON-array list of scopes required for HTTP MCP access |
ARIAOPS_HTTP_OAUTH_LEEWAY_SECONDS |
No | 30 |
Clock-skew tolerance for exp/nbf/iat |
ARIAOPS_HTTP_OAUTH_JWKS_CACHE_TTL |
No | 300 |
JWKS cache lifetime in seconds |
ARIAOPS_LOG_LEVEL |
No | INFO |
Log level |
ARIAOPS_ENABLE_WRITE_OPERATIONS |
No | false |
Enable mutating tools (alert management, maintenance, reports, resource lifecycle) |
LITELLM_BASE_URL |
No | — | Test UI LLM gateway base URL |
LITELLM_TOKEN |
No | — | Test UI JWT for the gateway |
AZURE_TENANT_ID |
No | — | Test UI Azure tenant ID for browser SSO |
AZURE_CLIENT_ID |
No | — | Test UI Azure client ID for browser SSO |
License
MIT
