◐ Docs

MCPIP Documentation

Connect an agent to a governed gateway, stand one up yourself, or look anything up — every part, and every way in, is a numbered step on this page.

Docs / Get started

Connect an agent#

You have an agent — MCP, REST, or an SDK — and need it to call governed tools through a gateway someone already runs.

7 steps · token · connect · authorize

shortcut · agent setup

Skip ahead — point a coding agent at one URL

Hand this to Claude Code, Cursor, or any coding agent and it will do the steps below for you: connect an agent to your gateway and build your company's skill catalog. The prompt is grounded in the same API this guide documents.

paste this to your agent
Fetch https://mcpip.ai/agent-setup/prompt.md and follow it.

Prefer to read it yourself? curl -s https://mcpip.ai/agent-setup/prompt.md — or open the agent-setup page.

01

Install & set up#

Two things run today from a fresh clone, no registry, no signup: the one-command sandbox, and the mcpip CLI straight from git. The published @mcpip/sdk / mcpip-sdk packages ship with the public release — they are marked as such below.

the blessed front door — clone + one command → sandbox gateway + walkthrough
git clone https://github.com/mcpip-security/mcpip.git && cd mcpip
./scripts/quickstart_demo.sh        # or, with the CLI on PATH:  mcpip up

Idempotent: it installs Redis if missing, creates a venv, boots the gateway on :8080 in sandbox mode, and runs the mcpip-inc walkthrough. Only prerequisite is Python 3.12. Self-contained — no external IdP, no vendor cloud.

02

Get a token#

MCPIP is identity-sovereign: it only ever verifies a JWT, it never mints one. In sandbox the in-process demo IdP mints a short-lived token so the walkthrough is runnable; in production your own IdP issues agent tokens and this endpoint does not exist.

POST/v1/dev/token
FieldTypeDescription
tenant_idstringTenant the agent acts under. Defaults to the demo tenant (tenant-acme).
agent_idstringAgent id recorded on the minted token. Defaults to agent-orchestrator-1.
rolestringDescriptive label ONLY — it authorizes nothing. Defaults to ops.
compartmentuuid | nullOptional compartment UUID this principal is scoped to (team/MCP separation).
capabilitiesuuid[] | nullOptional capability UUIDs. The well-known admin/audit caps are at GET /v1/dev/capabilities.
mint a demo JWT (sandbox) — the response is just { jwt }
TOKEN=$(curl -s localhost:8080/v1/dev/token \
  -H 'content-type: application/json' -d '{}' | jq -r .jwt)

Sandbox only. This endpoint returns 404 whenever MCPIP_SANDBOX_MODE=false (the production default) — the minter simply does not exist. A sandbox token lives ~5 minutes; after that every call is an opaque deny that looks like a broken connection but is just the lapsed token.

present it on every subsequent request — header only
Authorization: Bearer <jwt>     # never a URL, never a query string, never a log line

In production a real agent token is issued out-of-band by your IdP (the analog of /v1/dev/token is scripts/mint_principal.py, signed with your IdP key). Confirm what a token actually carries with GET /v1/whoami — it echoes the verified tenant, agent, compartment, and effective capabilities, so you never have to diagnose entitlements through opaque denies.

03

Connect your agent#

Two edges, one pipeline. The gateway is the MCP server (POST /v1/mcp), and the REST edge (POST /v1/authorize) speaks six declared provider dialects. The tools/list / catalog you receive is already pruned to your identity — another team’s skills are invisible, not merely forbidden.

The repo ships a project .mcp.json registering a self-minting, auto-refreshing stdio bridge (scripts/claude_mcp_bridge.py). Run claude inside the repo and approve the mcpip server — the bridge mints the sandbox token itself and refreshes it before expiry, so a long session keeps working.

.mcp.json (as shipped)
{
  "mcpServers": {
    "mcpip": {
      "command": "python3",
      "args": ["scripts/claude_mcp_bridge.py"],
      "env": {
        "MCPIP_URL": "http://localhost:8080",
        "MCPIP_TENANT": "mcpip-inc",
        "MCPIP_AGENT": "anthropic-claude-1"
      }
    }
  }
}

