Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault

No arguments

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
list_gpusA

List GPUs known to the daemon, with live utilization metrics.

cluster_statsB

Get aggregate cluster metrics (GPUs in use/total, services up).

list_servicesB

List services (apps, models and databases), newest first.

get_serviceC

Get one service.

wait_for_serviceA

Use when: a deploy is in flight and you need its real outcome.

    Blocks until the service converges (healthy) or fails, then returns
    ``{outcome: converged|failed|timeout|superseded, ...}`` — always, never
    raising on a timeout. After ``stop_service``, ``converged`` means the
    service is actually stopped.

    On ``outcome: failed`` the response also carries ``diagnosis`` — the
    FULL ``diagnose_service`` bundle (``remediation.code`` + ``detail``,
    crash forensics, health probe, binding readiness and a bounded log
    tail), folded in so a failure needs no second call. Branch on
    ``diagnosis.remediation.code``. It is ``null`` on every non-failed
    outcome, and ``null`` on a failure your token is not owner-or-admin for
    (the bundle carries app log lines) — in that case call
    ``diagnose_service`` yourself with a token that owns the app.
    
service_logsA

Use when: you need the raw output a service or its build printed.

    ``source`` picks which output you get. **Pass ``source='runtime'`` when
    you are debugging a running app** — the build and the app used to share
    one stream, so a bare ``grep`` over a deployed app still matches
    BuildKit layer chatter (``404B``, ``CACHED``, ``sha256:…``) from the
    build that produced it. Lines written by an older daemon are stored as
    stdout and therefore read as ``'runtime'`` whichever program printed
    them.

    ``grep`` keeps only lines CONTAINING that text. It is a plain
    **literal substring**, never a regex.

    All three filters run server-side inside the ``tail`` bound, so ``tail``
    counts matching lines: ``grep='Traceback', source='runtime', tail=20``
    gives the last 20 app tracebacks out of a million-line log in one
    bounded query. Prefer that over pulling a large tail and searching it
    yourself.
    
serveC

Register a service from a prebuilt image.

stop_serviceC

Stop a service (desired_state → stopped).

restart_serviceB

Restart a service (clears backoff).

remove_serviceB

Delete a service (teardown + remove).

diagnose_serviceA

Use when: a service is failing and you need the reason, not the logs.

    Bundles the deploy phase, crash forensics (``last_exit_code``,
    ``oom_killed``), a fresh health probe, AI-binding readiness, a bounded
    log tail and a single ``remediation_code`` to branch on — call it after
    ``wait_for_service`` returns ``failed`` instead of scraping logs.
    Owner-or-admin read: the bundle carries env/secret key *names* and app
    log lines, never secret values.
    
service_statsA

Get live CPU, memory, network and PID usage for a running service.

    Returns ``{available, stats: {cpu_pct, mem_used_bytes, mem_limit_bytes,
    mem_pct, net_rx_bytes, net_tx_bytes, pids}, gpus, cached}``.

    **Branch on ``available``, never on zeroes.** ``available: false`` with
    ``stats: null`` is the normal answer when the service has no running
    container or the daemon cannot reach Docker; every individual counter
    is likewise ``null`` when unknown, so a missing metric never
    masquerades as an idle one. ``cpu_pct`` follows the docker convention —
    ``100.0`` is one fully-used core, so a multi-core host can exceed 100.

    For *why* a service is unhealthy use ``diagnose_service``; this tool
    answers *how much* a healthy one is consuming. The daemon caches the
    sample for 2 seconds, so polling it in a loop is cheap.
    
run_commandA

Run a one-off command in a deployed service's image (migrations, seeds).

    The command runs in a throwaway container built from the service's
    current image with the same env, secrets, AI/DB bindings and named
    volumes as the live container — the service itself keeps serving.

    **Bounded-synchronous**: this call BLOCKS until the command exits or
    ``timeout_s`` seconds elapse, then returns ``{exit_code, timed_out,
    oom_killed, duration_s, run_id, log_tail, ...}``. Branch on
    ``exit_code``/``timed_out`` — a non-zero exit is a normal 200 result,
    not an error. One run at a time per service (409
    ``service.run_in_progress``).

    Because the call blocks for minutes, **supply your own
    ``idempotency_key`` and REUSE it verbatim if you retry** — that is the
    only thing standing between a client-side read timeout and a migration
    running twice. A retried key REPLAYS an ``idempotent_replay`` envelope,
    NOT the command's output — the run's stdout is deliberately never
    cached at rest. That replay is an HTTP 200 carrying ``code:
    "idempotent_replay"`` and NO ``exit_code``, so check for that code
    before reading ``exit_code``. Re-run under a fresh key if you need the
    output again, or read it from ``diagnose_service``'s ``last_run``.

    **Key reuse protects a COMPLETED run only.** A failed call un-pins the
    key, so a same-key retry really re-executes. In particular ``503
    run.interrupted`` means the container started and was lost — the
    command may have partially applied — and retrying it, with any key,
    runs it again: stop and inspect instead. And if the daemon died
    mid-run, the key stays claimed for 24 h and the retry returns ``409
    idempotency_in_progress``; mint a fresh key once you have confirmed
    the run is over.
    
serve_modelA

