MCP
Protocol & transport
Source: orchestration/mcp-gateway/index.ts, server.ts, session-do.ts, shared.ts,
tool-outcome.ts, execution.ts, execution-do.ts, leg-state.ts, conversation.ts, chat.ts,
tools.ts, tool-exec.ts, extras.ts, orchestration/api-gateway/routes/mcp.ts,
orchestration/api-gateway/lib/auth.ts, and the MCP TypeScript SDK 1.29.0
(node_modules/@modelcontextprotocol/sdk: server/index.js, server/mcp.js,
server/webStandardStreamableHttp.js, shared/protocol.js, types.js).
The PlungeAI MCP server speaks JSON-RPC 2.0 over Streamable HTTP at one endpoint:
POST https://mcp.plungeai.com/v1https://api.plungeai.com/v1/mcp is the same server, reached through the One API router. The
differences at the edge are listed in 4.11. This
chapter is for people writing their own client or debugging one. If you use Claude Code, Cursor or
another MCP client, the client does all of this for you (Connecting clients).
4.1 How a request flows
The big picture:
you type a sentence
└─ your client reads it and decides which tool to call
└─ POST https://mcp.plungeai.com/v1 (one JSON-RPC message)
└─ the platform: registry agents, the CNL engine, your saved workflowsEvery POST passes the same gates, in this order (index.ts). The first gate that fails answers,
and nothing after it runs:
1. HTTP method GET / DELETE without Mcp-Session-Id → 405 -32000 "Method not allowed."
2. Host not mcp.plungeai.com → 403 -32000 "Invalid Host header: …"
3. Size Content-Length over 1 MiB → 413 -32600
4. Auth X-API-Key, or Authorization: Bearer ozk_… → 401 -32001 (failed auth is metered per IP)
5. Body read with a hard 1 MiB cap → 413 -32600
Rate limit only when the body carries a tools/call → 429 -32000 + Retry-After
6. Parse body is not JSON → 400 -32700 "Parse error"
7. Pre-screen single tools/call: unknown tool / fenced tool → 200 -32602
single tools/call: arguments fail the schema → 200 result, outcome needs_input
single initialize: bad params → 200 -32602 "Invalid params: …"
8. Session Mcp-Session-Id present, or a sessions key initializing → session Durable Object (4.5)
9. SDK a fresh MCP server instance answers the message; tools/call streams as SSEWhat happens after gate 9 depends on the tool. A tool that runs something (a workflow, an agent, a
mission) writes a row to your execution history with source: mcp and broadcasts it to your other
apps. That is the same account and the same history Studio shows. Every authenticated request,
including protocol traffic such as initialize and resources/read, also lands in your activity log
(13.10).
4.2 Endpoint, paths, headers and methods
| Item | Value |
|---|---|
| Endpoint | POST https://mcp.plungeai.com/v1. /v1/ and the pre-2026-09-20 path /mcp answer identically. |
| Other hosts | Any other Host gets 403. |
OPTIONS | CORS preflight, answered for any origin: 204 with Access-Control-Allow-Origin: *, allowed methods GET, POST, DELETE, OPTIONS, allowed headers Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, X-API-Key, exposed header Mcp-Session-Id, max-age 86400 (live-verified 2026-09-24). |
GET, DELETE | 405 with Allow: POST, OPTIONS. They are only accepted with an Mcp-Session-Id (opt-in sessions, 4.5). |
| Unknown path | 404 {"error":"Not found. MCP endpoint: POST /v1 (Streamable HTTP)."} (not JSON-RPC). |
| No-auth info routes | GET /v1/info (server name, version, protocol version, capabilities), GET /health (dependency probes; 503 when one fails). |
Request headers:
| Header | Required | What the server does with it |
|---|---|---|
Authorization: Bearer $PLUNGE_API_KEY or X-API-Key: $PLUNGE_API_KEY | yes | Your ozk_ key (Authentication). Bearer tokens that do not start with ozk_ are not accepted. |
Content-Type: application/json | yes, unless Accept is incomplete | When Accept lacks one of the two types (below), the server rewrites both headers for you. With a complete Accept and another Content-Type, the SDK answers 415 -32000 "Unsupported Media Type: Content-Type must be application/json". |
Accept: application/json, text/event-stream | recommended | The spec requires both types. A request without them is not rejected: the server fills them in, so a bare curl works. |
MCP-Protocol-Version | optional | On requests after initialize, an unsupported value gets 400 (4.3). It is not checked on initialize itself, and a plain tools/list answered from the server's cache gets 200 whatever the value. Send a supported version, or leave the header out. |
Mcp-Session-Id | only for opt-in sessions | Sending one routes the request to the session host. An unknown id gets 404 (4.5). |
Response types:
| Request | Response |
|---|---|
tools/call (single, or any batch containing one) | 200, Content-Type: text/event-stream (4.6) |
| Every other request | 200, Content-Type: application/json on the default stateless path. On an opt-in session (4.5) every request, initialize included, is answered as SSE. |
Only notifications (no id), e.g. notifications/initialized | 202 Accepted, empty body |
| An edge gate failed (401, 403, 405, 413, 429, 400) | that status, a JSON-RPC error body (13.6) |
Server info without a key:
curl -s https://mcp.plungeai.com/v1/infoA GET on the endpoint is refused because the server offers no standalone stream:
curl -s -i https://mcp.plungeai.com/v14.3 Protocol version and initialize
The server implements MCP revision 2025-11-25 (PROTOCOL_VERSION in server.ts). It
negotiates exactly as the SDK does (_oninitialize): if the protocolVersion your client sends is
one of 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07, the server answers
with that version. Anything else gets 2025-11-25, and your client decides whether it can speak
it. /health describes this as "negotiates down to 2024-11-05".
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":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'Ask for an older revision and you get it back:
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":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'The initialize result:
| Field | Value |
|---|---|
protocolVersion | the negotiated version (above) |
serverInfo | { "name": "plungeai.com", "version": "2.7.0" } (SERVER_NAME, SERVER_VERSION; the version moves with deploys) |
capabilities | logging: {}, resources: {}, prompts: {}, tools: { listChanged: true }. The listChanged flag comes from the SDK; the stateless server never actually sends a list_changed notification (4.5). |
instructions | the server-level usage guide, injected into your client's context |
The instructions text is the one place that teaches every client how to use the platform. In
short, it tells the model to:
- relay every successful tool output verbatim and in full. The only parts meant for the model are the guidance tails: execution-id footers, poll hints and ⏸ blocks;
- treat "my agents" as the user's saved workflows (
plungeai_list_workflows).plungeai_list_agentsis the registry of building blocks; - discover first: search the registry in natural language, fetch a full card before first use, and take agent ids only from live results (the catalog is active-only);
- run structured agents with the triad
plungeai_list_agents→plungeai_get_tool_contract→plungeai_execute_tool, follow the outcome's remediation, never retry blind, and never approve a gated action on its own; - use
mode:"async"only for long runs and pollplungeai_get_workflow_status; - stop at every ⏸ AWAITING USER / AWAITING USER APPROVAL block, relay it, then call
plungeai_continue; - pass
user_request, the user's own words, on every tool call. Every tool accepts it, and the platform uses it for context, routing and your activity log (13.10).
initialize params are checked before the SDK sees them. A malformed request (for example a
numeric protocolVersion and no clientInfo) gets -32602 naming the bad fields:
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params: protocolVersion, clientInfo"},"id":1}
(HTTP 200). This check covers single-message requests only.
After initialize, a client sends notifications/initialized. It has no id, so the server
answers 202 with no body:
curl -s -o /dev/null -w '%{http_code}\n' 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","method":"notifications/initialized"}'The MCP-Protocol-Version header. You may send it on requests after initialize. The SDK
accepts any of the five supported versions. An unsupported value gets
400 -32000 "Bad Request: Unsupported protocol version: <v> (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)".
Two exceptions: initialize itself is never checked, and a single uncursored tools/list that a
warm server instance answers from its cache (index.ts) returns 200 before the check runs, so
the same bad header can get 200 or 400 depending on cache state. Leaving it out is fine.
4.4 JSON-RPC methods
Every method the server answers for a normal key (server.ts, SDK Protocol and McpServer):
| Method | Supported | Returns |
|---|---|---|
initialize | supported | 4.3 |
notifications/initialized | supported | 202, no body |
ping | supported | {} |
tools/list | supported | the 22 plungeai_* tools in catalog order, or only the tools your key is fenced to. No pagination: there is no nextCursor, and a cursor param is ignored (full list again). Details in 4.10 and the Tool reference. |
tools/call | supported | streamed as SSE (4.6) |
resources/list | supported | 5 resources (Prompts & resources) |
resources/templates/list | supported | 3 URI templates |
resources/read | supported | the resource's text, or -32002 for an unknown URI |
prompts/list | supported | 1 prompt, plungeai |
prompts/get | supported | the prompt's message, or -32602 for an unknown name |
logging/setLevel | partial | returns {}, but the stateless server keeps no state between requests, so the level is not applied to later calls. Progress fallback lines are always sent at level info (4.6). |
notifications/cancelled | ignored | 202. The notification arrives in a new request with its own server instance, which cannot reach the call in flight. To stop a run, use plungeai_executions {"action":"cancel"} (Runs, results & follow-ups). |
resources/subscribe, resources/unsubscribe, completion/complete | rejected | -32601 "Method not found". They exist only on the conformance-test keys. |
| any other method | rejected | -32601 "Method not found" |
The server never sends requests to your client. Sampling, elicitation and roots are not used by any production tool.
A quick liveness check:
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":7,"method":"ping"}'Batches. A JSON array of messages is accepted and answered as an array. The SDK answers each message on its own:
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":"ping"},{"jsonrpc":"2.0","id":2,"method":"prompts/list"}]'Batch caveats (index.ts):
- The pre-screen in gate 7 runs for single messages only. In a batch, an unknown or fenced tool
comes back as the SDK's own
isErrortool result (MCP error -32602: Tool <name> not found), and bad arguments come back as the SDK'sInput validation error: …text. You get no-32602envelope and noneeds_inputoutcome. Fenced tools are still never run: they are not registered on your server instance. - A batch that contains
initializetogether with other messages gets400 -32600 "Invalid Request: Only one initialization request is allowed". - A batch containing a
tools/callis answered as SSE.
4.5 Stateless by default, opt-in sessions
Stateless is the default. Each POST builds a fresh server instance, answers it, and forgets
it (server.ts: "Built fresh per request"). No Mcp-Session-Id is issued, and you do not need
one. Each request is self-contained: tools/list or tools/call work without an earlier
initialize in the same connection. Clients still send initialize once to learn the version,
capabilities and instructions.
What statelessness means in practice:
| Feature | On the stateless path |
|---|---|
Server-initiated stream (GET) | not offered: 405 |
notifications/tools/list_changed | never sent. If a tool seems missing after a platform update, reconnect your client so it re-reads tools/list. |
logging/setLevel | accepted, not persisted |
notifications/cancelled | cannot reach an in-flight call. Use plungeai_executions cancel. |
Resumable SSE (Last-Event-ID) | not available. A dropped tools/call stream is not replayed. The run itself keeps going and can be read with plungeai_get_result. |
| Sampling, elicitation | not available (and no production tool uses them) |
Opt-in sessions (session-do.ts). A key minted with sessions: true gets a stateful session.
This is an operator setting: self-service keys from Dashboard → One API → Keys do not carry it.
initializereturns anMcp-Session-Idresponse header. Send it on every later request.- On a session, every
POSTthat carries a request is answered astext/event-stream,initializeandtools/listincluded. When the request speaks2025-11-25(theinitializeparam, or theMCP-Protocol-Versionheader after it), the stream opens with a priming event (id:,retry: 1000, an emptydata:). Read theMcp-Session-Idresponse header, then parse thedata:lines as in 4.6. GETwith the header opens the standalone SSE stream. It is resumable withLast-Event-ID; the server sends a 1000 ms retry hint and stores up to 1,000 replay events per session.DELETEwith the header ends the session.- Sessions expire after 10 minutes without a request. The window slides with each request.
- A session is bound to the key that created it. The same id presented by another key, an expired
id, an id that never existed, or one longer than 128 characters all get
404 -32001 "Session not found". Per the spec, re-initialize to get a new one. - A second
initializeon a live session gets400 -32600 "Invalid Request: Server already initialized". - Sessions are available on
https://mcp.plungeai.com/v1only (4.11).
If your client sends an Mcp-Session-Id it picked up elsewhere, the request goes to the session
host and gets 404 Session not found. Drop the header.
4.6 tools/call over SSE: progress and keepalive
A tools/call response is a Server-Sent Events stream. Each frame is event: message, then one
data: line holding a JSON-RPC message, then a blank line. Progress notifications (if any) come
first. The last frame is the JSON-RPC response, and then the stream closes. With curl, keep the
data: lines:
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":"who am I"}}}' \
| sed -n 's/^data: //p'Some tools/call answers are plain JSON instead of SSE: the gate 7 pre-screen answers (unknown
tool, fenced tool, arguments that fail the schema) and every edge error (401, 413, 429). A robust
client checks Content-Type before parsing.
Live status: progress notifications
While a tool runs, the server streams what it is doing. There is one channel per call, never both
(server.ts wrap):
Your tools/call | Channel | Params |
|---|---|---|
carries params._meta.progressToken (Claude Code and Claude Desktop send one) | notifications/progress | progressToken, progress (strictly increasing), total (only when a raw done/total pair passes through unchanged), message (the human-readable line) |
| no progress token | notifications/message (logging) | level: "info", logger: "<tool name>", data: "<the same line>" |
progress is a sequence, not a percentage. The server keeps it strictly increasing as the spec
requires, and the readable "5/18" lives in message. Notifications stop the moment the call
settles.
A call that asks for progress (runs a model, costs credits, so it is not auto-verified):
curl -sN 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":4,"method":"tools/call","params":{"_meta":{"progressToken":"p1"},"name":"plungeai_execute_agent","arguments":{"user_request":"one-sentence answer","agent":"one-agent","prompt":"In one sentence: what is a Durable Object?"}}}'Frame shape while it runs (the shape server.ts builds; the numbers and text vary):
event: message
data: {"method":"notifications/progress","params":{"progressToken":"p1","progress":3,"message":"⚙️ Agents working — 0/1 agents complete · 1 active (one-agent)"},"jsonrpc":"2.0"}The lines you will see, by tool (execution.ts, conversation.ts, chat.ts):
| Tool | Status lines |
|---|---|
| workflow, agent, tool and mission runs | ⚙️ Agents working — 5/18 agents complete · ✓ brave-agent · 13 active (tavily-agent, exa-agent…) · ✍️ Generating answer · ✍️ Writing (3,516 chars) — …<the last ~72 characters of the answer> · ⚙️ one-agent still working — 45s elapsed… · ✅ Complete — 18 agents in 24.6s |
plungeai_followup | 🔎 Looking up the run and checking for an open session… · 💬 <agent> working — continuing the conversation… or 📚 No open session — loading the run's stored result as context… · ⏳ Collecting the answer… · 💾 Saving this turn to the conversation thread… · ✅ Complete — continued the existing session / ✅ Complete |
plungeai_continue | 🔎 Loading the paused conversation… · 🔓 Approval received · 💾 Saving this turn… · ✅ Complete |
plungeai_build_workflow | 📖 Loading the existing workflow… · 🧠 Generating CNL YAML — the builder picks real agents, validates and self-repairs (30-60s)… · 🔍 Validating the generated workflow (<n> chars)… · 💾 Saving "<name>" and syncing to all apps… · ✅ Complete |
plungeai_chat | Thinking… · Streaming… <n> chars |
| any tool, every 15 s | ⏳ Working — <n>s elapsed |
The 15-second keepalive runs for every tool call. It keeps the SSE stream from going idle long
enough for an intermediary to cut it. The ✍️ Writing lines carry only the tail of the answer as it
is produced. The full answer arrives in the final response.
Whether you see these lines depends on your client. Some render them under the running tool,
some ignore both channels. The work and the result are identical either way. mode:"async" runs
return at once, so they have no live feed (4.7).
4.7 Async mode
Four tools can run in the background. The call returns an execution id at once instead of waiting for the result:
| Tool | mode | Default |
|---|---|---|
plungeai_execute_workflow | sync | async | sync |
plungeai_execute_tool | sync | async | sync |
plungeai_run_mission | sync | async | async (anything but "sync") |
plungeai_learn (action: learn) | sync | async | async (anything but "sync") |
plungeai_execute_agent has no mode.
The immediate answer is an ok outcome. Its structuredContent has status: "ok",
execution_id, and a summary that says the run started in the background and tells the client
to poll plungeai_get_workflow_status. The wording differs per tool: "Started execution …"
(plungeai_execute_workflow), "Started <name> …" (plungeai_execute_tool), "Started mission …"
(plungeai_run_mission), "Started distilling …" (plungeai_learn). An ok outcome with
execution_id and no result means an async start, or a sync run whose output was not yet
available ("… completed (execution: …) but results not yet available"). Either way, poll
plungeai_get_workflow_status, then call plungeai_get_result. Don't branch on the summary text.
plungeai_execute_tool also sets agent_id and operation. The Markdown text ends with a footer
the client acts on:
---
execution: `<execution id>` — use this id for plungeai_get_result / plungeai_followup on this run.Then:
- Poll
plungeai_get_workflow_statuswith the id. ItsstructuredContent.statusreports the run, andcontinuationis non-null when the run paused for your approval or an answer. - When it reads
completed(orincomplete), fetch the output withplungeai_get_result(Runs, results & follow-ups).
A background run (runs a workflow, costs credits, not auto-verified):
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":5,"method":"tools/call","params":{"name":"plungeai_execute_workflow","arguments":{"user_request":"research in the background","mode":"async","format":"json","workflow_yaml":"name: bg-demo\ntasks:\n - id: answer\n agent: one-agent\n prompt: |\n In two sentences: what is edge computing?\n"}}}' \
| sed -n 's/^data: //p'How background runs are hosted (execution-do.ts, leg-state.ts, index.ts):
- Each run is held by its own Durable Object in legs of up to 10 minutes (the Ten-Minute Relay). A long run hands over at a turn boundary and resumes in the next leg from stored task results and checkpoints.
- A leg that is killed (deploy, host restart) is re-entered by a watchdog 12 minutes after it started. If it is killed 3 times in a row without writing a checkpoint, the run fails with "Run was killed 3 times in a row without yielding a checkpoint…".
- A
runningrow with no heartbeat for 16 minutes (960 s) is marked failed withInterrupted (run did not complete). A cron does this every 15 minutes, and a status poll does it at once. Its cost is left pending, notunpriced: the cost worker then prices whatever the run's agents recorded before it stopped.
When to use it: the tool descriptions say async is for runs over about 3 minutes. Most workflows
finish in seconds in sync. Claude Desktop cuts a single tool call at about 4 minutes, so use
async there for anything long.
4.8 Output formats: Markdown by default, JSON on request
Results come back as Markdown, the format an AI reads best: tables, headings, links, step indexes, conversation threads. The server's instructions tell clients to show it verbatim.
If your client needs machine-parseable output, add "format": "json" to a result-bearing tool.
The text becomes a JSON document, and the same object is attached as structuredContent
(tool-outcome.ts renderAs). Without format (or with "markdown"), nothing changes.
isError is the same in both forms.
| Tool | format:"json" returns |
|---|---|
plungeai_whoami | { user, auth_type, tier, key_label, key_id, rate_window: { minute_used, day_used } | null, server } |
plungeai_list_workflows | { workflows: [ { id, name, description, kind, folder } ] }. Without filters this is the recent list; the folder summary line is Markdown-only. |
plungeai_executions | list → { executions: [ { id, input, workflow_name, status, start_time, duration, source } ] }; conversation → { execution_id, messages }; get → the status object (4.9); output, cancel, delete → { content: "<markdown>" } |
plungeai_list_agents | { content: "<registry markdown>" } (typed rows are not available yet) |
plungeai_get_result | { content: "<markdown>" }, or the outcome object when the answer is guidance (e.g. an unknown id) |
plungeai_execute_workflow, plungeai_execute_agent, plungeai_execute_tool, plungeai_get_tool_contract | the outcome object (4.9) |
Tools without a format parameter ignore it. The server drops unknown arguments before the
tool runs.
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":6,"method":"tools/call","params":{"name":"plungeai_whoami","arguments":{"user_request":"who am I, as JSON","format":"json"}}}' \
| sed -n 's/^data: //p'Content annotations. Successful Markdown results built as plain text carry
annotations: { "audience": ["user","assistant"], "priority": 0.9 } (shared.ts), a hint that the
text is for the user to read as-is. Outcome-envelope results and error text carry no annotations,
and neither does format:"json" output.
4.9 outputSchema and structuredContent
Seven tools declare an outputSchema in tools/list. For these, structuredContent is always
present on a non-error result, and it matches the schema:
| Tools | Schema |
|---|---|
plungeai_execute_workflow, plungeai_execute_agent, plungeai_get_tool_contract, plungeai_execute_tool, plungeai_run_mission, plungeai_learn | the outcome envelope (below) |
plungeai_get_workflow_status | the status object (below) |
The outcome envelope (ToolOutcomeSchema, deliberately loose so it can grow fields):
| Field | Type | Notes |
|---|---|---|
status | ok | needs_input | needs_connection | needs_api_key | needs_approval | unavailable | error | the outcome (Errors, feedback & limits) |
summary | string | always present |
agent_id, operation | string | when a specific agent answered |
result | string | the run's output (on ok, or the partial output next to needs_approval) |
execution_id | string | the run's id, for plungeai_get_result / plungeai_followup |
missing | string[] | absent required fields (needs_input) |
warnings | string[] | visible notes, e.g. "prose mapped: …" |
remediation | { message, actions[] } | what to do next (13.2) |
contract | object | the tool contract (from plungeai_get_tool_contract, and appended to needs_input) |
The status object (plungeai_get_workflow_status):
| Field | Type |
|---|---|
execution_id | string |
status | string (running, completed, incomplete, failed, cancelled, …) |
error_message | string | null (set when the run failed) |
workflow_name, started_at (ISO), final_task_id | string | null |
duration_ms | number | null |
continuation | null, or { status, agent, question: string | null, pending_action: { summary, price? } | null } when paused |
isError follows the spec: it is true only for a terminal error outcome and for plain tool
failures. The guidance outcomes (needs_*, unavailable) are answers your client acts on, so they
carry isError: false. The SDK skips outputSchema validation on isError results, which is why
an error answer from these tools may carry plain text only.
Tools without an outputSchema still attach structuredContent when you ask for format:"json"
(4.8).
4.10 Tool annotations, read-only hints and metadata
Six tools carry annotations: { "readOnlyHint": true }. They only read. The hint is for your
client, for example to run them without a confirmation prompt:
plungeai_get_tool_contract · plungeai_list_agents · plungeai_list_workflows ·
plungeai_get_result · plungeai_get_workflow_status · plungeai_whoami
The other 14 tools carry no annotations: no destructiveHint, idempotentHint or
openWorldHint, so your client applies its own defaults. Some of them change state or spend:
the execute and mission tools run work, plungeai_executions can delete or cancel, and
plungeai_schedule, plungeai_workflow, plungeai_memory remember and plungeai_templates use write.
Other tool metadata in tools/list:
- No
titleis set. Clients display thename. - The SDK marks every tool
execution: { taskSupport: "forbidden" }: MCP task-augmented calls are not supported. Use async mode for long work. - Every tool's
inputSchemahas an optionaluser_requeststring (up to 4,000 characters) for the user's own words.
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/list"}'4.11 The two endpoints at the transport level
https://api.plungeai.com/v1/mcp forwards your JSON-RPC body (plus Accept, Content-Type and
Mcp-Session-Id) to this server with your own ozk_ key, which this server validates again
(orchestration/api-gateway/routes/mcp.ts). The tools and their answers are identical. The edge
is not:
https://mcp.plungeai.com/v1 | https://api.plungeai.com/v1/mcp | |
|---|---|---|
| Methods | POST, OPTIONS (+ GET/DELETE with a session) | POST only |
| Auth failure | 401 JSON-RPC -32001 + WWW-Authenticate | 401 One API envelope {"error":{"code":"unauthorized",…}} + X-Error-Code: unauthorized |
| Rate limit counts | only requests carrying a tools/call | only requests carrying a tools/call, counted by this server in the same per-account counter |
| Per-key restrictions: tool fence, per-key rate override, sessions | applied | applied. Your key is forwarded and this server validates it again. Sessions need GET/DELETE, which this route does not offer: open sessions on mcp.plungeai.com. |
plungeai_whoami shows | api-key, your tier and key label | the same |
| Other routes | /log, /v1/info, /health, /install, /docs | GET /v1/mcp/tools (a JSON-RPC tools/list without writing JSON-RPC, One API guide (opens in a new tab)) |
Use https://mcp.plungeai.com/v1 when you need sessions. The
unauthenticated difference, live-verified 2026-09-24:
curl -s -X POST https://mcp.plungeai.com/v1 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
echo
curl -s -i -X POST https://api.plungeai.com/v1/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'