MCP
Errors, feedback & limits
Source: orchestration/mcp-gateway/tool-outcome.ts, server.ts (wrap, validateToolArgs),
index.ts, shared.ts, auth-middleware.ts, rate-limiter.ts, session-do.ts, resources.ts,
prompts.ts, tools.ts, tool-exec.ts, extras.ts, activity-log.ts, log-view.ts,
execution-do.ts, leg-state.ts, core/core-base/content-guard.ts,
orchestration/api-gateway/lib/auth.ts, MCP SDK 1.29.0, live registry cards (2026-09-24).
When something cannot run, the platform does not just fail. It answers with what is missing
and how to fix it. Your client reads that answer and usually corrects itself. This chapter is so
that you know what is happening: the answer shapes, every error code, the limits, when to retry,
and how to read your own activity log. It applies to https://mcp.plungeai.com/v1. The same server
at https://api.plungeai.com/v1/mcp differs only at the edge (4.11).
13.1 Three kinds of "no"
Your request
├─ stopped at the edge: auth, size, rate limit, parse, → JSON-RPC error + HTTP status (13.6)
│ unknown or fenced tool, unknown method/resource/prompt nothing ran
├─ reached a tool that answered with guidance → a result with an outcome (13.2)
│ needs_input / needs_connection / needs_api_key / usually nothing ran
│ needs_approval / unavailable / error
└─ reached a tool that answered with a plain failure → a result, isError: true, text only (13.5)A client should branch in this order: first the HTTP status and the JSON-RPC error, then
result.isError, then result.structuredContent.status.
13.2 The structured-feedback contract
The six tools whose outputSchema is the outcome envelope never answer with a bare error:
plungeai_execute_workflow, plungeai_execute_agent, plungeai_execute_tool,
plungeai_get_tool_contract, plungeai_run_mission and plungeai_learn
(4.9). Every answer carries an outcome: a status
plus a summary, and usually a remediation saying exactly what to do. plungeai_get_result uses
the same envelope for its guidance answers (a missing or unknown id). A paused run is not an
envelope there: its result text ends with a ⏸ block, and format:"json" gives {"content": …}
with no status. For machine-readable pause state, read structuredContent.continuation from
plungeai_get_workflow_status (9.3). Two paths
hold the promise for every tool: argument validation (13.3) and
unexpected exceptions (13.4).
status | Text starts with | What it means | What to do | isError |
|---|---|---|---|---|
ok | the result itself | It ran. This is your result. | Nothing. | false |
needs_input | 📝 NEEDS INPUT | A required field is missing or malformed. The answer names the exact field and, where the agent has one, includes its full form (the contract) with a worked example. A run that paused with a question also uses this status (⏸ AWAITING USER: …). | Say "do what it suggests". Your client fills the form and retries. For a question, answer it (plungeai_continue). | false |
needs_connection | 🔌 NEEDS CONNECTION | The agent acts as you (Google, Microsoft, …) and your connected account is missing, expired, short of scopes, or lacks a licence. Nothing was run: the check comes first. | Connect the service once in Studio → Connectors, then ask again. | false |
needs_api_key | 🔑 NEEDS API KEY | A bring-your-own-key agent (wallets, private APIs) has no key saved for you. | Save your key once in Studio. Every surface can then use it. | false |
needs_approval | ⏸ NEEDS APPROVAL | Nothing has executed. Either an agent staged an action and paused (the summary starts ⏸ AWAITING USER APPROVAL: …, with an approve_via action), or the trust fence refused an outward, irreversible operation (send, pay, transfer, buy, withdraw) before it ran (no actions, no paused session). | Staged action: reply "yes"/"approve" to proceed (plungeai_continue), or anything else to change course. Trust-fence refusal: nothing to continue over MCP; the operation runs only from Ocean Studio, where a person approves it (6.6). | false |
unavailable | 🚫 UNAVAILABLE | The agent cannot run here. Its id does not exist (No such agent: …), it is not status:active, it is registered but not deployed, or it reported itself temporarily unable (for example its data provider's quota ran out). Apart from a made-up id, this is a platform-side gap, not your mistake. | Pick one of the live alternatives listed in the answer, or retry later if it gave a reason. | false |
error | ❌ ERROR | A genuine failure, with the real reason stated (for example an upstream API limit, or a timeout). | Fix the input and rerun, or follow the retry hint. Don't keep polling. | true |
Every non-ok answer ends with the line
_This is structured guidance, not a crash — do not retry the identical call. Follow the remediation, or relay it to the user._
The envelope is the structuredContent object: status, summary, agent_id, operation,
result, execution_id, missing[], warnings[], remediation { message, actions[] } and
contract (field reference in 4.9). The Markdown text
shows the same information as a status line, the summary, Missing required: …,
**To proceed:** <message> and one bullet per action. Each warning becomes a ⚠️ line.
Remediation actions (RemediationAction), and how each is rendered for your client:
action | Fields | Rendered as |
|---|---|---|
provide_field | field, description? | Provide <field> — <description> and retry the same call. |
connect_provider | provider, url, scopes? | Connect provider: open <url> → Connectors and connect the account. Relay this to the user — it needs their browser. |
provide_api_key | provider, how | Provide a provider API key: <how> |
approve_via | tool: plungeai_continue, execution_id, note? | Ask the user to approve, then call plungeai_continue {execution_id, approve: true}. NEVER approve on your own. |
respond_via | tool: plungeai_continue, execution_id, question? | Relay the question to the user, then call plungeai_continue {execution_id, message: "<their answer>"}. |
retry_with | changes, note? | Retry with {…} (for example {"mode":"async"} after a timeout). |
use_alternative | agent_id, note? | Alternative: <agent_id> — <note> |
Connection and key remediations point at Ocean Studio, https://studio.plungeai.com → Connectors.
A refusal is not a run. If the platform stops a call before dispatch (a missing field, a
missing connection, an unknown or inactive agent, an unknown model), no execution is created.
Nothing half-happens, and your history stays clean. Looking up a result by an id that does not
exist gets a needs_input answer naming the id (plungeai_get_result), never a bare "not found".
Limits are told to you, never silently applied. Search engines declare their maximum query
length on their card (live-verified 2026-09-24): serper-agent 2,048 characters, brave-agent
400, tavily-agent 400. Send more and you get needs_input with the limit. Your input is never
truncated behind your back.
The real reason comes back. When a run fails, the classifier (classifyExecutionError) passes
the upstream reason through: Execution failed: <reason>, The run timed out: <reason>, or the
agent's own message on unavailable. You do not get a generic "something went wrong".
Visible mapping, never guessing. Send only prose to plungeai_execute_tool for an agent whose
form has exactly one required free-text field (a report generator's content, say), and the
platform fills that field with your prose and says so:
⚠️ prose mapped: your prompt was passed as `content` (this card's only required parameter)….
Details are in Tools.
13.3 Argument validation
Every tools/call is checked against the tool's input schema before the tool runs
(server.ts validateToolArgs, called from index.ts). A failure is not a protocol error. It is
a needs_input outcome, returned as a normal JSON-RPC result (HTTP 200, plain JSON):
summary:`plungeai_list_agents` was not called — its arguments do not match the tool's schema.followed by one`field`: messageper problem;missing[]: required fields that were absent;remediation.actions: oneprovide_fieldper missing or bad field.
Nothing runs, but the call still counts toward your rate limit. Free to try:
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_list_agents","arguments":{"user_request":"list agents","limit":500}}}'The bounds every tool shares (shared.ts, tools.ts). Per-tool parameters are in the
Tool reference.
| Kind of argument | Bound |
|---|---|
ids (agent_id, execution_id, workflow_id, task_id, model, …) | string, at most 256 characters |
short text (search, folder, description, query, …) | at most 2,000 characters |
prompts and messages (prompt, input, goal, message, content, source, …) | at most 65,536 characters |
workflow_yaml, yaml | at most 262,144 characters (256 KiB) |
user_request | at most 4,000 characters |
limit | integer 1–100 |
inputs (plungeai_execute_workflow) | keys up to 128 characters, values up to 8,192 |
max_iterations (plungeai_run_mission) | integer 1–50 |
maxTokens / max_tokens (plungeai_execute_agent) | integer 1–64,000 |
This pre-screen covers single-message requests. Inside a JSON-RPC batch, bad arguments come
back as the SDK's own isError text (Input validation error: …) instead
(4.4).
13.4 Transient failures
If a tool throws unexpectedly (a database hiccup, a service-binding blip), the server still answers
with an outcome (server.ts wrap):
status: "error",isError: true;summary: the error message and(request <request id>). Messages that would expose internals (database or parser errors) are replaced withAn internal error occurred while running this tool.;remediation.message:This is a transient platform failure, not a bad request. Retry once in a few seconds; if it persists, report the request id above.
format: "json" is honoured on this path too. The full error goes to the server logs under the same
request id.
13.5 Plain tool errors
Tools without an outputSchema answer their own failures as a plain text result with
isError: true and no structuredContent. Examples from the code:
| Tool | Text |
|---|---|
| any tool without a user (internal calls only) | Authentication required. |
plungeai_get_workflow_status | Execution not found. (the id is unknown or not yours) |
plungeai_get_result, plungeai_get_workflow_status | That is a scheduler run id, which this tool cannot resolve. Use plungeai_schedule {action: "runs"} … (ids starting exec_) |
plungeai_executions | execution_id is required for this action. · Execution not found. · Execution is <status>, not running — nothing to cancel. |
plungeai_list_agents | Registry unavailable — try again shortly. · Registry search failed (<status>). |
They are short and say what to do. Treat them like a needs_input or error outcome.
13.6 JSON-RPC and HTTP errors
Protocol errors mean the request never reached a tool. The body is always
{"jsonrpc":"2.0","error":{"code":…,"message":"…"},"id":…}. id is null when the server had not
parsed one.
| HTTP | code | message | When |
|---|---|---|---|
| 400 | -32700 | Parse error | the body is not valid JSON |
| 400 | -32700 | Parse error: Invalid JSON-RPC message | valid JSON, but not a JSON-RPC message (SDK) |
| 400 | -32600 | Invalid Request: Only one initialization request is allowed | a batch holding initialize plus other messages |
| 400 | -32600 | Invalid Request: Server already initialized | a second initialize on an opt-in session |
| 400 | -32000 | Bad Request: Unsupported protocol version: <v> (supported versions: …) | an unsupported MCP-Protocol-Version header on a request after initialize (not checked on initialize, nor on a tools/list served from cache, 4.3) |
| 401 | -32001 | No valid authentication provided. Use an X-API-Key header or "Authorization: Bearer <api key>". | no key, or the key is wrong, revoked or expired, or a Bearer token that is not ozk_. WWW-Authenticate: Bearer when you sent no credentials, Bearer error="invalid_token" when you did. |
| 401 | -32001 | API key not permitted from <ip> | the key is fenced to certain IP addresses and this is not one |
| 403 | -32000 | Invalid Host header: <host> | the request reached the server under a hostname it does not serve |
| 404 | -32001 | Session not found | an Mcp-Session-Id that is unknown, expired, belongs to another key, or is over 128 characters (4.5) |
| 405 | -32000 | Method not allowed. | GET or DELETE without a session. Header Allow: POST, OPTIONS. |
| 413 | -32600 | Request body too large (1 MiB max) | over 1,048,576 bytes (13.8) |
| 415 | -32000 | Unsupported Media Type: Content-Type must be application/json | a complete Accept header with a non-JSON Content-Type |
| 429 | -32000 | Rate limit exceeded: Rate limit: <n> requests/minute (or /day) | tier limit reached (13.7). Retry-After header plus error.data.retryAfter (seconds). |
| 429 | -32000 | Too many failed authentication attempts | too many failed logins from your IP. Retry-After header. |
| 200 | -32602 | Tool not found: <name> | the tool name does not exist (single-message tools/call) |
| 200 | -32602 | Tool not permitted for this key: <name> | the tool exists, but your key is fenced to other tools |
| 200 | -32602 | Invalid params: <fields> | malformed initialize params |
| 200 | -32602 | MCP error -32602: Prompt not found: <name> | prompts/get with an unknown name |
| 200 | -32002 | MCP error -32002: Resource not found: <uri> (also Workflow not found / Execution not found) | resources/read (12.11) |
| 200 | -32601 | Method not found | a method the server does not implement (4.4) |
| 200 | -32603 | Registry error <status>, or the SDK's validation text | a registry failure behind a resource, or request params that fail the SDK schema |
| 500 | -32603 | Internal error | an unhandled server error |
| 404 | — | {"error":"Not found. MCP endpoint: POST /v1 (Streamable HTTP)."} | any path other than /v1, /v1/, /mcp and the public routes (not JSON-RPC) |
Unauthenticated requests, live-verified 2026-09-24:
curl -s -i -X POST https://mcp.plungeai.com/v1 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'curl -s -i -X POST https://mcp.plungeai.com/v1 -H 'Authorization: Bearer not-a-key' \
-H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'An unknown tool is a protocol error, so it cannot be mistaken for "the tool ran and failed":
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_nope","arguments":{}}}'On https://api.plungeai.com/v1/mcp, the router answers its own edge errors in the One API
envelope: 401 unauthorized (Bearer ozk_ key required / Invalid or revoked API key),
429 rate_limited, 413 payload_too_large, each with X-Error-Code
(One API errors (opens in a new tab)). Everything the MCP
server answers passes through unchanged.
13.7 Rate limits
Your key's tier sets two fixed windows, per minute and per day (rate-limiter.ts):
| Tier | Per minute | Per day |
|---|---|---|
free | 30 | 1,000 |
pro | 100 | 10,000 |
enterprise | 300 | 100,000 |
- Only tool calls count.
initialize,tools/list,resources/*,prompts/*andpingare free onhttps://mcp.plungeai.com/v1and onhttps://api.plungeai.com/v1/mcp, which forwards your key to the same gateway (4.11). - The counter is per account (your user id), not per key. Two keys on one account share it.
- Windows are fixed, not calendar-aligned. A window starts at the first counted call after the previous one ended and lasts 60 seconds (or 24 hours).
- A key with a missing tier is treated as
free. An operator can give a key its own per-minute limit, which replaces the tier's per-minute number (the daily limit stays). Self-service keys use the tier limits. - Failed logins are counted per client IP: 20 per minute and 500 per day. After that the server
answers
429 Too many failed authentication attemptsuntil the window resets.
On a limit hit you get 429 with Retry-After (seconds until the window resets, at least 1) and
error.data.retryAfter, and the call does not run. Ask "how much of my rate limit have I used?":
plungeai_whoami shows minute_used and day_used for the current windows. Its own call counts,
so the number includes it.
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":3,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"how much of my rate limit have I used?"}}}' \
| sed -n 's/^data: //p'These limits bound request rates. Spending (model tokens, paid APIs) is metered separately on your account, and money-moving actions stop at the trust fence (Tools).
13.8 Body caps and timeouts
| Limit | Value | What happens |
|---|---|---|
| Request body | 1 MiB (1,048,576 bytes) | 413 -32600. Checked against Content-Length before auth, then enforced on the stream itself (chunked bodies included). |
| Argument sizes | 13.3 | needs_input, nothing runs |
| Keepalive | a progress line every 15 s while a tool runs | keeps the SSE stream from going idle (4.6) |
| Registry-backed reads | 10 s | Registry error / Registry unavailable |
| Background runs | legs of 10 min; re-entered after 12 min; failed after 3 kills in a row without a checkpoint; Interrupted (run did not complete) after 16 min with no heartbeat | 4.7 |
| Opt-in sessions | 10 min idle | 404 Session not found; re-initialize |
The server itself sets no request timeout on a tools/call. Your client may: Claude Desktop cuts a
single tool call at about 4 minutes (server.ts). Claude Code lets you set a per-server timeout
(milliseconds) in .mcp.json, or MCP_TOOL_TIMEOUT
(Claude Code docs (opens in a new tab), read 2026-09-24). For anything long,
use mode: "async" (4.7).
13.9 Retry guidance
| You got | Do |
|---|---|
429 rate limited | Wait Retry-After seconds, then retry. Don't hammer: every retry inside the window is refused again. |
401 | Don't retry. Fix or replace the key (Authentication). From a new location, check whether the key is IP-fenced. |
-32602 Tool not found | Refresh the tool list: reconnect your client, or call tools/list again. |
-32602 Tool not permitted for this key | Don't retry. Use a key that allows the tool. |
404 Session not found | Drop the Mcp-Session-Id and re-initialize. |
413 | Shrink the request. Move long text into a saved workflow, or split the work. |
outcome needs_input, needs_connection, needs_api_key | Follow the remediation, then retry. Never retry the identical call blind. |
outcome needs_approval with an approve_via action (an agent staged the action and paused) | Ask the user. Only after an explicit yes, call plungeai_continue with approve: true. |
outcome needs_approval with no actions (trust-fence refusal: send / pay / buy / transfer / withdraw …) | Nothing to continue: relay it to the user. The operation runs only from Ocean Studio, where a person approves it (6.6). plungeai_continue answers No conversation session…. |
outcome unavailable | Use an alternative from the answer. If the agent gave a temporary reason, retry later. |
outcome error "transient platform failure" | Retry once after a few seconds. If it repeats, report the request id. |
outcome error "The run timed out" | From plungeai_execute_workflow, plungeai_execute_tool or plungeai_run_mission: retry with mode: "async" and poll plungeai_get_workflow_status. plungeai_execute_agent has no mode and drops it, so the same retry runs synchronously again: run the agent as a one-task workflow through plungeai_execute_workflow with mode: "async" instead. |
outcome error "Execution failed: …" | Read the reason and fix the input. Don't keep polling a failed run. |
-32603 / 500 | Retry with backoff. If it persists, report it with the request id from your activity log. |
13.10 The activity log
The server keeps a readable trail of everything your account did over MCP: one Markdown entry per
request, grouped per account per UTC day (activity-log.ts, log-view.ts). Use it to see exactly
what your client sent, which is often not what you typed, and what came back.
Read it with your own key. It is assembled live on every request, newest first:
curl -s "https://mcp.plungeai.com/log?date=$(date -u +%F)" \
-H "Authorization: Bearer $PLUNGE_API_KEY"| Query | Meaning |
|---|---|
date=YYYY-MM-DD | the UTC day. Defaults to today. Anything else gets 400 {"error":"date must be YYYY-MM-DD"}. |
format=md / format=html | force Markdown or HTML. Otherwise a browser (Accept: text/html) gets a rendered page, and curl gets text/markdown. |
refresh=1 | also rewrite the stored per-day document now (normally a cron does it every 15 minutes) |
key=<your key> | for opening the log in a browser, where you cannot set a header. The key then sits in the URL and your browser history, so prefer the header. |
The same handler also answers at /admin/log. Responses are Cache-Control: no-store. A key
reads only its own account's trail. A missing or wrong key gets
401 {"error":"Provide your API key: Authorization: Bearer <key>, X-API-Key, or ?key="}.
The log is served on https://mcp.plungeai.com only.
What an entry looks like (the shape activity-log.ts renders):
### 14:03:12 · `plungeai_whoami` · ✅ · 212ms
- **User:** you@example.com · <your user id> · <client IP> · `<request id>`
- **Key:** <key label>
> **Asked:** how much of my rate limit have I used?
**Q:**
- **format:** markdown
- **A:** <the answer, whole if 300 characters or fewer, else "<n> chars · <first 240 characters>…">| Part | What it holds |
|---|---|
| heading | UTC time, the event (a tool name, or initialize, tools/list, resources/read, …, or rate_limited, tool_denied), ✅/❌ for tool calls, duration |
| User / Key | the account, email, client IP and request id, and which key was used (its label). One account can hold several keys. Calls through https://api.plungeai.com/v1/mcp show your key label like any other call. |
| Asked | your user_request, in your own words, when the client sent one. This is why the server asks clients to always pass it. |
| Q | the tool arguments as fields, never truncated, including defaults the server filled in (such as format: markdown). YAML and multi-line text stay fenced. For protocol traffic, the params (e.g. the plungeai:// URI read). |
| Client context | anything else your client put in params._meta besides the progress token |
| A | the answer as a receipt: whole when 300 characters or fewer, otherwise its length plus the first 240 characters. The full output stays in your execution history (plungeai_get_result). |
| Error | the full error text, when there was one. For invalid arguments: invalid arguments: limit Too big: …. |
What is recorded. Every authenticated request: tool calls, argument rejections, rate-limit hits,
fenced-tool denials and protocol methods. Failed logins are recorded without an account, so they do
not appear in your log. Secrets are redacted before anything is written: the value of any
argument whose name looks like a secret (api_key, api_secret, token, secret, password,
passwd, credential, authorization, cookie) is stored as •••redacted•••, including
everything nested under it (core/core-base/content-guard.ts). A secret pasted into free text,
such as a prompt, is not detected. Don't paste keys into prompts.