In production set MCPIP_TOKEN to a JWT your IdP issued; the bridge attaches it verbatim and never mints — identity stays with your IdP.

04

Authorize a tool call#

POST /v1/authorize is the one choke point. One request authorizes exactly one tool call, and the outcome is one of three shapes: 200 executed, 202 staged behind a step-up, or 403 an opaque deny. The response never reveals the real target — only a coarse transport class.

POST/v1/authorize
FieldTypeDescription
source_formatrequiredenumOne of openai_tool_call, anthropic_tool_use, gemini_function_call, bedrock_tool_use, mcp_jsonrpc, raw_mcp, a2a_task. Supply exactly one of source_format / vendor.
vendorstringAlternative to source_format — a declared vendor id (e.g. openai, gemini, bedrock) resolved through a hash-pinned registry. An unknown vendor is an opaque 403.
tool_callrequiredobjectThe raw provider envelope, carrying the skill_* alias and its arguments. Deep-validated by the Bridge (depth / size / charset caps; an identity-shaped key is a hard deny).
jwtstring | nullOptional — identity is normally taken from the Authorization: Bearer header instead.
pin / challenge_idstring | nullThe step-up completion pair (step 05). Supplied together, never one alone.
200 · ExecutionReceipt — executed & WORM-logged before dispatch
{
  "correlation_id": "…",
  "decision": "allow",
  "status": "committed",
  "transaction_ref": "txn_…",
  "executed_target_class": "cloud_rest",   // coarse CLASS, never the real target
  "worm_sequence": 4213
  // "vended_credential": {…}  present ONLY for a cloud_iam skill
}
202 · StagedChallenge — a PIN_REQUIRED alias, no data yet
{
  "correlation_id": "…",
  "action_required": "approve in the enrolled authenticator",
  "challenge_id": "…",          // the payload-bound lock id (step 05)
  "risk_tier": "pin_required"
}
403 · ErrorResponse — opaque, always
{ "error": "MCPIP: request denied by policy.", "correlation_id": "…" }

Need only a verdict, not execution? POST /v1/authz/decision answers as an OpenID-AuthZEN PDP — { decision: bool } — and never executes, vends, or stages.

05

Clear a step-up#

A 202 means a human must approve this exact payload. The one-time PIN is delivered out-of-band to the enrolled authenticator; you complete the call by re-POSTing the identical tool call plus the pin and challenge_id. Change one byte of the payload and it no longer matches.

GET/v1/authenticator/{challenge_id}

Sandbox only. Stands in for the enrolled device by returning the staged one-time code ({ challenge_id, otp }). Requires a valid JWT and is 404 in production — where the PIN is pushed to your authenticator webhook and read by a human (below).

complete the step-up — the IDENTICAL tool_call plus pin + challenge_id
OTP=$(curl -s localhost:8080/v1/authenticator/$CHALLENGE_ID \
  -H "authorization: Bearer $TOKEN" | jq -r .otp)   # sandbox peek

curl -s localhost:8080/v1/authorize -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d "{
  \"source_format\":\"openai_tool_call\",
  \"tool_call\":{ …the SAME envelope… },
  \"pin\":\"$OTP\", \"challenge_id\":\"$CHALLENGE_ID\"}"
# → 200 ExecutionReceipt. Replay the spent triple → 403 (consumed exactly once).
SDK — .complete(staged, pin)
result = client.authorize("skill_financial_ledger_post", {"amount_cents": 2418000})
if result.is_staged:                       # 202 — a challenge_id came back
    receipt = client.complete(result, pin=one_time_code)   # same payload + pin → 200

The PIN is bound to sha256(canonical_json(tenant, agent, alias, arguments)) and consumed by a single atomic Redis operation — one byte of drift is payload_mismatch, and the lock survives a correct retry. In production the human who reads the delivered code proves possession of a per-user RFC 6238 TOTP authenticator (POST /v1/authenticator/reveal) — the human 2FA that gates reading the code, distinct from the payload-bound PIN itself.

