MCP
Authentication & keys
Source: orchestration/mcp-gateway/auth-middleware.ts, index.ts, rate-limiter.ts, core/core-api-keys/rate-limit-config.ts, extras.ts (plungeaiWhoami), core/core-api-keys/index.ts, apps/ocean-dashboard/src/worker/routes/platform-keys.routes.ts, orchestration/api-gateway/lib/auth.ts, orchestration/api-gateway/routes/mcp.ts. Unauthenticated 401 responses live-verified 2026-09-24.
Every MCP call needs an ozk_ key. The key is your identity: it resolves to one user account, and every tool reads and writes only what that account owns. There is no OAuth sign-in on the MCP server today; bearer keys are the only way in. This chapter uses https://mcp.plungeai.com/v1; https://api.plungeai.com/v1/mcp accepts the same key, with the differences noted in each section.
2.1 The ozk_ key
| Property | Value |
|---|---|
| Format | ozk_live_ + 48 hex characters |
| Where | Dashboard → One API → Keys (https://dashboard.plungeai.com), self-service while signed in |
| Name | required, 1–64 characters |
| Expiry | never, or 7 / 30 / 90 / 180 / 365 days, chosen when you create it |
| Shown | exactly once, at creation. It is stored only as a SHA-256 hash; nobody can show it to you again |
| Per account | up to 25 active keys; the 26th is refused until you revoke one |
| Tier | copied from your account's tier when the key is created (free, pro, enterprise; an unknown account tier gives free) |
| Works on | the MCP server and every One API plane, models included (https://api.plungeai.com/docs#2-authentication--keys) |
The Keys list shows each key's name, redacted prefix and last four characters, created and last-used dates, expiry (highlighted when it is less than a week away) and status (active, revoked, expired), with a revoke action on active keys.
Because the tier is copied at creation, a key keeps the tier it was created with. After your account changes tier, create a new key to get the new limits; plungeai_whoami shows the tier of the key you are using (2.7).
A shared team key is simply a key created under someone else's account: whoever uses it acts as that account. Ask the account owner for one; you cannot create a key for another account yourself.
2.2 Sending the key
| Header | Example |
|---|---|
Authorization: Bearer <key> | -H "Authorization: Bearer $PLUNGE_API_KEY" |
X-API-Key: <key> | -H "X-API-Key: $PLUNGE_API_KEY" |
Both are supported. The server tries X-API-Key first, then Authorization: Bearer. A bearer value that does not start with ozk_ is rejected (401): OAuth tokens and sk-ocean- model keys (legacy) are not credentials here. X-API-Key exists for integrations whose "API key" field does not send a bearer header:
curl -s https://mcp.plungeai.com/v1 \
-H "X-API-Key: $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'On https://api.plungeai.com/v1/mcp both headers work too, and the value must start with ozk_.
2.3 What an unauthenticated call returns
Any request to /v1 without a valid key gets 401 with a JSON-RPC error body and a WWW-Authenticate challenge. No credentials at all gives a bare Bearer challenge (live-verified 2026-09-24):
curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \
-X POST https://mcp.plungeai.com/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'A credential that was sent but is not valid adds error="invalid_token" (live-verified 2026-09-24, with a made-up key):
curl -s -o /dev/null -w '%{http_code} %header{www-authenticate}\n' \
-X POST https://mcp.plungeai.com/v1 \
-H 'Authorization: Bearer ozk_live_not_a_real_key' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The body is the same in both cases:
curl -s -X POST https://mcp.plungeai.com/v1 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'{"jsonrpc":"2.0","error":{"code":-32001,"message":"No valid authentication provided. Use an X-API-Key header or \"Authorization: Bearer <api key>\"."},"id":null}What each failure looks like:
| Cause | Status | WWW-Authenticate | error.message |
|---|---|---|---|
| no key | 401 | Bearer | No valid authentication provided. … |
unknown, mistyped, revoked or expired key, or a non-ozk_ bearer | 401 | Bearer error="invalid_token" | No valid authentication provided. … — the exact reason is logged server-side, not returned |
valid key used from an address outside its allowed_ips fence | 401 | Bearer error="invalid_token" | API key not permitted from <your IP> |
| more than 20 failed attempts in a minute (or 500 in a day) from one IP | 429 + Retry-After | — | Too many failed authentication attempts |
The same request on https://api.plungeai.com/v1/mcp answers in the One API envelope instead (live-verified 2026-09-24):
curl -s -X POST https://api.plungeai.com/v1/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'There: no key or a non-ozk_ value → 401 Bearer ozk_ key required; an ozk_ key that is invalid, revoked, expired or outside its IP fence → 401 Invalid or revoked API key; too many failures → 429 rate_limited (too many failed authentications). Every error carries X-Error-Code and x-request-id headers.
The server does not publish OAuth discovery metadata (/.well-known/oauth-protected-resource). Clients that can only connect through OAuth need a bridge — see Connecting clients.
2.4 Expiry, revocation and rotation
- Expiry is checked on every request. From the moment a key expires it answers
401. - Revoke a key from the same Keys list. The record stays (so the list and audit trail keep it) and is marked revoked. Each server instance caches a successfully checked key for up to 60 seconds, and the key store's own edge cache can add up to another 60 seconds, so a revoked key can keep working for up to about two minutes (about 75 seconds measured), then answers
401. If the key leaked, revoke it first and allow for that window before you treat it as dead. - Rotate without downtime: create the new key, update every client config, confirm it with
plungeai_whoami(2.7), then revoke the old key. - Leaked or lost? Create a replacement and revoke the old one yourself. A lost key cannot be recovered: only its hash is stored.
2.5 Key fences
A key can carry two optional fences. Keys you create yourself in the Dashboard have no fences (every tool, from any address). Fenced keys are issued by PlungeAI for you — ask the account owner who gave you the key, or Requests (opens in a new tab).
| Fence | Effect on https://mcp.plungeai.com/v1 |
|---|---|
allowed_ips | The key authenticates only from the listed addresses: exact IPv4 or IPv6 addresses, or IPv4 CIDR blocks (IPv6 ranges are not supported — list exact IPv6 addresses). From anywhere else: 401 API key not permitted from <ip>, counted as a failed attempt. |
allowed_tools | Only the listed tools are registered for this key: the others do not appear in tools/list, and a tools/call to one returns JSON-RPC error -32602 Tool not permitted for this key: <name>. |
If a tool this guide documents is missing from your tools/list, or a call is refused this way, the key is fenced: that is policy, not a fault.
2.6 Tiers and rate limits
Default limits per tier, from tier-limits.ts (the operator can change them, see the last point below):
| Tier | Tool-call requests per minute | Tool-call requests per day |
|---|---|---|
| free | 30 | 1,000 |
| pro | 100 | 10,000 |
| enterprise | 300 | 100,000 |
How they are counted:
- Per account, not per key. The counter is keyed by your user id, so every key on your account shares it. The tier and any per-key override of the key you are using decide the cap.
- Only
tools/callcounts onhttps://mcp.plungeai.com/v1: each HTTP request that carries one counts once (a JSON-RPC batch is one request).initialize,tools/list,resources/*andprompts/*are free.plungeai_whoamiis a tool call and counts. - On
https://api.plungeai.com/v1/mcpthe rule is the same: the One API does not countPOST /v1/mcp, and this server counts each request that carries atools/call, with your key's per-key override. - Fixed windows. A minute window opens with your first counted call and resets 60 seconds later; a day window lasts 24 hours from its first call, not until midnight.
- A per-key
rate_limitoverride, when PlungeAI sets one on a key, replaces the per-minute cap. - Set by the operator. The numbers above are the defaults. PlungeAI sets the per-tier numbers, or switches the limits off, in Operator › One API › Keys › Rate limits (
GET/PUT /admin/rate-limitsfor internal callers). While they are off, no call is counted and no tool call or One APIPOSTreturns429; the one limit left is the failed-authentication meter (20 invalid-key attempts a minute per client IP), which is always on. A change reaches the gateways within a minute. A per-keyrate_limitstill applies while limits are on.
When you are over the limit, the call is refused before anything runs:
HTTP/2 429
retry-after: 42
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Rate limit exceeded: Rate limit: 100 requests/minute","data":{"retryAfter":42}},"id":3}Retry-After (and error.data.retryAfter) is the number of seconds until the window resets. Wait that long, then continue; do not retry in a tight loop. On the One API host the body is {"error":{"code":"rate_limited","message":"rate limit exceeded for tier pro","retry_after":42}}. The full error catalogue is in Errors, feedback & limits.
2.7 Checking yourself — plungeai_whoami
plungeai_whoami shows who the server thinks you are and how much of your window you have used. It is read-only.
Try asking: "Who am I, what tier am I on, and how much of my rate limit have I used?" — or, right after connecting a client, "use plungeai_whoami to confirm my identity".
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
user_request | string (≤ 4,000 chars) | no | — | the user's own words |
format | markdown · json | no | markdown | json returns the fields below as a JSON document plus structuredContent |
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"how much of my rate limit have I used?"}}}'The Markdown answer:
# Who am I
- **User:** `<your user id>`
- **Auth:** api-key · tier **pro**
- **Key:** my-laptop (`<key id>`)
- **Rate window:** 3/min used · 41/day used
- **Server:** plungeai.com (mcp.plungeai.com)With format: "json":
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I, as JSON","format":"json"}}}'{
"user": "<your user id>",
"auth_type": "api-key",
"tier": "pro",
"key_label": "my-laptop",
"key_id": "<key id>",
"rate_window": { "minute_used": 3, "day_used": 41 },
"server": "plungeai.com (mcp.plungeai.com)"
}What to know:
- The counts include the
plungeai_whoamicall itself. Compare them with your tier's caps in 2.6; the card shows usage, not the caps. - The rate-window line is best-effort. If the counter cannot be read, the line is absent (
rate_window: null); that alone is not an error. - There is no permissions field. A tool fence shows up as fewer tools in
tools/list, not on this card. - Through
https://api.plungeai.com/v1/mcpthe card is the same: the One API forwards your key and the gateway reads its record. - Without an identity the tool answers
Authentication required.withisError: true— in practice you get the401from 2.3 first.
2.8 Identity and scoping
- Everything is scoped to the account behind the key: executions, workflows, schedules, conversations, memory. You see what that account owns, the same view it has in Studio — never the whole system.
- Id-based lookups check ownership before reading anything. Someone else's id, or a mistyped one, answers "not found", never "forbidden" (
plungeai_get_resultanswers with aneeds_inputoutcome naming the id). An id that looks right but is "not found" usually belongs to another account or is mistyped. - What you create through MCP carries your user id, so it shows up in Studio, the dashboard and the other PlungeAI apps under your account.
- Every authenticated request is written to your activity trail with its arguments and answer. You can read your own day at
https://mcp.plungeai.com/log?date=YYYY-MM-DDwith the same key (Errors, feedback & limits). This is also whyuser_requestmatters: it is where your own sentence survives into support diagnostics.
2.9 Keeping the key safe
- Treat it like a password. Anyone holding it acts as your account.
- Keep the literal key in user-level config (
~/.claude.json,~/.cursor/mcp.json, the client's secret store) or an environment variable. A config file inside a repo should only reference a variable (Connecting clients shows how per client). - Before the first commit in a repo you just configured, both of these must print nothing:
git grep -n "ozk_" # tracked files
grep -rn "ozk_" . --exclude-dir=.git --exclude-dir=node_modules # untracked files too- Never put the key in browser code or a client bundle. Apps call PlungeAI from a server with the key in a server-side secret.
- If a key may have leaked, rotate it now (2.4).