Skip to content

The MCP Server ​

nkapp mcp runs a Model Context Protocol (MCP) server for one project. An agent host such as Claude Code or Claude Desktop starts it and talks to it over stdio. The agent then drives Nanokit through typed tools instead of shell commands: it reads structured state, declares intent in nanokit.yml, and applies changes only after it has planned them.

Every tool call goes through the same engine the CLI uses, so an agent has exactly the operator's authority and no more. A remote environment still needs a Nanokit account that may operate remotely, as it does for the matching human command.

Register it in an agent host ​

The server is a local process. It opens no port and starts no listener. Install the CLI, then point your agent host at it:

bash
npm install -g @nkapp/cli

Claude Code, from the project directory:

bash
claude mcp add nanokit -- nkapp mcp --cwd /abs/path/to/project

Claude Desktop or any host that reads an mcpServers block:

json
{
  "mcpServers": {
    "nanokit": {
      "command": "nkapp",
      "args": ["mcp", "--cwd", "/abs/path/to/project"]
    }
  }
}

One server process serves exactly one project. --cwd selects it and defaults to the current directory. To work on a second project, register a second server with a different --cwd.

Call describe_policy first

The server's own instructions tell the agent to start with describe_policy. That tool returns the enabled tools, the allowed environments, which tools need approval, and the rate limits for this session. A later refusal is then expected rather than confusing.

stdout carries JSON-RPC frames only. Diagnostics and startup errors go to stderr. If nanokit.yml sets agent.enabled: false, the server refuses to start and exits non-zero.

The tools ​

28 tools ship today, in three classes.

Read (R): observe, never change anything ​

ToolWhat it returns
describe_policyThe constraints this session operates under.
list_environmentsProject environments with their resolved deploy target.
get_environmentThe full structured state of one environment, including a drift hash. Secret keys only, never values.
list_servicesDeclared services with runtime, hosts and dependencies (config only).
list_databasesDeclared databases with engine, mode and active branch.
list_secretsSecret keys and whether each is defined. Never a value.
list_domainsDeclared hosts with their TLS certificate status.
get_stateThe live cloud inventory, with per-resource blast-radius flags.
get_planComputes a plan and mints the planId that every apply tool requires.
get_deploy_scopeWhich services a deploy would touch, and why.
list_backupsBackup catalog entries, newest first.
get_logsRecent service log lines, capped and secret-redacted.
doctorEnvironment health checks.

Intent (W-intent): write nanokit.yml / .env, nothing else ​

ToolEffect
create_environmentDeclares an environment bound to an existing deploy target.
create_serviceDeclares a service.
create_databaseDeclares a database. Its root password is written as a ${VAR} reference only.
create_domainDeclares a host on a service or on the project.
set_secretWrites the value to the dotenv file and an env:// reference to nanokit.yml.
update_servicePatches allow-listed service fields.
delete_serviceRemoves a service from the root block and every environment override.
delete_domainRemoves a host mapping.
delete_secretRemoves a secret from the dotenv file and from nanokit.yml.

Intent tools never start, stop or remove a container. Every response includes the unified YAML diff of what changed. Every change is reversible with git checkout nanokit.yml. The whole resulting file is validated before it is saved, so a refused write leaves the file byte-identical and returns CONFIG_INVALID.

Secret values never come back: not in the response, not in the diff and not in the audit trail. Nanokit and cloud credential variables (for example NANOKIT_TOKEN, AWS_SECRET_ACCESS_KEY and CLOUDFLARE_API_TOKEN) are refused outright.

Apply (W-apply): reconcile real infrastructure ​

ToolSame code path as
applynkapp up: reconcile services against the config.
deploynkapp deploy: change-proportional deploy. It does not run the target's deploy.prepare; the response warns you when one is configured.
rollbacknkapp deploy --rollback: re-applies this environment's own previous state. It reports per-service failures.
backupnkapp backup: back up one database.
db_branchnkapp db fork / switch, then reconciles dependent services. An agent fork never copies data from another environment.
reconcile_unlocknkapp infra unlock, limited to stale locks. A live lock is reported as LOCK_HELD and never removed.

Plan, then apply ​

No apply tool runs without a planId. The loop:

  1. get_plan { env } computes the plan with the same engine as nkapp plan and returns a planId.
  2. The agent (or you) reads the plan.
  3. apply / deploy / … { env, planId, … } redeems the handle.

The planId is single use, expires after 10 minutes, and is bound to the environment and to the state it was computed from. If the project changed in between, the call returns PLAN_STALE with a fresh planId to use instead. If a call is refused (by the lock, an approval, an unknown service, and so on), the handle is not consumed. Planning an unchanged project again returns the same planId.