06

Read the receipt#

A 200 is an ExecutionReceipt. Branch on it, quote its worm_sequence to an operator, and note that topology never crosses the boundary — you learn the transport class, never the dotted target.

FieldTypeDescription
correlation_idstringThe one handle an agent may quote to a human operator to locate this decision in the audit log.
decisionstringAlways "allow" on a 200 (a deny never reaches this shape).
statusstringAlways "committed" — the WORM ALLOW record was written before dispatch.
transaction_refstringtxn_ + uuid4 — a per-execution reference.
executed_target_classstringcloud_rest | legacy_mainframe | cloud_iam — the coarse transport class only. Never entry.target.
worm_sequenceintThe audit anchor the operator can quote to find the sealed decision record.
vended_credentialobject | nullPresent ONLY for a cloud_iam skill: the short-lived, scoped cloud credential vended for THIS call. The agent’s deliverable — never persisted to WORM.

In the SDK the receipt is a frozen result object — read correlation_id, transaction_ref, and worm_sequence directly; a cloud_iam receipt also carries vended_credential.

07

Handle denials#

The boundary is fail-closed and opaque. Whatever the real reason, a policy denial gives the caller only the generic message plus a correlation_id — so the gate can never be turned into an oracle. The concrete reason lives only in the WORM log, for the operator.

OutcomeClassWhat the caller sees — and what to do
403 · ErrorResponsepolicyEvery policy denial — cross-tenant, compartment, capability, PIN mismatch, canary, revocation, disabled skill, and dozens more — collapses to the identical { error: "MCPIP: request denied by policy.", correlation_id }. Do not retry; quote the correlation_id to an operator.
422 · invalid requestschemaA malformed envelope (neither/both of source_format & vendor, a bad tool_call, or arguments over the depth/size/charset caps). Fix the request shape — this is rejected at the door before any engine work.
202 · not an errorstep-upA StagedChallenge is a success path, not a failure — complete it with the payload-bound PIN (step 05).
SDK · MCPIPDeniedclientBoth SDKs raise a single opaque deny carrying only a correlation_id, and never auto-retry. Secrets (JWT, OTP, vended credentials) never touch stdout/argv/logs.

The reason your call was denied is real and recorded — it just lives operator-side. The full deny-reason taxonomy (WORM-only) is in the reference; an operator resolves your correlation_id against it.

Docs / Administer

Run the gateway#

You operate the gateway: set up the host, verify the signed release, provision keys, register skills, boot fail-closed, and wire monitoring and audit.

9 steps · setup to cutover

Choose your edition#

The steps below are the same everywhere — what changes per tier is the distribution, the license, and the availability posture. Connecting an agent (the Get started track) is identical across all of them. Commercials live on pricing.

Free forever. The gateway core is source-available (BSL 1.1); the SDKs are Apache-2.0. Self-host it single-node — ideal for evaluation and non-critical fleets. Sandbox mode needs no license.

run the open core in sandbox, single-node
git clone https://github.com/mcpip-security/mcpip.git && cd mcpip
MCPIP_SANDBOX_MODE=true docker compose up --build
# then follow the steps below for a real fail-closed production boot
01

Set up the host#

The gateway runs entirely inside your perimeter — a stateless process plus a durable Redis, and no vendor cloud. Provision a container host (or a cluster) and the Redis the audit chain depends on.

a container runtime OR Kubernetes — pick your platform
# any OCI runtime
docker --version            # Docker (docker compose for single host)
podman --version            # or Podman (rootless-friendly)

# or a cluster
kubectl version --client    # Kubernetes (chart/ or k8s/)
helm version                # Helm, to install the chart (step 09)

TLS is terminated upstream (ingress / service mesh / identity-aware proxy); the gateway itself serves plain HTTP inside the perimeter — there are no in-process cert/key settings.

02

Verify the release#

Nothing is trusted on faith. The release ships an offline-root-signed manifest and a boot-integrity manifest; mcpip verify re-checks both with pure local Ed25519 — no network, no PKI, no phone-home. Gate every deploy on exit code 0.

