Muppy MCP — what an agent can do, and where it stops¶
Muppy ships an MCP server. An AI agent connected to it (Claude on claude.ai, Claude Desktop, Claude Code, or any MCP client) reads the same records you see in the GUI, runs a whitelist of methods on them, and is told to wait for a human on everything else. This page says exactly where the line runs.
There are two servers, and this page is about the first:
| Server | Provider code | Who it is for | What it sees |
|---|---|---|---|
| Muppy (Infrastructure) | muppy |
Muppy administrators | Hosts, PostgreSQL clusters, Kubernetes clusters and Releases, networks, App Servers, the task queue |
| Manganese | manganese |
Tenants of a Manganese instance | Their own App Servers only, through the mgx_* methods — see Setting up Manganese |
Connecting¶
The server is part of the Muppy web process: it answers at the Muppy URL, and it is down whenever the web server is. The endpoint of the infrastructure server is:
https://<your Muppy>/mcp/muppy/local
Authentication is OAuth 2.1 with PKCE. On the first request the client sends you to the Muppy login, then to a consent screen that lists the scopes it asks for. Approve only what the agent needs:
| Scope | What it unlocks | Approve it |
|---|---|---|
mcp:discovery |
the list of domains, models, fields and methods | always |
mcp:read |
searching and reading records | always |
mcp:source |
reading the Python source of an exposed method | for development |
mcp:execute |
calling an exposed method — subject to the rules below | when the agent must act |
mcp:write |
creating, modifying, deleting records — subject to the rules below | rarely; see Data |
A scope is the ceiling. What the agent can actually do under it is decided per model and per method, by the rules of engagement.
What the server exposes¶
Everything goes through fourteen generic tools: discovery (list_domains, list_models,
get_model_metadata, server_info), reading (search_objects, read_objects,
search_read_objects, and read_huge_content / grep_huge_content for long texts such as
pod logs or a Helm console output), source (get_method_source_code), writing
(write_objects, create_object, delete_objects) and call_method.
Records are grouped in domains, one per area of Muppy:
| Domain | Contents |
|---|---|
muppy-core |
hosts, their users, partitions, firewall rules, network interfaces, systemd units, software releases |
muppy-application |
App Servers and App Definitions, their environment variables, upgrades, install tasks, Code Servers |
muppy-k8s |
clusters, packages, profiles, Releases, dashboards, config journals, the Kubernetes objects Muppy syncs (pods, deployments, services, secrets…) |
muppy-postgresql |
PostgreSQL clusters and databases |
muppy-network |
DNS domains and records, Traefik |
imq |
the task queue: messages, their processing log, workers, queues |
common-tools |
vaults, S3 buckets, smart configs |
mcp-permissions |
the agent's own pending permission requests |
muppy-meta |
Odoo introspection: models, fields, views, menus, modules |
An agent starts with list_domains, then list_models on a domain, then
get_model_metadata on a model: the metadata carries the description of every exposed
field and method, written for agents. Secret fields (passwords, tokens, keys, kubeconfigs)
are declared as such and never returned, whatever the scope.
The rules of engagement¶
Three rules decide what happens when an agent tries to act. They are the same on every domain of the infrastructure server.
Data is read-only¶
Records can be searched and read. Writing, creating and deleting are closed on every model. When you want an agent to change data, you open the model yourself, for as long as you need, from Settings ▸ MCP ▸ Exposed Models — and even then the write goes through a permission request you approve. The profiles never grant a write silently.
Methods run freely on dev and test, and ask everywhere else¶
Every record that belongs to an environment carries a qualifier (Development, Test, Staging, Production, Infrastructure…), and each qualifier has a category. A method the agent calls on a record is judged on that category:
| Category of the record | Decision |
|---|---|
dev, test |
executed at once |
prod, staging, demo, infra, decom, tmpl |
a permission request is created; nothing runs until you approve it |
| unknown — the record has no qualifier, or none can be derived (an IMQ message, a Kubernetes object that belongs to no Release) | a permission request, the same way |
A record inherits the qualifier of what it belongs to: a pod is judged on its Release, a systemd unit on its host, a Vault link on its App Server.
Hosts are the one exception, because a host is not a dev or prod thing by itself: it
carries an AI Managed switch instead. Grant for Duration on the host form opens it
for a chosen time (the grant records who opened it and until when) and Revoke now closes
it. While the grant runs, every method on that host executes at once; otherwise each call
is a permission request. The _agent_* methods (run a command, read or write a file over
SSH) follow that switch, and refuse outright on a host that is not in the managed state.
Every exec is journaled¶
A method whose name ends in _exec — running a command on a host, on an App Server, or
inside a Kubernetes pod — is treated apart, because an exec is not one operation but
whatever the agent writes. Every call writes a permission request, whether or not
anyone had to approve it: the command, its output and its exit code, on the record it
ran against. A call that ran at once is marked as a journal entry and names no
approver; a call that was held is the same record, with your decision on it. There is
one screen for both, under MCP ▸ Permission Requests, and the Journal entries
filter separates them.
A password passed on a command line is masked on that screen. The output is kept up to 512 KiB per call — above that the end of it survives, which is where a command says how it went.
Journal entries are deleted after a while (Settings ▸ MCP, 180 days by default); requests you decided are kept.
A permission request is approved by a human, in Muppy¶
When a call is held, the agent receives a pending_approval answer with the request id,
the URL of the request, and the reason. You receive a notification with an Open
Request button; the request is also listed under MCP ▸ Permission Requests. It
shows the record, the method, and its arguments decoded in words, so that what you
approve is what will run. On approval Muppy executes the call with the arguments the agent
sent; the agent reads the outcome with get_status on the request (domain
mcp-permissions), or lists what it is still waiting for with get_pending_permissions.
A request that nobody answers expires.
What it does not do, by design¶
- No shell on the Muppy server itself, no upgrade, no restart of Muppy: the server is the process the agent talks to.
-
No kubeconfig, no
kubectl, nohelmhanded out. Kubernetes is reached through Muppy's own methods: a Release syncs its objects (sync_k8s_objects, the RBAC objects its Parts Config declares included), a pod gives its logs (get_logs, withgrep_patternandtail_lines), an object can be deleted or patched (api_delete,api_patch) under the rules above, a Release is created from a profile (create_release) or moved to another one (upgrade_to_profile).A command can be run inside a pod, and only that way:
_agent_pod_execon the pod takes a list of words — there is no shell unless the agent asks for one and is seen doing it — and gives back what the command printed with its exit code. It runs at once on adevortestRelease and is a permission request anywhere else, and it is journaled either way. It is what lets an agent reach the application it just deployed; it is not a way aroundget_logs,api_patchor a health check, each of which does its own job with a bounded effect. - No ad-hoc HTTP request either. To look at what a deployed application answers, an agent uses a health check: it creates one on the Release (setup_health_check, with the path and the parser it wants), runs it (health_check), and reads the newest result, whoseraw_valueis the response body verbatim next to the parsed verdict and the alerts. The check then keeps running on its schedule, so a probe leaves a history behind rather than nothing. - No secret leaves through the MCP. A Vault is readable as a record, its password field is not. - Tenant isolation is the other server's job. The infrastructure server is for Muppy administrators; a tenant works through Manganese and itsmgx_*methods.
The agent's working pattern¶
- Discover before reading:
list_domains→list_models→get_model_metadata. The descriptions say which method does what, and which arguments it takes. - Read with
search_read_objectsand an Odoo domain filter; page long fields withread_huge_contentand search them withgrep_huge_contentinstead of reading them whole. - Diagnose Kubernetes from Muppy's side:
sync_k8s_objectson the Release, then the objects' status and conditions, thenget_logson the pod; the config journal keeps the Helm command, exit code and console of every install and upgrade. To read what the application itself answers, set up and run a health check on the Release and read its result. - Reach the instance you deployed: a Release you created carries the email of its
first user, and Muppy never returns that user's password. Give yourself one from
inside the running application —
_agent_pod_execon the Release'sguipod, withik-cli user setupand a password you choose — then open the Release's own URL. The call is journaled, with the password masked. Set a random one again when you are done. - Act with
call_methodon records ofdevortestenvironments; anywhere else, expectpending_approval, tell the user which request to open, and only then retry or pollget_status. Never work around a held request by another route. - Long tasks are asynchronous: many methods dispatch a message on the task queue and
return at once; follow it in the
imqdomain (imq.message, its processing log).
Where this is configured¶
Settings ▸ MCP holds the providers, the instances, the domains, the exposed models and methods, and the permission requests. The exposed models and methods are generated from profiles shipped with Muppy; what you change there by hand is reset when the profile is applied again at the next upgrade, so a durable change is a change of profile.