Before an apply runs, the call passes these checks in order:

  1. Policy: the agent: block (denied tools, allowed environments).
  2. Production approval: an environment whose deploy target is production: true needs a human approval.
  3. Rate limit: 4 applies per minute by default (RATE_LIMITED, with retryAfterMs).
  4. The plan handshake.
  5. One apply at a time per environment (LOCK_HELD). A human nkapp up and an agent apply share the same reconcile lock, so they never interleave.

Human approval ​

Some calls need a person: any mutating call against a production: true deploy target, any tool listed in agent.approval.required, and any write to a command-bearing field in every environment. Command-bearing fields are start, command, hooks.*, provisioning (including provisioning.install), volumes, ports, cron.*.command, and an environment's deploy / deploy.prepare binding. They run arbitrary code on your machine and on every deploy target, so an agent can never write one silently.

The flow:

  1. The agent's call is refused with APPROVAL_REQUIRED. The refusal carries an approvalId and the exact command for you to run.

  2. You review and grant it in your own terminal:

    bash
    nkapp mcp list                 # outstanding requests
    nkapp mcp approve <id>         # shows the full request, asks y/N
    nkapp mcp deny <id>            # refuse it
  3. The agent re-issues the identical call. It passes once. A different value, environment, resource or plan is refused.

Approvals expire after 10 minutes (agent.approval.ttlMinutes). For unattended runs, NANOKIT_MCP_PREAUTH=<project>:<tool>[,<tool>] pre-authorizes named tools. The project segment must equal the project name, wildcards are not accepted, and it never overrides a human deny or covers a command-bearing field.

Every call, allowed or denied, is appended to .nanokit/mcp/audit.jsonl (mode 0600, rotated at 10 MB). Read it with:

bash
nkapp mcp audit                       # newest 50 records
nkapp mcp audit --tool deploy -e production --json

See nkapp mcp for every flag.

The agent: policy block ​

The policy is declarative and committed with your config. Every field is optional, and so is the block itself. Unknown keys are rejected, because a typo here could widen what an agent may do.

yaml
agent:
  enabled: true              # false ⇒ `nkapp mcp` refuses to start
  envs: [local, stage]       # allow-list for every tool, reads included
  tools:
    deny: [deploy]           # deny wins over everything
    # allow: [...]           # a further restriction, never a re-grant
  approval:
    required: [rollback]     # extra tools that need a human approval
    ttlMinutes: 10
  rateLimit:
    applyPerMinute: 4
  limits:
    logLines: 500            # get_logs total cap
    responseBytes: 262144    # per-response size cap

With no agent: block, every tool is enabled, every environment is reachable, and mutating calls against a production: true deploy target need approval.

rateLimit.sessionMutations and rateLimit.destructivePerHour are accepted by the schema but not enforced yet.

Team roles do not filter the tool list yet

The policy resolver already knows that a viewer may only read, but the server does not yet resolve your team role from the account token. Today the tool list is filtered by the agent: block only.

Refused by design ​

These are not missing features. No policy, role or approval turns them on. The refusal names the human command to run instead.

CapabilityRun yourself
shell, exec, run_commandnkapp shell
clean, teardownnkapp clean, nkapp teardown
migratenkapp migrate
host_trustnkapp host trust
provider_auth, secrets_pullnkapp provider auth, nkapp secrets pull
sharenkapp share. Widening your machine's network exposure is never an agent decision.
write_file, set_config_raw, patch_yamlUse the typed intent tools, or nkapp config set
switch_projectStart a second nkapp mcp with a different --cwd

Destructive tools are not available either. destroy, restore and db_delete_branch answer NOT_SUPPORTED: this server does not register tools that can lose data or availability at all. Run nkapp destroy, nkapp restore or nkapp db delete yourself.

Error codes an agent should handle ​

Every response is an envelope: { ok, data?, error?, warnings[], meta }. error.retryable tells the agent whether calling again can help.

CodeRetryableMeaning
PLAN_STALE, PLAN_NOT_FOUNDyesThe plan changed, expired or was consumed. Call get_plan again.
LOCK_HELDyesAnother reconcile is running on this environment.
RATE_LIMITEDyesWait retryAfterMs.
RESPONSE_TOO_LARGEyesNarrow the request (fewer services, a smaller tail).
APPROVAL_REQUIREDnoA human must run nkapp mcp approve <id>, then the identical call is re-issued.
ENV_NOT_ALLOWED, TOOL_DISABLEDnoBlocked by the agent: policy.
FIELD_NOT_ALLOWED, CONFIG_INVALIDnoThe write is outside the allow-list, or the result would not validate.
ENTITLEMENT_REQUIREDnoThe remote environment needs a Nanokit account that may operate remotely.
PROVIDER_NOT_CONFIGUREDnoThe environment has no infrastructure provider (infra.provider). Fix the config.
ENV_MISMATCHnoThe rollback snapshot belongs to another environment.
NOT_SUPPORTEDnoRefused by design, or a destructive tool.

Where to go next ​