mcpip verify — read-only release verification (fails closed, exit 2)
# verify the signed release manifest + every listed artifact on disk
mcpip verify --manifest release/manifest.json \
  --pubkey release/keys/release_root_ed25519.pub.pem --base-dir .

# independently re-verify the tamper-evident WORM chain (Merkle + Ed25519)
mcpip export-audit --redis-url "$MCPIP_REDIS_URL" --out audit_export.jsonl --verify

The same integrity manifest is re-hashed at boot: the running process re-checks every shipped source file against MCPIP_INTEGRITY_MANIFEST_PATH before a socket is bound, and refuses to start on any mismatch. There is no self-update and no self-heal — an upgrade is a signed redeploy by immutable digest.

03

Provision secrets & keys#

Config is env-driven (prefix MCPIP_), resolved once at boot into an immutable settings object. With MCPIP_SANDBOX_MODE=false (the default) the boot sequence fails closed on any missing key material — it refuses to start rather than run in a forgeable posture.

You generate two Ed25519 keypairs the vendor never holds (scripts/provision_gateway_keys.py): a WORM epoch-signing key (MCPIP_WORM_SIGNING_KEY_PATH, gateway-held) and your IdP’s public key (MCPIP_JWT_PUBLIC_KEY_PATH, verify-only — the IdP private key never touches the gateway). The signed license (gen_license.py) and the release/license-root public keys round out the set. The complete boot surface is the environment variable reference.

the key ceremony (run on an offline signer / into a KMS)
python scripts/provision_gateway_keys.py --keys-dir <offline> --public-dir <staging>
#  worm_signing_ed25519.key  -> MCPIP_WORM_SIGNING_KEY_PATH  (private, gateway-held)
#  worm_signing_ed25519.pub  -> auditors  (mcpip export-audit --verify --pubkey ...)
#  idp_signing_ed25519.key   -> your token minter / KMS  (NEVER the gateway)
#  idp_signing_ed25519.pub   -> MCPIP_JWT_PUBLIC_KEY_PATH  (public, gateway verifies)
04

Register skills#

A skill binds an opaque alias the agent sees (e.g. skill_aws_s3) to a hidden target it never sees, with a permission model and a risk tier. Register additive-only via the admin API (or the operator console); a config alias can never be shadowed.

POST/v1/admin/skills/register

Requires CAP_DIRECTORY_ADMIN (a capability, never a role). Transport is cloud_rest only; a RESTRICTED skill must be pin_required. Name aliases skill_{platform}_{tool} — risk is a per-alias data field, never a name prefix.

FieldTypeDescription
aliasrequiredstringThe opaque agent-facing name, e.g. skill_aws_dynamodb, skill_spend_summary.
targetrequiredstringThe real, hidden target the agent never sees. cloud_rest only.
servicestring | nullAdvisory display label for the permission-model console view (e.g. "AWS DynamoDB"). Never an enforcement input.
access"read" | "write"Advisory access mode for the console permission model. Never an enforcement input.
risk_tier"auto" | "pin_required"auto executes immediately; pin_required stages a payload-bound step-up. Defaults to auto.
classificationstringunclassified | restricted | classified — display/annotation only. Defaults to unclassified.
register a new alias → target (additive-only)
curl -s localhost:8080/v1/admin/skills/register -H "authorization: Bearer $ADMIN" \
  -H 'content-type: application/json' -d '{
  "alias":"skill_aws_dynamodb",
  "target":"aws.dynamodb.orders-table",
  "service":"AWS DynamoDB", "access":"read",
  "risk_tier":"auto", "classification":"unclassified"}'
# → {"registered":"skill_aws_dynamodb"}
05

Boot the gateway#

Run it non-root, read-only, inside your perimeter. With sandbox mode off the boot fails closed unless every gate passes — integrity manifest, license, WORM/JWT keys, and the Redis durability posture — all before a socket is bound.

