Hmc
FreeNot checkedMCP server and CLI for IBM HMC REST API, enabling AI agents to inventory Power systems, manage LPARs/VIOS, and submit jobs like power on/off, with tools for ada
About
MCP server and CLI for IBM HMC REST API, enabling AI agents to inventory Power systems, manage LPARs/VIOS, and submit jobs like power on/off, with tools for adapters, storage, networking, and more.
README
MCP server and CLI for the IBM Hardware Management Console (HMC) REST API
(/rest/api/uom/...). It lets you — or an AI agent over MCP — inventory
Power systems, inspect LPARs/VIOS, and submit jobs such as power on/off.
Stack
- Python ≥3.11, managed with uv. The project supports every stable, non-EOL CPython release at or above that floor.
- MCP server: FastMCP (stdio or streamable HTTP)
- CLI: Typer + Rich tables
- REST transport: httpx (async), XML parsed with defusedxml
- CLI passthrough: asyncssh — tools that
shell out to HMC CLI commands (
lssyscfg,lshwres,chsyscfg, ...) over SSH
Contributing, security, and license
See Contributing for the development and pull-request path. Report suspected vulnerabilities through the private channel in the Security policy. This project is available under the License.
Install
For reusable-library use, install the bare project dependencies:
cd ~/src/hmc-mcp
uv sync --no-dev
To use the CLI or MCP server, install the app extra as well:
cd ~/src/hmc-mcp
uv sync --no-dev --extra app
Reusable Python API
Import reusable library code only from hmc_mcp.api. This example reads connection settings from
the HMC_* environment variables described below, constructs a client, and runs an exported domain
operation:
import asyncio
from hmc_mcp.api import HMCClient, HMCConfig, capacity_report
async def main() -> None:
async with HMCClient(HMCConfig()) as hmc:
for system in await capacity_report(hmc):
print(system)
asyncio.run(main())
hmc_mcp.api is the only supported reusable-library import path. Its explicit __all__ is the
complete compatibility manifest. For HMCClient, only __init__, __aenter__, __aexit__,
is_logged_on, logon, and logoff are supported lifecycle members. Other import paths, generic
UOM helpers, inherited mixin methods, XML and parser helpers, SSH primitives, and CLI and MCP
presentation modules are implementation details. They may remain importable or discoverable, but
they are unsupported and may change without a compatibility release.
The distribution ships a PEP 561 py.typed marker, so a type-checker reads the facade's inline
annotations instead of treating every value as Any. That covers exactly the surface __all__
declares: each export's call signature, the fields and constructor of each exported package-owned
model, each exported exception type, and the members and values of each exported enum and literal
alias. Modules outside hmc_mcp.api carry annotations too, but they are implementation details and
their types are not part of the contract.
What the marker does not do is make the open-ended HMC payloads specific. Operations that return a
raw resource mapping are annotated dict[str, Any], and ADR 0029 keeps them that way deliberately
so an IBM-side field addition is not a breaking change — the call is typed, the payload contents
stay opaque. Operations that return a package-owned result model are typed all the way down.
One consequence is worth planning for: the operations annotate the concrete HMCClient, so a
type-checker now rejects a duck-typed fake passed in a consumer's own tests even though the call
still runs. ADR 0029 deliberately promises no alternate-client protocol, so pass such a fake through
typing.cast(HMCClient, fake) at the call site, or silence that call with
# type: ignore[arg-type].
While hmc-mcp is in 0.x, strict SemVer applies to this supported surface: removing or renaming an
export, invalidating a compatible call, changing an owned model incompatibly, changing an exported
enum or literal value set, or adding a facade export requires a minor release. Patch releases are
limited to compatible fixes that change neither the export set nor enum and literal value sets.
See ADR 0029 for the complete contract.
Configure
Configuration priority (highest to lowest): CLI flags > HMC_* env vars > TOML profile.
TOML profile (recommended for multi-HMC setups)
Create ~/.config/hmc-mcp/config.toml (Linux / macOS ~/Library/Application Support/hmc-mcp/config.toml / Windows %APPDATA%/hmc-mcp/config.toml):
default_profile = "prod"
[profiles.prod]
host = "hmc.example.com"
port = 443 # optional; omit to allow legacy fallback
user = "admin"
password_env = "HMC_PROD_PASSWORD" # resolved from the environment at runtime # pragma: allowlist secret
[profiles.dev]
host = "hmc-dev.example.com"
user = "devadmin"
password = "devpassword" # or store inline for non-production # pragma: allowlist secret
Select a profile with --profile <name> or HMC_PROFILE=<name>.
password_env keeps secrets out of the file; password is accepted for convenience.
Profile nicknames (friendly names)
When you run more than one HMC, remember the exact [profiles] key for every
--profile / HMC_PROFILE call. A top-level nicknames table maps a friendly
name to an existing profile key, so a memorable name resolves to a profile:
[nicknames]
big-iron = "prod"
staging = "stg-hmc-03"
big-iron and staging now work anywhere a profile name does — --profile big-iron, HMC_PROFILE=staging, and even default_profile = "big-iron" —
because resolution is a single name-selection step inside the profile loader, so
the CLI and every MCP tool inherit it with no per-tool change.
Rules:
- One level deep. A nickname resolves to a profile key; it never resolves to another nickname (no chains, no cycles).
- Case-sensitive.
big-irondoes not matchBIG-IRON. - Profile key wins on collision. A name that is both a profile key and a nickname key selects the profile.
- Clear failures. A nickname whose target is not a profile, an unknown name,
or a malformed
nicknamestable raises aConfigErrornaming the available profiles and nicknames.
Nicknames are surfaced, not hidden. config list prints each nickname as
nick -> target (flagging a dangling target), config show <nick> shows the
resolved profile with a resolved_from field naming the nickname, and
hmc_list_configured_hosts reports each nickname and its target-existence — none
of which resolves a secret. config init scaffolds a commented nicknames
example. A guardrail (just nicknames, in just verify) validates a committed
fixture: every nickname target exists, no nickname collides with a profile key,
and no target is itself a nickname.
Environment variables (single-HMC / MCP server)
| Setting | Env var | CLI flag | Default |
|---|---|---|---|
| Profile | HMC_PROFILE |
--profile |
— |
| HMC host / IP | HMC_HOST |
--host |
— |
| REST port | HMC_PORT |
— | 443 |
| User | HMC_USER |
--user, -u |
— |
| Password | HMC_PASSWORD |
--password, -p |
— |
| Verify TLS | HMC_VERIFY_SSL |
--verify-ssl |
false |
| HTTP timeout (s) | HMC_TIMEOUT |
— | 60.0 |
| SSH timeout (s) | HMC_SSH_TIMEOUT |
— | 300.0 |
| SSH key file | HMC_SSH_KEY_FILE |
— | — |
| Audit memento | HMC_AUDIT_MEMENTO |
— | hmc-mcp |
| Schema version | HMC_SCHEMA_VERSION |
— | (unset) |
When the REST port is omitted, hmc-mcp tries port 443 and retries logon once on
legacy port 12443 only if the first attempt fails at the transport layer. Setting
port in TOML or HMC_PORT selects that port explicitly: a connection failure
is returned immediately and never falls back. On an older HMC, leaving the port
unset can therefore add the duration of the failed 443 attempt. HMC_TIMEOUT
applies to each HTTP timeout phase rather than to the combined two-attempt wall
clock time. If a 443 logon response is lost, that unreachable attempt may leave
a server-side session until the HMC expires it.
See docs/environment-variables.md for the full reference, including descriptions and usage notes.
HMCs ship self-signed certificates, so TLS verification is off by default and
hmc-mcp warns on every logon while it stays off. To verify the HMC
certificate, install its CA locally and set HMC_VERIFY_SSL=true
(--verify-ssl) — otherwise the HMC credentials are at risk of
man-in-the-middle interception.
HMC version compatibility
hmc-mcp targets HMC V8 through V11 and all the POWER generations they
manage. All uom XML documents are written with schemaVersion="V1_0" — the
floor every supported HMC understands — so create/modify operations succeed
regardless of firmware age.
| HMC version | POWER generations managed | uom schema floor |
|---|---|---|
| HMC V8 | POWER6, POWER7, POWER8 | V1_0 |
| HMC V9 | POWER7, POWER8, POWER9 | V1_0 |
| HMC V10 | POWER8, POWER9, POWER10 | V1_0 |
| HMC V11 | POWER9, POWER10, POWER11 | V1_0 |
Three VIOS backup catalog tools have a narrower floor:
hmc_list_vios_backups, hmc_backup_vios, and hmc_restore_vios require
HMC V10 or newer. Their supported HMC commands do not exist in the V9.1.940
command inventory, so these tools have no runtime version probe or V8/V9
fallback. Other tools retain the general HMC V8 through V11 support stated
above.
HMC_SCHEMA_VERSION — leave this unset for normal operation.
hmc-mcp omits the X-HMC-Schema-Version request header from all write
paths (PUT/POST) regardless of this setting — some HMC firmware versions
return HTTP 406 on every UOM write when that header is present. The variable
only affects GET requests. Set it only if you are debugging schema
negotiation on a specific HMC read path; it has no effect on LPAR creation,
adapter configuration, storage operations, or any other mutating call.
See docs/environment-variables.md for all
supported variables.
Firmware write-path compatibility
Some HMC V10 firmware builds return HTTP 406 for all UOM write paths — even
without the schema-version header — for child-resource endpoints such as
ClientNetworkAdapter and VirtualSCSIClientAdapter PUT. On those builds:
- LPAR creation (
hmc_create_lpar,hmc_provision_lpar): automatically falls back tomksyscfgover SSH.HMC_PASSWORD(orHMC_SSH_KEY_FILE) must be set for SSH auth; the fallback is transparent to the caller. - Virtual adapter attachment (
hmc_add_network_adapter,hmc_add_vscsi_adapter): no automatic fallback. Configure adapter profiles via the HMC GUI, the HMC CLI (chhwres), or the opt-inhmc_run_commandescape hatch if this affects your firmware. - Virtual disk creation (
hmc_create_virtual_disk): no automatic fallback. The disk can be created directly on the VIOS withmkbdspand then mapped withhmc_map_storage_to_lpar.
CLI usage
hmc-mcp console info # connectivity check / HMC version
hmc-mcp systems list # table of managed systems
hmc-mcp systems show <uuid>
hmc-mcp systems summary <uuid> # one-call summary: state, MTMS, firmware, LPARs, free resources
hmc-mcp systems health # exception-only fleet health; add --json for automation
hmc-mcp lpars list # all LPARs
hmc-mcp lpars list --system <uuid> # LPARs of one system
hmc-mcp lpars show mylpar # by name or UUID (JSON)
hmc-mcp lpars state mylpar # just "running", "not activated", ...
hmc-mcp lpars summary mylpar # one-call summary: state, RMC, memory, CPU, OS, adapters
hmc-mcp lpars get-minimum-affinity-policy mylpar sys1 --json
hmc-mcp lpars system-memopt-score sys1
hmc-mcp lpars plan-memopt-scores sys1 --prioritize-name web --exclude-name batch
hmc-mcp lpars plan-system-memopt-score sys1 --prioritize-id 3 --exclude-id 9 --json
hmc-mcp lpars create web01 --system <uuid> --mem 8192 --vcpus 2
hmc-mcp lpars modify web01 --mem 16384 --procs 2.0 # assign resources
hmc-mcp lpars delete web01 # destroy (must be powered off)
hmc-mcp lpars decommission web01 --system <uuid> --dry-run # preview blast radius
hmc-mcp lpars power-on mylpar # submits a PowerOn job (asks first)
hmc-mcp lpars power-off mylpar --immediate
hmc-mcp adapters list mylpar # network adapters (default type)
hmc-mcp adapters list mylpar --type VirtualSCSIClientAdapter
hmc-mcp adapters add-network mylpar --vlan 100 # add a NIC on VLAN 100
hmc-mcp adapters add-vscsi mylpar --vios-id 1 --vios-slot 5
hmc-mcp adapters add-vfc mylpar --vios-id 1 --vios-slot 6
hmc-mcp adapters delete mylpar --type ClientNetworkAdapter --uuid <adapter-uuid>
hmc-mcp vios list
hmc-mcp jobs list # recent jobs (default 20)
hmc-mcp jobs list -n 5 # last 5 jobs
hmc-mcp jobs show <job-uuid>
hmc-mcp raw get /rest/api/uom/VirtualSwitch # escape hatch, prints XML
Add --json to list commands for machine-readable output. Composite workflows
such as hmc-mcp lpars decommission and hmc-mcp lpars provision return a
stable envelope with workflow_completed, ordered steps, warnings, and the
blast-radius or provisioning summary, which is friendlier for automation than
raw HMC payloads. Every entry is the parsed uom resource:
{UUID, title, link, ResourceType, Resource} where Resource is the flattened
XML (namespace-stripped, HMC bookkeeping attributes removed).
The memory-affinity planning commands are read-only lsmemopt calculations.
Repeat --prioritize-name, --prioritize-id, --exclude-name, or --exclude-id
to describe a scenario, using names or IDs consistently. Calculated scores are
predictions, not guarantees of placement, and these commands never start optimization.
The minimum-affinity policy command is also read-only. It first checks whether the managed
system advertises POWER11 processor compatibility, then explicitly requests
min_affinity_score and min_affinity_score_action through lssyscfg. The score is validated
as an integer from 0 through 100 and the action as none, warn, or fail. Systems without
that capability remain usable and return an actionable capability-unavailable reason. This
surface does not provide a setter; portable snapshots record the policy only when supported.
Portable snapshots capture one named LPAR profile together with source identity and separate timestamped placement and affinity observations:
hmc-mcp snapshot capture sys1 aix1 default --output aix1.snapshot.json
hmc-mcp snapshot validate aix1.snapshot.json
hmc-mcp snapshot inspect aix1.snapshot.json
Capture refuses to overwrite an existing local file. Validation and inspection are local and perform no HMC I/O. Snapshots do not expose a replay command; observation data is diagnostic and never part of the replayable profile configuration.
hmc-mcp lpars decommission enforces the ADR 0011 ownership token even for
--dry-run; use --ownership-override only after explicit operator approval.
MCP server
--access-policy NAME is required. The server refuses to start without one
rather than serving unbounded. If you are upgrading, generate a policy matching what
your server exposed before, read it, then select it — see
Migrating to a required access policy.
# Once: write a policy for review. It activates nothing and never overwrites.
hmc-mcp config init-access-policy
hmc-mcp serve --access-policy lab # stdio — what MCP clients/agents expect
hmc-mcp serve --access-policy lab --http --listen-host 127.0.0.1 --port 8000
# Explicitly enable the arbitrary-command MCP escape hatch when required. The policy
# must also grant hmc_run_command by name; the flag alone is not enough:
hmc-mcp serve --access-policy lab --enable-arbitrary-command
hmc-mcp serve --access-policy lab --audit-level WARNING # authorization denials only on stderr
--access-policy NAME enforces the named policy from the platform-native
access-policy.toml: the server registers only the tools that policy permits, so
a withheld tool never appears in tools/list and cannot be called by name. Omitting
it is a usage error (exit 2) that starts nothing; a policy that is selected but
cannot be read, parsed, or compiled — or whose file does not exist — exits 1 and
starts nothing. Both refusals name the generator and the file they looked for.
All three dimensions — tools, connections, and targets — are enforced, and a
call is permitted only when a single grant covers all three together. A
grant that names your connection and a different grant that names your target
do not combine. Every refusal happens before the server opens any connection to
an HMC. Call hmc_effective_permissions on a running server to see what the
selected policy actually applies.
Connection scope is decided on the connection the call will actually select,
not on the profile string the caller passes, so a nickname is resolved to the
profile it targets before the check. Three consequences are worth knowing
before you author a policy:
connectionsentries must be profile keys, not nicknames. A nickname is resolved away before the comparison, so granting one never matches.- When
HMC_HOSTis set, the server can reach exactly one HMC and theprofileargument is ignored — that is howbuild_confighas always resolved. Every call is therefore evaluated as<default>, so grantconnections = ["<default>"]; a policy naming profile keys denies everything in that deployment, and says so in the denial. <default>binds late, and it binds to whatever the deployment resolves. It is not a fixed HMC: absentHMC_HOSTit followsHMC_PROFILE, thendefault_profile— which may itself be a nickname, so the granted connection can be two hops from anything written in the policy. Granting<default>beside a narrow profile list therefore also grants the current default, even when that is a profile the policy deliberately withholds. Do not grant it unless the deployment's default is a connection you mean to allow.
Omitting profile means <default>, which is not covered by a grant naming
the profile that happens to be the deployment default — grant both if callers
may omit the argument.
What the policy does not bound
The access policy bounds this MCP server. It does not bound hmc-mcp
commands run at a shell, and it does not bound a Python program importing the
supported reusable API — both reach the HMC directly under the operator's own
credentials, and
ADR 0029 places MCP
tools, CLI commands, and the server composition modules outside that API's
contract for the same reason. If you need a constraint that binds a human at a
shell, use HMC-side user roles.
Every MCP tool is dispatch-wrapped.
For a connectionless tool, the connection dimension is vacuous because the call
selects no profile to authorize. A grant must still reach the tool by tool or
effect class, and target authorization still applies. hmc_list_configured_hosts
returns every configured profile's name, host, user, and default flag, while
hmc_effective_permissions returns the policy's own grants. Both are
non-exhaustive connectionless tools, so a targets table cannot authorize either
one; they require targets = "all-targets". A connections = ["lab"] read grant
with all-targets can therefore still disclose the prod inventory. When the
configuration or policy is sensitive, withhold these tools by name: enumerate
the permitted tools without granting the read effect class.
Migrating to a required access policy
An access policy used to be optional; a server started without one exposed every tool on every configured connection. It is now required, so an existing deployment that upgrades without one stops serving. Two commands and a read:
# 1. Write it. This activates nothing, and prints the path it wrote to stdout.
hmc-mcp config init-access-policy
# 2. Read it. It is one grant, and the tools array is the whole of your exposure.
$EDITOR ~/.config/hmc-mcp/access-policy.toml # macOS: ~/Library/Application Support/hmc-mcp/
# 3. Select it, in whatever launches your server.
hmc-mcp serve --access-policy legacy-equivalent
The generated legacy-equivalent policy grants exactly what the unpolicied server granted:
every ordinary tool, every configured connection plus <default>, and
targets = "all-targets". It is a migration aid, not a recommended posture. A new
deployment should start from the read-only example below and add what it needs — the
generated policy is the widest one this system can express.
Six things are worth knowing before you run it.
hmc_run_commandis not in it. The escape hatch stays a separate decision:--enable-arbitrary-commandalone never exposed it, and a generated grant that named it would undo that. If you ran with the flag, addhmc_run_commandto the grant'stoolsby hand — and note it will show as a deletion in every future regeneration diff, because the generator cannot emit it.Run it as the identity, and with the environment, that
serveruns under. Both resolveaccess-policy.tomlthrough the same config directory, and the connection list is read from that identity'sconfig.toml. Generating as your login user and serving as a systemdUser=or a container uid writes a policy the server never reads, naming profiles it does not have.A container or unit needs a resolvable
HOMEorXDG_CONFIG_HOME. Under a uid with no passwd entry and neither variable set, the path cannot be resolved and the server cannot start at any setting.It never overwrites. To check a deployed policy after an upgrade, run:
hmc-mcp config diff-access-policy ~/.config/hmc-mcp/access-policy.tomlThis renders what the current build plus your current
config.tomlwould generate and prints a unified diff against the deployed policy, exiting non-zero on any difference — see Detecting access-policy drift below. Compare both thetoolsand theconnectionsarrays in that diff. The policy is a snapshot of each: a tool a later release adds, and a profile you add toconfig.toml, are both ungranted until you add them to the deployed file by hand. Nothing in a running server surfaces either gap —hmc_effective_permissionsreports what was registered, which is exactly what the policy produced — so this diff is the detection path.If
servereports a policy that will not compile and the generator reports the file already exists, the file is truncated or corrupt. Delete it and re-run the generator.config.tomlandaccess-policy.tomlare different files with different jobs.config.tomlholds HMC connection profiles — which consoles you can reach, and how.access-policy.tomlholds server access policies — what an MCP server may do with them. They have separate lifecycles, and a grant'sconnectionsentries are profile keys fromconfig.toml, never profile contents.
Detecting access-policy drift
A generated policy is a snapshot of two things that both move on: the tool surface of
the build that ran the generator, and the profile keys in config.toml. After an
upgrade adds a tool, or you add [profiles.newsite] and restart, the deployed policy
grants neither — silently, because hmc_effective_permissions reports only the
registered set, which is exactly what the policy produced.
config diff-access-policy makes the comparison routine instead of something an
operator has to remember to do:
hmc-mcp config diff-access-policy ~/.config/hmc-mcp/access-policy.toml
It renders the legacy-equivalent policy exactly as config init-access-policy would —
same generator, same config-directory resolution, so run it as the identity and with
the environment serve runs under — and prints a unified diff against the deployed
document. Exit codes: 0 identical, 1 different (the diff goes to stdout, so a CI
log captures it directly), 2 usage error, 3 the deployed file could not be read,
4 generation failed. A CI gate or health check asserts on exit status 1.
The command compares full documents, so a deliberately narrow authored policy always
differs from the generated one — point it at deployments running the generated
legacy-equivalent policy, or at a copy of it.
Policies live in access-policy.toml, beside config.toml in the same
platform-native directory. A minimal read-only policy:
[[policies.lab.grants]]
effects = ["read"] # "read", "mutate", "destructive"
connections = ["<default>"] # profile names, or "<default>" for the env HMC
targets = "all-targets" # or a table; see "Narrowing targets" below
A grant must name at least one tool through effects, tools, or both, and
must name at least one connection. targets is either the string
"all-targets" or a table of target kind to selector strings — a bare array is
rejected. hmc_run_command cannot be reached by effect class: name it in a
grant's tools to grant it, start the server with --enable-arbitrary-command
as well, and grant it under "all-targets", since it declares no target
selector. All three are required, and they compose conjunctively.
A limited-mutation policy — read anywhere the deployment reaches, but change only the lab, and never destroy anything:
[[policies.limited.grants]]
effects = ["read"]
connections = ["lab", "prod", "<default>"]
targets = "all-targets"
[[policies.limited.grants]]
effects = ["mutate"] # "destructive" is deliberately absent
connections = ["lab"]
targets = "all-targets"
And the legacy-equivalent policy, which hmc-mcp config init-access-policy writes in full
— every ordinary tool named explicitly, which is why it is generated rather than typed:
[[policies.legacy-equivalent.grants]]
tools = [
"hmc_add_network_adapter",
"hmc_add_vfc_adapter",
# ... every ordinary tool is named. hmc_run_command is not among them.
]
connections = ["<default>", "lab", "prod"] # every profile key in config.toml
targets = "all-targets"
It names tools rather than granting effects deliberately: an effect-class grant would
silently confer every tool a later release adds, which is the silent privilege retention
the generator exists to replace.
Narrowing targets
Substituting a table for "all-targets" is a much stronger statement than it
looks, so a narrowed policy is normally two grants:
# Exactly one partition, on exactly one system.
[[policies.lab.grants]]
tools = ["hmc_delete_lpar", "hmc_power_off_lpar"]
connections = ["lab"]
targets = { managed_system = ["Server-9080-HEX-SN123456"], lpar = ["scratch-01"] }
# Everything console-wide, which no table can express.
[[policies.lab.grants]]
effects = ["read"]
connections = ["lab"]
targets = "all-targets"
Four rules explain why:
Every selector a tool declares must be supplied and must match — not only the required ones.
hmc_power_off_lpartakes an optionalsystem_name_or_uuid; omitting it means "whichever system has a partition by that name", which amanaged_systemallowlist did not grant, so the call is refused. Likewisehmc_list_lparswithout a system means every system.Matching is exact string equality. No globs, no case folding, and no name-to-UUID resolution — resolving would mean asking the HMC inside the check that is supposed to run before any HMC request. A policy written in names does not cover a call written in UUIDs, or the reverse.
A table never grants a tool it cannot bound. Some tools declare no target selector at all (
hmc_list_systems,hmc_run_command— they act on the console, whose identity is the connection). Others act on something the selectors do not name:hmc_provision_lparmutates a VIOS chosen inside itsstorageargument, the LPAR-profile backup and restore pair write an arbitrary path on the HMC's own filesystem, three adapter tools take a VIOS partition ID (a slot number reused on every system in the fleet), and the two job tools accept ajob_hrefwhose path replaces the job UUID outright. Each of those needs a grant whose targets are"all-targets". So dohmc_effective_permissionsandhmc_list_configured_hosts, which read local state rather than an HMC and so have nothing a table could bind either.hmc_effective_permissionsreportsexhaustive_targets: falsefor every one of them. Naming any such tool in a grant'stoolsbeside a table is refused at startup, with no exceptions.A table-only policy therefore denies
hmc_effective_permissionsitself, so a client has no way to ask the server what it may do. Give it the second grant above — that is what theeffects = ["read"]/"all-targets"grant in the example is for — or expect the startup warning naming it.An LPAR name is unique within a system, not across the fleet. Every tool that takes an
lpar_name_or_uuidalso takes an optionalsystem_name_or_uuid, and every LPM tool names its source system the same way (ADR 0063), so the rule above pins the system on all of them: under a table, a call that omits the selector is refused, and one allowlist entry must match both migration endpoints. Where you grant a tool under"all-targets", or call it outside a table-constrained policy and omit the selector, a partition name is still matched on whichever system has one — list partition UUIDs there, which are unique across the fleet.
Startup warnings
serve writes these to stderr, never stdout, which carries JSON-RPC on stdio — with
one caveat that applies to the audit records below as well: a launcher merging the
descriptors (serve 2>&1, or a unit file doing the same) makes stderr become the
JSON-RPC channel, and nothing inside the process can detect that.
| Condition | What it means |
|---|---|
| The served surface has no tools | The policy withholds everything reachable; nothing the server is asked to do will succeed. Suppresses the next line. |
The policy withholds hmc_effective_permissions |
The server cannot report its own permissions to a client. Any policy that neither grants the read effect class nor names the tool in a grant's tools causes this. |
--enable-arbitrary-command was passed but the policy does not grant hmc_run_command |
The flag and the ceiling compose conjunctively, so the escape hatch is not exposed. Name it in a grant's tools to allow it. |
A grant's targets table cannot bind some of the tools it reaches |
One line per such grant, naming them. Those tools are registered and advertised, and every call to them is denied — a table cannot bound them, so only an "all-targets" grant reaches them. Watch for hmc_effective_permissions in the list: when it is there, the server cannot report its own permissions even though the policy grants the tool. |
Security: the streamable-HTTP transport is unauthenticated. It exposes enabled tools — including user administration — to anyone who can reach the port. Keep the default loopback bind.
serve --httprefuses a non-loopback--listen-hostunless--allow-remoteis passed; if you need remote access, put an authenticated reverse proxy (MCP gateway or HTTPS proxy with bearer-token auth) in front and never expose the port directly. The arbitraryhmc_run_commandescape hatch is disabled unless the server starts with--enable-arbitrary-command.
Every tool is documented in the
MCP tool reference:
one page per operation domain, each row carrying the tool's effect class, operation, target
kind, and summary. Those pages are generated from the server's own registry by
scripts/gen_tool_reference.py, and just tool-docs-check fails CI when they fall behind the
code — which the hand-maintained table this replaced had no way to do. MCP clients also receive
a rendered description for every tool parameter, including fields nested inside structured
inputs such as LPAR resources, firmware update parameters, and VIOS update repositories.
hmc_effective_permissions discloses the selected policy's name, its absolute
path, every connection token, and every target selector to any MCP client that
can call it. It carries no credential. One caveat since the generator exists: a
generated legacy-equivalent policy has config.toml's profile keys as its
connections, so those names reach the client through this tool — names only, and
strictly less than hmc_list_configured_hosts discloses to the same caller. If your
profile keys are themselves sensitive, withhold both tools by name. The policy
path is the exception it is not — it is built from XDG_CONFIG_HOME (Linux),
%APPDATA% (Windows), or your home directory, so it names the account, and it
is disclosed deliberately so an operator can tell which file is in effect. Any
policy that neither grants the read effect class nor names the tool in a
grant's tools withholds it; a policy granting read reaches it and cannot
exclude it.
hmc_fleet_health and systems health return only exceptions across the whole
estate: non-operating systems, non-running VIOS partitions, LPARs with inactive
RMC, and recent failed jobs. This is not equivalent to composing N
hmc_system_summary calls or using hmc_capacity_report, which report per-system
inventory and capacity rather than individual unhealthy resources. On HMCs that
do not support global Job listing, the stable response keeps failed_jobs empty
and includes a warning that recent-job health is unavailable; that warning must
not be interpreted as a healthy job feed.
Authorization audit records
With a policy selected, every authorization decision writes one line of JSON to stderr
on the hmc_mcp.audit logger — policy, tool, effect class, decision, a stable reason
code, the connection selector, and the declared target selectors. Denials are WARNING
and permits are INFO. Credentials, whole argument sets, command text, and response
bodies are absent by construction.
--audit-level LEVEL on hmc-mcp serve tunes that stream: DEBUG and INFO keep both
records (the default), WARNING keeps denials only, and ERROR or CRITICAL silences it.
An unknown level name is a usage error that starts nothing.
Every deployment writes these, because every deployment now selects a policy. Delivery is
asynchronous and droppable: records go onto a bounded in-memory queue drained by one
background thread, so a destination nobody is reading can cost you records but cannot stop
the server answering. A dropped line is never silent — the next line that lands is preceded
by {"event": "records-dropped", "count": N}. See
ADR 0043; a full trail still wants
something reading the server's stderr (under stdio that is the MCP client, not you; under
--http it is whatever supervisor or journal collects the unit's stderr). ADR 0011
ownership-override records are not policy-gated and are emitted on the CLI and Python API
paths too.
See docs/authorization-audit.md for the field set, the reason codes, and how to route or silence them.
Collection limits
Feed-backed collection tools accept an optional client-side limit. The complete HMC feed is
still transferred and parsed before the result is truncated: the limit bounds only the number of
entries returned to the agent, not HMC work, network bytes, parsing cost, or the size of each
entry. hmc_list_recent_jobs defaults to 20 entries; the other affected collection tools are
unbounded when limit is omitted.
Public parameter units and selectors
Storage quantities use binary-unit suffixes: capacity_mib for virtual disks,
size_mib for media repositories and optical media, and lu_size_gib for
Shared Storage Pool logical units. Numeric virtual-switch selectors are named
virtual_switch_id. Verified vNIC mutations instead select the backing VIOS,
SR-IOV adapter, and physical port; they do not accept a virtual-switch selector.
The NIM install tools distinguish hmc_timeout_minutes from the client-side
wait_timeout_seconds. With wait=True and no explicit client budget, the
client waits for the HMC timeout converted to seconds plus one polling interval,
so it can observe the terminal state at the HMC deadline. LPM's separate
wait_time value is an HMC migration/validation field measured in seconds.
Every partition tool accepts an optional system_name_or_uuid
(SystemName or UUID) that disambiguates its lpar_name_or_uuid; omitted, the
name is searched fleet-wide. The LPM tools take it as the source-system
selector beside their required target_system_name_or_uuid, so a policy table
must match both endpoints against its managed_system allowlist.
SSH/CLI tools — some tools reach the HMC by running CLI commands over SSH rather than through the REST API. Their system/LPAR arguments (
system_name_or_uuid/lpar_name_or_uuid) accept either a CLI name or a UUID. Names are used as-is; UUIDs are resolved to their CLI names via the REST API first, falling back to anlssyscfgname lookup over SSH when the REST API is unreachable. Resolution happens before the command runs, so a UUID that cannot be resolved surfaces as an error rather than being passed through to the CLI. VIOS backup and restore are an exception: a direct system name plus VIOS UUID is SSH-ready, but a system UUID requires REST to resolve its unique MTMS identity and has nolssyscfgfallback. Separately, the opt-inhmc_run_commandtool runs whatever command you give it verbatim without selector resolution. See docs/hmc-cli-cheatsheet.md for a concise reference to all HMC CLI commands used by this project.
hmc_backup_vios and hmc_restore_vios retain required managed-system and VIOS
selector metadata for authorization diagnostics and audit. Because their ssp
mode can affect the wider cluster, both tools are non-exhaustive and require
targets = "all-targets". Prefer a grant that explicitly names only the needed
tool. An effect-class grant with all-targets can reach either tool; a targets
table cannot authorize either one even when it contains both selector kinds.
The verified vNIC mutation contract is admitted only for POWER9 8375-42A managed by HMC V10R3
M1060. hmc_add_vnic selects one backing with vios_name, vios_lpar_id, adapter_id,
physical_port_id, and capacity_percent, plus the vNIC port_vlan_id. The HMC assigns the
logical-port ID; callers do not supply one. A successful or unchanged result reports the assigned
ID in backing_after[].logical_port_id after correlated HMC readback. hmc_remove_vnic accepts
the slot_num reported by hmc_list_vnics, verifies its single active Operational backing, and
removes that slot.
Add and remove return the stable fields operation, mutation_dispatched, changed, selector,
slot_num, vnic_before, backing_before, vnic_after, backing_after,
vnic_after_read_succeeded, backing_after_read_succeeded, output, and errors. Once a
mutation has been dispatched, a command or reconciliation failure raises a structured partial
error carrying the same result evidence; there is no rollback promise. An empty inventory with
its matching read-succeeded flag set means verified absence.
| vNIC capability | Admitted | Notes |
|---|---|---|
| One SR-IOV backing on POWER9 8375-42A / HMC V10R3 M1060 | Yes | Exact family and HMC level only |
| Caller-selected logical-port ID | No | The HMC allocates and readback reports it |
| Multiple backings or failover topology | No | Ambiguous or degraded topology fails before mutation |
| Priority or maximum-capacity inputs | No | HMC defaults remain in effect |
| Other server families or HMC levels | No | Capability checks fail before mutation |
| Rollback after a dispatched mutation | No | Partial errors retain observed before/after evidence |
PCM notes: metrics are stored as JSON, reached via an Atom feed of links. The
*_metric_linkstools return the link list, while the*_metricstools download the most recent document (or{}when none are in range). The CLImetrics showaccepts--fetchto do both in one step. Logical-partition processed and aggregated metrics require the owning managed system throughsystem_name_or_uuid(MCP) or--system(CLI); their endpoint is nested below that system. Long-term monitoring + aggregation must be enabled viahmc_set_pcm_preferencesbefore processed/aggregated metrics accumulate. Preferences and raw Long Term Monitor feeds are documented only forManagedSystem, notLogicalPartition.
hmc_update_firmware accepts IBM's nested PlatformUpdateParameter JSON.
It rejects older HMC releases before submitting the destructive operation.
For example, a system-firmware update is:
{
"system_name_or_uuid": "system1",
"platform_update": {
"SystemFirmwareUpdate": {
"UpdateType": "Update",
"UpdateOrder": 1
}
}
}
hmc_vios_update uses IBM's operation-specific parameter names. Every source
requires ResourceType. Updates accept HMC, NFS, SFTP, USB, or
IBMWebsite and may include RestartVIOS; upgrades accept HMC, NFS,
SFTP, or USB and require Disks. For example:
HMC sources require Name, NFS/SFTP sources require ServerHostOrIP and
RemoteDirectory, USB sources require USBDevice, and every upgrade requires
Disks. Setting SaveFile to true also requires Name.
{
"vios_name_or_uuid": "vios1",
"kind": "update",
"repository": {
"ResourceType": "NFS",
"ServerHostOrIP": "repo.example.com",
"RemoteDirectory": "/images/vios",
"Name": "update.iso",
"RestartVIOS": "false"
}
}
{
"vios_name_or_uuid": "vios1",
"kind": "upgrade",
"repository": {
"ResourceType": "HMC",
"Name": "install.iso",
"Disks": "hdisk1"
}
}
With wait=true, a terminal job's documented stdOut result is also exposed
at the top level when present. Invalid or cross-operation parameters are
rejected before connecting to the HMC.
Normalized PCIe inventories share this envelope:
resource_kind:dedicated_slot,sriov_adapter,sriov_physical_port, orsriov_logical_port;capability:availableorcapability-unavailable;system: the resolved managed-system name;selector: nullableadapter_id,physical_port_id, andlogical_port_idvalues copied from the request;items: stable records for the selected resource kind; andunavailable_reason:nullwhen available, otherwise a stable evidence-bound explanation.
Dedicated-slot identity is (system, drc_index). Its description and owner_lpar come from the
exact ADR 0053 projection; availability remains null because an empty owner does not prove that
a slot is assignable. SR-IOV identities are (system, adapter_id), adapter identity plus
physical_port_id, and adapter identity plus logical_port_id. Their mode, availability,
ownership/use, location, capacity, and compatibility category fields are explicit nullable values.
ADR 0053 admits selectors and percentage capacity semantics but no SR-IOV read projection, so these
three collections currently return capability-unavailable with no items and perform no inventory
command. Percentage fields use decimal percentages, never bytes, bandwidth, or integer weights.
The CLI equivalents are hmc-mcp network list-dedicated-pcie-slots, list-sriov-adapters,
list-sriov-physical-ports, and list-sriov-logical-ports. All accept --json; the SR-IOV
commands accept the applicable --adapter-id, --physical-port-id, and --logical-port-id
selectors. The older hmc_list_io_slots / network list-io-slots surface remains raw and is not
the normalized contract.
End-to-end: give an LPAR a bootable disk
# 1. create the partition
hmc-mcp lpars create web01 --system <sys-uuid> --mem 8192 --vcpus 2
# 2. give it a vSCSI adapter paired to the VIOS (find IDs via `vios list`)
hmc-mcp adapters add-vscsi web01 --vios-id 1 --vios-slot 5
# 3. carve a virtual disk out of a VIOS volume group
hmc-mcp storage list-vgs <vios-uuid> # find the VG + free space
hmc-mcp storage create-disk <vios-uuid> --vg <vg-uuid> --name web01_root --capacity-mib 51200
# 4. map the disk to the partition
hmc-mcp storage map <vios-uuid> --lpar web01 --disk web01_root
# 5. (optionally) network + power on
hmc-mcp adapters add-network web01 --vlan 100
hmc-mcp lpars power-on web01
Note on the storage model: an LPAR's vSCSI/vFC adapter (added with
adapters add-vscsi/add-vfc) is just plumbing — it pairs the partition with a VIOS server slot. The actual disk lives on the VIOS or in a Shared Storage Pool: carve it out of a Volume Group (storage create-disk) or a Cluster/SSP (cluster create-lu), then connect it with a mapping (storage map). Both the VIOS Volume Group / Virtual Disk model and the Cluster / SSP Logical Unit model are wrapped. Once a disk is mapped, partitioning it into filesystems is the guest OS's job (NIM, cloud-init,mkfs), not the HMC's.
Use with Hermes Agent
hermes mcp add hmc -- uv run --directory ~/src/hmc-mcp hmc-mcp serve \
--access-policy legacy-equivalent
Testing
1. Unit tests (no HMC needed)
The client and XML parser are tested against an HMC mocked with respx — no real hardware required. The default command reports only the configured test and coverage result:
just test
# test: passed; configured coverage gate passed
Use just test-verbose for native pytest diagnostics and missing-lines coverage.
2. MCP protocol smoke test (no HMC needed)
Verifies the server actually speaks MCP over stdio. It connects with a real FastMCP client and reports the exposed tool count:
just smoke
# Connected. <N> tools exposed.
Use just smoke-verbose to list every exposed tool when diagnosing the registry.
3. Live check against a real HMC
With credentials configured (TOML profile, env vars, or flags), the cheapest end-to-end check
is console info — one Logon + one ManagementConsole GET:
hmc-mcp console info
hmc-mcp systems list # should print a table of your Power servers
If console info prints the HMC version, auth, TLS and the session
lifecycle all work; everything else uses the same path.
Layout
src/hmc_mcp/
__init__.py # package version and the `hmc-mcp` console-script entry point
api.py # supported reusable-library facade (ADR 0029)
config.py # pydantic-settings config (TOML profile + env vars + CLI flags)
xmlutil.py # defusedxml Atom-feed -> dict parsing
errors.py # HMCError (shared by client and its mixins)
error_translation.py # presentation-neutral wording for identified HMC failures
client.py # async HMCClient: session, transport, uom helpers, jobs
client_*.py # per-domain mixins (users, systems, lpars, storage, pcm, ...)
client_parse.py# defusedxml wrappers tagging failures with the HMC call
common.py # shared HMCClient/config helpers for tool definitions
operations_*.py# workflows and policies shared by MCP and CLI presentations
affinity_assessment.py # evidence-first, read-only LPAR NUMA-affinity assessment
snapshot.py # version-1 portable LPAR snapshot values and local I/O
ssh.py # transport-only asyncssh session and command execution
ssh_commands.py# resource operations implemented with the HMC CLI
ssh_selectors.py # public resource selectors for the HMC SSH commands
console_capture.py # bounded, non-interactive LPAR console capture (mkvterm)
documents.py # XML request-document builders (LPAR, adapters, storage, users, ...)
jobs.py # JobRequest XML templates (PowerOn/PowerOff/...)
pcm.py # PCM metrics/preferences parsing + XML documents
access_policy.py # server access policy: TOML loading, validation, compilation
legacy_policy.py # the legacy-equivalent access policy, built and compiled
dispatch_scope.py # the dispatch-boundary authorization decision
target_scope.py # dispatch-time authorization of the targets a call names
connection_scope.py # dispatch-time authorization of the connection a call selects
audit.py # one audit record per authorization decision: vocabulary, rendering, sink
tool_registry.py # local MCP tool collection, each tool carrying ToolSecurity
_app.py # shared FastMCP instance, sync-run and SSH helpers, entry points
server.py # thin aggregator importing every server_*.py tool module
server_*.py # resource-domain @mcp.tool definitions (systems, lpars, VIOS, ...)
server_lpar_config.py # SSH-only LPAR configuration handlers
server_system_resources.py # SSH-only managed-system resource handlers
cli.py # thin aggregator importing every cli_*.py command module
cli_app.py # root Typer app, GlobalOpts/GLOBALS, shared CLI helpers
cli_*.py # per-domain CLI commands (systems, lpars, storage, ...)
tests/ # pytest + respx, no real HMC needed
scripts/ # smoke/manual harnesses
Notes on the HMC API
- Auth:
PUT /rest/api/web/Logonwith a LogonRequest XML body returns anX-API-Sessiontoken, sent as a header on every subsequent call;DELETE /rest/api/web/Logonlogs off. - Resources are Atom feeds of vendor media type
application/vnd.ibm.powervm.uom+xml; type=<ResourceType>. .../quick/<Property>returns a single property cheaply;.../search/(<Property>==<Value>)filters server-side.- State changes are asynchronous jobs: POST a JobRequest to
/rest/api/uom/<Type>/<uuid>/do/<Operation>, then poll/rest/api/uom/Job/<job-uuid>.
Installing Hmc
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/randomparity/hmc-mcpFAQ
Is Hmc MCP free?
Yes, Hmc MCP is free — one-click install via Unyly at no cost.
Does Hmc need an API key?
No, Hmc runs without API keys or environment variables.
Is Hmc hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Hmc in Claude Desktop, Claude Code or Cursor?
Open Hmc on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Fetch
Web content fetching and conversion for efficient LLM usage.
AWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzMCP-Agent
A simple, composable framework to build agents using Model Context Protocol by [LastMile AI](https://www.lastmileai.dev)
by lastmile-aiSpring AI MCP Client
Provides auto-configuration for MCP client functionality in Spring Boot applications.
mcp.natoma.ai
A Hosted MCP Platform to discover, install, manage and deploy MCP servers by [Natoma Labs](https://www.natoma.ai)
MCPHub
Website to list high quality MCP servers and reviews by real users. Also provide online chatbot for popular LLM models with MCP server support.
MCP Servers Rating and User Reviews
Website to rate MCP servers, write authentic user reviews, and [search engine for agent & mcp](http://www.deepnlp.org/search/agent)
mkinf
An Open Source registry of hosted MCP Servers to accelerate AI agent workflows.
Compare Hmc with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
