maasmcp
Integrates with the Canonical MAAS 3.7 REST API to provide bare-metal provisioning capabilities, exposing 126 type-checked, safety-gated tools. Includes high-level workflow verbs like acquiring machines, deploying machines, applying storage layouts, configuring network layouts, and bulk power control across a fleet, plus direct per-resource tools for machines, tags, users, networks, fabrics, VLANs, subnets, and storage, with auto-pagination, hostname/system_id resolution, and confirmation gating on destructive writes.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
maasmcp
An MCP server that puts Canonical MAAS bare-metal provisioning in the hands of LLM agents — safely, type-checkedly, and without writing a curl one-liner ever again.
maasmcp speaks the Model Context Protocol over streamable-HTTP and exposes the MAAS 3.7 API to any MCP-aware client (Claude Code, Cursor, custom agents) as a single, fully hand-written surface:
126 hand-written tools — opinionated, type-checked, safety-gated. High-level workflow verbs (acquire_machine, deploy_machine, apply_storage_layout, network_layout, bulk_power_control_machines, …) for the jobs you actually do, plus direct per-resource tools (machines_create, tag_update, user_delete, …) for everything else. Every tool is a small class with name, description, and run, Pydantic-Field-documented parameters, and a confirm gate on every destructive write. Each tool also publishes MCP ToolAnnotations (title, readOnlyHint, destructiveHint, idempotentHint) and a leading [domain] tag in its description, so clients can navigate the surface without memorizing 126 names.
Current version: 0.10.1 · License: Apache-2.0 · Python: ≥ 3.11
Table of contents
Related MCP server: MCP VPS Manager
Why maasmcp?
MAAS is the de-facto bare-metal provisioning system for Linux fleets. It has a comprehensive REST API — hundreds of endpoints across machines, networks, storage, fabrics, VLANs, subnets, users, etc. — but the raw surface has rough edges for an LLM:
Path/query/body parameter shapes vary per resource (
/machines/?hostname=XvsPOST /machines/with multipart form fields).Identifier drift: internal MAAS ids (VLANs, fabrics) vs IEEE 802.1Q VIDs vs system_ids vs hostnames. The agent has to know which one each call wants.
Pagination is universal but client-side invisible — list endpoints return paginated slices by default and an LLM rarely thinks to walk them.
Destructive writes don't confirm. One slip of a
delete_*orrelease_machinecall can wipe a fleet.
maasmcp solves these by:
Hand-writing every tool with Pydantic-typed parameters and
Field(description=…)docs taken from MAAS's own API documentation (vendored indocs/reference/maasapi.md), so the LLM sees the same description a human would.Curating high-level verbs that handle the messy parts — VLAN-vs-VID translation, auto-pagination, hostname-to-system_id resolution, partial-failure aggregation on bulk ops.
Gating every destructive write behind a
confirm: bool = Falsedefault. Callingdelete_block_device(system_id, id)withoutconfirm=TrueraisesValueErrorbefore any MAAS call. Useget_block_deviceto verify the target first.
Architecture
┌────────────────────────────────────────┐
│ MAAS region controller │
│ /api/2.0/ (REST API) │
└─────────────────┬──────────────────────┘
▲
│ async httpx + OAuth
┌──────────────────────────┴───────────────────────┐
│ src/maasmcp/maas/ │
│ ├── client.py (async httpx + OAuth) │
│ ├── errors.py (typed exception hierarchy)│
│ ├── paginated_get.py (auto-paginate helper) │
│ ├── binding.py (hide client param from │
│ │ published tool schemas) │
│ └── singleton.py (shared client at runtime) │
└──────────────────────┬───────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ src/maasmcp/curated/ │
│ 126 hand-written tools │
│ (one small class per file) │
└─────────────────┬──────────────────┘
▼
┌─────────────────────────────┐
│ src/maasmcp/registry.py │
│ MCPServer wiring + rebind │
└─────────────────────────────┘
▼
┌─────────────────────────────┐
│ Starlette + uvicorn │
│ /healthz (no auth) │
│ /mcp (bearer auth) │
└─────────────────────────────┘
▲
│ streamable-HTTP
│
┌────────┴────────┐
│ MCP client │
│ (Claude Code, │
│ Cursor, …) │
└─────────────────┘Two key design properties:
Everything is hand-written. One standard per tool: a small class (
name,description,run) in its own module undersrc/maasmcp/curated/, registered and enriched viacurated/_common.py::register_curated. The server is deterministic and installable without a live MAAS.Dependency direction is one-way. Tools depend on the shared MAAS client (
maas/client.py); high-level workflow verbs additionally compose direct tools or raw calls.
Quickstart
Install
pip install maasmcpOr from a checkout:
pip install -e ".[dev]"Run
The server needs three settings: the MAAS base URL, a MAAS API key (to talk to MAAS), and a bearer token (to gate /mcp so random agents can't drive your fleet). Environment variable names use the MAASMCP_ prefix with __ as the section delimiter:
export MAASMCP_MAAS__BASE_URL="https://maas.example.com/MAAS"
export MAASMCP_MAAS__API_KEY="<your-maas-api-key>"
export MAASMCP_SERVER__BEARER_TOKEN="<choose-a-strong-random-token>"
maasmcp serveBy default it listens on 0.0.0.0:8765. Override with MAASMCP_SERVER__HOST and MAASMCP_SERVER__PORT.
Point your MCP client at http://localhost:8765/mcp and authenticate with the bearer token. Liveness check is at GET /healthz (no auth):
curl http://localhost:8765/healthz
# → healthyReadiness probe: GET /readyz checks the MAAS upstream and returns 503 when MAAS is unreachable. It requires the bearer token (only /healthz is unauthenticated) — pass -H "Authorization: Bearer $TOKEN" from your probe config.
maasmcp speaks streamable-HTTP only (no stdio transport). For Claude Code:
claude mcp add --transport http maas http://localhost:8765/mcp \
--header "Authorization: Bearer <your-bearer-token>"For Claude Desktop or other stdio-only clients, bridge through an stdio↔HTTP proxy such as mcp-proxy.
Configuration
Settings come from a YAML config file or MAASMCP_* environment variables — the two modes are exclusive (when --config is given, the environment only fills ${VAR} placeholders inside the YAML; there is no per-key env override of file values). Secrets use ${VAR} indirection so they never sit in committed config:
# config.yaml
maas:
base_url: https://maas.example.com/MAAS
api_key: ${MAAS_API_KEY}
server:
bearer_token: ${MAASMCP_BEARER_TOKEN}
host: 0.0.0.0
port: 8765
log_level: infomaasmcp serve --config config.yamlThe relevant settings:
Setting | Default | Purpose |
| (required) | MAAS base URL |
| (required) | MAAS API OAuth key |
| (required) | Bearer token gating |
|
| Bind address |
|
| HTTP port |
|
| uvicorn / structlog level |
|
| Hard cap on auto-paginated list results |
The API key and bearer token are read by the runtime, never logged. The observability layer logs bearer tokens only as a 6-char correlation prefix.
Tools
All tools that target a machine accept either system_id or hostname (exactly one); hostname is resolved server-side. (Exceptions: get_machine takes a combined identifier; acquire_machine filters by hostname rather than addressing; machines_power_parameters takes id; machine_update's hostname field sets the new name.)
High-level workflow verbs
These are the opinionated verbs for the jobs you actually do. The tool surface registers 126 hand-written tools organised by domain, of which the workflow verbs compose others or raw MAAS calls — the plumbing is invisible to the agent.
Reads (no gate)
find_available_machines, list_machines, get_machine, power_status, wait_for_state, summary, network_layout, storage_summary, list_block_devices, get_block_device, list_partitions, get_partition, list_raids, get_raid, list_volume_groups, get_volume_group, list_fabrics, get_fabric, list_spaces, get_space, list_vlans, get_vlan, list_subnets, get_subnet, list_ip_ranges, get_ip_range, list_interfaces, get_interface.
Machine lifecycle (gated writes)
acquire_machine (allocating a machine removes it from the shared pool, so it is gated too), release_machine, deploy_machine, power_control.
Single-host tag ops (gated)
tag_machine, untag_machine.
Bulk fleet operations (gated, partial-failure aware)
bulk_release_machines, bulk_tag_machines, bulk_untag_machines, bulk_power_control_machines.
Storage writes (gated, confirm=True)
create_block_device, update_block_device, delete_block_device, set_boot_disk, clear_boot_disk (gap-closure — surfaces MAAS's missing clear/unset verb as a structured status: "unsupported" response with available_workarounds; never issues a fake write call), format_block_device, mount_block_device, create_partition, delete_partition, format_partition, mount_partition, unmount_partition, create_raid, update_raid, delete_raid, create_volume_group, update_volume_group, delete_volume_group, create_logical_volume, delete_logical_volume.
Networking writes (gated, confirm=True)
create_fabric, update_fabric, delete_fabric, create_space, update_space, delete_space, create_vlan, update_vlan, delete_vlan, create_subnet, update_subnet, delete_subnet, create_ip_range, update_ip_range, delete_ip_range, create_interface, update_interface, delete_interface, assign_vlan_to_interface, create_bond, create_bridge, create_interface_vlan, link_subnet, unlink_subnet.
Composite writes (v0.6.0, gated, confirm=True)
apply_storage_layout.
Direct MAAS API tools
Beyond the workflow verbs, maasmcp exposes 33 hand-written direct tools mapping 1:1 to MAAS REST operations (same names the MAAS API uses) for anything the workflow layer does not cover:
Machine ops:
machine_abort,machine_commission,machine_delete,machine_details,machine_lock,machine_unlock,machine_mark_broken,machine_mark_fixed,machine_power_parameters,machine_update,machines_accept,machines_create,machines_is_registered,machines_list_allocated,machines_power_parameters,machines_read,machines_set_zoneCollection reads:
fabrics_read,spaces_read,subnets_read,vlans_read,ipranges_readTags:
tag_delete,tag_read,tag_update,tags_create,tags_list,tags_readUsers:
user_delete,user_read,users_create,users_list,users_read
List endpoints auto-paginate up to max_paginated_items (default 1000). Collection read tools (*_read, *_list) accept paginate=False to fetch exactly one MAAS-server-sized page instead.
Discovering tools
With 126 tools, browsing tools/list can be overwhelming. Use the tools_index helper to get a grouped catalog:
tools_index()
# → {
# "groups": {
# "machines": [
# {"name": "list_machines", "title": "List Machines",
# "description": "List machines in MAAS.", "read_only": true,
# "destructive": false, "idempotent": true},
# ...
# ],
# "storage": [...],
# "network": [...],
# "tags": [...],
# "users": [...],
# "zones": [...],
# "fleet": [...],
# "system": [...],
# "meta": [
# {"name": "tools_index", "title": "Tools Index", ...}
# ]
# }
# }Every tool description also begins with a [domain] tag (for example, [storage] Create a block device...) and exposes MCP ToolAnnotations:
title— human-friendly Title Case name.readOnlyHint—truefor reads, lists, and composite reads.destructiveHint—truefor all state-mutating writes (every tool that is not read-only).idempotentHint—truefor read-only calls.
These hints let an MCP client filter or sort the tool list without parsing free-text descriptions.
The confirm safety gate
Every destructive verb accepts confirm: bool = False and raises ValueError before any MAAS call when confirm is missing or False. The default exists for one reason: if an LLM calls the tool without thinking, nothing happens. To actually execute the operation, the agent must re-invoke with confirm=True — ideally after using a read verb to verify the target.
agent → delete_block_device(system_id="abc", block_device_id=42)
maasmcp → ValueError: delete_block_device requires confirm=True
agent → get_block_device(system_id="abc", block_device_id=42)
maasmcp → {name: "sdc", size: 500107862016, …}
agent → delete_block_device(system_id="abc", block_device_id=42, confirm=True)
maasmcp → {status: "ok", operation: "delete"}Networking writes are cluster-wide — a typo'd VLAN or subnet can affect every machine on a fabric. They use the same gate but have no business-rule pre-flight checks; the agent is responsible for using get_vlan / get_subnet / get_fabric to verify the target first.
Composite workflows
The three composite tools are the most distinctive features of maasmcp. They exist because the equivalent MAAS calls would require a half-dozen round-trips and careful sequencing that an LLM reliably gets wrong.
network_layout — full topology in one call
Returns the machine's interface → VLAN → subnet → fabric tree, plus link-state classification that flags LACP member-suppression as the expected pattern (not a fault). Replaces 5–10 sequential reads with one.
network_layout(system_id="eec43g")
# → {
# "system_id": "eec43g",
# "hostname": "living-ewe",
# "interfaces": [
# { "name": "eno1", "vlan": 79, "subnet": "10.0.79.0/24", "fabric": "fabric-0",
# "link_state_classification": "bond_up_members_down" }, # ← expected for LACP
# { "name": "bond0", "vlan": 79, "subnet": "10.0.79.0/24",
# "members": [42, 43], "link_state_classification": "bond_up_members_down" },
# ],
# "errors": []
# }On Stage 1 failure it returns partial=True plus errors; the agent decides whether to retry.
storage_summary — storage at a glance
Counts of block devices, partitions, total size in GiB, plus RAID, volume group, and logical volume counts — fetched in two stages (sequential list_block_devices, then a fat gather of per-device partitions + raids + VGs in parallel).
storage_summary(system_id="eec43g")
# → {
# "block_device_count": 2, "partition_count": 4, "total_size_gib": 1788.5,
# "raid_devices": 1, "volume_groups": 0, "logical_volumes": 0
# }apply_storage_layout — RAID 1 with /boot/efi carved out
The classic RAID-with-ESP layout is a sequence of dependent operations: carve partitions, format the ESP, format the data partitions, build the RAID, format the RAID, mount everything, and tell MAAS which disk is the boot disk. Forget one step or get the order wrong and the system won't boot.
apply_storage_layout takes a single dict spec and sequences create_partition, format_partition, mount_partition, create_raid, and set_boot_disk for you, in the right order. Three layout types are supported:
raid_with_esp— partitions carved on each member disk, ESP (fat32mounted at/boot/efi) on raw disks, RAID over the remaining partitions, rootfs on RAID. Use this for RAID 1 with/boot/efioff-array.raid— partitions carved on each member disk, RAID over all of them. No ESP (BIOS boot only).plain— no RAID, one or more disks partitioned and optionally formatted + mounted.
apply_storage_layout(
system_id="eec43g",
layout={
"layout_type": "raid_with_esp",
"disks": [
{"block_device_id": 1, "partitions": [
{"size": "1G", "fstype": "fat32", "mount_point": "/boot/efi"},
{"size": "remaining"},
]},
{"block_device_id": 2, "partitions": [
{"size": "1G", "fstype": "fat32"}, # mirror copy of the ESP, no mount
{"size": "remaining"},
]},
],
"raid": {"level": 1, "name": "md0", "filesystem": "ext4", "mount_point": "/"},
"boot_disk": {"block_device_id": 1},
},
confirm=True,
)Returns {status: "complete" | "partial", completed_steps, failed_step, errors, rollback_hint}. On partial there is no automatic rollback — the agent decides whether to retry, restore via MAAS's raw op-restore_storage_configuration endpoint (not a curated verb), or live with the partial state.
Requires MAAS cluster setting default_storage_layout=blank so MAAS does not re-layout storage on commission/deploy. If your MAAS uses a different default, this layout will not survive to deploy.
Manual recipe (driving leaf verbs directly)
For incremental workflows where you want full control:
with MAAS cluster setting default_storage_layout=blank:
create_partition × N # one per disk, sized to fit ESP + rootfs
format_partition × N # ESP and rootfs partitions
mount_partition × N # /boot/efi and /boot on the appropriate ones
create_raid(level=1, partitions=[...])
format_block_device(raid_id, fstype)
mount_block_device(raid_id, "/")
set_boot_disk(block_device_id=...)
deploy_machine(system_id, distro="ubuntu/noble")Bulk fleet operations
The four bulk verbs fan out across many machines and aggregate results so partial failures are visible without raising:
bulk_release_machines(system_ids, erase=True, comment=…)— uses MAAS's batch endpoint, returns per-host status.bulk_tag_machines(system_ids, tag)—asyncio.gatherwith aSemaphore(10)cap.bulk_untag_machines(system_ids, tag)— same shape.bulk_power_control_machines(system_ids, action)—action ∈ {on, off, cycle}; per-host errors reported asupstream <status>.
bulk_power_control_machines(system_ids=["eec43g", "8smce7"], action="cycle")
# → [{"system_id": "eec43g", "status": "ok"},
# {"system_id": "8smce7", "status": "error", "error": "upstream 504"}]Inspect every entry — bulk verbs never raise on partial failure, so an agent that doesn't iterate the result list will silently leave failing hosts in their pre-call state.
Deployment
A Dockerfile is included. The MAAS URL, API key, and bearer token drive the whole process (nested MAASMCP_ names, same as docker-compose.yml):
export MAASMCP_MAAS__BASE_URL="https://maas.example.com/MAAS"
export MAASMCP_MAAS__API_KEY="<your-maas-api-key>"
export MAASMCP_SERVER__BEARER_TOKEN="<choose-a-strong-random-token>" # required, no defaultBuild and run:
docker build -t maasmcp:0.10.1 .
docker run --rm -p 8765:8765 \
-e MAASMCP_MAAS__BASE_URL -e MAASMCP_MAAS__API_KEY -e MAASMCP_SERVER__BEARER_TOKEN \
maasmcp:0.10.1A compose file (docker-compose.yml) is included and uses the same variable names. Note: compose substitutes an unset ${VAR} as an empty string — the server rejects an empty bearer token at startup, so a missing MAASMCP_SERVER__BEARER_TOKEN fails closed rather than running unauthenticated.
Health probe (no auth, useful for k8s liveness/readiness):
curl -fsS http://localhost:8765/healthzDevelopment
Adding or changing a tool against a newer MAAS
All tools are hand-written. To support a newer MAAS API version, consult the vendored spec (docs/reference/maasapi.md) or a live /api/2.0/describe/, then add or update the tool class under src/maasmcp/curated/, register it in curated/_common.py, and make sure src/maasmcp/curated/_catalog.py maps its name to the right domain and annotations. Document the change in CHANGELOG.md, bump the package version, run the full suite, then cut a release tag.
Tests
pytest # unit + integration (702 passed, 3 env-gated skips)
pytest -m e2e --no-cov # end-to-end against a live MAAS (gated; --no-cov: the
# coverage floor is sized for the full suite)
ruff check src tests
ruff format --check .
mypy --strict srcUnit tests cover every tool, the MAAS client, config, and auth.
Integration tests boot the Starlette app under
uvicornand drive MCPinitialize/tools/list/tools/callover streamable-HTTP, verifying the bearer middleware.E2E tests (
pytest -m e2e) assert against a real MAAS instance. Off by default (env-gated skips): setMAASMCP_E2E_MAAS_URL+MAASMCP_E2E_SYSTEM_ID(bulk/storage flows) andMAASMCP_E2E_URL+MAASMCP_E2E_KEY(acquire/release flow). Nothing runs them automatically — they're for manual pre-release verification.
Layout
src/maasmcp/
├── cli.py # typer: serve
├── config.py # pydantic-settings + ${VAR} expansion
├── server.py # Starlette app factory + uvicorn entry
├── registry.py # MCPServer instance + tool registration
├── auth.py # bearer-token middleware (healthz exempt)
├── observability.py # structlog config + sanitizers
├── maas/ # async httpx client, errors, pagination, rebind
└── curated/ # 126 hand-written tools
├── _common.py # registration + shared helpers
└── … # one file per tool
tests/
├── unit/ # all 126 tools, MAAS client, config, auth, registry pin
├── integration/ # HTTP transport handshake under uvicorn
├── fixtures/maas/ # response fixtures
└── e2e/ # live MAAS, gated by markerThe full design rationale lives at docs/specs/2026-07-07-maasmcp-design.md. Implementation plans for each release are at docs/plans/2026-07-07-*.md. The vendored copy of the MAAS API v2 documentation (the authoritative reference for endpoint semantics) is at docs/reference/maasapi.md.
Security notes
Two credentials live in this process:
MAASMCP_MAAS__API_KEY(used to call MAAS) andMAASMCP_SERVER__BEARER_TOKEN(used to gate/mcp). Neither is ever logged in full. Bearer tokens appear in logs only as a 6-char correlation prefix. The bearer token must be non-empty (empty is rejected at startup and by the middleware) — an empty token would otherwise authenticate headerless requests.machine_power_parameters/machines_power_parametersexpose BMC/IPMI credentials — MAAS'spower_parametersop returns the BMC username/password for IPMI-type machines. Anyone holding the MCP bearer token can read them; treat the bearer token accordingly.The
confirmgate is your last line of defence. Destructive and state-mutating verbs (including machine allocation and tagging, which change fleet state) raise before any MAAS call whenconfirm=Trueis missing. Treat the gate as the contract — never weaken it.Bulk verbs return per-host status, never raise on partial failure. An agent that doesn't iterate the result list will silently leave the failing hosts in their pre-call state. Inspect every entry.
apply_storage_layoutis destructive and irreversible at the storage layer. Usestorage_summaryandlist_block_devicesfirst to confirm disk identity.Networking writes are cluster-wide. A typo'd VLAN deletes more than you intended. Verify with
get_vlan/get_subnet/get_fabricbeforeconfirm=True.LACP bond member suppression is normal.
link_state_classification: "bond_up_members_down"is the expected pattern for healthy LACP — the slave interfaces are suppressed by the kernel. Only"bond_down_members_down"indicates a real physical-layer fault.
License
Apache-2.0. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Provides capabilities that let LLM agents perform a range of infrastructure management tasks.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Protocol-native energy infrastructure orchestration for AI data centers. Provides 46 MCP tools across 8 grid protocols (IEC-61850, DNP3, Modbus, OCPP, OpenADR, IEEE 2030.5, IEC 60870-5-104, ICCP) with 5 core API primitives: connect, dispatch, settle, comply, and intel. Enables AI agents to programmatically interact with substations, grid interfaces, and energy assets for real-time workload-grid coordination.
Independent directory of agentic AI tools — search, compare & recommend via MCP. Read-only.
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables AI agents to interact with Microsoft Fabric by exposing tools for managing workspaces, notebooks, SQL queries, pipelines, and Livy Spark sessions. It provides a comprehensive set of operations for data engineering and analytics tasks using standard Azure authentication.374MIT
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to securely manage Virtual Private Servers via SSH, with features including command execution, file operations, system monitoring, and service management.1MIT
- AlicenseBqualityBmaintenanceEnables AI CLI agents to operate an aaPanel (btPanel fork) hosting panel through its signature-authenticated HTTP API, exposing 57 described operations covering websites, databases, SSL certificates, backups, and system status plus resources for panel inventories. Ships with a read-only default mode and a separate gate over credential-exposing and file-level operations, redacting generated passwords from tool results.597 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables LLM agents to operate Cloud.ru MLSpace public API v2 through 17 action-based tools covering training, inference, notebooks, TensorBoards, workspaces, allocations, queues, Docker Registry, data transfer, and resource catalog, with read-only mode, dry-run, and confirmation gates for irreversible actions.MIT