single-host production — docker-compose.prod.yml (bundled durable Redis)
# place the SIX files in ./secrets (never commit): the IdP public key, the WORM
# signing key, license.json, integrity_manifest.json, and the release/license
# root public keys. Then:
docker compose -f docker-compose.prod.yml up --build
curl -s http://localhost:8080/healthz     # {"status":"live","glyph":"◐",...}
curl -s http://localhost:8080/readyz      # {"status":"ready","redis":"up"}

# for a sandbox eval instead: the one-command ./scripts/quickstart_demo.sh, or the
# manual bring-up below
sandbox bring-up — the exact verified sequence (from a repo checkout, Python 3.12)
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt
redis-server --port 63790 --daemonize yes --appendonly yes --appendfsync always
MCPIP_SANDBOX_MODE=true MCPIP_REDIS_URL=redis://localhost:63790/0 \
  .venv/bin/python -m uvicorn app.main:app --port 8080 &
curl -s http://localhost:8080/healthz      # {"status":"live","version":"3.0.0"}
curl -s http://localhost:8080/readyz       # {"redis":"up"}

With the gateway up in sandbox mode, connect an agent and run the governed calls from Get started (the CLI tab): the verified connect → authorize → step-up → sandbox audit verify sequence. Production drops sandbox mode and swaps the dev-token minter for your IdP (scripts/mint_principal.py).

Boot gateEnvBehavior on failure
Verified bootINTEGRITY_MANIFEST_PATH + _PUBLIC_KEY_PATHRe-hashes every shipped source file against the signed manifest; any mismatch is an opaque "integrity verification failed" and a nonzero exit. No self-heal — redeploy.
LicenseLICENSE_PATH + _PUBLIC_KEY_PATHEd25519-signed entitlement, checked at boot only (never per request). Expired / tampered → refuses to start.
WORM durabilityREDIS_URL (AOF)Refuses to boot unless Redis is appendfsync always — the write-before-execute ordering guarantee.
Sender-constraint lintcatalogRefuses to boot if any RESTRICTED/CLASSIFIED non-pin_required alias lacks require_sender_constraint (a bearer could otherwise read it).
Authenticator webhookAUTHN_WEBHOOK_URL + _SECRET_PATHRequired together for any PIN_REQUIRED skill; setting exactly one is a fail-closed boot error. AUTO-only deployments leave both unset.
06

Operators & 2FA#

The operator/team roster is a management surface — the role label authorizes nothing; capabilities, compartments, and grants do. Agents get their own IdP-minted JWTs. Humans who clear step-ups enroll a per-user RFC 6238 authenticator.

POST/v1/admin/users/invite

Invite a new operator by email (CAP_DIRECTORY_ADMIN). Additive-only; returns the record plus a one-time invite reference (stored only as a hash). The role admin / member / viewer — is a label that authorizes nothing.

invite an operator
curl -s localhost:8080/v1/admin/users/invite -H "authorization: Bearer $ADMIN" \
  -H 'content-type: application/json' -d '{"email":"ops@acme.example","role":"member"}'
# → 201 {"user":{...},"invite_token":"…once…"}
POST/v1/authenticator/enroll

Per-user 2FA gates reading a delivered step-up code. Gated by MCPIP_AUTHN_TOTP_KEY_PATH — absent, every authenticator surface is an opaque 404. Enroll returns the provisioning material exactly once; confirm proves possession of a live code.

enroll → confirm (RFC 6238 TOTP — SHA-1 / 6 digits / 30 s)
# 1) begin — returns { secret, provisioning_uri (otpauth://), digits, period_s } ONCE
curl -s -X POST localhost:8080/v1/authenticator/enroll -H "authorization: Bearer $JWT"
# 2) confirm a live code from the app → activates the enrollment
curl -s -X POST localhost:8080/v1/authenticator/enroll/confirm \
  -H "authorization: Bearer $JWT" -H 'content-type: application/json' -d '{"code":"123456"}'
# → {"enrolled":true}

