Building an MCP Server for Munki: Give Claude Read Access to Your Software Repo

You already have an MCP server pointed at Intune. If you followed Setting Up an Intune MCP Server on Your Mac, you know the shape of the win: the model stops guessing about your fleet because it can go look. Munki is the more interesting target, and almost nobody has built it, because a Munki repo isn't an API. It's a directory tree full of XML property lists. That's precisely the kind of thing a language model reads badly and reasons about well — once something else has done the parsing.
Here's how to build that something else.
Why not just point the filesystem server at it
This is the first thing everyone tries, and it half works. We covered the general case in Filesystem MCP: When the LLM Gets to Touch Your Disk, and the objections there apply here with extra force.
A Munki repo has four directories that matter: pkgsinfo, pkgs, catalogs and manifests. Pkginfo files are XML plists describing an installer item — name, version, description, an installs array, receipts, minimum_os_version, blocking_applications, requires, update_for, unattended_install, and more. Clients never read them. Munki clients read catalogs, which the makecatalogs tool builds by scanning repo/pkgsinfo.
So a filesystem server pointed at the repo gives the model the raw, verbose, pre-aggregated form of the data, one file at a time, and asks it to do the joining. Ask "which catalogs is Zoom in, and is the production version older than testing?" and you'll watch it read thirty files to answer a question the catalogs already answer directly.
The second objection is the serious one. The filesystem server's write tools are as broad as whatever you granted the process. A Munki repo is the thing that installs software on every Mac you own. You do not want a chat session one confident edit away from your production catalog.
What Munki 7 changed for anyone writing tooling
Worth knowing before you start: in Munki 7, the command-line tools and their helpers — previously Python, requiring an interpreter with PyObjC — were rewritten in Swift and ship as compiled executables. The Munki 7 command-line tools are built with a deployment target of macOS 10.15.
Practically, that closes a door and opens another. You can no longer import munkilib and lean on Munki's own parsing inside a Python server. What you get instead is a set of fast, dependency-free binaries in /usr/local/munki that your server can shell out to, plus plists you can parse yourself with plistlib. For a read-only server, parsing the plists directly is usually simpler and faster than shelling out.
The tools worth exposing
Start with four, all read-only. The current spec revision is 2026-07-28, which moved MCP from a stateful bidirectional protocol to a stateless request/response one and formally deprecated the legacy HTTP+SSE transport. For a local server talking to a local repo, none of that changes much — you want stdio, and you want Python 3.10 or higher and the Python MCP SDK 2.0.0 or higher.
from pathlib import Path
import plistlib
from mcp.server import MCPServer
mcp = MCPServer("munki-repo")
REPO = Path("/Volumes/munki_repo")
@mcp.tool()
async def find_item(name: str) -> str:
"""Find every pkginfo in the repo matching a software name.
Args:
name: Software name to match, case-insensitive (e.g. zoom)
"""
hits = []
for path in (REPO / "pkgsinfo").rglob("*"):
if not path.is_file():
continue
try:
with path.open("rb") as handle:
info = plistlib.load(handle)
except Exception:
continue
if name.lower() not in str(info.get("name", "")).lower():
continue
hits.append(
f"{info.get('name')} {info.get('version')} "
f"min_os={info.get('minimum_os_version', 'any')} "
f"unattended={info.get('unattended_install', False)} "
f"path={path.relative_to(REPO)}"
)
return "\n".join(sorted(hits)) or f"No pkginfo matching {name!r}."
if __name__ == "__main__":
mcp.run(transport="stdio")
Set it up with uv init munki-mcp, uv venv, uv add "mcp[cli]". The decorator plus the type hints plus the docstring are the whole tool definition — the SDK generates the schema from them, which is why the Args: block earns its keep. Write it badly and the model calls the tool badly.
The other three follow the same pattern. list_catalog(catalog) reads one file out of catalogs/ and returns name, version and installer size per item — that single file answers most of the questions people actually ask. read_manifest(client) returns a manifest's managed_installs, managed_uninstalls and its catalogs array, which is the one that produces genuinely useful answers, because catalog order is load-bearing: Munki catalogs are searched in the order the manifest lists them, and an item present in more than one is taken from the earliest listed. That's why testing goes above production and never the other way around. (all is reserved; don't name a catalog that.) And catalog_freshness() compares the newest mtime under pkgsinfo/ against the mtimes of the catalog files — a two-line check that answers "did somebody forget to run makecatalogs," which is the single most common Munki failure in a team that has more than one admin.
A fifth tool is worth adding once the first four have earned their place: dependency tracing. Pkginfo files carry requires and update_for keys, and together those form a graph that nothing in the stock toolset gives you a good view of. "What breaks if I pull this item out of production?" is answerable from those two fields alone, and it's the question most likely to be answered wrong by a human skimming plists at four o'clock on a Friday. It's also the cleanest argument for a purpose-built server over a generic one: that graph only exists once something walks every pkginfo and joins them, and the walk is code you write once rather than work the model re-derives on every question.
One note for the caching-minded — the 2026-07-28 revision added ttlMs and cacheScope to list results, so a catalog listing that changes twice a week doesn't need re-reading on every turn.
Filesystem MCP pointed at the repo | A purpose-built Munki MCP server | |
Setup cost | Minutes - one config entry | An afternoon of Python |
What the model sees | Raw plist XML, one file at a time | Parsed fields, already joined for it |
Answering "is Zoom in production?" | Reads a pile of files to work it out | One tool call, one short answer |
Catalog awareness | None - it does not know makecatalogs exists | Built in - catalogs are the query surface |
Blast radius of a bad write | As wide as the process permissions | Zero, if every tool is read-only |
Where it falls down | Large repos and deep directory trees | Anything you did not think to expose |
Read-only is the design, not a limitation
The temptation is to add a create_pkginfo tool. Resist it for now, and put the boundary at makecatalogs.
Munki's own documentation is blunt about the ordering: you should run makecatalogs after any change to pkginfo items to update all catalogs. The tool scans repo/pkgsinfo, builds the catalogs, and runs sanity checks along the way, including verifying that the packages referenced actually exist. Both of those checks have off switches — -f, --force disables the sanity checks and -s, --skip-pkg-check skips package verification.
Those flags are exactly the kind of thing a stuck agent reaches for. An agent that can edit pkginfo files and run makecatalogs --force can push a catalog referencing a package that isn't there, and the first you'll hear about it is Managed Software Center failing on a few thousand machines. Let the model read the repo, draft the pkginfo, and explain what it changed. Let a human run munkiimport and makecatalogs.
At fleet scale
On a personal test repo this is a convenience. At ten thousand devices it's something else, because the question that costs real money isn't "what version of Zoom is in production" — it's "what's in the production catalog that nothing in any manifest actually installs," or "which items have a minimum_os_version that just aged out from under a hardware refresh." Those are joins across four directories that nobody does by hand, so nobody does them, so repos accumulate years of dead weight. A read-only MCP server makes them a question you ask out loud in thirty seconds. In the increasingly common pattern where Intune handles enrollment, profiles and compliance while Munki handles third-party application deployment — which Stabilise's 2026 write-up argues is now the dominant architecture — that's also the only place your software inventory truth actually lives.
The practical takeaway
Build it, keep it read-only, and start with list_catalog rather than find_item. The catalogs are pre-joined data; pointing the model at the catalog files first gets you eighty percent of the value for about thirty lines of Python, and you can add pkginfo-level tools once you know which questions you're really asking.
Run it over stdio, locally, against a repo mounted read-only if you can arrange it. Register it in ~/Library/Application Support/Claude/claude_desktop_config.json under the mcpServers key, with command set to the absolute path from which uv and args of --directory /path/to/munki-mcp run server.py. And when you're tempted to give it write access, remember what the write is: not a file edit, but a software deployment to every Mac in the company. That's a human's signature, not a tool call.
Sources: Model Context Protocol — Build an MCP server, Model Context Protocol Blog — The 2026-07-28 Specification, MCP Python SDK, Munki Wiki — Pkginfo Files, Munki Wiki — Makecatalogs, Munki Wiki — Using Munki Catalogs, Munki Wiki — Glossary, munki/munki Releases, Stabilise — Is Munki Still Relevant in 2026?



Comments