Horizon MCP Server
Horizon MCP Server
MCP (Model Context Protocol) server for Omnissa Horizon VDI management. Exposes the Horizon REST API as MCP tools covering inventory, monitoring, configuration, entitlements, Active Directory, and help desk functions. Verified against the Horizon Server REST API spec for versions 2512 through 2606 — call get_api_coverage for the full list of supported tools and known gaps.
Quickstart
The fastest path to a working setup, using Claude Code with stdio transport:
Clone and install:
git clone https://github.com/matt-coppinger/horizon-mcp.git cd horizon-mcp uv syncRegister the server — add this to
~/.claude/settings.json(see Configuration below for what each variable means):{ "mcpServers": { "horizon": { "command": "uv", "args": ["run", "--project", "/absolute/path/to/horizon-mcp", "horizon-mcp"], "env": { "HORIZON_BASE_URL": "https://horizon.corp.example.com" } } } }Use the absolute path to where you cloned the repo. Omit
HORIZON_ACCESS_TOKENfor now — you'll get one in the next step.Restart Claude Code, then get a token by asking it to call
horizon_login(see Getting an Access Token) with your AD credentials.Verify it works — ask Claude Code to call
list_desktop_poolsorget_infrastructure_health. If you get real data back, you're set. Copy theaccess_tokenfrom step 3 intoHORIZON_ACCESS_TOKENin your config so you don't have to log in again on restart.
Running the server standalone over HTTP instead (for remote/multi-user access, or in Docker)? See HTTP (remote / multi-user) and Docker.
Requirements
Python 3.11+
uv (recommended) or pip
Horizon Connection Server 2512 or later
Installation
git clone https://github.com/matt-coppinger/horizon-mcp.git
cd horizon-mcp
uv syncConfiguration
The server reads configuration from environment variables:
Variable | Required | Description |
| Yes | Connection Server URL, e.g. |
| Yes* | Bearer token — obtain via |
| No | Set to |
| No |
|
| No | Bind host for HTTP transport (default |
| No | Port for HTTP transport (default |
| No | HTTP transport only — clients must send |
*HORIZON_ACCESS_TOKEN can also be obtained at runtime by calling the horizon_login tool.
Usage
stdio (Claude Desktop / Claude Code)
Add to your MCP client configuration:
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"horizon": {
"command": "uv",
"args": ["run", "--project", "/path/to/HorizonMCP", "horizon-mcp"],
"env": {
"HORIZON_BASE_URL": "https://horizon.corp.example.com",
"HORIZON_ACCESS_TOKEN": "your-access-token-here"
}
}
}
}Claude Code (~/.claude/settings.json):
{
"mcpServers": {
"horizon": {
"command": "uv",
"args": ["run", "--project", "/path/to/HorizonMCP", "horizon-mcp"],
"env": {
"HORIZON_BASE_URL": "https://horizon.corp.example.com",
"HORIZON_ACCESS_TOKEN": "your-access-token-here"
}
}
}
}HTTP (remote / multi-user)
MCP_TRANSPORT=streamable-http \
MCP_PORT=8000 \
HORIZON_BASE_URL=https://horizon.corp.example.com \
HORIZON_ACCESS_TOKEN=your-token \
horizon-mcpThe server exposes a single endpoint at http://host:8000/mcp.
Set MCP_API_KEY to require clients to authenticate with Authorization: Bearer <MCP_API_KEY>:
MCP_TRANSPORT=streamable-http \
MCP_PORT=8000 \
MCP_API_KEY=your-secret-key \
HORIZON_BASE_URL=https://horizon.corp.example.com \
HORIZON_ACCESS_TOKEN=your-token \
horizon-mcpClients (Claude Desktop, Claude Code) pass the key in their MCP config:
{
"mcpServers": {
"horizon": {
"url": "http://your-server:8000/mcp",
"headers": {
"Authorization": "Bearer your-secret-key"
}
}
}
}stdio transport always skips authentication regardless of MCP_API_KEY.
HTTP transport security: For any non-localhost deployment, place the server behind a reverse proxy (nginx, Caddy, Traefik) that enforces TLS. Each user should run a separate server instance with their own
HORIZON_ACCESS_TOKENandMCP_API_KEYto maintain session isolation.
Docker
The provided Dockerfile runs the server with streamable-http transport (Docker containers don't have an interactive stdio channel for an MCP client to attach to, so HTTP is the practical option here).
docker build -t horizon-mcp .
docker run -d -p 8000:8000 \
-e HORIZON_BASE_URL=https://horizon.corp.example.com \
-e HORIZON_ACCESS_TOKEN=your-token \
-e MCP_API_KEY=your-secret-key \
horizon-mcpOr with docker-compose.yml (reads HORIZON_BASE_URL, HORIZON_ACCESS_TOKEN, HORIZON_VERIFY_SSL, and MCP_API_KEY from your shell environment or a .env file):
HORIZON_BASE_URL=https://horizon.corp.example.com MCP_API_KEY=your-secret-key docker compose up -dMCP_API_KEY is required by docker-compose.yml on purpose — a containerized deployment is reachable over the network by definition, so leaving the endpoint unauthenticated is not a safe default (see Security Notes). Point your MCP client at http://host:8000/mcp with the matching Authorization: Bearer header as shown above.
Getting an Access Token
If you don't have a token yet, omit HORIZON_ACCESS_TOKEN from the config and call horizon_login as the first tool:
Call horizon_login with:
username: jsmith
password: ****** ← treated as a secret, masked in server logs
domain: CORP
base_url: https://horizon.corp.example.comThe tool returns access_token and refresh_token, and immediately activates the new token for the current server session. Copy the access_token value into your MCP client config and restart the server to persist it across restarts.
Security: Treat
access_tokenandrefresh_tokenas passwords. After copying the token to your config, clear it from the conversation context. Do not commit tokens to version control.
Use horizon_refresh_token with the refresh_token to renew the access token (~8 hour expiry) without re-entering credentials.
Available Tools
Auth
Tool | Description |
| Authenticate with AD credentials (password masked in logs), returns access + refresh tokens |
| Refresh an expired access token |
| Invalidate current session |
Inventory
Tool | Description |
| List all VDI and RDS desktop pools |
| Get pool details |
| Create a new desktop pool (VDI or RDS, automated or manual) |
| Update an existing desktop pool's configuration |
| Delete a desktop pool and all its machines ⚠️ — requires |
| Enable/disable a pool, or enable/disable-provisioning |
| List virtual desktops (filterable by pool, state) |
| Get machine details |
| Shutdown, restart, reset, rebuild, recover, maintenance |
| Assign or unassign users to a dedicated (non-floating) desktop |
| List RDS farms |
| Get farm details |
| Create a new RDS farm (automated or manual) |
| Update an existing RDS farm's configuration |
| Delete an RDS farm and all its servers ⚠️ — requires |
| Enable or disable one or more RDS farms — may fail on Horizon instances that strictly enforce the full farm update schema (see docstring) |
| List published application pools |
| Get application pool details |
| Publish a new application pool from an RDS farm |
| Update an existing application pool's configuration |
| Unpublish an application pool ⚠️ — requires |
| List active user sessions |
| Get session details |
| Disconnect sessions (keep running) |
| Log off sessions (terminates apps) |
| Hard-reset or gracefully restart the VMs backing sessions |
| Send pop-up notification to sessions |
Monitor
Tool | Description |
| Health across all components in one parallel call (summary, connection servers, gateways, vCenters, AD domains, farms) |
| Capacity metrics in one parallel call (pools, sessions, machines, system, RDS servers, license) |
| Detailed health for a specific Connection Server |
Config
Tool | Description |
| List connection servers |
| Get connection server config |
| List configured vCenters |
| Environment version and features |
| Global Horizon settings |
| USB, clipboard, multimedia policies |
| License list and status |
| Event DB config |
| Instant clone domain accounts |
| Image management streams, versions, or tags (pass |
| Registered UAGs |
| Trigger Connection Server backup |
Entitlements
Tool | Description |
| All entitlements for desktop or application pools |
| Users/groups for a specific pool |
| Add, replace, or remove entitlements (desktop or application) — |
External / Active Directory
Tool | Description |
| Find AD users and groups |
| Get AD entity details |
| List configured AD domains |
| NETBIOS → DNS domain name map |
| Administrative audit log |
| VMs available for pool base images |
| Snapshots of a base VM (snapshot_id for instant clone pools) |
| Datastores for provisioning (requires vcenter_id + host_or_cluster_id) |
| VM folders in vCenter |
| Datacenters in a vCenter Server |
| Hosts and clusters in a datacenter |
| Resource pools on a host or cluster |
| Network port groups on a host or cluster |
| NICs on a host or cluster (nic_id for pool NIC config) |
| VM templates for full/linked-clone pools |
| Storage DRS datastore clusters (requires vcenter_id + host_or_cluster_id) |
| Sysprep/QuickPrep specs for OS customization during provisioning |
Discovery
Tool | Description |
| Lists all tools, resources, and unsupported operations — call this to understand what can be managed via this server |
Resources (read-only)
MCP Resources expose read-only Horizon data without consuming tool slots. Access them via horizon://<path> using your MCP client's resource protocol.
Config resources (horizon://config/...):
URI | Description |
| RBAC roles and their privileges |
| Role-to-principal permission assignments |
| All selectable admin privileges |
| Local access groups for admin delegation |
| CPA federation access groups |
| SAML 2.0 authenticator configurations |
| RADIUS authenticator configurations |
| GSSAPI/Kerberos authenticator configurations |
| JWT authenticator configurations |
| App Volumes Managers registered with Horizon |
| User Environment Manager servers |
| TrueSSO connector configurations |
| TrueSSO enrollment servers |
| Compute profiles for provisioning |
| Sysprep/QuickPrep specs (config view) |
| General settings |
| Security settings |
| Client feature settings |
| Feature toggle settings |
| Allowed agent versions/types |
| Syslog configuration |
| CEIP enrollment status |
| URL content redirection rules |
| Pre-logon banner/message settings |
| Component log levels |
| Log collection tasks |
Monitor resources (horizon://monitor/...):
URI | Description |
| App Volumes Manager health |
| Event database status |
| RDS server health and session load |
| SAML authenticator health |
| TrueSSO health and certificate status |
| Datastore usage per pool/farm |
| Remote pod health (CPA) |
| Aggregate session counts across pods |
| Message client health |
Help Desk
Tool | Description |
| All session diagnostics in one parallel call: logon timing, display performance, historical performance, processes, remote applications |
| MSRA ticket for remote support |
| Force-close a published app in a session |
Horizon Filter Syntax
Most list tools accept a filter parameter using Horizon's JSON filter format:
// Equals
{"type": "Equals", "name": "state", "value": "AVAILABLE"}
// Contains (string)
{"type": "Contains", "name": "name", "value": "win11"}
// AND combination
{
"type": "And",
"filters": [
{"type": "Equals", "name": "desktop_pool_id", "value": "pool-id"},
{"type": "Equals", "name": "state", "value": "CONNECTED"}
]
}Creating Pools and Farms
create_desktop_pool and create_rdsh_farm accept a spec dict that maps directly to the Horizon REST API request body. The required fields vary by pool type:
Automated Instant Clone desktop pool (minimum):
{
"name": "MyPool",
"display_name": "My Pool",
"type": "AUTOMATED",
"source": "INSTANT_CLONE",
"user_assignment": "FLOATING",
"provisioning_settings": {
"virtual_center_id": "<id from list_virtual_centers>",
"parent_vm_id": "<id from list_base_vms>",
"snapshot_id": "<snapshot id>",
"datacenter_id": "<datacenter id>",
"vm_folder_id": "<id from list_vm_folders>",
"host_or_cluster_id": "<host/cluster id>",
"resource_pool_id": "<resource pool id>",
"datastores": [{"datastore_id": "<id from list_datastores>"}],
"nics": [{"nic_id": "<network id>", "network_label_id": "<network id>"}],
"naming_method": "PATTERN",
"naming_pattern": "MyPool-{n:fixed=2}",
"max_machine_count": 10
}
}Resource ID lookup chain — follow this sequence to resolve all IDs before calling create_desktop_pool or create_rdsh_farm:
list_virtual_centers
├─ list_customization_specifications(vcenter_id) ← Sysprep/QuickPrep spec ID
├─ list_vm_templates(vcenter_id) ← template_id (full/linked-clone pools)
└─ list_datacenters(vcenter_id)
├─ list_vm_folders(vcenter_id, datacenter_id)
└─ list_hosts_or_clusters(vcenter_id, datacenter_id)
├─ list_datastores(vcenter_id, host_or_cluster_id)
├─ list_datastore_clusters(vcenter_id, host_or_cluster_id)
├─ list_resource_pools(vcenter_id, host_or_cluster_id)
├─ list_network_labels(vcenter_id, host_or_cluster_id) ← network_label_id
└─ list_network_interface_cards(vcenter_id, ...) ← nic_id
list_base_vms(vcenter_id) ← parent_vm_id (instant-clone pools)
└─ list_base_vm_snapshots(vcenter_id, base_vm_id) ← snapshot_idcreate_application_pool uses explicit parameters instead — pass name, farm_id, executable_path, and optional fields directly.
For updates, retrieve the current config with get_desktop_pool / get_rdsh_farm / get_application_pool, modify the relevant fields, and pass the result to the corresponding update_* tool.
Delete operations (delete_desktop_pool, delete_rdsh_farm, delete_application_pool) require confirm=True to proceed. Always call get_desktop_pool / get_rdsh_farm and list_sessions first to verify intent before passing confirm=True.
Running Tests
uv run pytest tests/ -vSecurity Notes
Store credentials in your MCP client's
envblock, not in code or config files tracked by git.In production, always keep
HORIZON_VERIFY_SSL=true(default).Passwords passed to
horizon_loginare typed asSecretStrand masked in server-side logs.Access and refresh tokens are returned in the login response so you can copy them to your config — treat them as passwords and clear them from the conversation after use.
For multi-user HTTP deployments, run separate server instances per user and protect the endpoint with a reverse proxy that enforces authentication.
Destructive operations (logoff, rebuild, machine actions) require explicit user confirmation — always verify intent before executing.