Agent identities are minted by your IdP, not MCPIP — the production analog of the sandbox minter is scripts/mint_principal.py, scoping an agent to a tenant plus capability/compartment entitlements the gateway enforces.

07

Monitor & audit#

Denials are opaque to the agent but concrete in WORM — the operator sees everything the attacker cannot. Point Prometheus at /metrics, gate traffic on /healthz + /readyz, and verify the tamper-evident chain.

ConcernSurfaceNotes
Liveness / readinessGET /healthz · /readyz/healthz reports the running version; /readyz is gated on Redis. Both fail closed.
MetricsGET /metricsDecision + latency counters, Prometheus exposition. Closed-enum labels only — no tenant/agent/alias/deny_reason, so it can’t be scraped as a deny oracle.
Decision feedGET /v1/admin/decisions/recentA tenant-scoped whitelist projection of the WORM tail (alias, decision, deny_reason, class, correlation). CAP_DIRECTORY_ADMIN.
Tamper-evidenceGET /v1/audit/proof/{event_id} · /v1/audit/attestationO(log n) inclusion proofs and the signed epoch attestation. /v1/audit/verify is reachable over HTTP in sandbox; in production use mcpip export-audit --verify.
Consoledashboard :5173The operator console renders the live decision feed, the WORM ledger, and the honest dark-feature posture from /v1/admin/stats.

The durable audit record is the Redis-backed, signed Merkle-epoch WORM chain — written before dispatch — not a stdout log line. Every protected request lands as one of three decisions with a WORM-only reason code; the full taxonomy is the WORM decision reference. Structured JSON logs still go to stderr for your SIEM, but the WORM ledger is the authoritative evidence.

08

Production cutover#

There is no cutover flag to flip — MCPIP_SANDBOX_MODE defaults to false, so production is fail-closed by construction. The switch toward the demo is the exception, and the boot gates refuse to start unless the production posture is complete.

SettingKindRequirement
MCPIP_SANDBOX_MODEenvKeep false (the default). true mounts the dev-token forge and the OTP peek behind a loud banner — never in production. The sandbox routes (/v1/dev/token, /v1/authenticator/{id}, /v1/audit/verify) 404 in production.
JWT / WORM / integrity / license keysenvMCPIP_JWT_PUBLIC_KEY_PATH, MCPIP_WORM_SIGNING_KEY_PATH, the integrity manifest + key, and the license + key are all hard boot dependencies. Missing or invalid → the process refuses to boot.
MCPIP_REDIS_URLenvMust prove appendfsync always at boot (assert_persistence_posture). No env skips the check — the WORM ordering guarantee is the product.
MCPIP_AUTHN_WEBHOOK_URL / _SECRET_PATHenvBoth required for any PIN_REQUIRED skill (out-of-band OTP delivery). Setting exactly one is a fail-closed boot error; AUTO-only deployments leave both unset.
Sender-constraint lintcatalogAny RESTRICTED/CLASSIFIED non-pin_required alias must carry require_sender_constraint, or boot refuses. Validate in staging with sandbox mode off first.
09

Deploy & roll#

The gateway is stateless at the process level; all durable state lives outside the container — the Redis-backed WORM buffer and its out-of-tamper-domain anchor. Releases roll by immutable digest with a continuous audit chain across cutover and rollback.

Deploy by image.digest, never a mutable tag — chart/ (Helm) or k8s/ (plain manifests) for a cluster, docker-compose.prod.yml for single host. The chart runs ≥2 replicas behind a PodDisruptionBudget with a default-deny NetworkPolicy and an internal-only durable Redis. An upgrade is a verified redeploy of a new digest; roll back by redeploying the previous verified digest. Because replicas share the same Redis WORM buffer and durable anchor, the audit chain stays continuous — no lost or duplicated evidence.

/healthz reports the running version — confirm it before admitting traffic. TLS is terminated upstream, so there is no in-process certificate to validate here.

Docs / Reference

API reference#

The complete configuration and audit surface — every environment variable, every WORM decision reason, and every endpoint, each linking back to the step that uses it.

Environment variables#

