Skip to content

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, no helm handed 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, with grep_pattern and tail_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_exec on 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 a dev or test Release 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 around get_logs, api_patch or 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, whose raw_value is 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 its mgx_* methods.

The agent's working pattern

  1. Discover before reading: list_domainslist_modelsget_model_metadata. The descriptions say which method does what, and which arguments it takes.
  2. Read with search_read_objects and an Odoo domain filter; page long fields with read_huge_content and search them with grep_huge_content instead of reading them whole.
  3. Diagnose Kubernetes from Muppy's side: sync_k8s_objects on the Release, then the objects' status and conditions, then get_logs on 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.
  4. 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_exec on the Release's gui pod, with ik-cli user setup and 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.
  5. Act with call_method on records of dev or test environments; anywhere else, expect pending_approval, tell the user which request to open, and only then retry or poll get_status. Never work around a held request by another route.
  6. Long tasks are asynchronous: many methods dispatch a message on the task queue and return at once; follow it in the imq domain (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.