Skip to main content
Glama
ellswoau

Omnissa Horizon MCP Server

by ellswoau

Omnissa Horizon MCP Server (Python / FastMCP)

A Model Context Protocol server exposing support/helpdesk operations for an Omnissa Horizon Server 2506 environment, built with Python FastMCP. It targets the Horizon REST API documented at https://developer.omnissa.com/horizon-apis/horizon-server/versions/2506/.

Works both as a direct python -m process and as a Docker container (see "Running via Docker" below).

Features (helpdesk / support)

User & session management

  • find_user_session / list_sessions — find a user's desktop/session(s)

  • restart_user_desktop / restart_session — reboot a user's desktop

  • logoff_user / logoff_session — log a user off (graceful or forced)

  • disconnect_session — detach the display without logging off

  • send_session_message — popup a message to a user's session

Utilization (processes / CPU / memory / network)

  • user_processes / session_processes — running processes + CPU/mem/disk %

  • user_cpu_memory / session_utilization — CPU, memory, disk & latency stats

  • session_network_performance — estimated bandwidth (kbps), round-trip latency and packet loss (PCoIP/BLAST) to diagnose slow/thin sessions

  • end_session_process — terminate a runaway process

Pool & environment health

  • list_desktop_pools / desktop_pool_status — pool health report (total / available / in-use / errors), view model described below

  • list_machines + restart / reset / shutdown / enter-maintenance / exit-maintenance machine

  • monitor_connection_servers / monitor_gateways / monitor_farms / monitor_rds_servers / monitor_ad_domains / monitor_event_database

  • enable_desktop_pool / disable_desktop_pool — re-enable (or disable) a pool and its provisioning after it was stopped by errors / before maintenance

  • desktop_pool_push_history — recent golden-image PUSH_IMAGE tasks per pool

  • rollback_desktop_pool_image — re-push the previous golden-image snapshot to a pool to roll back a bad update, keeping the pool's current compute profile. Defaults to a dry run (confirm=True to schedule)

  • list_sites — the configured connection servers (primary + DR); every tool accepts an optional site argument (primary / secondary / dr / a site name, and all on read-only tools)

  • horizon_ping / horizon_config_summary — connectivity & config

The MCP exposes the most common support/helpdesk actions so an assistant can "restart Jim's hung desktop", "check Bob's CPU/memory", or "is the Sales pool healthy?" directly against Horizon.

Related MCP server: vcf-mcp-sddc-vc

Geometry & requirements

  • Python 3.9+ (tested on 3.12)

  • Install: pip install -r requirements.txt

Configuration (secure service-account credentials)

Credentials are never hard-coded. Provide them via a config JSON file or environment variables (precedence: explicit args > env > file).

cd omnissa-horizon-mcp
python -m omnissa_horizon_mcp config init --config ./horizon.json

This prompts for the Connection Server URL, AD domain, service account username and password (password entered without echoing), and writes the file with 0600 owner-only permissions. Example result:

{
  "base_url": "https://horizon.example.com",
  "domain": "EXAMPLE",
  "username": "svc-horizon-mcp",
  "password": "…",
  "verify_ssl": true
}

Option B — environment variables

export HORIZON_BASE_URL="https://horizon.example.com"
export HORIZON_DOMAIN="EXAMPLE"
export HORIZON_USERNAME="svc-horizon-mcp"
export HORIZON_PASSWORD="…"
export HORIZON_VERIFY_SSL="false"        # default true; set false for test CAs
export HORIZON_TIMEOUT="30"
# optional, to point at a config file:
export HORIZON_CONFIG_FILE="./horizon.json"

A .env file is also honored if python-dotenv is installed and loaded (see .env.example). See omnissa_horizon_mcp/config.py for all HORIZON_* vars.

Multiple connection servers (primary + DR site)

The environment may span more than one Horizon Connection Server (e.g. a primary datacenter plus a DR site). Add a sites list to the config file, or set HORIZON_SITES to the same JSON array. Each entry may override the top-level credentials; anything it omits is inherited.

{
  "base_url": "https://cloud.wellertruck.com",
  "domain": "WELLER",
  "username": "horizonaiagent",
  "password": "…",
  "verify_ssl": true,
  "sites": [
    {"name": "primary",   "base_url": "https://cloud.wellertruck.com",   "role": "primary"},
    {"name": "secondary", "base_url": "https://cloud-b.wellertruck.com", "role": "secondary"}
  ]
}

Every tool then accepts an optional site argument:

  • omitted / primary -> the default (base_url) connection server

  • secondary / dr -> the DR site

  • a configured name -> that site

  • all -> read-only listing/monitoring tools query every site (a dict keyed by site name)

list_sites shows the configured sites. Each site keeps its own inventory and golden images, so pool/snapshot/rollback lookups always resolve against the selected site.

Verify credentials before connecting to an MCP client

python -m omnissa_horizon_mcp ping --config ./horizon.json