The gateway is configured entirely through MCPIP_ environment variables, resolved once at boot into a validated, immutable settings object. Variables marked required are hard boot dependencies with MCPIP_SANDBOX_MODE=false (the default) — provisioning them is Administer, step 03.

VariableKindDescription
MCPIP_SANDBOX_MODEbool · falseThe posture switch. false (default) is fail-closed production; true mounts the sandbox forge — the dev-token minter and the OTP peek — behind a loud banner. Never true in production.
MCPIP_REDIS_URLrequiredurlRedis holding payload locks, grants, quarantine, and the durable WORM buffer. Must be linearizable with AOF appendfsync always and noeviction; production refuses to boot if it cannot confirm that persistence posture.
MCPIP_JWT_ISSUERrequiredstringExpected iss of minted principal tokens — must match your IdP. Default mcpip-demo-idp.
MCPIP_JWT_AUDIENCErequiredstringExpected aud of minted tokens — your gateway’s audience. Default mcpip-gateway.
MCPIP_JWT_PUBLIC_KEY_PATHrequiredpath · PEMYour IdP’s public signing key. Identity is verify-only: the gateway checks tokens, never mints them. (Wire a JWKS provider for rotating keys.)
MCPIP_WORM_SIGNING_KEY_PATHrequiredpath · PEMEd25519 private key that signs the WORM audit epochs. Gateway-held; materialized 0600 at deploy.
MCPIP_WORM_PATHrequiredpathWORM ledger path on a durable volume (default ./mcpip_worm.jsonl; /var/lib/mcpip/mcpip_worm.jsonl in the image).
MCPIP_WORM_ANCHOR_PATHrequiredpathAppend-only head anchor (the rollback / truncation watermark). Must sit on a durable volume distinct from Redis — it is the out-of-tamper-domain witness.
MCPIP_INTEGRITY_MANIFEST_PATHrequiredpathSigned boot-integrity manifest. At startup the gateway re-hashes every shipped source file against it and refuses to boot on any mismatch. Required in production.
MCPIP_INTEGRITY_PUBLIC_KEY_PATHrequiredpath · PEMRelease-root public key that verifies the integrity manifest. Required in production.
MCPIP_LICENSE_PATHrequiredpath · JSONEd25519-signed entitlement document. Gates process boot only — never consulted by the per-request authorization pipeline. Required in production.
MCPIP_LICENSE_PUBLIC_KEY_PATHrequiredpath · PEMLicense-root public key that verifies the license signature and validity window. Required in production.
MCPIP_AUTHN_WEBHOOK_URLurlHTTPS sink the payload-bound one-time PIN is pushed to (SSRF-guarded, HMAC-SHA256-signed). Required together with the secret path for any PIN_REQUIRED skill; setting exactly one is a fail-closed boot error. AUTO-only deployments leave both blank.
MCPIP_AUTHN_WEBHOOK_SECRET_PATHpathFile holding the ≥32-byte HMAC-SHA256 signing secret for the step-up delivery webhook. Materialized 0600 at deploy.
MCPIP_AUTHN_TOTP_KEY_PATHpathMaster key that enables per-user RFC 6238 TOTP 2FA (enrollment + the TOTP-gated OTP reveal). Absent → every authenticator surface is an opaque 404.
MCPIP_API_HOSTstring · 0.0.0.0Bind address. TLS is terminated upstream (ingress / mesh / IAP) — the gateway serves plain HTTP inside the perimeter.
MCPIP_API_PORTint · 8080Listen port.
MCPIP_MAX_IN_FLIGHTint · 64Per-worker concurrency ceiling; arrivals above it shed (503) to protect tail latency. A shed request never reaches authorize().

WORM decisions#

Every protected request lands as exactly one decision in the signed Merkle-epoch WORM log (written before dispatch — see Administer, step 07). A deny additionally records a deny_reason; an admin mutation records an admin_action. None of these ever cross the agent boundary — the caller sees only the generic denial plus a correlation_id.

decision

allowdenyadmin_action

deny_reason (WORM-only)

