Appearance
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/cliClaude Code, from the project directory:
bash
claude mcp add nanokit -- nkapp mcp --cwd /abs/path/to/projectClaude 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
| Tool | What it returns |
|---|---|
describe_policy | The constraints this session operates under. |
list_environments | Project environments with their resolved deploy target. |
get_environment | The full structured state of one environment, including a drift hash. Secret keys only, never values. |
list_services | Declared services with runtime, hosts and dependencies (config only). |
list_databases | Declared databases with engine, mode and active branch. |
list_secrets | Secret keys and whether each is defined. Never a value. |
list_domains | Declared hosts with their TLS certificate status. |
get_state | The live cloud inventory, with per-resource blast-radius flags. |
get_plan | Computes a plan and mints the planId that every apply tool requires. |
get_deploy_scope | Which services a deploy would touch, and why. |
list_backups | Backup catalog entries, newest first. |
get_logs | Recent service log lines, capped and secret-redacted. |
doctor | Environment health checks. |
Intent (W-intent): write nanokit.yml / .env, nothing else
| Tool | Effect |
|---|---|
create_environment | Declares an environment bound to an existing deploy target. |
create_service | Declares a service. |
create_database | Declares a database. Its root password is written as a ${VAR} reference only. |
create_domain | Declares a host on a service or on the project. |
set_secret | Writes the value to the dotenv file and an env:// reference to nanokit.yml. |
update_service | Patches allow-listed service fields. |
delete_service | Removes a service from the root block and every environment override. |
delete_domain | Removes a host mapping. |
delete_secret | Removes 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
| Tool | Same code path as |
|---|---|
apply | nkapp up: reconcile services against the config. |
deploy | nkapp deploy: change-proportional deploy. It does not run the target's deploy.prepare; the response warns you when one is configured. |
rollback | nkapp deploy --rollback: re-applies this environment's own previous state. It reports per-service failures. |
backup | nkapp backup: back up one database. |
db_branch | nkapp db fork / switch, then reconciles dependent services. An agent fork never copies data from another environment. |
reconcile_unlock | nkapp 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:
get_plan { env }computes the plan with the same engine asnkapp planand returns aplanId.- The agent (or you) reads the plan.
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:
- Policy: the
agent:block (denied tools, allowed environments). - Production approval: an environment whose deploy target is
production: trueneeds a human approval. - Rate limit: 4 applies per minute by default (
RATE_LIMITED, withretryAfterMs). - The plan handshake.
- One apply at a time per environment (
LOCK_HELD). A humannkapp upand 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:
The agent's call is refused with
APPROVAL_REQUIRED. The refusal carries anapprovalIdand the exact command for you to run.You review and grant it in your own terminal:
bashnkapp mcp list # outstanding requests nkapp mcp approve <id> # shows the full request, asks y/N nkapp mcp deny <id> # refuse itThe 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 --jsonSee 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 capWith 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.
| Capability | Run yourself |
|---|---|
shell, exec, run_command | nkapp shell |
clean, teardown | nkapp clean, nkapp teardown |
migrate | nkapp migrate |
host_trust | nkapp host trust |
provider_auth, secrets_pull | nkapp provider auth, nkapp secrets pull |
share | nkapp share. Widening your machine's network exposure is never an agent decision. |
write_file, set_config_raw, patch_yaml | Use the typed intent tools, or nkapp config set |
switch_project | Start 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.
| Code | Retryable | Meaning |
|---|---|---|
PLAN_STALE, PLAN_NOT_FOUND | yes | The plan changed, expired or was consumed. Call get_plan again. |
LOCK_HELD | yes | Another reconcile is running on this environment. |
RATE_LIMITED | yes | Wait retryAfterMs. |
RESPONSE_TOO_LARGE | yes | Narrow the request (fewer services, a smaller tail). |
APPROVAL_REQUIRED | no | A human must run nkapp mcp approve <id>, then the identical call is re-issued. |
ENV_NOT_ALLOWED, TOOL_DISABLED | no | Blocked by the agent: policy. |
FIELD_NOT_ALLOWED, CONFIG_INVALID | no | The write is outside the allow-list, or the result would not validate. |
ENTITLEMENT_REQUIRED | no | The remote environment needs a Nanokit account that may operate remotely. |
PROVIDER_NOT_CONFIGURED | no | The environment has no infrastructure provider (infra.provider). Fix the config. |
ENV_MISMATCH | no | The rollback snapshot belongs to another environment. |
NOT_SUPPORTED | no | Refused by design, or a destructive tool. |
Where to go next
- The Declarative Contract: what the intent tools write.
- Machine-Readable State: the other channels an agent can read.
nkapp mcp: command and subcommand reference.