frappe-cloud-mcp
by muthanii
README.md
# frappe-cloud-mcp
An MCP server for the **Frappe Cloud control plane** — the `press` API behind
[frappecloud.com](https://frappecloud.com). It gives an agent an operator's view of your hosting
account: sites, bench groups, servers, backups, deploys, logs, analytics and billing.
This is not another ERPNext data connector. Existing Frappe MCP servers talk to the DocTypes
*inside* a site; this one talks to the layer that runs the site. Ask it why a deploy failed, what
is hammering your database, or what next month's invoice looks like.
## Install
Requires Python 3.11+.
```bash
git clone <this repo> && cd frappe-cloud-mcp
uv sync
```
## Credentials
In the Frappe Cloud dashboard, go to **Settings > Developer** and press **Create New API Key**.
Despite the button's name it gives you *two* values — an API key and an API secret — and the
secret is shown only once, so copy both before closing the dialog.
```bash
export FRAPPE_CLOUD_API_KEY=...
export FRAPPE_CLOUD_API_SECRET=...
```
Both halves are required. Frappe reads the header as `token <api-key>:<api-secret>` and splits it
on the colon; given only one value it discards the credential silently and serves the request as
an anonymous Guest, so a missing secret surfaces as `Not Permitted` rather than as a login error.
This server refuses to start on a half credential instead of letting you hit that.
If you have the pair as a single `key:secret` string, `FRAPPE_CLOUD_TOKEN` takes it whole.
## Register with Claude Code
### From this repo (project scope)
A [`.mcp.json`](.mcp.json) ships in the repo root, so opening this project in Claude Code offers
the server automatically. It reads credentials from your environment rather than storing them, so
the file is safe to commit:
```json
{
"mcpServers": {
"frappe-cloud": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "${CLAUDE_PROJECT_DIR:-.}", "frappe-cloud-mcp"],
"env": {
"FRAPPE_CLOUD_API_KEY": "${FRAPPE_CLOUD_API_KEY}",
"FRAPPE_CLOUD_API_SECRET": "${FRAPPE_CLOUD_API_SECRET}",
"FRAPPE_CLOUD_URL": "${FRAPPE_CLOUD_URL:-https://frappecloud.com}",
"FRAPPE_CLOUD_TEAM": "${FRAPPE_CLOUD_TEAM:-}",
"FRAPPE_CLOUD_READ_ONLY": "${FRAPPE_CLOUD_READ_ONLY:-0}",
"FRAPPE_CLOUD_ALLOW_DESTRUCTIVE": "${FRAPPE_CLOUD_ALLOW_DESTRUCTIVE:-0}"
}
}
}
}
```
Claude Code asks for approval the first time it sees a project-scoped server. Everything except
the key and secret has a `:-default`, so exporting those two is enough to start.
### From anywhere (user scope)
To use it across all your projects, paste this into `~/.claude.json` under `mcpServers` — with a
real absolute path, since `${CLAUDE_PROJECT_DIR}` means nothing outside this repo:
```json
{
"mcpServers": {
"frappe-cloud": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/home/you/projects/frappe-cloud-mcp", "frappe-cloud-mcp"],
"env": {
"FRAPPE_CLOUD_API_KEY": "fc_key_...",
"FRAPPE_CLOUD_API_SECRET": "fc_secret_..."
}
}
}
}
```
The same block works in Claude Desktop's `claude_desktop_config.json` and in any other MCP client
that takes an `mcpServers` object. If `uv` is not on your `PATH`, use its absolute path
(`which uv`) as `command`.
Or add it from the CLI, without editing files:
```bash
claude mcp add frappe-cloud \
-e FRAPPE_CLOUD_API_KEY=... \
-e FRAPPE_CLOUD_API_SECRET=... \
-- uv run --directory /path/to/frappe-cloud-mcp frappe-cloud-mcp
```
Verify with `/mcp` in Claude Code: the server should show `connected`. Bad credentials show
`failed` with the HTTP status from Frappe Cloud.
### Read-only or destructive
Both flags are ordinary env vars, so set them in the `env` block of whichever config you used:
```json
"env": {
"FRAPPE_CLOUD_API_KEY": "...",
"FRAPPE_CLOUD_API_SECRET": "...",
"FRAPPE_CLOUD_READ_ONLY": "1"
}
```
A practical pattern is two entries — `frappe-cloud` read-only for everyday questions and
`frappe-cloud-admin` with `FRAPPE_CLOUD_ALLOW_DESTRUCTIVE=1` for sessions where you intend to
change infrastructure.
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `FRAPPE_CLOUD_API_KEY` | — | required |
| `FRAPPE_CLOUD_API_SECRET` | — | required |
| `FRAPPE_CLOUD_TOKEN` | — | alternative to the pair above, as `key:secret` |
| `FRAPPE_CLOUD_TEAM` | your default team | sent as `X-Press-Team`; needed to target a child team |
| `FRAPPE_CLOUD_URL` | `https://frappecloud.com` | point at a self-hosted press instance |
| `FRAPPE_CLOUD_READ_ONLY` | `0` | expose read tools only |
| `FRAPPE_CLOUD_ALLOW_DESTRUCTIVE` | `0` | unlock the destructive tier |
| `FRAPPE_CLOUD_TIMEOUT` | `60` | request timeout in seconds |
## Safety model
Tools fall into three tiers, and a tool that is not enabled is **never registered** — a read-only
server truthfully advertises no mutating tools rather than offering them and refusing.
| Tier | Default | Examples |
| --- | --- | --- |
| read | on | `list_sites`, `site_log`, `bench_candidate`, `mariadb_processlist`, `upcoming_invoice` |
| write | on (off under `READ_ONLY`) | `create_site_backup`, `clear_site_cache`, `install_app`, `add_site_domain` |
| destructive | **off** unless `ALLOW_DESTRUCTIVE` | `archive_site`, `deploy_bench`, `change_site_plan`, `reboot_server`, `press_call` |
`FRAPPE_CLOUD_READ_ONLY=1` wins over `FRAPPE_CLOUD_ALLOW_DESTRUCTIVE=1`.
Run it read-only or default for day-to-day work; turn on the destructive tier only for a session
where you actually intend to change infrastructure.
## Tools
86 tools by default; 115 with the destructive tier on.
**Sites** — list/get, installed and available apps, config, domains, backups and download links,
logs, agent jobs, activity, update checks, plans. Writes: backup, clear cache, install app, add
domain, update config, activate, auto-update toggles. Gated: create, update, migrate, deactivate,
archive, reinstall, restore, uninstall app, remove domain, change plan, version upgrade, move
server, admin login link.
**Bench groups** — list/get, apps, config, dependencies, deployed versions, deploy information and
status, recent deploys, deploy candidates (with build output), jobs, logs, processes, branches,
regions. Writes: fetch latest app update, restart. Gated: deploy, update, rebuild, add/remove app,
change branch, update config, archive.
**Servers** — list/get, overview, usage, bench groups, plans, jobs, Ansible plays, reclaimable
disk. Gated: reboot, change plan, archive.
**Observability** — site analytics, daily usage, request count and duration by path, background
job duration, request logs, server CPU/memory, MariaDB process list and slow queries, deadlock
report, structured log browsing, notifications.
**Billing** (read-only throughout) — summary, upcoming invoice, current amount, unpaid and past
invoices, balances, forecast, invoice line items, subscriptions.
**Generic** — `press_methods` searches a bundled catalogue of 278 whitelisted press endpoints;
`press_list`, `press_get` and `press_search_link` read arbitrary press documents. Gated:
`press_call`, `press_run_doc_method` and `press_set_value` reach anything without a named tool.
### Refreshing the method catalogue
`press_methods` is backed by `src/frappe_cloud_mcp/catalog.json`, generated from the `frappe/press`
source. Regenerate it when press adds endpoints:
```bash
uv run python scripts/refresh_catalog.py --ref master
```
## Development
```bash
uv run pytest # 44 tests, no network
uv run ruff check src tests scripts
uv run mcp dev src/frappe_cloud_mcp/__main__.py
```
## Notes on the press API
Built against the `frappe/press` source rather than the published docs, which are stale and
currently returning 500s. Behaviour worth knowing:
- Responses wrap the return value in `{"message": ...}`; errors arrive as 4xx/5xx with
`_server_messages`, a JSON string holding a list of JSON strings.
- Reads go out as GET with non-scalar arguments JSON-encoded per parameter; writes go as POST
with a JSON body so argument types survive.
- `press.api.site.get` answers a custom domain with a redirect to the real site name; the client
follows it.
- The generic `press.api.client.*` layer is limited server-side to an allowlist of ~55 doctypes
and to documents owned by the active team.
## Out of scope
OAuth/browser login, marketplace app publishing, partner and SaaS onboarding flows, data-plane
access to the sites themselves, and remote HTTP transport (stdio only).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues