MCP Tools
The 31 tools exposed by the MCP server at https://mcp.xhostd.com/mcp/,
grouped by what stage of the app lifecycle they serve.
Auth is OAuth (Google sign-in) for both claude.ai connectors and Claude Code —
there is no token to mint or paste. The tools carry the same semantics as the
HTTP API; llms-full.txt
is the authoritative, always-current tool list. MCP clients cache the tool set
at connect time, so if a tool documented here is missing from your session,
reconnect the connector to pick it up.
Two argument conventions coexist: lifecycle tools take ids
(app_id, channel_id — returned by
create_app / list_apps), while data-surface tools
take names (app_name, channel, e.g. "prod").
When two accessible apps share a name, qualify it as
owner_username/app_name.
| Group | Tools |
|---|---|
| Apps | list_apps, create_app, get_app, delete_app |
| Channels | list_channels, create_channel, delete_channel |
| Files & deploy | list_files, read_file, commit_files, deploy, get_deploy_log, sync_git |
| Env & secrets | set_env, delete_env, list_env, get_deploy_env |
| Database & snapshots | list_channel_snapshots, restore_channel_db |
| Object storage | get_blob_credentials, get_blob_usage |
| Custom domains | add_custom_domain, verify_custom_domain, list_custom_domains, remove_custom_domain |
| Credentials & git | get_credentials |
| Observability | get_app_stats, list_activity |
| Exports | export_data, get_export_status |
| Feedback | submit_feedback |
Apps
The app is xhost's unit of ownership: one git repo
(https://git.xhostd.com/<user>/<app>.git) plus one or
more channels. These four tools open and close that lifecycle — every other
tool operates inside an app they create.
list_apps
Orientation at the start of a session, and the source of the ids the other tools need. Arguments: none.
Returns: {apps: [...]} — each app's
id, name, repo_url, template,
created_at, and channels (with id,
hostname, current_sha, status).
create_app
The first call of any new project. Provisions the git repo and the
auto-created prod channel in one step.
| Argument | Type | Description |
|---|---|---|
name | string required | DNS label: lowercase, digits, hyphens, max 40 chars. Reserved prefixes rejected: git, api, www, admin, preview, staging. |
template | string optional | "static" (default — the committed files are served as-is), "app" (your install.sh + launch.sh run in a Node 22 / Python 3.12 runtime), or "docker" (the Dockerfile at your repo root is built and run; listen on $PORT). |
Returns: the app object — id, repo_url,
and channels[0], the prod channel, with its
id and public hostname
(<app>-<user>.xhostd.com), current_sha: null,
status: "provisioning". Hold onto app_id and the prod
channel_id.
create_app(name="my-site", template="static")
→ {"id": "f47ac10b-...", "repo_url": "https://git.xhostd.com/alice/my-site.git",
"channels": [{"id": "7c9e6679-...", "name": "prod",
"hostname": "my-site-alice.xhostd.com", "status": "provisioning"}]}
get_app
Fetch a single app by app_id when you need its
repo_url or fresh channel status without listing everything.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
Returns: the app object, same shape as one list_apps entry.
delete_app
Permanent teardown: stops all containers, removes the git repo, and cleans up
routes for the app and every channel. This is also the only way to remove the
prod channel. Irreversible.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
Returns: a confirmation string.
create_app alone puts nothing
online — the prod channel sits in provisioning until
its first deploy. A running app knows about the platform only through injected
env (XHOST_USER, XHOST_SHA, DATABASE_URL,
S3_*, PORT). After delete_app, containers
stop and the hostnames stop routing.
MCP vs console. The full app lifecycle is available via MCP.
Sharing a project with other users (members, roles, ownership transfer) is
console-only. A plan_limit_exceeded error is an upgrade prompt,
not a retryable failure — upgrading happens in the browser.
Channels
A channel is a deployable environment: bound to one git branch, with its own
hostname, its own Postgres schema, its own object-storage bucket, and its own
env overrides. prod is created with the app; additional channels
give preview and staging URLs
(<channel>-<app>-<user>.xhostd.com).
list_channels
Recover channel ids and hostnames for an app later in a session.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
Returns: an array of channel objects — id,
name, hostname, git_ref_binding,
current_sha, status.
create_channel
Stand up a preview/staging environment bound to a branch — one explicit channel per branch.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The parent app id. |
name | string required | DNS label. Cannot be prod (auto-created). |
git_ref_binding | string required | branch:<name>. The legacy branch:* wildcard is deprecated and rejected at create time. |
Returns: the new channel object (status: "provisioning").
create_channel(app_id, name="staging", git_ref_binding="branch:staging")
→ {"id": "a3bb189e-...", "hostname": "staging-my-site-alice.xhostd.com",
"status": "provisioning"}
delete_channel
Permanently remove a non-prod channel: tears down its container, route, and
Postgres schema. The git repo and other channels are unaffected. Refuses
prod — use delete_app for that. Irreversible.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name (as shown in list_apps). |
channel | string required | The channel name to delete (must not be prod). |
Returns: {"ok": true}.
DATABASE_URL and S3_* point at that
channel's schema and bucket — staging cannot read prod's data.
MCP vs console. Fully available via MCP.
Files & deploy
Shipping is deliberately two-step: a commit stores code, a deploy builds and
ships it — committing (or pushing) alone puts nothing live. These six tools
cover reading the repo's current state, writing commits without git, and
driving builds. For sizable local checkouts, plain git push via
get_credentials is the faster path —
the deploy step is the same either way.
list_files
See what is in the repo before editing — essential for a stateless agent.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
ref | string optional | Branch name or 40-char sha. Default "master". |
Returns: {ref, sha, files: [{path, kind, size}]}.
read_file
Fetch one file's contents before modifying it.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
path | string required | Repo-relative file path. |
ref | string optional | Branch name or sha. Default "master". |
Returns: the file contents as text.
commit_files
Write files as one real git commit, no local git required. Sparse: send only what's changing.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
message | string required | Commit message (non-empty). |
files | object required | Map of path → content. A string upserts the file, null deletes it, absent paths are unchanged. |
ref | string optional | Branch to commit on. Default "master"; created if it doesn't exist. |
Returns: {sha} — pass it to deploy.
On GitHub-connected apps this returns an error; push to GitHub instead.
deploy
The build-and-ship trigger. Runs asynchronously; follow progress with
get_deploy_log.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
channel_id | string required | The target channel id. |
sha | string optional | A 40-char hex commit. Exactly one of sha/ref is required; if both are supplied, sha wins. |
ref | string optional | A branch name ("master" or "refs/heads/master") — resolved to that branch's current HEAD at deploy time. |
Returns: {deploy_id, channel_id, status: "queued"}.
On GitHub-connected apps, each deploy first auto-syncs the mirror from GitHub;
a failed sync rejects the deploy with the sync error. Docker-template deploys
stream [build] ... lines (queue position, build duration, image
size vs the plan cap) into the deploy log.
get_deploy_log
The plain-text build/run log — poll it until the deploy reaches
success or failed. The most common first-deploy
failure reads health check returned non-200: the app didn't answer
HTTP 200 at / on $PORT in time. For docker
deploys the log also carries [build] ... lines — queue position,
build duration, and the built image's size against the plan cap.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
channel_id | string required | The channel id. |
deploy_id | string required | The deploy id from deploy. |
Returns: the log as plain text.
sync_git
For apps connected to a GitHub source: fetch the latest GitHub commits into the app's xhost mirror without deploying. Deploys auto-sync anyway, so this is for refreshing the mirror or surfacing sync errors on their own.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. Errors if no GitHub source is connected. |
Returns: the mirror status —
{last_sync_status, last_sync_error, last_sync_refs, last_synced_at}.
commit_files(app_id, message="add landing page",
files={"index.html": "<!doctype html>..."})
→ {"sha": "def456..."}
deploy(app_id, channel_id, sha="def456...")
→ {"deploy_id": "9b1deb4d-...", "status": "queued"}
get_deploy_log(app_id, channel_id, deploy_id="9b1deb4d-...") # poll until success
XHOST_SHA). For the
static template the committed files are served straight from the
repo root. For the app template, install.sh runs, then
launch.sh; the server must bind 0.0.0.0:$PORT and
answer HTTP 200 at / within 120s, inside a ~128 MB memory
budget that applies to install.sh too. For the docker
template, the Dockerfile at the repo root is built on the app's
cell and the container runs with its own ENTRYPOINT/CMD;
it must listen on $PORT and answer the same GET /
health check. Env vars are injected at run time only — never as build args, so
secrets are unavailable during the build. Charged image size (total minus
warm-base layers) is capped per plan — free 512 MiB / basic 2 GiB /
pro 4 GiB — with the warm bases (node:22-slim,
node:24-slim, python:3.11-slim,
python:3.12-slim, python:3.13-slim,
debian:trixie-slim) exempt from the cap.
MCP vs console. All six tools are MCP. Connecting or
disconnecting a GitHub source has no MCP tool — use the console or the HTTP
API (POST /apps/{app_id}/github/connect,
DELETE /apps/{app_id}/github); while connected,
commit_files and git push to xhost are rejected —
GitHub is the source of truth.
Env & secrets
Runtime configuration without hardcoding it in the repo. Two kinds:
env (plain, readable back) and secret (write-only
through MCP). Two scopes: an app-level default shared by all channels, and a
per-channel override that wins at deploy time. All values are encrypted at
rest, and every change takes effect at the next deploy.
set_env
Create or update a variable or secret.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
key | string required | Must match ^[A-Z_][A-Z0-9_]*$. System-injected keys are reserved and rejected: XHOST_USER, XHOST_SHA, DATABASE_URL, DATABASE_HOST, DATABASE_PASSWORD, S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION. |
value | string required | The value; stored encrypted. |
secret | boolean optional | Default false. With true the value is a secret: never readable back through MCP. The HTTP API reveal endpoint (GET /apps/{app_id}/env/{key}/value) and the console's click-to-reveal can return it, and each reveal is audit-logged. |
channel | string optional | A channel name (e.g. "prod"). With it, the value is a channel-scoped override that beats the app-level default on that channel; without it, the app-level default. |
Returns: a confirmation string.
delete_env
Remove a variable or secret.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
key | string required | The key to delete. |
channel | string optional | With a channel name, deletes only that channel's override — the app-level default, if any, stays in effect. Without it, deletes the app-level entry. |
Returns: a confirmation string.
list_env
Inspect current configuration. Plain values come back in cleartext;
secret values are never returned via MCP — secret entries
carry metadata only (value is null).
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
channel | string optional | Without it: every raw entry (app-level defaults and all channel overrides). With a channel name: the resolved view for that channel, with scope (app or channel) reporting which entry wins at deploy time. |
Returns: {env: [{key, kind, scope, channel_id, updated_at, value}]}.
get_deploy_env
Debugging "what config was live when this broke": the env snapshot recorded when a past deploy started — what the app actually ran with, not what is configured now.
| Argument | Type | Description |
|---|---|---|
app_id | string required | The app id. |
channel | string required | The channel name. |
deploy_id | string required | The deploy id. |
Returns: user-set plain vars with values, secrets masked
(value: null), and system-injected keys listed by name only —
their values are credentials and are not stored in the snapshot. 404 for
deploys that predate env snapshots.
set_env(app_id, key="STRIPE_SECRET_KEY", value="sk_live_...", secret=True)
set_env(app_id, key="LOG_LEVEL", value="debug", channel="staging")
deploy(app_id, channel_id, ref="master") # changes take effect here
process.env.MY_VAR, os.environ["MY_VAR"]) at the next
deploy — never mid-run. Resolution order at deploy time: system-injected keys
beat channel overrides, which beat app-level defaults.
MCP vs console. Editing env is deliberately MCP/API-only — the
console displays each channel's resolved env but doesn't edit it. Revealing a
secret's value is the one thing that goes the other way: there is no MCP tool,
but the HTTP API endpoint GET /apps/{app_id}/env/{key}/value and the
console can reveal it, and every reveal is audit-logged.
Database & snapshots
Every channel automatically gets its own Postgres schema with
DATABASE_URL injected — there is nothing to enable and no tool to
call for normal use. Before every non-static deploy the platform snapshots the
channel's schema; these two tools are the safety net for when a deploy's
migration goes wrong.
list_channel_snapshots
Enumerate the automatic pre-deploy snapshots, newest first.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name (as shown in list_apps). |
channel | string required | The channel name. |
Returns: a list of
{snapshot_id, deploy_id, created_at, size_bytes}, newest first.
restore_channel_db
Roll a channel's schema back to a snapshot. The restore is staged: the live
schema is renamed aside and only dropped after the restore succeeds, so a
failed restore loses nothing. Refuses prod unless the app's env
contains XHOST_ALLOW_PROD_RESTORE=1, and refuses while any deploy
on the channel is queued or running.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
snapshot_id | string required | From list_channel_snapshots. |
Returns: the channel's updated Postgres status.
list_channel_snapshots(app_name="my-site", channel="staging")
→ [{"snapshot_id": "5f2c...", "deploy_id": "9b1d...",
"created_at": "2026-07-01T09:00:00Z", "size_bytes": 81920}, ...]
restore_channel_db(app_name="my-site", channel="staging", snapshot_id="5f2c...")
DATABASE_URL
from its environment; the connection's search_path is pinned to
the channel's schema, so code uses unqualified table names. Migrations are
100% user-managed — put alembic upgrade head /
prisma migrate deploy in install.sh (the database is
reachable before it runs). After a restore, the same DATABASE_URL
keeps working; the data is simply back at the snapshot moment.
MCP vs console. Snapshot list/restore is MCP (with the prod
guard above). SQL reset (drop + recreate the schema) and SQL
dump download have no MCP tool — they are HTTP API endpoints
(also surfaced in the console) meant for deliberate, human-approved use. The
per-project external database access toggle that lets outside
tools connect via db.xhostd.com is console-only.
Object storage
Per-channel S3-compatible storage for unstructured blobs (uploads, generated assets) — auto-provisioned like the database, no enable step. A running app needs no tool at all: its credentials are injected. These two tools exist for working with a channel's bucket from outside the container, and for monitoring.
get_blob_credentials
Fetch the endpoint, region, bucket, and key pair for any S3 client. The secret access key is sensitive — treat it like a password. Each read is audit-logged. Accessing the bucket from outside xhost additionally requires the console's external-access toggle to be on.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
Returns: a rendered configuration block — endpoint, region, bucket, access key ID, secret access key.
get_blob_usage
Check consumption before it hits the per-user quota (over quota, writes return 507).
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
Returns: bytes used and the provisioning status (plus the last error if failed).
S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID,
S3_SECRET_ACCESS_KEY, S3_REGION — point any S3 SDK at
those env vars rather than constructing values. Object versioning is always
on, a channel's key can only address its own data, and each deploy records a
snapshot marker that a restore (console or API) can roll back to.
MCP vs console. Credentials and usage are MCP. Storage
snapshot restore has no MCP tool — it is an HTTP API endpoint
(POST .../blob/restore) also surfaced in the console; the
app-wide external-access toggle is console-only. A data
rollback and opening storage to the outside are deliberate human actions.
Custom domains
Channels get canonical *.xhostd.com hostnames automatically;
these four tools put a channel on the user's own domain instead. Up to 5
domains per channel; domains are globally unique across xhost. The flow is:
attach → the user creates DNS records at their registrar → verify.
See Custom domains for the full story.
add_custom_domain
Attach a domain and get the DNS records the user must create: a TXT ownership
token plus a routing record (CNAME for subdomains, A for an apex). The result
includes a ready-to-relay instructions string — pass it to the
user verbatim. Re-adding the same domain on the same channel is idempotent
(token preserved); domain_taken (409) means another channel owns it.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
domain | string required | The hostname to attach (e.g. myapp.com). Invalid-domain reasons: is_ip_address, invalid_idna, too_long, invalid_label, platform_domain_forbidden, needs_dot. |
Returns: the domain object —
{domain, status: "pending", reason, dns_records: {txt_host, txt_value,
cname_target, a_values}, created_at, verified_at} — plus
instructions.
verify_custom_domain
Re-check the DNS records after the user creates them. Idempotent and
retryable — propagation usually takes a few minutes, so a pending
status with a transient reason on the first call is normal. On a passing check
the status flips to verified and the public route plus on-demand
TLS go live. A verified domain only downgrades on the four definitive reasons
(txt_nxdomain, txt_token_mismatch,
dns_not_pointing, domain_nxdomain); transient lookup
failures never downgrade it.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
domain | string required | The attached hostname. |
Returns: the same domain-object shape, with fresh status/reason and instructions.
list_custom_domains
See which domains are verified and which still need DNS work — check the
count before proposing another add_custom_domain (limit 5).
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
Returns: {domains: [...]}, each item the same shape as add_custom_domain's result.
remove_custom_domain
Detach a domain. The route is removed, certificate renewals stop, and the existing cert expires on its own — nothing to do at the registrar.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string required | The channel name. |
domain | string required | The hostname to detach. |
Returns: {ok: true}.
add_custom_domain(app_name="my-site", channel="prod", domain="example.com")
→ instructions: "Add these DNS records at your registrar:
TXT _xhost.example.com xhost-verify-abcdef01...
A example.com 198.51.100.7"
# ...user creates the records, a few minutes pass...
verify_custom_domain(app_name="my-site", channel="prod", domain="example.com")
→ {"status": "verified", ...}
$PORT. HTTPS is automatic — the certificate mints on the first
request after verification. Google sign-in works on the custom domain with no
extra config; the identity cookie's aud is the inbound hostname,
so a login on myapp.com is separate from the canonical channel
host.
MCP vs console. The full domain lifecycle is available via MCP; the console offers the same operations as a convenience.
Credentials & git
MCP sessions are authenticated by OAuth — no token ever surfaces. When the
work steps outside MCP (a local git push,
psql, curl against the API), this tool mints the one
credential that covers all three.
get_credentials
Mint a 30-day unified credential. Arguments: none. The token is simultaneously
the git password, the Postgres password (when external database access is
enabled in the console), and a platform API bearer, carrying the full default
scopes. Revocable any time on the console's /tokens page; re-mint
by calling the tool again after expiry.
Returns: {token, username, expires_at, scopes}.
get_credentials()
→ {"token": "xh_...", "username": "alice", "expires_at": "...", "scopes": [...]}
git remote add xhost "https://alice:xh_...@git.xhostd.com/alice/my-site.git"
git push xhost master
deploy(app_id, channel_id, ref="master") # pushing alone does not deploy
MCP vs console. Mintable via MCP or on the console tokens
page. The underlying POST /credentials endpoint also accepts an
optional scopes subset for a least-privilege credential.
Observability
Two read-only views: what the app has been doing (traffic and usage), and what people and agents have been doing to it (an attributed audit trail). Consult these before recommending scale/perf changes, or when reconstructing "who changed what".
get_app_stats
Access/usage stats over a recent window. Timestamps are UTC.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
channel | string optional | A channel name. When omitted, stats are summed across all channels. |
window | string optional | One of "24h" (default), "7d", "30d". |
Returns: the stats object for the window.
list_activity
The project's audit trail, newest first: member changes, deploys, env writes, database operations, git pushes and commits — each attributed to the user who did it. No secret values ever appear.
| Argument | Type | Description |
|---|---|---|
app_name | string required | The app's name. |
limit | integer optional | Max events to return, 1–100. Default 50. |
Returns:
{events: [{id, actor_username, action, target, detail, created_at}], next_before}.
MCP vs console. Both available via MCP; the console shows the same stats and activity on the project pages.
Exports
Portable takeout — no lock-in. An export is a self-contained archive of the deployed code, a Postgres schema dump, and (when small enough) the channel's object-storage files, reloadable with standard tools and no xhost. Env variable keys are included as blank placeholders; secret values are never exported. Builds run asynchronously: queue, then poll.
export_data
Queue an export of one channel or a whole app.
| Argument | Type | Description |
|---|---|---|
scope | string required | "channel" (one channel) or "app" (every channel). |
app | string required | The app's name. |
channel | string optional | The channel name — required when scope is "channel", ignored for "app". |
Returns: {export_id, status, detail}.
get_export_status
Poll an export by id. While building, it reports progress; when ready, it
mints a short-lived download token and returns ready-to-paste
curl commands for the archive (and, when included, a separate one
for the object-storage files); when failed, it surfaces the error.
| Argument | Type | Description |
|---|---|---|
export_id | string required | From export_data. |
Returns:
{status, detail, progress_pct, size_bytes, blobs_included, blobs_reason,
expires_at}, plus download instructions when ready or
error when failed.
export_data(scope="channel", app="my-site", channel="prod")
→ {"export_id": "e1a2...", "status": "queued"}
get_export_status(export_id="e1a2...") # poll until "ready"
→ {"status": "ready", "size_bytes": 1048576, "download": "curl ..."}
MCP vs console. Available in both: the console project page has the same trigger, status, and download links.
Feedback
The agent driving these tools sees the rough edges first, so it gets a direct channel to the xhost team. Fire-and-forget — no permission needed, no result to block on.
submit_feedback
Call proactively whenever something gets in the way: a task that took several iterations, an unclear tool or error, a hard-to-diagnose failure, a missing capability.
| Argument | Type | Description |
|---|---|---|
message | string required | The feedback text, in your own words. Non-empty, max 4000 characters. |
app_id | string optional | The app being worked on, for context. An unknown id is silently dropped; the feedback still lands. |
Returns: {id, status: "new"}.
MCP vs console. MCP/API only — it exists precisely for the agent in the loop.
MCP vs console matrix
Almost everything is agent-drivable. The exceptions are deliberate: access-widening toggles and project sharing are console-only, and destructive data actions have no MCP tools — they exist as HTTP API endpoints (and in the console) meant for deliberate, human-approved action, not something an agent triggers mid-session.
| Capability | Surface |
|---|---|
| Create/delete apps and channels, commit, deploy, logs | MCP + API |
| Env & secrets: set, delete, list, deploy snapshots | MCP + API (console views only) |
| Secret value reveal (each reveal audit-logged) | API + console (no MCP tool) |
Database snapshots: list & restore (prod gated by XHOST_ALLOW_PROD_RESTORE=1) | MCP + API |
| SQL reset (drop + recreate schema) | API + console (no MCP tool) |
| SQL dump download | API + console (no MCP tool) |
| Object storage: credentials & usage | MCP + API |
| Object-storage snapshot restore | API + console (no MCP tool) |
| External-access toggles (database + object storage) | Console only |
| Custom domains: add, verify, list, remove | MCP + API |
| Project sharing: members, roles, ownership transfer | Console only |
GitHub source: connect & disconnect (sync via sync_git) | API + console (no MCP tool) |
| Credentials, stats, activity, exports, feedback | MCP + API |
The pattern: wiping a schema, rolling storage back, revealing a secret, or
opening a data surface to the outside world all have a blast radius that
warrants a deliberate human decision — so none of them are MCP tools, and
secret reveals carry an audit trail attributing the action to the user who
took it. For everything an app
needs day to day, write code that reads the injected
DATABASE_URL and S3_* env instead of driving those
surfaces directly.