jwt_invalidjwt_claims_missingidentity_injectionschema_violationdepth_exceededsize_exceededillegal_characterunknown_formatunknown_vendorunknown_aliascross_tenantcompartment_deniedcapability_deniedsender_constraint_requiredpin_requiredpin_not_foundpin_mismatchpayload_mismatchcanary_trippedagent_quarantinedprincipal_revokedalias_disabledotp_delivery_failedpolicy_deniedpolicy_gate_deniedrate_limitedtransport_errorlock_errorinternal

admin_action

skill_registerskill_disableskill_enableprincipal_revokeprincipal_reactivateoperator_user_inviteauthenticator_enrollauthenticator_confirmauthenticator_disableotp_revealforensic_read

API endpoints#

Every wire surface documented in the guides, in one index. Each row links to the step that covers it end to end.

POST/v1/authorize

The single authorization choke point. 200 ExecutionReceipt (executed) · 202 StagedChallenge (step-up) · 403 opaque deny. Identity via Authorization: Bearer. Authorize a tool call

MCP/v1/mcp

MCP-native JSON-RPC 2.0 (initialize, tools/list, tools/call). The gateway IS the MCP server; tools/list is pruned to the verified identity. Connect your agent

GET/v1/catalog

The opaque aliases this identity may see — metadata only (risk tier, transport class, classification), never the real target. Connect your agent

POST/v1/authz/decision

OpenID-AuthZEN PDP: subject/action/resource → { decision: bool }. Decision-only — never executes, vends, or stages. Authorize a tool call

POST/v1/dev/token

SANDBOX ONLY — mint a demo EdDSA JWT → { jwt }. Returns 404 in production (identity stays with your IdP). ~5-minute expiry. Get a token

GET/v1/authenticator/{challenge_id}

SANDBOX ONLY — peek the staged one-time PIN, standing in for the enrolled device. 404 in production. Clear a step-up

GET/v1/version

Running / latest release, entitlement channel, and signed provenance (notifier only — an upgrade is a redeploy). JWT-gated. Connect your agent

GET/v1/license

The boot-verified entitlement document, for operator visibility. Never consulted per request. JWT-gated. Boot the gateway

GET/healthz

Liveness plus the running version (read dynamically from the VERSION file). Boot the gateway

GET/readyz

Readiness — gated on Redis. Fails closed when the backing store is unreachable. Boot the gateway

GET/metrics

Prometheus exposition. Closed-enum labels only — no tenant, agent, alias, or deny_reason ever appears, so /metrics can’t be scraped as a deny oracle. Monitor & audit

POST/v1/admin/skills/register

Register a new opaque alias → hidden target for your tenant (service/access + risk tier + classification). Additive-only, cloud_rest only. CAP_DIRECTORY_ADMIN. Register skills

POST/v1/admin/users/invite

Invite an operator by email; the role label authorizes nothing. Returns the record + a one-time invite reference. CAP_DIRECTORY_ADMIN. Operators & 2FA

POST/v1/authenticator/enroll

Begin per-user TOTP 2FA: returns an otpauth:// URI once; confirm with /v1/authenticator/enroll/confirm. Gated by MCPIP_AUTHN_TOTP_KEY_PATH. Operators & 2FA

GET/v1/admin/decisions/recent

The live decision feed — a tenant-scoped whitelist projection of the WORM tail (alias, decision, deny_reason, class, correlation). CAP_DIRECTORY_ADMIN. Monitor & audit

GET/v1/admin/stats

Deployment / license / usage plus the honest dark-feature posture block. CAP_DIRECTORY_ADMIN, aggregates only. Monitor & audit

GET/v1/audit/proof/{event_id}

O(log n) Merkle inclusion proof for one audited event. Monitor & audit

GET/v1/audit/attestation

The signed WORM attestation bundle — the latest sealed epoch header + evidence. Monitor & audit

GET/v1/audit/verify

SANDBOX ONLY — recompute the signed Merkle-epoch chain over HTTP. 404 in production; use mcpip export-audit --verify there. Monitor & audit