# AppCrane (npm · appcrane-mcp)

Deploy and manage apps on your own AppCrane instance, the self-hosted PaaS for AI-built apps.

- Trust score: 75/100 (medium)
- Change this week: +3
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-09-25

## Components

- npm · `appcrane-mcp`: 75/100 (this document), [markdown](https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp.md), [page](https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp)

## Channel facts

- Registry: `npm`
- Package: `appcrane-mcp`
- Version: `1.0.0`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-09-25.

- **Supply Chain Security**: 98/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - No install/post-install scripts declared.
  - 31 of 95 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 11 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 67/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 13973 tokens (~245/item across 57 items; 57 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 60/100
  - Stability observed for 18 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 94/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 83% of tool parameters carry a description.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 5 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "appcrane_deploy" implies "deploy" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 57 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### How do I install the AppCrane MCP server?

AppCrane runs locally as an npm package, launched with npx -y appcrane-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add gitayg-appcrane -- npx -y appcrane-mcp
```

### Cursor

```json
{
  "mcpServers": {
    "gitayg-appcrane": {
      "command": "npx",
      "args": [
        "-y",
        "appcrane-mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "gitayg-appcrane": {
      "command": "npx",
      "args": [
        "-y",
        "appcrane-mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add gitayg-appcrane -- npx -y appcrane-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "gitayg-appcrane": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "appcrane-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add gitayg-appcrane --command npx --arg -y --arg appcrane-mcp
```

### Hermes

```yaml
mcp_servers:
  gitayg-appcrane:
    command: "npx"
    args: ["-y", "appcrane-mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "gitayg-appcrane": {
      "Transport": "stdio",
      "Command": "npx",
      "Arguments": [
        "-y",
        "appcrane-mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add gitayg-appcrane -t stdio -c npx -a -y appcrane-mcp
```

### Other

```json
{
  "mcpServers": {
    "gitayg-appcrane": {
      "command": "npx",
      "args": [
        "-y",
        "appcrane-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-09-25 (score 75, +1)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-09-23 (score 74, +1)

No change was recorded against any check on this day. Stability & Change Management went from 50 to 53. That category is still filling its 30-day observation window: 15 days of observed history at the previous scan, 16 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-09-20 (score 73, +1)

No change was recorded against any check on this day. Stability & Change Management went from 40 to 43. That category is still filling its 30-day observation window: 12 days of observed history at the previous scan, 13 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-09-18 (score 72, +1)

No change was recorded against any check on this day. Stability & Change Management went from 33 to 37. That category is still filling its 30-day observation window: 10 days of observed history at the previous scan, 11 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-09-16 (score 71, +1)

No change was recorded against any check on this day. Stability & Change Management went from 27 to 30. That category is still filling its 30-day observation window: 8 days of observed history at the previous scan, 9 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-09-14 (score 70, +4)

- [functional improvement] Stability: unverified → 0.23

### 2026-09-08 (score 66, +15)

- [security improvement] Malware scan: unverified → pass

### 2026-09-07 (score 51)

First indexed and scored.

## MCP tools (57)

### `appcrane_list_apps` (~91 tokens)

List all AppCrane apps the current user has access to. Each app includes slug, name, description, urls (production + sandbox), and the version currently live in each environment. Call this first when the user asks about "my apps", "what apps exist", or before doing anything app-specific. Non-admin users see only their assigned apps; admins (admin or platform_admin) see everything.

### `appcrane_get_app` (~241 tokens)

Get detailed info for a single app: URLs, current versions per environment, recent deployments, and health state. Use this when the user asks "what's the status of <app>", "is <app> deployed", or after a deploy to confirm what landed. Returns 404-equivalent error if the slug doesn't exist or the caller has no access. config.auth_mode tells you whether the app gets identity at all: `authenticated` means routes go through forward_auth and arrive at the container with X-AppCrane-* headers; `headless` means forward_auth is skipped for the whole app and those headers NEVER arrive. Check it first when debugging "my app sees no identity headers", but note it is per-APP, not per-request: an `authenticated` app can still have auth_bypass_paths prefixes that skip forward_auth, and a custom domain is never gated either. The request itself is authoritative — the app can read X-AppCrane-Auth-Mode (authenticated / headless / bypass), which AppCrane stamps on every route it proxies.

Input parameters:

- `slug` (string, required): App slug, e.g. "mysite"

### `appcrane_top_apps` (~118 tokens)

Top apps by distinct active users in a lookback window. Useful for "which apps are getting the most use this week" or "what should I deprecate" type questions. Sourced from app_visits which is recorded on every Caddy forward_auth (one row per user/app/day). Returns rows ordered by user count descending. Admin only.

Input parameters:

- `days` (integer): Lookback window. Default 7, max 90.
- `top` (integer): How many rows. Default 10, max 50.

### `appcrane_top_users` (~112 tokens)

Top users by distinct apps opened in a lookback window. Surfaces who the heaviest cross-app users are — handy for finding power users to interview, or spotting churn risk (a user who used 10 apps last month and 0 this week). Sourced from app_visits. Active users only. Admin only.

Input parameters:

- `days` (integer): Lookback window. Default 7, max 90.
- `top` (integer): How many rows. Default 10, max 50.

### `appcrane_get_health` (~139 tokens)

Fetch the deployed app's health endpoint server-side, bypassing AppCrane's auth proxy. Use this to validate that a deploy actually landed the expected version, or to check if the app is responding. AppCrane hits the app's configured health endpoint (default /api/health) on the internal port directly — no Caddy, no SSO redirect — and returns the response status + body. Defaults to sandbox; pass stage="production" only when the user asks about prod.

Input parameters:

- `slug` (string, required): App slug, e.g. "mysite"
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_get_secret` (~196 tokens)

List an app's secrets (encrypted env vars) with their values MASKED — safe to show in chat. For each key you get: is_set, length, a short preview (last 3 chars, rest masked; fully masked for short values), a sha256 `fingerprint` (compare two envs or detect a changed value without seeing it), and updated_at. This is what you want for "is X set?", "did the key change?", "which vars exist?". Does NOT return plaintext — a secret never lands in the transcript. To read one actual value, use appcrane_reveal_secret with a specific key. Defaults to sandbox; pass env="production" only when the user explicitly says production. App-admin or AppCrane admin only.

Input parameters:

- `slug` (string, required): App slug, e.g. "mysite"
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_reveal_secret` (~197 tokens)

Reveal the PLAINTEXT of ONE secret by key. Use ONLY when the user explicitly needs the actual value — it will appear in this conversation transcript, so treat the transcript as sensitive afterward (and consider rotating the secret if the transcript may be stored/shared). For checking whether a var is set, comparing values, or seeing what exists, use appcrane_get_secret (masked) instead — do NOT reveal just to inspect config. Single key only — it never dumps the whole env. Every reveal is audit-logged. Defaults to sandbox; env="production" only when the user explicitly asks. App-admin or AppCrane admin only.

Input parameters:

- `key` (string, required): Exact env var name to reveal (e.g. "RESEND_API_KEY"). One key per call.
- `slug` (string, required): App slug, e.g. "mysite"
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_deploy` (~169 tokens)

Trigger a deployment — this IS how you "update an env to the latest". For github and managed apps it pulls the latest commit from the app's configured branch on GitHub (server-side, using the app's stored credentials — you do NOT need your own github token or to push/upload anything), builds a fresh Docker image, and swaps in a new container. Use it whenever the user says things like "update sandbox to the latest", "deploy the newest version", "pull my latest github changes", or "redeploy". Returns a deployment ID; use appcrane_get_logs to monitor progress. Defaults to sandbox; production requires explicit confirmation from the user.

Input parameters:

- `slug` (string, required): App slug to deploy
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_list_releases` (~111 tokens)

List the deploy/release history for an app + env, newest first — each release is id, version, commit, status (live / rolled_back / failed / pending), who deployed it, and when. Use this to see what is live and to pick a target for appcrane_rollback. App access required.

Input parameters:

- `limit` (integer): Max rows (default 10).
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_rollback` (~146 tokens)

Roll an env back to a prior release. Pass deployment_id (from appcrane_list_releases) to target a specific release, or omit it to roll back to the immediately previous one. Re-runs that release from its recorded build (re-uses the cached per-commit image — no rebuild when it is still retained) and health-checks it. Records a NEW deployment and marks the previous live one rolled_back. Owner-only (or global admin).

Input parameters:

- `deployment_id` (integer): Target release id. Omit to roll back to the previous release.
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_promote` (~102 tokens)

Promote the current live SANDBOX release to production — the gated sandbox→prod path. Refuses unless sandbox is live AND currently healthy (you do not ship a broken sandbox to prod), and the promoted prod release is health-checked with auto-revert. For github apps this rebuilds production from the EXACT sandbox commit; for managed/upload apps it copies the exact tested sandbox release. Owner-only (or global admin).

Input parameters:

- `slug` (string, required)

### `appcrane_list_requests` (~161 tokens)

List enhancement requests filed against an app via the AppCrane intake form. Use this when the user asks "what should I work on?", "what's queued for X?", or wants to pick up tickets. Returns id, message, app_slug, submitter, and bucket. Buckets: triage (unclaimed), in_progress (someone is working on it), shipped (merged + deployed), validated (requester confirmed). Filter by bucket="triage" to find work to pick up.

Input parameters:

- `bucket` (string): Filter to one bucket. Most useful: "triage" for unclaimed work.
- `limit` (number)
- `slug` (string): Filter by app slug. Omit to see across all accessible apps.

### `appcrane_set_request_status` (~106 tokens)

Move a request through the lifecycle: triage → in_progress → shipped → validated. Use this when the user says "I'll take #42" (set to in_progress), after merging a PR (set to shipped), or after confirming a fix works (set to validated). Validated requests are considered closed. Requires app-admin or AppCrane admin role.

Input parameters:

- `bucket` (string, required): Target bucket.
- `id` (number, required): Request id (the # column).

### `appcrane_ls` (~102 tokens)

List files inside a running app container at a specific path. Use to verify what actually got built / what files made it into the deployed image. Read-only; bound to safe roots (/app and /data only). Returns the directory listing as text.

Input parameters:

- `path` (string): Absolute path inside the container, must start with /app or /data
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_cat` (~103 tokens)

Print the contents of a file inside a running app container. Read-only; bound to safe roots (/app and /data only). Refuses files larger than 256KB; truncate by reading the first N bytes via path tricks if you need a tail.

Input parameters:

- `path` (string, required): Absolute path inside the container, must start with /app or /data
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_stage_from_url` (~245 tokens)

Stage a file by having AppCrane DOWNLOAD it, and return its token. THE CHEAPEST WAY to get a large artifact in: the bytes go host-to-host and never pass through your context, so a 600KB bundle costs the same handful of tokens as a 6KB one. Prefer this over appcrane_stage_chunk for anything bigger than a small text file. Give it a URL your build already produces — a GitHub release asset, an S3/R2 presigned URL, any https link the server can reach. Pass sha256 to have the downloaded bytes verified before a token is issued. Then deploy with appcrane_deploy_artifact. https only; redirects are not followed; private and link-local addresses are refused.

Input parameters:

- `filename` (string): Name to stage it under. For a deploy it must end in .zip, .tar.gz or .tgz. Defaults to the last path segment.
- `sha256` (string): Optional hex SHA-256 of the file, verified before the token is issued.
- `url` (string, required): Direct https URL to the file. Must not redirect — give the final URL.

### `appcrane_stage_chunk` (~277 tokens)

Upload one part of a SMALL file to AppCrane over MCP. For anything bigger than a few KB use appcrane_stage_from_url instead — the bytes here are emitted by the model, so they cost output tokens per character and fail on a single typo. Capped at 8 parts for that reason. Split the file into parts small enough for a tool call (~256KB of base64 each is comfortable), send each with the same `session` and `of`, then call appcrane_stage_assemble. Parts may be sent in any order and re-sent to replace a corrupted one; the reply lists which parts are still missing. Use encoding="base64" for anything binary. Pass sha256 of THIS part to have it verified on arrival.

Input parameters:

- `content` (string, required): This part's bytes, encoded per `encoding`.
- `encoding` (string): Defaults to utf-8. Use base64 for binary.
- `of` (integer, required): Total number of parts. Identical across every part of a session.
- `part` (integer, required): 1-based part number.
- `session` (string, required): Opaque id grouping the parts of one file. Any unique string; reuse it for every part.
- `sha256` (string): Optional hex SHA-256 of this part, verified on arrival.

### `appcrane_stage_assemble` (~172 tokens)

Join the parts pushed with appcrane_stage_chunk into one staged file and return its token. Hand that token to appcrane_deploy_artifact to deploy it — that pair is a complete, MCP-native deploy for an app with no repo, and it does not touch GitHub. Pass sha256 of the WHOLE original file to have the reassembled bytes verified before the token is issued; without it you are trusting that every part arrived intact.

Input parameters:

- `filename` (string, required): Name for the assembled file. For a deploy it must end in .zip, .tar.gz or .tgz.
- `session` (string, required): The session id used for the parts.
- `sha256` (string): Optional hex SHA-256 of the whole original file, verified before the token is issued.

### `appcrane_rename_app` (~229 tokens)

Rename an app's slug. The slug is its URL, its container name and its data directory, so this changes all three — but it is NOT destructive: deploy history, env vars, ports, per-app roles and grants are keyed on the app id, not the slug, and survive untouched. The old slug is kept as a redirect unless redirect=false. Platform admin. Use this instead of recreating an app under a new name, which is what loses the history. To free a slug held by an app you no longer want, rename THAT app out of the way with redirect=false rather than deleting it — deleting clears the database rows but leaves data/apps/<slug> on disk, and the rename then refuses because the directory is still there.

Input parameters:

- `new_slug` (string, required): New slug: lowercase letters, digits and dashes, starting with a letter or digit.
- `redirect` (boolean): Keep the old slug redirecting to the new one. Pass false when you are freeing the old slug for another app to take.
- `slug` (string, required): Current app slug.

### `appcrane_deploy_artifact` (~325 tokens)

Deploy a release from an uploaded BUNDLE instead of from git. For an app with no GitHub repo, and the fallback when the repo path is unavailable — an expired service-account PAT blocks every managed-repo write, and this route does not touch GitHub at all. Two steps: (1) upload the bundle with `curl -F file=@dist.zip -H "X-API-Key: <your dhk_mcp_ key>" https://<host>/api/files/staged` — your MCP key IS allowed on that endpoint; it returns { token, sha256, size_bytes }. (2) Call this tool with that token. Accepts .zip, .tar.gz, .tgz, up to the staged-file limit. The release is identified by a SHA-256 AppCrane computes over the bytes, recorded as commit_hash "sha256:<digest>"; the tool re-hashes the staged bytes and refuses if they no longer match what was staged. Returns that digest — compare it against the one you computed locally. Deploys to sandbox unless env=production.

Input parameters:

- `commit_message` (string): Optional release note, shown in the deploy history
- `commit_sha` (string): Optional git SHA from the machine that BUILT the bundle. Recorded as context only — it is not verified and does not become the release identity.
- `slug` (string, required): Target app slug
- `stage` (string): Target stage (legacy alias: env).
- `token` (string, required): Token returned by POST /api/files/staged

### `appcrane_push_staged_file` (~249 tokens)

Move a previously-staged file into a running container at a path under /app or /data. THE WAY TO GET LARGE BINARIES (DMGs, datasets, bundles) into a container when they're too big to inline through appcrane_cp. Two steps: (1) upload the bytes with a plain multipart POST to `curl -F file=@local.dmg -H "X-API-Key: <your dhk_mcp_ key>" https://<host>/api/files/staged` — your MCP key IS allowed on this endpoint (v2.10.6+); it returns { token, sha256, size_bytes }. (2) Call this tool with that token and a dest path. The container must be running. Path is validated (no "..", must start with /app or /data). The staged blob is deleted on success.

Input parameters:

- `dest` (string, required): Absolute container path under /app or /data — destination file or directory
- `slug` (string, required): Target app slug
- `stage` (string): Target stage (legacy alias: env).
- `token` (string, required): Token returned by POST /api/files/staged

### `appcrane_wait_deploy` (~140 tokens)

Block until a deployment reaches a terminal state (live / failed / rolled_back), then return its final status. Use after appcrane_deploy instead of polling appcrane_get_logs in a loop. Returns immediately if the deployment is already terminal. Defaults to 180s timeout, hard-capped at 600s. On timeout, returns { status: "pending", timed_out: true } so the caller can decide whether to keep waiting.

Input parameters:

- `deployment_id` (number, required): Deployment id from appcrane_deploy
- `timeout_sec` (number): How long to wait. Default 180s, max 600s.

### `appcrane_get_deploy_log` (~210 tokens)

Read the deploy/build log for a specific deployment — the output that came out of clone / npm install / docker build / health-validate, BEFORE the container started running. This is what you want when a deploy fails fast (1-2 second failures are almost always pre-build errors that never reach the runtime container, so appcrane_get_logs has nothing to show). Pass a deployment_id from appcrane_deploy / appcrane_get_app.recent_deployments, OR omit it and pass slug+env to get the latest deployment's log.

Input parameters:

- `deployment_id` (integer): Specific deployment id. Preferred — unambiguous.
- `slug` (string): App slug. Required when deployment_id is not given.
- `stage` (string): Required when deployment_id is not given. Target stage (legacy alias: env).
- `tail` (integer): Return only the last N lines. Defaults to 500; full log can be many KB on a long build.

### `appcrane_get_logs` (~164 tokens)

Get recent runtime logs from a running app container (docker logs). Use this for runtime issues — once the container is up. Returns the most recent N lines (default 100, max 1000). Pass search to filter to lines containing a substring (case-insensitive). NOT the right tool for fast deploy failures (1-2 second exits, "no such container" errors): those happen during clone / npm install / docker build / health-validate, BEFORE any container exists. Use appcrane_get_deploy_log for that.

Input parameters:

- `lines` (number)
- `search` (string): Filter to lines containing this substring (case-insensitive)
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_create_app` (~697 tokens)

Register a new app in AppCrane, either from a GitHub repository or from a prebuilt container image. Use this only after the user has explicitly confirmed they want to onboard a new app and told you which source to use — a real github URL, or an image reference for source_type='image'. Allocates ports, creates the data directories, configures Caddy routing, and starts health checks. After this returns, call appcrane_set_secret to set any required secrets, then appcrane_deploy to ship the first build. For an image app there is nothing to build: the deploy pulls image_ref and starts it, so also pass container_port and health_path unless the image happens to match AppCrane's own 3000 + /api/health defaults. Requires the create-apps permission (global admins, or any role a platform admin granted at Settings → Roles).

Input parameters:

- `branch` (string): Branch to track. Default: main
- `container_port` (integer): Port the image listens on inside the container. Omit only if the image really listens on 3000 (what an AppCrane-built image does); a third-party image usually does not — odoo is 8069, nginx is 80. Ge…
- `description` (string)
- `domain` (string): Optional custom domain. If omitted, the app lives under CRANE_DOMAIN/<slug>/.
- `github_token` (string): GitHub PAT for private repos. Stored encrypted; only used to clone.
- `github_url` (string): GitHub repo URL, e.g. https://github.com/me/mysite. Required unless source_type is 'image'.
- `health_path` (string): HTTP path AppCrane health-checks. Omit only if the image serves /api/health (the AppCrane-built default). A stock image will not, and a health check against a 404 marks a working app unhealthy.
- `image_ref` (string): Prebuilt container image to run instead of building from source, e.g. "odoo:19" or "ghcr.io/owner/app@sha256:<64 hex>". Sets source_type='image'. The reference MUST name a version: a bare name ("odoo…
- `max_cpu_percent` (number): Per-container CPU cap. Default: 50.
- `max_ram_mb` (number): Per-container memory cap. Default: 512.
- `name` (string, required): Display name (shown in dashboard)
- `slug` (string, required): URL-safe identifier — lowercase letters, digits, dashes; must start with a letter or digit. Lives at /<slug>/.
- `source_type` (string): 'github' (default) clones and builds a repo; 'image' runs a prebuilt image and never builds. Implied when image_ref is passed.

### `appcrane_update_app` (~682 tokens)

Patch fields on an existing app. Use this to fix a missing github_url after the fact, change branch, rotate the github_token, retag with category/visibility, point an image app at a new image_ref, or adjust resource limits — anything you would otherwise need direct DB access for. Only includes fields you pass; omitted fields are left alone. To clear a string field pass an empty string. Returns the same shape as appcrane_get_app.

Input parameters:

- `auth_bypass_paths` (array): v2.7.27: array of path prefixes (e.g. ["/ws/local-runner"]) that bypass SSO forward_auth on this app. Requests under these prefixes reach the container with NO X-AppCrane-* identity headers — the app…
- `branch` (string)
- `category` (string)
- `container_port` (integer): Port the image listens on inside the container. Omit only if the image really listens on 3000 (what an AppCrane-built image does); a third-party image usually does not — odoo is 8069, nginx is 80. Ge…
- `description` (string)
- `domain` (string)
- `frame_ancestors` (string)
- `github_token` (string): PAT for private clones. Stored encrypted (AES-256-GCM). Omit to leave the existing token alone; pass empty string to clear it; pass a value to rotate.
- `github_url` (string): github.com URL of the source repo. Pass empty string to clear.
- `health_path` (string): HTTP path AppCrane health-checks. Omit only if the image serves /api/health (the AppCrane-built default). A stock image will not, and a health check against a 404 marks a working app unhealthy. Pass…
- `image_ref` (string): Prebuilt container image to run instead of building from source, e.g. "odoo:19" or "ghcr.io/owner/app@sha256:<64 hex>". Sets source_type='image'. The reference MUST name a version: a bare name ("odoo…
- `image_retention` (integer)
- `max_cpu_percent` (number): Per-container CPU cap (0-100).
- `max_ram_mb` (number): Per-container memory cap.
- `name` (string)
- `public_access` (integer)
- `slug` (string, required): App slug to update.
- `source_type` (string)
- `visibility` (string)

### `appcrane_set_app_meta` (~646 tokens)

Set an app's category, visibility, auth_mode, and/or auth_bypass_paths — the owner self-service fields (same controls the dashboard Launcher exposes to owners). Owner of the app (or global admin) required. visibility is one of public / private / hidden. auth_mode is `authenticated` (default — all routes go through AppCrane SSO) or `headless` (the app bypasses forward_auth ENTIRELY and is reachable without identity — right tool for telemetry ingest, public webhooks, status pages; the app's own server is responsible for any payload-level authn). A headless app is still served BY CADDY over HTTP — TLS, security headers and access logging all still apply; it is not a raw port. An app that does not speak HTTP at all needs ingress_type='tcp', and an app that speaks HTTP AND needs a second raw port for non-HTTP clients needs ingress_type='dual' (both via appcrane_set_app_ingress, platform admin only) — different and far more exposed things. auth_bypass_paths (v2.7.27+) is an array of path prefixes (e.g. ["/ws/local-runner"]) that bypass SSO on this app only — narrower than headless mode; the app authenticates those paths itself (e.g. token in query string). The platform strips incoming X-AppCrane-* headers on bypass paths (forgery defense intact) and suppresses access logging for them (token-in-query never sits in log storage). Owners may only assign an EXISTING category; creating a brand-new category is reserved for global admins. For powerful fields (github_url, branch, token, source_type, resource limits) use appcrane_update_app (admin only).

Input parameters:

- `auth_bypass_paths` (array): v2.7.27: array of path prefixes (e.g. ["/ws/local-runner"]) that bypass SSO forward_auth on this app. Requests under these prefixes reach the container with NO X-AppCrane-* identity headers — the app…
- `auth_mode` (string): authenticated = AppCrane SSO + per-app role checks; headless = NO auth at the proxy (the entire app is reachable by anyone on the internet).
- `category` (string): Category/tag. Owners must pick one already in use; pass empty string to clear.
- `domain` (string): v2.10.0: custom domain (e.g. "raise.glick.run") that serves this app at the ROOT of that domain with NO AppCrane SSO and NO topbar — the app does its own auth. Maps to production. Requires the domain…
- `slug` (string, required): App slug.
- `visibility` (string): public = anyone; private = assigned users; hidden = not discoverable.

### `appcrane_get_app_ingress` (~328 tokens)

Read HOW an app is reachable. ingress_type is `http` (default — every request goes through Caddy: TLS, AppCrane SSO/forward_auth, X-AppCrane-* identity headers, security headers, access logs), `tcp` (the container port is published DIRECTLY on the host at public_port, with Caddy not in the path at all — for apps that do not speak HTTP, e.g. a forward/CONNECT proxy handing back a raw tunnel), or `dual` (v2.45.0 — BOTH at once: an ordinary HTTP control plane still served through Caddy on container port 3000 with every Caddy control intact, PLUS a raw data plane published at 0.0.0.0:<public_port> -> <data_plane_port>, a DIFFERENT port inside the same container, with none of them). For a dual app the two planes have different security properties and the answer to "is this app behind AppCrane auth" is different for each — read `exposure.control_plane` and `exposure.data_plane` rather than the enum. Read this before debugging "why does my app see no identity headers" or "what port do clients connect to". IMPORTANT — this is NOT auth_mode: auth_mode=`headless` still goes through Caddy and only skips forward_auth, so it keeps TLS, security headers and request logging; a published port under `tcp` or `dual` keeps NONE of that. Requires access to the app.

Input parameters:

- `slug` (string, required): App slug.

### `appcrane_set_app_ingress` (~1266 tokens)

Switch an app between HTTP ingress, raw TCP ingress and dual (both), and choose its public host port. PLATFORM ADMIN ONLY — this is not an owner self-service field like auth_mode. ingress_type=`tcp` publishes 0.0.0.0:<public_port> -> the container in ADDITION to the loopback publish every app has, so clients connect straight to the host and Caddy never sees the traffic: no forward_auth/SSO, no identity headers, no per-request audit, no rate limiting, no security headers, no TLS from AppCrane. Every control AppCrane has assumes Caddy is the only door; this adds a second one the platform does not control, and the app then owns authentication completely. Do NOT reach for this to make an app reachable without login — that is auth_mode=`headless`, which still goes through Caddy and keeps TLS, security headers and logging. Use `tcp` ONLY when the app does not speak HTTP at all (a forward/CONNECT proxy tunnels raw bytes; no reverse proxy can express that). ingress_type=`dual` (v2.45.0) is for an app that is BOTH: its HTTP control plane keeps being served through Caddy on container port 3000 with every control intact (and stays the plane its health check probes), while a SECOND listener inside the same container — data_plane_port, which is REQUIRED and must not be 3000 — is published raw at 0.0.0.0:<public_port> with none of them. Setting data_plane_port to 3000 is refused: that would republish the ordinary HTTP origin Caddy fronts, unauthenticated and unaudited, which is the exact surface Caddy is in the path to protect. Omit public_port to keep the existing allocation, or to have the lowest free port in 31000-31999 allocated — a dedicated band so the operator firewalls one predictable block; an explicitly NAMED port may be anything in 1024-65535, because clients are configured with a port by hand or by MDM and a number like 8080 is often not the platform's to choose. Naming a port outside the auto band is legal and safe — the guards that matter (the WHATWG blocked list,…

Input parameters:

- `data_plane_port` (integer|null): CONTAINER port the raw publish targets, 1024-65535. REQUIRED with ingress_type='dual'. On any other type the only accepted value is null, which DROPS a data plane the app still has pinned — required…
- `ingress_type` (string, required): http = through Caddy (default). tcp = the container port is published on the host, with no AppCrane authentication in front of it. dual = both — the HTTP control plane stays behind Caddy on container…
- `public_port` (integer): HOST port to publish. Only valid with ingress_type='tcp' or 'dual'. Omit to keep the port the app already holds, or to have one allocated from 31000-31999; name one explicitly and it may be anything…
- `sandbox_public_port` (integer|null): HOST port to publish for the SANDBOX container, so a raw data plane can be exercised before it goes live. Opt-in and independent of public_port — omit it and sandbox publishes nothing, exactly as bef…
- `slug` (string, required): App slug.

### `appcrane_list_app_members` (~85 tokens)

List every user who has access to an app, with their per-app role (owner / admin / user / viewer / none). Use this before granting or revoking to see who is already in. Returns email + name + role for each member. App-admin or owner of the app required (or global admin / platform_admin).

Input parameters:

- `slug` (string, required)

### `appcrane_grant_app_access` (~115 tokens)

Grant a user access to an app at a specific per-app role. `user` accepts a numeric user id, an email, or a username — first match wins. role defaults to "user". Idempotent: existing rows are upgraded/downgraded to the new role. App-admin or owner of the app required (or global admin).

Input parameters:

- `role` (string)
- `slug` (string, required)
- `user` (string, required): User id (numeric string), email, or username

### `appcrane_revoke_app_access` (~79 tokens)

Remove a user's access from an app entirely. Idempotent: returns ok even if the user had no access. App-admin or owner of the app required (or global admin). Refuses to remove the only remaining owner.

Input parameters:

- `slug` (string, required)
- `user` (string, required): User id, email, or username

### `appcrane_list_app_roles` (~194 tokens)

List the roles an app defines FOR ITSELF (approver, auditor, reviewer — whatever that app invented). Any member sees the roles and how many hold each; an owner or admin also gets `members`, the roster of who holds what. These are NOT AppCrane permissions: they grant nothing on the platform and are only handed to the app, in the X-AppCrane-App-Roles request header and in /api/me's app_roles array, for the app's own code to enforce. For AppCrane's own per-app tier — owner/admin/user/viewer, i.e. who may deploy, read env vars, or delete the app — use appcrane_list_app_members instead. Call this before creating a role (to avoid duplicating one) or before setting a user's roles (to see the valid keys). Requires being assigned to the app.

Input parameters:

- `slug` (string, required)

### `appcrane_create_app_role` (~242 tokens)

Define a new role for an app to enforce itself — the vocabulary side of app-defined roles. This does NOT grant AppCrane privileges of any kind, and it does NOT give anyone the role: use appcrane_set_user_app_roles for that. To change who may deploy / read env / delete, you want appcrane_grant_app_access, not this tool. key is what the app's code compares against and is immutable once created; it must match /^[a-z][a-z0-9_-]{0,31}$/ and may not be one of the AppCrane-reserved words (owner, admin, user, viewer, none, platform_admin). label is the human name shown in the dashboard. An app may define at most 16 roles. Owner or admin of the app required.

Input parameters:

- `description` (string): Optional. What the app lets this role do.
- `key` (string, required): Machine key the app matches on, e.g. "approver". Lowercase, immutable.
- `label` (string, required): Human-readable name, e.g. "Budget approver"
- `slug` (string, required)

### `appcrane_set_user_app_roles` (~230 tokens)

Set which app-defined roles a user holds on one app. Replaces their whole set: keys omitted from the list are removed, and keys: [] clears every role they hold. This changes only what the app itself enforces — it does NOT change the user's AppCrane per-app tier, so it can neither grant nor remove deploy / env / delete power. Use appcrane_grant_app_access for that. Every key must already be defined on this app (appcrane_create_app_role) and the user must already have access to the app (appcrane_grant_app_access) — a role on a non-member is unenforceable, since the app never sees them. The result is what the app receives in X-AppCrane-App-Roles on the user's next request. Owner or admin of the app required.

Input parameters:

- `keys` (array, required): The COMPLETE set of app-defined role keys this user should hold. [] removes all of them.
- `slug` (string, required)
- `user` (string, required): User id (numeric string), email, or username

### `appcrane_list_access_requests` (~84 tokens)

List pending access requests — enhancement_requests rows whose message starts with "Access request for app …" (the portal's Request-access button posts these). With slug, scopes to one app; without, returns access requests across every app the caller can administer. App-admin / owner / global admin required.

Input parameters:

- `slug` (string): Optional. Limit to one app.

### `appcrane_approve_access_request` (~90 tokens)

Approve a pending access request: grants the requester access to the app at `role` (default "user") and marks the enhancement_request as done. Verifies the request is actually an access request before acting. App-admin / owner / global admin required.

Input parameters:

- `request_id` (integer, required): enhancement_requests.id from appcrane_list_access_requests
- `role` (string)

### `appcrane_deny_access_request` (~84 tokens)

Deny a pending access request: marks the enhancement_request as done WITHOUT granting access. Optionally appends a reason to the original message so the requester (and the audit trail) sees why. App-admin / owner / global admin required.

Input parameters:

- `reason` (string): Optional. Appended to the request message.
- `request_id` (integer, required)

### `appcrane_set_app_icon` (~196 tokens)

Set the tile icon for an app (shown on the Dashboard, the Launcher cards, the Manage table, and the frame topbar). Accepts a base64-encoded image in PNG / SVG / WEBP / JPEG / GIF. For repo-tracked icons prefer committing public/icon.png to the repo — AppCrane picks it up automatically on each deploy. Use this MCP tool when the icon needs to change without a redeploy, or when the source isn't in the repo. Replaces any existing icon. App-admin or owner required (or global admin).

Input parameters:

- `base64` (string, required): Base64-encoded image payload. May or may not include the data URL prefix (data:image/png;base64,…) — both work. Max 500 KB decoded.
- `format` (string, required): Image format. Determines the on-disk file extension (icon.<format>).
- `slug` (string, required): App slug.

### `appcrane_get_guide` (~165 tokens)

Fetch the latest AppCrane playbook on a given topic. Use this at the START of any non-trivial workflow so you operate on the current authoritative guidance, not on whatever you remember from a past session. Topics: "onboarding" = the full new-app onboarding playbook (paths a/b/c/d, health-endpoint contract, common pitfalls). "operations" = the comprehensive agent operations guide (deploy, env, logs, rollback, every appcrane_* tool). "email" = how a hosted app sends email through AppCrane (the /api/service/email endpoint, env vars, recipient rules). Topic defaults to "onboarding" if omitted. Returns markdown.

Input parameters:

- `topic` (string): Which guide to fetch. Default: onboarding.

### `appcrane_create_managed_app` (~324 tokens)

Create a new app using AppCrane's GitHub service-account — the platform creates a repo on the configured org/user, owns it, and the agent works against it through github_* tools without the end user ever needing their own PAT. Use this when the user does not have a GitHub account or does not want to deal with GitHub at all. Requires the platform admin to have configured the service-account in Settings → GitHub. Returns the same shape as appcrane_create_app, plus the auto-created repo metadata. IDEMPOTENT RECOVERY: if the slug already exists as a managed app but its AMC_ repo was never created (a half-created app from an earlier failure — push then returns REPO_NOT_FOUND), calling this again re-provisions the missing repo and returns { repaired: true } instead of erroring. So if a create attempt half-failed, just call it again with the same slug. Owner-or-admin to repair an existing one.

Input parameters:

- `branch` (string): Default branch for the new repo. Defaults to "main".
- `description` (string): Optional. Used as both app description and repo description.
- `domain` (string): Optional custom domain.
- `max_cpu_percent` (number): Per-container CPU cap. Default: 50.
- `max_ram_mb` (number): Per-container memory cap. Default: 512.
- `name` (string, required): Display name (human-readable)
- `slug` (string, required): URL slug, lowercase-alphanumeric-with-dashes. Becomes the repo name.

### `appcrane_push_to_managed_app` (~432 tokens)

Push a batch of files to a managed app's AMC_<slug> repo, authenticated server-side via AppCrane's service-account credential. Use this — NOT github_push_files — for managed apps, because github_* tools authenticate with the caller's personal PAT, which has zero access to the service account's repos. Multiple files become a single commit. files: [{ path, content, encoding? }] where encoding defaults to "utf-8" (use "base64" for binaries like icons). Requires the app to already exist via appcrane_create_managed_app. v2.7.22: response now includes per-file `sha256` (hex) and decoded `bytes` length so you can verify integrity — compute the SHA-256 of the bytes you sent, compare to the server's echo, and fail loudly if they differ. Essential for binary files where inline-string truncation or trailing-byte issues would otherwise produce a silently-broken commit. v2.10.7: for a large CODE file, do NOT inline it — upload the bytes over HTTP and commit by token. (1) `curl -F file=@big.js -H "X-API-Key: <your dhk_mcp_ key>" https://<host>/api/files/staged` returns { token, sha256, size_bytes }. (2) Pass that file as { path, staged_token } instead of { path, content }. The server reads the staged bytes and commits them verbatim, so 100+ KB sources push reliably without the model having to emit the content (which is where inline truncation comes from). Per file, provide exactly one of content or staged_token. Staged tokens are owner-scoped and expiring.

Input parameters:

- `branch` (string): Target branch. Defaults to the repo's default branch (usually "main").
- `files` (array, required)
- `message` (string): Commit message. Defaults to "chore: scaffolding for <slug>".
- `slug` (string, required): Managed app slug. Repo name resolved as AMC_<slug>.

### `appcrane_managed_push_chunk` (~446 tokens)

Stage ONE part of a large file for a managed app, entirely over MCP (no HTTP upload). Use this + appcrane_managed_assemble when a file is too large to emit reliably inline via appcrane_push_to_managed_app (roughly >64 KB of code). Workflow: pick an opaque `session` id (any unique string, e.g. "app.tsx-1"), split the file into N parts, and call this once per part with part=1..N and of=N. The parts are held server-side keyed by (session, part); appcrane_managed_assemble then concatenates them in order, verifies the whole, and commits. Split on any boundary you like (byte or line) — assemble concatenates the decoded bytes verbatim, so the split points do not need to be newlines. Keep each part small (≤ ~48 KB of content) so inline emission stays reliable. encoding defaults to "utf-8"; use "base64" for binary. If you provide `sha256` (hex SHA-256 of THIS part's decoded bytes), the server verifies it on arrival and rejects a corrupted part immediately. Re-sending the same (session, part) overwrites it, so a failed part is safe to retry.

Input parameters:

- `content` (string, required): This part's bytes, encoded per `encoding`.
- `encoding` (string): Defaults to utf-8. Use base64 for binary files.
- `of` (integer, required): Total number of parts. Must be identical across every part of a session.
- `part` (integer, required): 1-based part number.
- `path` (string, required): Repo-relative destination path (no leading slash, no ".."). Must be identical across every part of a session.
- `session` (string, required): Opaque upload id grouping the parts. Any unique string; reuse the same value for every part of one file.
- `sha256` (string): Optional hex SHA-256 of this part's decoded bytes. If given, the server verifies it and rejects a mismatch.
- `slug` (string, required): Managed app slug (repo AMC_<slug>).

### `appcrane_managed_assemble` (~266 tokens)

Finish a chunked upload started with appcrane_managed_push_chunk: concatenate all parts of a `session` in order, verify the whole, and commit the assembled file to the managed app's AMC_<slug> repo as a single commit. Fails if any part is missing. If you pass `sha256` (hex SHA-256 of the ENTIRE original file's bytes), the server verifies the reassembled bytes against it and refuses to commit on mismatch — always pass it for large or binary files. On success the staged parts are deleted. Returns the commit sha plus the committed file's sha256 and byte length.

Input parameters:

- `branch` (string): Target branch. Defaults to the app's branch / repo default.
- `message` (string): Commit message. Defaults to "chore: update <path>".
- `path` (string, required): The destination path; must match what the parts were staged with.
- `session` (string, required): The upload id you used for appcrane_managed_push_chunk.
- `sha256` (string): Optional hex SHA-256 of the whole original file. If given, the reassembled bytes are verified against it before committing.
- `slug` (string, required): Managed app slug (repo AMC_<slug>).

### `appcrane_managed_patch` (~336 tokens)

Edit an existing text file in a managed app's AMC_<slug> repo by applying a unified diff, entirely over MCP — you emit only the changed hunks, not the whole file. Ideal for small edits to a large file (avoids re-emitting the whole thing, which is where inline truncation comes from). The server fetches the current file, applies your `unified_diff`, and commits the result as a single commit. The diff must be a standard unified diff (as from `git diff` / `diff -u`): `@@ -old,len +new,len @@` hunk headers, lines prefixed with " " (context), "-" (remove), "+" (add). Include a few context lines around each change. Hunks are matched by CONTENT (not just line numbers), so small line drift is tolerated — but if a hunk's context does not match the current file, the whole patch is rejected and nothing is committed (re-read the file and regenerate the diff). Only single-file diffs are supported; the target is `path`, not the diff's ---/+++ headers.

Input parameters:

- `branch` (string): Target branch. Defaults to the app's branch / repo default.
- `message` (string): Commit message. Defaults to "chore: patch <path>".
- `path` (string, required): Repo-relative path of the file to patch (no leading slash, no "..").
- `slug` (string, required): Managed app slug (repo AMC_<slug>).
- `unified_diff` (string, required): A standard unified diff to apply to the current contents of `path`.

### `appcrane_set_secret` (~137 tokens)

Set or update a secret (an encrypted environment variable injected into the app). Encrypted at rest; only the running app process can read the plaintext. Defaults to sandbox; require explicit stage="production" only when the user asks. App-admin or AppCrane admin only. Respects the caller's mcp_app_scope.

Input parameters:

- `key` (string, required): Env var name. Letters, digits, underscores; must not start with a digit.
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).
- `value` (string, required): The value to store (will be encrypted server-side).

### `appcrane_cp` (~287 tokens)

Copy/upload a file straight into the app's persistent /data volume on the host (aliases: appcrane_upload, appcrane_set_data_blob) — single hop, no container round-trip, no GitHub round-trip, no inline size ceiling. The bytes land at /data/apps/<slug>/<env>/shared/data/<path>, which is the SAME path the running container sees mounted as /data/<path>. Right tool for multi-MB datasets, large fixtures, or anything where appcrane_push_to_managed_app's tool-arg ceiling would force chunking. Returns the SHA-256 + byte count of what was stored so the caller can verify integrity. App-admin or owner of the app required. NEVER returns secrets in the response. Path must be repo-relative, no `..`, no leading slash.

Input parameters:

- `content` (string, required): The data to write. utf-8 string or base64-encoded bytes depending on encoding.
- `encoding` (string): Defaults to utf-8. Use base64 for binary blobs.
- `path` (string, required): Path within /data, e.g. "datasets/threats.json" or "cache/build.tar.gz". No leading slash, no "..".
- `slug` (string, required): App slug.
- `stage` (string): Which env's /data volume to write to. Target stage (legacy alias: env).

### `appcrane_list_cron` (~120 tokens)

List the scheduled jobs declared in an app's deployhub.json `cron` array (after the most recent deploy). Each entry includes the cron schedule, the command, when it last ran, the exit code, and the tail of the last run's stdout/stderr. Use to verify a job was registered, debug a missing run, or read the recent log. App-admin or owner.

Input parameters:

- `slug` (string, required)
- `stage` (string): Optional — omit to list both envs. Target stage (legacy alias: env).

### `appcrane_run_cron_now` (~159 tokens)

Trigger a scheduled cron job RIGHT NOW, regardless of its schedule. Useful for "I want to test my daily rebuild without waiting until midnight" or "rerun yesterday's failed job." Runs the same `docker exec` the tick loop would, against the app's container; updates last_run_at / last_exit_code / last_log just like a scheduled run. Returns the exit code and last-log tail. App-admin or owner. Idempotent: if the job is already running (mutex held), reports it and skips rather than overlapping.

Input parameters:

- `name` (string, required): Job name from deployhub.json `cron[].name`.
- `slug` (string, required)
- `stage` (string): Target stage (legacy alias: env).

### `appcrane_check_resource_limits` (~229 tokens)

Which containers are NOT running with the CPU/RAM limits AppCrane has configured for them? Compares every app row against the limits actually in force on its container and reports only the mismatches. `--memory` and `--cpus` are `docker run` flags, so changing a limit rewrites the database and nothing else until the container is RECREATED — a container created before the limit was set keeps running without it, and every other AppCrane surface reports the CONFIGURED number, so the two are indistinguishable without this. `memory state=not_applied` means NO limit at all: that container can take the whole host, and on a host with no swap that ends as a global OOM kill of whatever the kernel judges largest. Answers `applied: null` (unknown) rather than guessing when Docker cannot be read. ADMIN ONLY.

Input parameters:

- `include_ok` (boolean): Also list containers whose limits ARE applied. Default false — the point is the exceptions.
- `slug` (string): Check one app instead of the whole fleet.

### `appcrane_get_backup_status` (~158 tokens)

Is off-site backup actually working? Reports the scheduled S3/R2 backup config together with a verdict — `configured`, `enabled`, `healthy`, when it last ran and what is missing — so "are we backed up" is one call rather than an inference from raw settings. The backup covers the SQLite database (apps, users, settings, encrypted env vars), .env, icons and appdata, uploaded nightly as one zip. NEVER returns the secret access key; `has_secret` reports only whether one is stored. Read the `summary` first: a config can be fully populated and still not be running (enabled=false), and it can be enabled and failing every night (see last_error). PLATFORM ADMIN ONLY.

### `appcrane_set_backup_config` (~401 tokens)

Configure the scheduled off-site (S3 / S3-compatible, e.g. Cloudflare R2) backup. Every field is optional — only what you pass is changed. Enabling is REFUSED unless bucket, access_key_id and a stored secret are all present, because an enabled-but-unconfigured backup fails silently every night while every status surface reads "enabled", which is worse than being plainly off. SECURITY: `secret_access_key` is write-only — AppCrane encrypts it and never returns it — but passing it here means the plaintext value travels through this conversation and whatever logs it. Prefer Settings → Backup in the dashboard for the secret itself, and use this tool for the rest. PLATFORM ADMIN ONLY: this names the destination a copy of every secret on the platform is written to, so pointing it at the wrong bucket is an exfiltration path, not a misconfiguration.

Input parameters:

- `access_key_id` (string): Access key id. Not a secret on its own; stored in the clear.
- `bucket` (string): Destination bucket name.
- `enabled` (boolean): Turn the nightly schedule on or off. Refused with enabled=true unless bucket, access_key_id and a stored secret all exist.
- `endpoint` (string): Custom S3 endpoint for a non-AWS provider (Cloudflare R2, MinIO). Leave empty for AWS.
- `hour` (integer): Hour of day (server local time) to run. Default 3.
- `prefix` (string): Key prefix inside the bucket, e.g. "appcrane/". Optional.
- `region` (string): Region, e.g. us-east-1. Defaults to us-east-1.
- `secret_access_key` (string): Secret access key. Write-only: encrypted at rest, never returned by any read surface. NOTE: passing it here puts the plaintext in this conversation — the dashboard is the better place for it.

### `appcrane_run_backup_now` (~117 tokens)

Run the off-site backup immediately and report what was uploaded. Use this to PROVE a new configuration works instead of waiting for the nightly run to fail quietly — it exercises the real credentials, the real bucket and the real upload path, and records the result in last_run / last_error exactly as the scheduled job does. Works whether or not the schedule is enabled, so a configuration can be verified before turning it on. Uploads a zip of the SQLite database, .env, icons and appdata. PLATFORM ADMIN ONLY.

### `appcrane_memory_budget` (~243 tokens)

Does the sum of every app's CONFIGURED memory ceiling fit in this host's RAM? Adds up the per-container `--memory` limits AppCrane has on file and compares the total against total host memory. THESE ARE CONFIGURED CEILINGS, NOT MEASURED USAGE: a report of "25 GB committed on a 7.6 GB host" does NOT mean the host is using 25 GB, and must never be relayed as one — it means the limits promise more than the host can deliver if the containers ever ask for it at once. Nothing here reads a running container; for what is actually in force on the containers use appcrane_check_resource_limits, and for live consumption read the host. Over-commitment is normal and is not by itself a fault (containers idle far below their ceilings) — it matters because it means there is no headroom guarantee, so a correlated event such as a post-reboot cold start, when every container loads at once, is resolved by the kernel's global OOM killer. Counts both stages of every app, which is exactly what a cold start brings up. ADMIN ONLY.

### `appcrane_scan_report` (~489 tokens)

Which hosted apps have known-vulnerable dependencies? Reports the recorded CVE scan state for the whole fleet, or for one app with `slug`. REPORT ONLY: this scan has never blocked a deploy and cannot — the apps belong to other teams who did not choose the control, so findings are recorded and mailed and the deploy proceeds either way. Never relay a finding as a deploy failure. READ `status` BEFORE READING COUNTS. It is four-valued: `ok` (scanned, nothing found) and `findings` (scanned, something found) are results; `skipped` (no lockfile AppCrane can read) and `error` (OSV unreachable, unparseable lockfile) mean the app was NOT SCANNED, as does having no scan row at all. Those two carry no findings for the same reason an unopened box is empty, and an agent that reports such an app as clean has stated the opposite of what is known — "no vulnerabilities found" is only ever true of an app whose status is `ok`. `assurance` (none / partial / complete), `unscanned_count` and `unscanned_by_status` say how much of the fleet the numbers actually cover; read them before the findings. EVERY FINDING CARRIES `ecosystem` AND `fixed` beside `name`, `version` and `ids`. `fixed` is the version that resolves those advisories, or null when OSV PUBLISHED NO FIXED VERSION — a null means there is nothing to upgrade to yet, NEVER that no fix is needed and never that AppCrane did not look, so a null-`fixed` finding is not a harmless one. `manifests_scanned` says WHICH manifests were actually read, because coverage is per manifest and not per app: one scan row reads ONE manifest — the ecosystem named on it — so an app whose Go service was never read appears here beside its scanned npm frontend with an empty findings list, and that emptiness is evidence about the frontend only. Scans run at deploy AND daily, and the daily run is the one that matters, because it catches an advisory published against code that was already deployed and has not changed since. ADMIN ONLY.

Input parameters:

- `slug` (string): Report one app (both stages) instead of the whole fleet.

### `appcrane_scan_app` (~275 tokens)

Scan one app's dependencies against OSV right now and record the result, instead of waiting for its next deploy or the nightly run. Use it after fixing a lockfile to confirm a finding is gone, or on an app whose last result was `skipped` or `error` to find out what it actually contains. REPORT ONLY — it records a row and feeds the daily digest; it never blocks, fails or rolls back anything, and running it cannot disturb the app. It does not throw on a failed scan either: an unreachable OSV or a missing lockfile comes back as `ok: false` with status `error` or `skipped`, meaning the app was NOT scanned and is NOT known to be clean. `ok: true` with status `findings` is the opposite case — the scan worked and found something. Reads the LIVE release through the same `current` symlink the running container was built from. Defaults to the PRODUCTION stage: most AppCrane tools default to sandbox, but the code an advisory applies to is the code that is serving. ADMIN ONLY.

Input parameters:

- `slug` (string, required): App to scan.
- `stage` (string): Stage to scan. Defaults to production — that is the deployed code an advisory applies to. Target stage (legacy alias: env).

### `appcrane_platform_policy` (~266 tokens)

Read or set the two platform-wide policy levers, and list the apps currently in violation. `ban_public_apps` refuses visibility=public on every write path; `mandate_security_scans` reports every app without a completed scan in the last 48h. Both default OFF, so an upgrade enforces nothing until an admin turns one on. Call with no arguments to read the current policy plus violations; pass either boolean to change it. POLICY IS NOT RETROACTIVE: turning a lever on refuses the NEXT write and REPORTS what is already in violation — it does not reach into the database and change existing apps, and the violations it lists keep working exactly as they did. That is deliberate: silently making live public apps private would break their URLs with no warning to their owners and no record of what changed, so the list exists for an admin to work through deliberately. Never describe enabling a lever as having fixed the apps it reports; nothing about them has changed. PLATFORM ADMIN ONLY.

Input parameters:

- `ban_public_apps` (boolean): Refuse visibility=public everywhere. Existing public apps keep serving and are reported as violations.
- `mandate_security_scans` (boolean): Report every app with no completed dependency scan in the last 48h (two missed daily runs).

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp#diagnostics

## Score history

- 2026-09-25: 75
- 2026-09-24: 74
- 2026-09-23: 74
- 2026-09-22: 73
- 2026-09-21: 73
- 2026-09-20: 73
- 2026-09-19: 72
- 2026-09-18: 72
- 2026-09-17: 71
- 2026-09-16: 71
- 2026-09-15: 70
- 2026-09-14: 70
- 2026-09-13: 66
- 2026-09-12: 66
- 2026-09-11: 66
- 2026-09-10: 66
- 2026-09-09: 66
- 2026-09-08: 66
- 2026-09-07: 51

## Common questions

### What is the AppCrane MCP server?

AppCrane is an MCP server listed in the public MCP registry as io.github.gitayg/appcrane. Deploy and manage apps on your own AppCrane instance, the self-hosted PaaS for AI-built apps. This page covers its npm package (appcrane-mcp).

### Is the AppCrane MCP server safe to use?

AppCrane scores 75 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 25 September 2026. It declares no install or post-install scripts. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the AppCrane MCP server expose?

AppCrane exposes 57 tools: appcrane_list_apps, appcrane_get_app, appcrane_top_apps, appcrane_top_users, appcrane_get_health, and 52 more. Their descriptions and schemas cost roughly 13,973 tokens of context every time the server is loaded.

### Is the AppCrane MCP server still maintained?

AppCrane is still listed as active in the MCP registry. We last reached this channel on 25 September 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the AppCrane MCP server under?

AppCrane declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- npm package: https://www.npmjs.com/package/appcrane-mcp
- Socket report: https://socket.dev/npm/package/appcrane-mcp
- Repository: https://github.com/gitayg/appcrane-mcp
- Website: https://glick.run/appcrane.html
- Changelog RSS feed: https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp.json
- HTML version of this page: https://verifymcp.io/servers/gitayg-appcrane/appcrane-mcp
