◐ 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.
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.
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.
git clone https://github.com/mcpip-security/mcpip.git && cd mcpip
./scripts/quickstart_demo.sh # or, with the CLI on PATH: mcpip upIdempotent: 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.
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.
/v1/dev/token| Field | Type | Description |
|---|---|---|
tenant_id | string | Tenant the agent acts under. Defaults to the demo tenant (tenant-acme). |
agent_id | string | Agent id recorded on the minted token. Defaults to agent-orchestrator-1. |
role | string | Descriptive label ONLY — it authorizes nothing. Defaults to ops. |
compartment | uuid | null | Optional compartment UUID this principal is scoped to (team/MCP separation). |
capabilities | uuid[] | null | Optional capability UUIDs. The well-known admin/audit caps are at GET /v1/dev/capabilities. |
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.
Authorization: Bearer <jwt> # never a URL, never a query string, never a log lineIn 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.
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.
{
"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.
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.
/v1/authorize| Field | Type | Description |
|---|---|---|
source_formatrequired | enum | One 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. |
vendor | string | Alternative 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_callrequired | object | The 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). |
jwt | string | null | Optional — identity is normally taken from the Authorization: Bearer header instead. |
pin / challenge_id | string | null | The step-up completion pair (step 05). Supplied together, never one alone. |
{
"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
}{
"correlation_id": "…",
"action_required": "approve in the enrolled authenticator",
"challenge_id": "…", // the payload-bound lock id (step 05)
"risk_tier": "pin_required"
}{ "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.
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.
/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).
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).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 → 200The 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.
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.
| Field | Type | Description |
|---|---|---|
correlation_id | string | The one handle an agent may quote to a human operator to locate this decision in the audit log. |
decision | string | Always "allow" on a 200 (a deny never reaches this shape). |
status | string | Always "committed" — the WORM ALLOW record was written before dispatch. |
transaction_ref | string | txn_ + uuid4 — a per-execution reference. |
executed_target_class | string | cloud_rest | legacy_mainframe | cloud_iam — the coarse transport class only. Never entry.target. |
worm_sequence | int | The audit anchor the operator can quote to find the sealed decision record. |
vended_credential | object | null | Present 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.
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.
| Outcome | Class | What the caller sees — and what to do |
|---|---|---|
403 · ErrorResponse | policy | Every 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 request | schema | A 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 error | step-up | A StagedChallenge is a success path, not a failure — complete it with the payload-bound PIN (step 05). |
SDK · MCPIPDenied | client | Both 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
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.
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 bootSet 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.
# 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.
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.
# 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 --verifyThe 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.
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.
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)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.
/v1/admin/skills/registerRequires 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.
| Field | Type | Description |
|---|---|---|
aliasrequired | string | The opaque agent-facing name, e.g. skill_aws_dynamodb, skill_spend_summary. |
targetrequired | string | The real, hidden target the agent never sees. cloud_rest only. |
service | string | null | Advisory 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. |
classification | string | unclassified | restricted | classified — display/annotation only. Defaults to unclassified. |
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"}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.
# 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 belowpython3.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 gate | Env | Behavior on failure |
|---|---|---|
Verified boot | INTEGRITY_MANIFEST_PATH + _PUBLIC_KEY_PATH | Re-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. |
License | LICENSE_PATH + _PUBLIC_KEY_PATH | Ed25519-signed entitlement, checked at boot only (never per request). Expired / tampered → refuses to start. |
WORM durability | REDIS_URL (AOF) | Refuses to boot unless Redis is appendfsync always — the write-before-execute ordering guarantee. |
Sender-constraint lint | catalog | Refuses to boot if any RESTRICTED/CLASSIFIED non-pin_required alias lacks require_sender_constraint (a bearer could otherwise read it). |
Authenticator webhook | AUTHN_WEBHOOK_URL + _SECRET_PATH | Required together for any PIN_REQUIRED skill; setting exactly one is a fail-closed boot error. AUTO-only deployments leave both unset. |
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.
/v1/admin/users/inviteInvite 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.
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…"}/v1/authenticator/enrollPer-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.
# 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.
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.
| Concern | Surface | Notes |
|---|---|---|
Liveness / readiness | GET /healthz · /readyz | /healthz reports the running version; /readyz is gated on Redis. Both fail closed. |
Metrics | GET /metrics | Decision + 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 feed | GET /v1/admin/decisions/recent | A tenant-scoped whitelist projection of the WORM tail (alias, decision, deny_reason, class, correlation). CAP_DIRECTORY_ADMIN. |
Tamper-evidence | GET /v1/audit/proof/{event_id} · /v1/audit/attestation | O(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. |
Console | dashboard :5173 | The 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.
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.
| Setting | Kind | Requirement |
|---|---|---|
MCPIP_SANDBOX_MODE | env | Keep 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 keys | env | MCPIP_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_URL | env | Must 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_PATH | env | Both 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 lint | catalog | Any RESTRICTED/CLASSIFIED non-pin_required alias must carry require_sender_constraint, or boot refuses. Validate in staging with sandbox mode off first. |
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.
| Variable | Kind | Description |
|---|---|---|
MCPIP_SANDBOX_MODE | bool · false | The 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_URLrequired | url | Redis 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_ISSUERrequired | string | Expected iss of minted principal tokens — must match your IdP. Default mcpip-demo-idp. |
MCPIP_JWT_AUDIENCErequired | string | Expected aud of minted tokens — your gateway’s audience. Default mcpip-gateway. |
MCPIP_JWT_PUBLIC_KEY_PATHrequired | path · PEM | Your 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_PATHrequired | path · PEM | Ed25519 private key that signs the WORM audit epochs. Gateway-held; materialized 0600 at deploy. |
MCPIP_WORM_PATHrequired | path | WORM ledger path on a durable volume (default ./mcpip_worm.jsonl; /var/lib/mcpip/mcpip_worm.jsonl in the image). |
MCPIP_WORM_ANCHOR_PATHrequired | path | Append-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_PATHrequired | path | Signed 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_PATHrequired | path · PEM | Release-root public key that verifies the integrity manifest. Required in production. |
MCPIP_LICENSE_PATHrequired | path · JSON | Ed25519-signed entitlement document. Gates process boot only — never consulted by the per-request authorization pipeline. Required in production. |
MCPIP_LICENSE_PUBLIC_KEY_PATHrequired | path · PEM | License-root public key that verifies the license signature and validity window. Required in production. |
MCPIP_AUTHN_WEBHOOK_URL | url | HTTPS 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_PATH | path | File holding the ≥32-byte HMAC-SHA256 signing secret for the step-up delivery webhook. Materialized 0600 at deploy. |
MCPIP_AUTHN_TOTP_KEY_PATH | path | Master 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_HOST | string · 0.0.0.0 | Bind address. TLS is terminated upstream (ingress / mesh / IAP) — the gateway serves plain HTTP inside the perimeter. |
MCPIP_API_PORT | int · 8080 | Listen port. |
MCPIP_MAX_IN_FLIGHT | int · 64 | Per-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
deny_reason (WORM-only)
admin_action
API endpoints#
Every wire surface documented in the guides, in one index. Each row links to the step that covers it end to end.
/v1/authorizeThe single authorization choke point. 200 ExecutionReceipt (executed) · 202 StagedChallenge (step-up) · 403 opaque deny. Identity via Authorization: Bearer. → Authorize a tool call
/v1/mcpMCP-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
/v1/catalogThe opaque aliases this identity may see — metadata only (risk tier, transport class, classification), never the real target. → Connect your agent
/v1/authz/decisionOpenID-AuthZEN PDP: subject/action/resource → { decision: bool }. Decision-only — never executes, vends, or stages. → Authorize a tool call
/v1/dev/tokenSANDBOX ONLY — mint a demo EdDSA JWT → { jwt }. Returns 404 in production (identity stays with your IdP). ~5-minute expiry. → Get a token
/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
/v1/versionRunning / latest release, entitlement channel, and signed provenance (notifier only — an upgrade is a redeploy). JWT-gated. → Connect your agent
/v1/licenseThe boot-verified entitlement document, for operator visibility. Never consulted per request. JWT-gated. → Boot the gateway
/healthzLiveness plus the running version (read dynamically from the VERSION file). → Boot the gateway
/readyzReadiness — gated on Redis. Fails closed when the backing store is unreachable. → Boot the gateway
/metricsPrometheus 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
/v1/admin/skills/registerRegister a new opaque alias → hidden target for your tenant (service/access + risk tier + classification). Additive-only, cloud_rest only. CAP_DIRECTORY_ADMIN. → Register skills
/v1/admin/users/inviteInvite an operator by email; the role label authorizes nothing. Returns the record + a one-time invite reference. CAP_DIRECTORY_ADMIN. → Operators & 2FA
/v1/authenticator/enrollBegin per-user TOTP 2FA: returns an otpauth:// URI once; confirm with /v1/authenticator/enroll/confirm. Gated by MCPIP_AUTHN_TOTP_KEY_PATH. → Operators & 2FA
/v1/admin/decisions/recentThe live decision feed — a tenant-scoped whitelist projection of the WORM tail (alias, decision, deny_reason, class, correlation). CAP_DIRECTORY_ADMIN. → Monitor & audit
/v1/admin/statsDeployment / license / usage plus the honest dark-feature posture block. CAP_DIRECTORY_ADMIN, aggregates only. → Monitor & audit
/v1/audit/proof/{event_id}O(log n) Merkle inclusion proof for one audited event. → Monitor & audit
/v1/audit/attestationThe signed WORM attestation bundle — the latest sealed epoch header + evidence. → Monitor & audit
/v1/audit/verifySANDBOX ONLY — recompute the signed Merkle-epoch chain over HTTP. 404 in production; use mcpip export-audit --verify there. → Monitor & audit