Running the MCP server

# stdio transport (used by Claude Desktop / MCP clients)
python -m omnissa_horizon_mcp --config ./horizon.json   # note: --config passthrough

By default the server resolves config from HORIZON_CONFIG_FILE / env vars. To point at a specific file, set export HORIZON_CONFIG_FILE=./horizon.json before launching, and add a .env load if desired.

Example MCP client config (Claude Desktop claude_desktop_config.json)

{
  "mcpServers": {
    "omnissa-horizon": {
      "command": "python",
      "args": ["-m", "omnissa_horizon_mcp"],
      "env": {
        "HORIZON_CONFIG_FILE": "/abs/path/to/horizon.json"
      }
    }
  }
}

How desktop_pool_status computes "available" vs "used"

It combines three sources:

  • /inventory/v1/desktop-pools — pool metadata (id, name, type, source, enabled)

  • /inventory/v1/machines — machines grouped by pool; non-AVAILABLE/erroring machines are excluded from availability

  • /inventory/v1/sessions + /monitor/desktops — in-use session count and the pool's aggregated monitor status (OK/ERROR/…)

Result summary: total_machines, total_in_use_sessions, total_errors, plus per-pool machines_by_state, in_use_sessions, available_estimate and any cloning/state errors.

Horizon 2506 API notes

A few Horizon REST details that this server handles explicitly (verified against the 2506 OpenAPI spec and the Horizon Server REST Pagination, Filter and Sorting Guide):

  • Inventory filters use a JSON filter object, not the field 'value' string form. list_sessions / list_machines build e.g. {"type":"Equals","name":"desktop_pool_id","value":"…"}, chained with an "And" wrapper for multiple clauses. Field/operator choices: user_name (Contains), desktop_pool_id (Equals), session_state (Equals) for sessions; name (Contains), desktop_pool_id (Equals), state (Equals) for machines.

  • Sessions are listed from /inventory/v7/sessions (not v1): the base SessionInfo model returned by /inventory/v1/sessions has no user_name field (only the raw user_id SID), so it cannot be filtered or displayed by username. user_name is added from the versioned session models (v4+); v7 is the current 2506 model. find_user_session therefore filters server-side on user_name (Equals/Contains) and falls back to an unfiltered client-side scan. If sessions are visible but none expose a user_name, the tool raises a clear error about the likely missing service-account privilege instead of silently reporting zero matches.

  • Helpdesk performance calls use /helpdesk/v1/... with session_id (historical-data, process, display-protocol, and remote-process/action/end-remote-process). The /helpdesk/v2/... endpoints take internal_session_id, a different internal identifier that rejects an inventory session id (helpdesk.session.find.error).

How rollback_desktop_pool_image chooses the snapshot

  1. Resolve the pool by name/id and read its v7 provisioning_settings: current parent_vm_id + base_snapshot_id, and the compute profile (compute_profile_num_cpus, compute_profile_num_cores_per_socket, compute_profile_ram_mb).

  2. Read the pool's push history (/inventory/v1/desktop-pools/{id}/tasks) and pick the most recent distinct pushed image that is not the current one.

  3. If that image no longer exists on the golden image (its snapshot was deleted), fall back to the golden image's snapshot chronology -- the snapshot created immediately before the pool's current snapshot.

  4. Re-push it via POST /inventory/v1/desktop-pools/{id}/action/schedule-push-image.

Shared golden images (pool_image_filters). When several pools share one golden image but each must use its own snapshot family (e.g. bos1 uses the 1GB snapshots, bos2 the 2GB ones), add per-pool rules so the fallback can't cross over. A rule applies when its pool regex matches the pool name; a candidate snapshot is accepted only when its name contains the require text (the parent path is deliberately not matched, since it can contain the other pool's token):

"pool_image_filters": [
  {"pool": "bos1", "require": "1GB"},
  {"pool": "bos2", "require": "2GB"}
]

(Pools matching no rule are unrestricted. HORIZON_POOL_IMAGE_FILTERS sets the same list via env.)

The endpoint does not accept a compute profile, so the pool's current vCPUs / cores-per-socket / RAM are preserved. The tool returns the resolved plan (current image, target image, compute profile, request body and how the target was chosen) and only schedules the push when confirm=True.

Pushing an image is a maintenance operation: existing sessions are logged off (per logoff_policy) and the pool is rebuilt. Confirm the target first.

Security notes

  • Passwords are never logged; redacted() masks them.

  • All requests use verify_ssl (on by default). Set false only for self-signed/test environments.

  • Mutating operations (restart, logoff, reset, end_session_process) require the appropriate Horizon privileges; check the Connection Server roles of the service account.

Running via Docker

The project ships a Dockerfile, docker-compose.yml and .dockerignore. Run as a non-root user; the app runs over stdio by default (embedded MCP client) and can also run as a long-lived HTTP/SSE daemon.

Build

docker build -t omnissa-horizon-mcp .

Option A — stdio (embedded MCP client, e.g. Claude Desktop)

Create your config file with config init, then run the container interactively so stdio stays attached; pass credentials via env or a mounted config file:

docker run -i --rm \
  -e HORIZON_CONFIG_FILE=/config/horizon.json \
  -v "$(pwd)/horizon.json:/config/horizon.json:ro" \
  omnissa-horizon-mcp

Point the MCP client at the container, e.g. Claude Desktop:

{
  "mcpServers": {
    "omnissa-horizon": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "-e", "HORIZON_CONFIG_FILE=/config/horizon.json",
               "-v", "/abs/path/horizon.json:/config/horizon.json:ro",
               "omnissa-horizon-mcp"]
    }
  }
}

Or pass secrets via -e instead of mounting a file:

docker run -i --rm \
  -e HORIZON_BASE_URL=https://horizon.example.com \
  -e HORIZON_DOMAIN=EXAMPLE \
  -e HORIZON_USERNAME=svc-horizon-mcp \
  -e HORIZON_PASSWORD=*** \
  -e HORIZON_VERIFY_SSL=false \
  omnissa-horizon-mcp

File-permission note: the container runs as an unprivileged user, so a mounted horizon.json must be world/group-readable for it to open, or it will fail on permissions. Prefer env vars, or chown the file to the container UID, if you hit Permission denied.

Option B — network daemon (HTTP / SSE / streamable-http)

docker run -d --name horizon-mcp -p 8000:8000 \
  -e HORIZON_CONFIG_FILE=/config/horizon.json \
  -v "$(pwd)/horizon.json:/config/horizon.json:ro" \
  omnissa-horizon-mcp --transport http --host 0.0.0.0 --port 8000

or with Compose (bundled):

docker compose up -d --build

Then connect an MCP client that supports HTTP/SSE to http://<host>:8000/mcp. Available transports: stdio (default), sse, streamable-http, http.

Verify the daemon is up

curl -i http://localhost:8000/health        # HTTP 200 + JSON status
curl -i http://localhost:8000/healthz       # alias
curl -i http://localhost:8000/mcp           # expect JSON-RPC "missing session ID"

A health check that requires no Horizon credentials and does not block on a login is exposed at /health (alias /healthz) whenever the daemon transport (http / sse / streamable-http) is running:

{"status":"ok","service":"omnissa-horizon-mcp","version":"0.1.0","uptime_seconds":123,"healthy":true}

The Docker HEALTHCHECK and the bundled docker-compose.yml healthcheck both hit /health automatically.

Authentication (bearer token)

The network transport is gated behind a bearer token when configured. With a token set, every endpoint except /health and /healthz requires Authorization: Bearer <token> and returns 401 otherwise.

Set it via the env var HORIZON_MCP_AUTH_TOKEN or the --token flag. Generate a strong one:

export HORIZON_MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
docker run -d --name horizon-mcp -p 8000:8000 \
  -e HORIZON_CONFIG_FILE=/config/horizon.json \
  -e HORIZON_MCP_AUTH_TOKEN="$HORIZON_MCP_AUTH_TOKEN" \
  -v "$(pwd)/horizon.json:/config/horizon.json:ro" \
  omnissa-horizon-mcp --transport http --host 0.0.0.0 --port 8000

Clients must then send the header on every request:

curl -H "Authorization: Bearer $HORIZON_MCP_AUTH_TOKEN" http://host:8000/mcp

Notes:

  • When HORIZON_MCP_AUTH_TOKEN is unset/empty, auth is disabled (open), preserving the default behaviour. The bundled docker-compose.yml requires the variable so daemon deployments are secured by default.

  • If the token is ever compromised, rotate it and redeploy — the running process does not cache it across restarts.

  • /health stays public so load balancers / uptime monitors can probe liveness without a secret.

Project layout

omnissa-horizon-mcp/
├── README.md
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
└── omnissa_horizon_mcp/
    ├── __init__.py
    ├── __main__.py          # python -m entrypoint
    ├── server.py            # FastMCP app + CLI (config init / ping / run)
    ├── config.py            # secure config & credential resolution
    ├── client.py            # Horizon REST client (auth, pagination, errors)
    └── tools/
        ├── __init__.py      # registers all tool modules
        ├── _common.py       # user-session helpers
        ├── connection_tools.py
        ├── session_tools.py
        ├── utilization_tools.py
        ├── pool_tools.py
        └── machine_tools.py

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants to manage and monitor VergeOS virtualization platforms through natural language, including VM operations, network management, tenant administration, and cluster monitoring.
    21
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables natural language interaction with VMware SDDC Manager and vCenter APIs through MCP tools, allowing users to query workload domains, VMs, clusters, and more.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables management of Omnissa Horizon VDI infrastructure through the Horizon REST API, offering tools for inventory, monitoring, configuration, entitlements, Active Directory, and help desk operations.
    72
    1
    MIT