Serve a local AI model as an OpenAI-compatible endpoint.

    Serving is asynchronous: the call returns immediately with the
    model row in ``building`` while the daemon pulls the image and weights
    off-tick; poll ``list_models`` (or ``get_service``) until it is
    ``running``. Lifecycle (stop / restart / delete) goes through the
    existing service tools (``stop_service`` / ``restart_service`` /
    ``remove_service``).

    An app connects to this model by declaring an ``[ai.<name>]`` binding
    (provider ``ollama``, model = this model); at each launch it receives
    ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
    sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` so an unmodified
    OpenAI SDK reaches this endpoint with no code change.
    
list_modelsA

List served AI models (kind=model workloads), bounded to limit per page.

    Returns a cursor-paginated page; pass ``cursor`` from the previous page
    to continue. A newly served model shows ``building`` until its off-tick
    pull completes; lifecycle actions go through the service tools.
    
create_databaseA

Provision a managed database (kind=database workload).

    Provisioning is asynchronous: the call returns immediately with the
    database row in ``building`` while the daemon pulls the image off-tick
    and launches the server; poll ``list_databases`` (or ``get_service``)
    until ``db_ready`` is true. Lifecycle (stop / restart / delete) goes
    through the existing service tools.

    The daemon mints the password server-side and stores it write-only — it
    is NEVER returned by this tool (key names only). An app connects to this
    database by declaring a ``[db.<name>]`` binding (``provider="managed"``,
    ``database`` = this database's name — the ``database`` field is
    required, there is no implicit default); at each launch it receives
    ``NERDIT_DB_<NAME>_URL``, and the ``default`` binding also sets
    ``DATABASE_URL`` (Postgres) or ``REDIS_URL`` (Redis) with the composed
    DSN so an unmodified client library connects with no code change.
    
list_databasesA

List managed databases (kind=database workloads), bounded to limit per page.

    Returns a cursor-paginated page. A newly created database shows
    ``building`` until its off-tick image pull and readiness probe complete
    (``db_ready`` true).
    The ``endpoint`` is password-free by construction; lifecycle actions go
    through the service tools.
    
dump_databaseA

Capture a logical, application-consistent dump of a managed database.

    The daemon runs the engine's own dump tool — ``pg_dump --format=custom``
    for Postgres, ``redis-cli --rdb`` for Redis — from a throwaway container
    built from the database's OWN image, against the running server. This is
    the dump to take before a migration or a destructive change: unlike the
    volume backup (``nerdit backup --volume``, no tool), it is consistent by
    the engine's rules rather than by the filesystem's.

    CUSTODY: the artifact is a server-side tar under the daemon's backups
    directory holding **every row** of ``name``; it is never served over
    HTTP and nothing but metadata comes back here — basename, size, sha256,
    engine, format, duration. Treat the basename as the handle: it is what
    ``nerdit db restore`` takes.

    RETENTION: the daemon keeps the last ``[retention].dump_keep_last``
    dumps per database (default 5) and prunes older ones on its hourly
    sweep, so a dump is not an archive. Copy one off the box if it must
    outlive that window.

    TIMEOUT: the default matches the usual MCP client read budget, not
    the route's own 900: if your client gives up first, **a longer dump still completes
    server-side and appears in ``list_database_dumps``**. So a read timeout
    here is not a failure — call ``list_database_dumps`` and take the newest
    entry. Do NOT blind-retry: this tool mints its own idempotency key per
    call, so a retry is a second real dump (and the first one may still be
    running, which answers ``409 dump.in_progress``).

    Authorization is **owner-or-admin plus token scope**: a scoped submitter
    can dump the databases it created, which is what makes this usable over
    the (submitter-capped) cloud link. Refusals you should branch on: ``409
    dump.database_not_ready`` (not running, or still doing initdb — poll
    ``list_databases`` for ``db_ready``), ``409 dump.in_progress`` (this
    database already has one running; distinct from the daemon-wide
    ``backup.in_progress``), ``409 dump.too_many_in_flight``, ``409
    dump.insufficient_disk`` (the error object carries top-level
    ``required_bytes`` / ``free_bytes``), ``422 dump.not_a_database``, and
    ``500 dump.failed``
    whose ``hint`` is the tool's own last output line. There is deliberately
    **no restore tool** — restoring replaces live data and stays on the CLI
    (``nerdit db restore``) and REST.
    
list_database_dumpsA

List the dump tars this daemon holds for one managed database.

    Newest first, ``{dump, size_bytes, created_at}`` per row, where
    ``created_at`` is the tar's mtime (the listing never opens an archive).
    Metadata only, like ``dump_database``: the tars themselves stay under
    the daemon's backups directory and are never served over HTTP, and each
    one holds every row of ``name``.

    Two things this read is for. It is the recovery path when a
    ``dump_database`` call outran your client's read budget — **the dump
    still completes server-side and shows up here**, so list before you
    retry. And it is how you check retention: the daemon keeps the last
    ``[retention].dump_keep_last`` per database (default 5) and prunes the
    rest hourly, so on a default daemon what is listed here is all there is.
    Retention can be turned off (``dump_keep_last = 0``), so the listing is
    capped at the newest 500 instead: ``truncated`` is true when older tars
    exist on the box that this page does not name.

    Owner-or-admin plus token scope — the same gate as the dump that
    produced them, because knowing which dumps a database has is knowing
    something about the data.
    
deployA

Use when: the source is a local folder. Asynchronous — follow with wait_for_service.

    Deploys that folder (build + run + URL), or rolls back with
    ``rollback=True``. The response carries ``next_step`` — a machine-shaped
    ``{tool, args, why}`` naming the very call to make next — because a 201
    here means "the build was accepted", never "the app is up".

    ``path`` is read by the process serving this tool, so it works only on
    the local ``nerdit mcp`` process; on a remote/HTTP connection it is
    refused with ``mcp.local_path_unavailable``. Use ``write_app_files`` +
    ``deploy_app`` there, or ``deploy_git`` / ``deploy_template``.
    ``rollback=True`` needs no path and works over either transport.

    If the app's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
    injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
    ``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
    OPENAI_MODEL`` — point the app's OpenAI SDK at these; no code change
    needed. ``dry_run=True``
    returns the build/config plan diff (env key names only, values never).

    ``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
    reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
    ``start``, ``health``, ``health_type`` (``http``|``tcp``),
    ``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
    ``auto_deploy``, ``edge_auth``, ``build_settings``. Generated Node builds run
    ``scripts.build`` automatically;
    override it through ``[deploy.build_settings]`` or use a Dockerfile.

    ``release`` runs once inside the new image before traffic moves; if it
    fails the deploy settles ``failed`` and the image reverts, never the data —
    whatever the migration already committed stays, so write idempotent
    migrations. ``cutover`` null = zero-downtime swap
    when eligible, false = same-port swap.
    On a successful non-dry-run deploy the
    response carries ``summary`` (app/status/version/public_url), ``hints``
    (ordered one-liners, never empty) and ``next_step`` (the structured
    follow-up call); a ``dry_run`` plan and a ``rollback`` response carry
    none of the three — read a plan's advisories from its ``warnings``. On a
    node in ``path`` proxy mode the app is served at ``/<name>/``, so a
    frontend must be built with that base
    path; in ``subdomain`` mode it is served at the root of its own
    hostname and needs none — read ``capabilities.proxy.mode``.

    ``build_settings`` overrides
    preset/install/build/start/node_version/package_manager/subdir/public_env.
    ``preset`` selects node/nextjs/python/dockerfile; null resets to
    repository/default detection.
    An existing Dockerfile retains precedence.
    Omit it to preserve saved overrides; a null field resets to repository/default,
    and ``build: false`` skips compilation.
    ``public_env`` passes public build-time variables (shown in clear in a dry
    run; secret references are refused). Secret mounts are unsupported;
    runtime env is unchanged.

    SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
    (``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
    (``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
    read the live profile in ``capabilities.sandbox`` rather than assuming: an
    operator may have relaxed it. Under that default, an image whose entrypoint
    does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
    privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
    your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
    image and a non-root ``USER`` that owns the files it writes: for a static
    site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
    ``port=8080``) or any tiny static server. A crash from this reports
    remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
    CONTRACT: the container MUST listen on the declared ``port`` (the tool
    argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
    the only port the daemon publishes and health-checks, so listening anywhere
    else fails the health probe with a healthy-looking container. A port below
    1024 is NOT the reason a root entrypoint is needed: every container runs on
    the docker bridge in its own network namespace (never host networking),
    where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
    needs no capability and no root — the chown/setuid work does.
    
deploy_gitA

Use when: the app lives in a Git repo. Asynchronous — follow with wait_for_service.

    Clones ``repo_url`` server-side (public GitHub by default), then builds
    and runs it like ``deploy``. The token reference is resolved server-side
    — a raw credential never passes through the tool.
    ``dry_run=True`` returns the build/config plan diff (env key names only,
    values never).

    If the repo's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
    injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
    ``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
    OPENAI_MODEL`` — point the app's OpenAI SDK at these; no code change
    needed.

    ``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
    reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
    ``start``, ``health``, ``health_type`` (``http``|``tcp``),
    ``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
    ``auto_deploy``, ``edge_auth``, ``build_settings``. Generated Node builds run
    ``scripts.build`` automatically;
    override it through ``[deploy.build_settings]`` or use a Dockerfile.

    ``release`` runs once inside the new image before traffic moves; if it
    fails the deploy settles ``failed`` and the image reverts, never the data —
    whatever the migration already committed stays, so write idempotent
    migrations. ``cutover`` null = zero-downtime swap
    when eligible, false = same-port swap.
    On a successful non-dry-run deploy the
    response carries ``summary`` (app/status/version/public_url), ``hints``
    (ordered one-liners, never empty) and ``next_step`` (the structured
    follow-up call); a ``dry_run`` plan carries none of the three — read a
    plan's advisories from its ``warnings``. On a node in ``path``
    proxy mode the app is served at ``/<name>/``, so a frontend must be
    built with that base path.

    ``build_settings`` overrides
    preset/install/build/start/node_version/package_manager/subdir/public_env.
    ``preset`` selects node/nextjs/python/dockerfile; null resets to
    repository/default detection.
    An existing Dockerfile retains precedence.
    Omit it to preserve saved overrides; a null field resets to repository/default,
    and ``build: false`` skips compilation.
    ``public_env`` passes public build-time variables (shown in clear in a dry
    run; secret references are refused). Secret mounts are unsupported;
    runtime env is unchanged.

    SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
    (``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
    (``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
    read the live profile in ``capabilities.sandbox`` rather than assuming: an
    operator may have relaxed it. Under that default, an image whose entrypoint
    does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
    privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
    your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
    image and a non-root ``USER`` that owns the files it writes: for a static
    site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
    ``port=8080``) or any tiny static server. A crash from this reports
    remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
    CONTRACT: the container MUST listen on the declared ``port`` (the tool
    argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
    the only port the daemon publishes and health-checks, so listening anywhere
    else fails the health probe with a healthy-looking container. A port below
    1024 is NOT the reason a root entrypoint is needed: every container runs on
    the docker bridge in its own network namespace (never host networking),
    where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
    needs no capability and no root — the chown/setuid work does.
    
redeploy_serviceA

Use when: a git app needs a new commit. Asynchronous — follow with wait_for_service.

    Re-clones the repository and branch/tag the service was deployed from
    and rebuilds it, so picking up a new commit is one call with the service
    name alone. Use this instead of ``deploy_git`` when the service already
    exists — every coordinate (repo, ref, subdir, private-repo credential
    reference) is read back off the service. A service deployed from a
    folder/zip has no recorded source and is refused. ``dry_run=True`` returns
    the build/config plan diff (env key names only, values never). A
    non-dry-run response carries
    ``summary``, ``hints`` (never empty) and ``next_step`` — the structured
    follow-up call.
    
list_app_templatesA

List the app template store — deployable starter apps (id/name/coordinates).

    Deploy one with ``deploy_template``. These are full app templates
    (web/API/AI starters), not per-service presets.
    
deploy_templateA

Use when: you want a starter app. Asynchronous — follow with wait_for_service.

    Resolves ``template_id`` (see ``list_app_templates``), clones its
    catalog repo server-side, and deploys it under ``name``. ``env`` and the
    deploy overrides layer over the template's defaults.
    The response carries ``summary``, ``hints``
    (never empty) and ``next_step`` — the structured follow-up call — since
    a 201 here means the build was accepted, not that the app is up.

    An existing Dockerfile retains precedence over ``build_settings``.
    Omission preserves overrides; a null field resets to repository/default.
    ``dry_run=True`` previews without writes,
    building or storing secrets. ``public_env`` passes public build-time
    variables (shown in clear in a dry run; secret references are refused);
    secret mounts are unsupported.

    If the template declares ``[ai.*]`` bindings, each is injected at launch
    as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
    sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` — point the app's
    OpenAI SDK at these; no code change needed. WARNING: the ``secrets`` values you pass
    here transit this agent's transcript in plaintext; for high-value keys
    prefer ``nerdit secrets set`` from the CLI and deploy without ``secrets``.
    
write_app_filesA

Write files into an app's server-side workspace, then deploy it.

    The two-step idiom for building an app from this conversation:
    ``write_app_files`` (as many batches as you like) then ``deploy_app``.
    The workspace survives the deploy and IS the working copy, so editing is
    "write the one changed file → ``deploy_app`` again".

    One batch is all-or-nothing: an invalid entry rejects the whole request
    and nothing is written. Binary content is refused — deploy binaries from
    a repo with ``deploy_git``; assets normally come from the build
    (npm/pip). A secret belongs in ``set_secret``, which injects it at launch
    without it ever living in a file. WARNING: file contents you pass here
    transit this agent's transcript in plaintext; never write secrets into
    app files.

    SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
    (``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
    (``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
    read the live profile in ``capabilities.sandbox`` rather than assuming: an
    operator may have relaxed it. Under that default, an image whose entrypoint
    does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
    privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
    your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
    image and a non-root ``USER`` that owns the files it writes: for a static
    site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
    ``port=8080``) or any tiny static server. A crash from this reports
    remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
    CONTRACT: the container MUST listen on the declared ``port`` (the tool
    argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
    the only port the daemon publishes and health-checks, so listening anywhere
    else fails the health probe with a healthy-looking container. A port below
    1024 is NOT the reason a root entrypoint is needed: every container runs on
    the docker bridge in its own network namespace (never host networking),
    where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
    needs no capability and no root — the chown/setuid work does.
    
list_app_filesA

List an app's workspace: paths, sizes, sha256 and totals.

    What this lists is exactly what ``deploy_app`` would build — the exclude
    rules are enforced at write time, not at deploy time. Bounded at 500
    files by construction, so there is no paging.
    
read_app_fileA

Read one file back from an app's workspace as text.

    Use it before patching a file you did not write in this conversation, so
    the next ``write_app_files`` replaces content you have actually seen.
    
deploy_appA

Use when: you used write_app_files. Asynchronous — follow with wait_for_service.

    Distinct from ``deploy`` (which uploads a folder from the machine the MCP
    server runs on) and from ``deploy_git`` (which clones a repository): this
    one deploys the workspace of the same name, so it is the deploy verb for
    an app you wrote in this conversation. Call it again after each
    ``write_app_files`` to ship an edit — ``redeploy_service`` refuses
    workspace apps by design and points back here.

    If the workspace's ``nerdit.toml`` declares ``[ai.*]`` bindings, each is
    injected at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
    ``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
    OPENAI_MODEL``. A dry run returns the build/config plan diff with env key
    names only, values never, and mints no idempotency key.

    ``nerdit.toml`` ``[deploy]`` keys — anything else is IGNORED and
    reported back in the response ``hints``: ``name``, ``port``, ``gpus``,
    ``start``, ``health``, ``health_type`` (``http``|``tcp``),
    ``memory_limit``, ``cpu_limit``, ``volumes``, ``release``, ``cutover``,
    ``auto_deploy``, ``edge_auth``, ``build_settings``. Use ``[deploy.build_settings]``
    for build overrides, or a Dockerfile for custom builds.

    ``release`` runs once inside the new image before traffic moves; if it
    fails the deploy settles ``failed`` and the image reverts, never the data —
    whatever the migration already committed stays, so write idempotent
    migrations. ``cutover`` null = zero-downtime swap
    when eligible, false = same-port swap. ``env`` values are literal and
    never resolve ``${secrets.*}``: a secret reaches the app through
    ``set_secret`` / ``nerdit secrets set`` (injected at launch, overriding
    an ``env`` key of the same name).
    On a successful non-dry-run deploy the
    response carries ``summary`` (app/status/version/public_url), ``hints``
    (ordered one-liners, never empty) and ``next_step`` (the structured
    follow-up call); a ``dry_run`` plan carries none of the three — read a
    plan's advisories from its ``warnings``. On a node in ``path``
    proxy mode the app is served at ``/<name>/``, so a frontend must be
    built with that base path.

    ``build_settings`` overrides
    preset/install/build/start/node_version/package_manager/subdir/public_env.
    ``preset`` selects node/nextjs/python/dockerfile; null resets to
    repository/default detection.
    An existing Dockerfile retains precedence.
    Omit it to preserve saved overrides; a null field resets to repository/default,
    and ``build: false`` skips compilation. Commands run inside the build container.
    ``public_env`` passes public build-time variables (shown in clear in a dry
    run; secret references are refused). Secret mounts are unsupported;
    runtime env is unchanged.

    SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
    (``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
    (``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
    read the live profile in ``capabilities.sandbox`` rather than assuming: an
    operator may have relaxed it. Under that default, an image whose entrypoint
    does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
    privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
    your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
    image and a non-root ``USER`` that owns the files it writes: for a static
    site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
    ``port=8080``) or any tiny static server. A crash from this reports
    remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
    CONTRACT: the container MUST listen on the declared ``port`` (the tool
    argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
    the only port the daemon publishes and health-checks, so listening anywhere
    else fails the health probe with a healthy-looking container. A port below
    1024 is NOT the reason a root entrypoint is needed: every container runs on
    the docker bridge in its own network namespace (never host networking),
    where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
    needs no capability and no root — the chown/setuid work does.
    
share_serviceA

Use when: you need a URL you can open (public_url is LAN-only).

    Exposes the app at ``https://<name>--<slug>.<domain>/`` via the cloud
    link. ``access='private'`` (default) opens only for the owner of this
    machine's Nerdit account, signed in to the Nerdit App, and the people they
    allow there — works on every linked node, nothing to configure.
    ``access='public'`` makes the URL world-reachable: it needs an
    account in good standing (free during the public beta; else 409
    ``share.not_entitled``) AND either a
    ``[deploy].edge_auth`` on the app or ``consent=true`` (else 409
    ``share.unprotected``) — public means public. The node must be linked
    (409 ``share.link_required``). Returns ``{access, url, state}``; ``state``
    is ``ready`` | ``link_down`` | ``not_entitled``. The same URL then appears
    in ``get_service`` / ``list_routes`` under ``public_urls`` (kind
    ``hosted``).

    ``state`` and ``origin`` answer two DIFFERENT questions and you need
    both. ``state`` is about the LINK — is the hosted path provisioned.
    ``origin`` is about the APP behind it: ``{status, answers, hint}``, where
    ``answers`` is true only while the service is running. A share can be
    ``state: ready`` with ``origin.answers: false``, and then the URL returns
    ``share.not_shared`` (the hosted resolver refuses a service with no live
    origin) — which reads exactly like "never shared". When ``answers`` is
    false, ``origin.hint`` says what to do; call ``diagnose_service``.
    
unshare_serviceA

Use when: an app should stop being reachable at its hosted URL.

    The URL stops answering on the next request (the tunnel re-reads the row
    per stream — there is no cache to wait out). Idempotent: removing a
    share that does not exist is a success returning ``removed: false``,
    never an error. The response is the removal receipt
    (``{service_name, removed}``), not a share view — read ``share_service``
    or ``get_service`` for the state of a share that still exists.
    
add_domainA

Serve a deployed app at a domain YOU own, from this node's own proxy (P26).

    Unlike ``share_service`` (Nerdit's domain, over the cloud tunnel), this
    is the node's embedded Caddy answering ``https://<domain>/`` directly —
    in either proxy mode, at the ROOT, so a frontend needs no ``/<app>/``
    base path. The one thing the daemon cannot do for you: point the
    domain's DNS (A/AAAA/CNAME) at this machine. By default the domain is
    served with the node's internal CA, so a client must run
    ``nerdit trust`` once. ``acme=True`` requests a public ACME
    certificate; it needs an ADMIN token (403 ``domain.acme_forbidden``,
    ``reason: acme_admin_only`` — the node has one ACME account whose rate
    limits every domain on it shares) and 409 ``domain.acme_disabled``
    unless the daemon has ``[proxy.acme].enabled``.

    Refusals:
    422 ``domain.invalid`` — the name broke exactly one grammar rule and the
    message names it (``empty``, ``whitespace``, ``not_bare``,
    ``has_port``, ``wildcard``, ``ip_literal``, ``idn``, ``single_label``,
    ``grammar``, ``reserved``); 409 ``domain.taken`` (one domain, one
    service — never silently re-pointed); 422 ``domain.kind_unsupported``
    (models and databases are not routed).

    Returns ``{domain, url, state, cert_state, created}``; ``state`` is
    ``ready`` | ``withheld`` (``withheld`` = the proxy is off, the app is
    not routed yet, or its ``[deploy].edge_auth`` secret does not resolve —
    the row is stored either way and answers once it is). ``cert_state`` is
    the orthogonal certificate fact: ``internal`` (this node's CA) |
    ``disabled`` (``acme`` asked for, ``[proxy.acme]`` off) | ``pending``
    (issuing, or failing — check ``doctor``) | ``issued`` | ``expired``.
    The URL then appears in ``get_service`` / ``list_routes`` under
    ``public_urls`` (kind ``domain``).
    
remove_domainA

Remove a custom domain; its route disappears on the next reconcile tick. Idempotent.

list_hooksA

List a deployed app's inbound webhooks (owner or admin only).

    Returns ``{service_name, hooks: [{name, sender, path, header, prefix,
    secret_ref, secret_key, url, created_at}]}``. ``url`` is null until the
    machine is linked, and answers only once the app is shared.
    
add_hookA

Give a deployed app an inbound webhook address a provider can POST to.

    The machine verifies each delivery's signature before the app sees it,
    then POSTs the provider's headers and exact body to ``path``. No secret
    travels through this tool: the response carries ``url`` (paste it at the
    provider) and ``secret_key``; then store the provider's signing secret
    with ``set_secret(name, {secret_key: value})``. ``url`` is null until
    the machine is linked, and answers only once the app is shared.

    Refusals: 422 ``hook.invalid`` (``reason`` ``name`` or ``shape``), 422
    ``hook.kind_unsupported`` (models and databases), 409 ``hook.exists``
    (a different webhook already has that name). A replay of the same
    webhook answers ``created: false``.
    
remove_hookA

Remove an inbound webhook; its address stops answering. Idempotent.

    The signing secret stays set (remove it with ``remove_secret``). A
    webhook still declared in nerdit.toml comes back with a NEW address on
    the next deploy.
    
create_projectA

Create an empty project, reserving its name for your token.

    A project groups one app's services, the models and databases they
    reference, and their public addresses. The name is reserved for the
    token that created it: another token's deploy, ``serve_model`` or
    ``create_database`` of that name is refused with 409 ``project.owned``
    (an admin bypasses), and the project remains after ``remove_service``
    until ``delete_project`` releases it. You rarely need this before a
    deploy — every deployed app already becomes a project of its own name —
    but it lets you hold a name ahead of the first deploy. A name another
    token already reserved through ``set_secret`` is 409
    ``service.name_claimed``; a name in use is 409 ``project.exists``.
    
list_projectsA

List the projects this token can see, bounded to limit per page.

    Each entry carries the project's services (state, endpoint) and every
    advertised address. A scoped token sees only the projects its scope
    names. Cursor-paginated; use ``get_project`` for the full picture of one.
    
get_projectA

The one-call picture of a project: read this before you deploy or diagnose.

    Returns the project's services with their state, last deploy and
    endpoint; the models and databases they reference with a readiness
    flag (``null`` when readiness cannot be judged without a secret); every
    public address under ``addresses``; and ``home``, the node it lives on
    (always this node today). A name outside your scope is 403; an unknown
    name is 404. Secret values never appear here.
    
delete_projectA

Delete a project: every service in it, then the row, releasing the name.

    Services go one by one through the same cascade as ``remove_service``
    (owner-or-admin each; a dependency guard or an in-flight run refuses a
    service exactly as there). The project row goes last and only when
    every service went: a refusal leaves the project in place with 409
    ``project.delete_incomplete`` listing ``deleted`` and ``failed``, and
    re-running after the fix is idempotent. Owner or admin only.
    
set_variableA

Set/merge a project's variables; write-only unless secret=false. Returns names only.

    WARNING: the values you pass here transit this agent's transcript in
    plaintext, whether ``secret`` is omitted, ``true`` or ``false``. For any
    credential, do not pass the value at all: ask the human to run
    ``nerdit vars set <project> --secret --prompt KEY`` (hidden input) and
    only ever name ``KEY`` yourself. Use this tool for values that are fine
    to appear in a transcript (a log level, a feature flag, a public URL).

    A variable reaches a service's environment at launch. Precedence, lowest
    to highest: the manifest ``env`` table, the project scope, the service
    scope, then platform-injected bindings (``OPENAI_BASE_URL``,
    ``DATABASE_URL``…), which a variable can never override. Setting a key
    again flips its plain/secret flag in that scope. Only the project's owner
    or an admin may write (another token gets 403).
    A running service picks the change up on its next restart or redeploy.
    The machine-wide scope is not reachable here: use ``set_secret`` with
    ``shared``.
    
resolve_variablesA

What a launch would see: per key, the winning scope and whether it is plain. No values.

    The loop is "the agent asks, the human supplies". Call this before a
    deploy to see which keys the service will receive (``scope`` is
    ``project`` or ``production/<service>``). When a key the app needs is
    missing, do NOT invent or request the value in chat: ask the human to
    run ``nerdit vars set <project> --secret --prompt KEY``, then call this
    again to confirm the key is present. A secret value is never returned by
    any tool. Owner or admin only; a name outside your scope is 403.
    
write_project_filesA

Write a project's source tree (its nerdit.toml included), then apply_project.

    Exactly ``write_app_files`` on the workspace named after the project:
    same all-or-nothing batch, same text-only rule, same caps, and
    ``list_app_files`` / ``read_app_file`` read it back under the project
    name. One tree holds EVERY service; a service built from a subfolder
    says so with ``build_settings.subdir``. The root ``nerdit.toml`` is the
    declaration::

        [project]
        name = "asso"

        [services.web]
        port = 3000

        [services.api]
        port = 8000
        build_settings = { subdir = "apps/api" }

        [vars]
        required = ["API_KEY"]

    ``[services.<name>]`` takes the ``[deploy]`` keys except ``name``: the
    service ``web`` is served under the project name (``asso``), any other
    under ``<service>--<project>`` (``api--asso``), and that label is the
    name every other tool takes. ``[ai.*]`` / ``[db.*]`` bindings stay
    top-level tables shared by every service; a declaration cannot create
    a database (``create_database`` first, then bind it). WARNING: file
    contents you pass here transit this agent's transcript in plaintext;
    never write secrets into project files.
    
apply_projectA

Apply a project's declaration: deploy every [services.<name>] of its nerdit.toml.

    The loop: ``get_project`` (read) → ``apply_project(dry_run=true)`` →
    if ``status`` is ``waiting_for_variables``, do NOT invent or request
    the values in chat: ask the human to run ``nerdit vars set <project>
    --secret --prompt KEY`` for each name in ``missing``, then dry-run
    again → ``apply_project`` → ``wait_for_service`` once per ``label`` in
    ``services`` → ``diagnose_service`` on any that failed.

    The response lists ``services`` in declaration order, each with its
    ``label`` (the name every other tool takes: ``web`` is the project
    name, any other service ``<service>--<project>``), ``action``
    (``fresh``/``redeploy``) and, on a dry run, its ``plan`` (env key names
    only, values never); ``public_urls`` carries every advertised address.
    ``waiting_for_variables`` deploys nothing and returns names only.
    Asynchronous like every deploy: ``applied`` means accepted, not healthy.

    Every refusal lands before the first build, and a dry run creates
    nothing and mints no idempotency key. A project another token owns is
    409 ``project.owned``. There is NO rollback across services: a failure
    on one is ``project.apply_incomplete``, whose ``services`` lists the
    ones already applied and ``failed`` the one that was not — fix it and
    apply again (done services redeploy, the rest are retried). A
    ``[deploy]``-only ``nerdit.toml`` is not a declaration: use
    ``deploy_app`` / ``deploy_git`` for it (they answer 422
    ``deploy.use_apply`` on a declaration).

    SANDBOX: by DEFAULT containers run with all Linux capabilities dropped
    (``cap_drop=["ALL"]``) and no-new-privileges — both are operator settings
    (``[containers].drop_all_caps`` / ``[containers].no_new_privileges``), so
    read the live profile in ``capabilities.sandbox`` rather than assuming: an
    operator may have relaxed it. Under that default, an image whose entrypoint
    does root work — ``chown``/``chmod`` of its own runtime dirs, dropping
    privileges via ``setuid``/``setgid``, writing ``/var/run`` — dies before
    your app ever starts. Stock ``nginx`` is the classic trap. Use a non-root
    image and a non-root ``USER`` that owns the files it writes: for a static
    site, ``nginxinc/nginx-unprivileged`` (listens on **8080**, so set
    ``port=8080``) or any tiny static server. A crash from this reports
    remediation code ``image_needs_privileges`` in ``diagnose_service``. PORT
    CONTRACT: the container MUST listen on the declared ``port`` (the tool
    argument, else ``nerdit.toml`` ``[deploy].port``, else **8000**) — that is
    the only port the daemon publishes and health-checks, so listening anywhere
    else fails the health probe with a healthy-looking container. A port below
    1024 is NOT the reason a root entrypoint is needed: every container runs on
    the docker bridge in its own network namespace (never host networking),
    where Docker sets ``net.ipv4.ip_unprivileged_port_start=0``, so binding 80
    needs no capability and no root — the chown/setuid work does.
    
project_logsA

Read bounded logs of one service resolved within an explicit project.

Prefer the immutable project ID returned by list_projects. Membership is
resolved by the daemon; a same-name replacement cannot inherit a retired ID.
Existing node grants and local token roles still apply.
diagnose_projectA

Diagnose one service of an explicit project under the existing owner/admin policy.

Prefer an immutable project ID. The daemon resolves project membership and
reuses diagnose_service's failure bundle; this grants no additional authority.
set_secretA

Set/merge write-only secrets for a service; returns key names only.

    WARNING: the values you pass here transit this agent's transcript in
    plaintext; for high-value shared keys (broadly-scoped API keys,
    credentials shared across every app) prefer ``nerdit secrets set --shared`` from the CLI
    instead of routing them through an agent. The leak-free path for any
    value: a human runs ``nerdit secrets set <service> --prompt KEY``
    (hidden input) and you only ever name ``KEY``; it is injected at
    launch, overriding an ``env`` key of the same name. You may set secrets
    before the service exists; the name is then reserved for your token
    until you deploy it (another token's deploy of that name gets 409
    ``service.name_claimed``).
    
list_secret_namesA

List a service's secret key names (values are never returned).

remove_secretA

Delete one secret key, or all of a service's secrets when key is omitted.

    Deleting a shared key can break every app still referencing it via
    ``${secrets.shared.KEY}``.
    
list_ai_routesA

List the machine AI gateway's aliases and whether the gateway is enabled.

    Each alias shows its provider, provider model, base_url and the
    ``${secrets.shared.KEY}`` reference of its key: never a key value.
    
list_ai_provider_modelsA

List the model ids an OpenAI-compatible provider offers (submitter or admin).

    The daemon calls ``GET {base_url}/models`` with the referenced shared
    key, so you can pick a model for ``set_ai_route`` while the key stays on
    the machine. Returns ``{base_url_host, models}`` (sorted ids only).
    Read-only despite the POST underneath (no idempotency key).
    Refusals: 422 ``ai_gateway.base_url_invalid`` / ``ai_gateway.secret_ref_invalid``,
    503 ``secret_missing``, 502 ``ai_gateway.provider_unreachable`` /
    ``ai_gateway.provider_error`` (the HTTP status, never the provider's text).
    
set_ai_routeA

Create or replace a machine AI gateway alias (submitter or admin).

    Apps then declare ``[ai.default] provider = "gateway"`` and
    ``model = "<alias>"``: the daemon injects the gateway URL and a per-app
    virtual key, and the provider key never enters a container. Answers
    ``enabled: false`` while ``[ai_gateway]`` is off (the alias is stored
    and serves once it is enabled). Refusals: 422 ``ai_gateway.alias_invalid``,
    ``ai_gateway.base_url_invalid`` (not https, private address, userinfo),
    ``ai_gateway.secret_ref_invalid``.
    
remove_ai_routeA

Remove a machine AI gateway alias (submitter or admin).

    Refused with 409 ``ai_gateway.alias_in_use`` (naming the apps) while a
    running app's ``[ai.*]`` uses it, unless ``force``.
    
get_ai_usageA

AI gateway usage per (UTC day, app, alias): requests, prompt and completion tokens, upstream errors. No cost estimate, no prompt trace.

get_configA

Read daemon config: one section view, or all sections when omitted.

    Secret-bearing values are redacted server-side.
    
set_configB

Write a daemon config section (admin-scoped, validated, audited).

apply_configA

Declaratively apply a multi-section daemon config document (all-or-nothing).

    A dry run needs neither an ``If-Match`` nor an idempotency key.
    
get_app_configA

Read a deployed app's daemon-persisted config (deploy fields + ai bindings).

    Secret values are never returned — only ``${secrets.X}`` refs and env
    key names. Each ``[ai.<name>]`` binding shown here is injected into the
    app at launch as ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the
    ``default`` binding also sets ``OPENAI_BASE_URL/OPENAI_API_KEY/
    OPENAI_MODEL`` (drop-in OpenAI SDK).

    ``deploy.release`` is the command run once inside the new image before
    traffic moves (a failure aborts the deploy and reverts the image,
    never the data — write idempotent migrations); ``deploy.cutover`` null =
    zero-downtime swap when eligible, false = same-port swap. ``env_keys``
    are literal runtime env names; secret refs are accepted only by
    ``ai.*.api_key``, ``db.*.password``, ``edge_auth.password`` and a git
    ``token_ref`` — a ``${secrets.shared.KEY}`` there resolves at launch for
    any owner, so a human can place the secret and the agent only names it.
    
set_app_configA

Write an app's deploy or ai config section (admin/owner-scoped).

    Writing the ``ai`` section re-wires the app's bindings: on the next
    launch/restart each ``[ai.<name>]`` binding is injected as
    ``NERDIT_AI_<NAME>_URL/_KEY/_MODEL``, and the ``default`` binding also
    sets ``OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL`` — pass
    ``restart=True`` to apply it immediately. A dry run needs neither an
    ``If-Match`` nor an idempotency key.
    
get_auditA

Read the audit log (admin-scoped), bounded to limit entries per page.

    Returns an ``AuditLogPage`` dict. Non-admin tokens get a structured
    ``forbidden`` (403) error.

    All filters compose with AND, and all are applied in SQL — narrowing is
    cheap, so prefer a filter over paging.
    
get_eventsA

Use when: you need the daemon's own account of what it just did.

    The durable feed of autonomous transitions (``service.failed``,
    ``service.degraded``, ``service.deploy_succeeded``, ``model.ready``,
    ...), readable by any authenticated token. Two directions, and picking
    the wrong one walks the wrong way through history.
    
capabilitiesA

Describe this daemon: version, caller role/quotas, URL grammar, backends.

    Role-aware, pure in-memory read — call it first to learn what the
    daemon can do (buildpacks, model backends, proxy mode and public-URL
    shape, deploy limits, git allowlist, feature flags) instead of probing
    endpoints. Admin callers additionally receive ``proxy.admin_addr`` and
    the ``paths`` block.
    
doctorA

Run the daemon's bounded environment health checks (docker/gpu/proxy/db/...).

    Returns per-check ``{status, detail}`` plus a worst-of top-level
    status. Every check is timeout-bounded server-side; details carry key
    *names* only, never paths or secret values — safe for every role.
    
proxy_statusA

Get the embedded proxy's typed state (TLS, apex, respawn, live routes).

    Authenticated read: proxy state, TLS subjects + CA fingerprint, apex
    registration, respawn backoff, and a tri-state live route table read
    from the Caddy admin API. Complements ``list_routes`` (the
    DB-authoritative inventory).
    
list_routesA

List the DB-authoritative route inventory, annotated with live Caddy state.

    Models appear with ``route: null`` (loopback-only by design); each row
    carries its live-table annotation.
    
system_diskA

Report disk usage: docker totals + named-volume/model/archive/backup trees.

    Any authenticated read. Names and byte counts only (no absolute paths);
    lists orphan images and orphan data dirs (reclaim them with
    ``system_gc``). Concurrent ``du`` walks run under a soft budget — an
    overrun returns a partial body with a ``scan_timeout`` warning.
    
system_gcA

Garbage-collect orphan app images (admin-scoped, audited).

    Reclaims ``nerdit-app/*`` image repos with no live workload row, verified
    by re-listing so an in-use tag is reported skipped, never removed.
    
restart_daemonA

Restart the daemon to apply restart-required config (admin-scoped, audited).

    Returns 202 with drain counts. **Over the HTTP MCP transport this kills
    the connection serving this call**: a dropped response after issuing it
    means the restart most likely started. Confirm with ``capabilities``
    (``uptime_s`` resets); retrying with the same idempotency key answers
    ``409 daemon.restart_in_progress`` or a fresh 202, never a second
    restart mid-drain.
    

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

B3.4/5.0

Scored across 67 tools

Disambiguation3/5

The descriptions work hard to separate tools ('Distinct from deploy...', 'Use this instead of deploy_git when...'), but six deploy-family verbs (deploy, deploy_git, deploy_app, deploy_template, redeploy_service, apply_project) plus paired log/diagnose tools (service_logs vs project_logs, diagnose_service vs diagnose_project) force an agent to read a paragraph to pick correctly. Resource-oriented lists (list_services/list_models/list_databases/list_projects) are cleanly distinct, but the parallel project-scoped vs service-scoped duplicates create real hesitation.

Naming Consistency3/5

The dominant pattern is snake_case verb_noun, but it breaks in several places: bare verbs/nouns (deploy, serve, doctor, capabilities), noun-first reads (service_logs, project_logs, system_disk, system_gc), and mismatched creation/deletion verbs across similar entities (create_database vs add_domain vs add_hook; remove_service vs delete_project). Still readable, but not a predictable grammar.

Tool Count2/5

67 tools for a single-node PaaS is far past a well-scoped surface; several families (four deploy verbs, four config tools, three file tools, paired project/service variants) exist largely to model the same operations at different scopes. The result is a heavy, hard-to-navigate set where an agent can miss the right tool even though each one is documented.

Completeness4/5

Coverage of the domain is genuinely broad: deploys from folder/git/template/workspace, full service lifecycle, models, managed databases with dumps, projects, secrets/variables, AI routing, domains, hooks, config, audit, events, and system/health. Minor gaps are deliberate and documented (no restore tool, no volume-backup tool), though some of the breadth is bloat rather than coverage.

Maintenance

ActivityActive
ResponsivenessNo issues