In your tool
plungeai-in-v0
Connect v0 (Vercel) to PlungeAI (Ocean Studio) over MCP — the + menu → MCPs form, why the generated Next.js app must call PlungeAI through a server Route Handler (v0 apps can't call MCP directly), and a plungeai_whoami + plungeai_list_agents verify. Use when the user is in v0 and says connect / install / set up PlungeAI, mentions the MCPs panel, or a generated v0 app 401s against PlungeAI. For another builder use its plungeai-in-<tool> skill; for an unlisted but MCP-capable client use plungeai-mcp-setup's generic client config.
Download zip (opens in a new tab) · View raw SKILL.md (opens in a new tab)
v0 is a hosted builder: no local config file and no JSON entry anywhere — the MCP form is the only surface, and the key lives only in that entry.
Prerequisites
- An
ozk_key: Dashboard → One API → Keys (https://dashboard.plungeai.com (opens in a new tab)) — self-service, shown once, copy it now.
Connect (once)
Open the + menu in the prompt form → MCPs → add your own server (the project settings page offers the same via Add MCP):
| Field | Value |
|---|---|
| Name | PlungeAI |
| URL | https://mcp.plungeai.com/v1 |
| Authentication | Bearer Token → paste the ozk_ key itself (v0 adds the Bearer prefix); or Custom Headers → Authorization = Bearer ozk_YOUR_KEY. If one form 401s, try the other. (No Auth and OAuth don't apply — PlungeAI OAuth hasn't shipped.) |
Then pick the server's permission mode: Disabled, Ask for
Approval (Manual), or Always Run (Auto). Manual is the sane default;
money-verb PlungeAI operations still pause with a structured
needs_approval outcome even under Auto — relay it, then
plungeai_continue.
The generated app cannot call MCP
v0's generated code cannot use the MCP tools directly — pair the MCP with Environment Variables. So the split is: the builder chat operates PlungeAI over MCP; the generated Next.js app calls the One API server-side only, e.g. a Route Handler:
// app/api/plungeai/route.ts
export async function POST(req: Request) {
const { input, agentId } = await req.json()
const r = await fetch(
`https://api.plungeai.com/v1/agents/${agentId}/execute`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PLUNGEAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input, sync: true }),
},
)
return Response.json(await r.json())
}Key in a Vercel env var (PLUNGEAI_API_KEY, every environment that needs
it), never in client components, never prefixed NEXT_PUBLIC_.
What to tell v0 about the One API (2.3.0)
When the app needs more than one agent call, paste this block into the prompt
that asks v0 for its Route Handlers. Every route stays server-side and sets
Authorization: Bearer ${process.env.PLUNGEAI_API_KEY} itself.
The minimal saved-workflow body (workflow: / tasks: YAML) is in plungeai-api-setup.
PlungeAI One API 2.3.0. Base URL https://api.plungeai.com. Spec: GET /v1/openapi.json (no auth) - never invent a route.
AUTH: Authorization: Bearer ozk_YOUR_KEY on every call; key in an env var, server side only. When a connector attaches auth, NEVER add your own Authorization header.
ME: GET /v1/whoami -> {user_id, via, tier, key_label}. GET /v1/key -> {data:{label, limit, usage, rate_limit}}.
FIND (never from memory): GET /v1/discovery/search?q=<capability>&kind=agents -> {cards} (without kind, skill cards come first). POST /v1/discovery/resolve {task}. GET /v1/agents. GET /v1/tools. GET /v1/tools/{id} = the contract.
DOOR 1, one agent or tool: POST /v1/agents/{id}/execute {prompt|input, sync?, model?} -> 200 {content, workflow_id, task_id, request_id}.
POST /v1/tools/{id}/execute {operation, params} -> {ok, content, outcome}; 422 invalid_params echoes the contract.
DOOR 2, saved workflow or bot: POST /v1/workflows/{id}/execute {input?, inputs?} -> {success, workflow_id, final_task_id, request_id, content}, synchronous.
POST /v1/workflows/{id}/execute-stream = SSE. Inline YAML: POST /v1/workflows/execute. No content? GET /v1/workflows/results/{workflowId}/{taskId} (taskId = final_task_id).
DOOR 3, Agent Call: POST /v1/execute, the body is the program: {agent:"<saved id>[@n]", input} or {workflow:{...}} or {tasks:[...]}. dry_run:true plans without running;
on_behalf_of:"<customer id>" uses that customer's connections.
ASYNC: POST /v1/agents/{id}/execute {sync:false} -> 202 {workflow_id, task_id, request_id}; redeem GET /v1/agents/results/{workflowId}/{taskId} -> {content}; 404 not_ready = poll, 2s+.
EXECUTIONS: the run id is the body's workflow_id (also the x-execution-id header, and your x-trace-id UUID if you sent one; replaying one is 409 duplicate_execution_id).
GET /v1/executions (source, status, workflow_id). GET /v1/executions/{id}: running | completed | incomplete | failed | cancelled | needs_input | needs_approval.
GET /v1/executions/{id}/output -> {content, steps}. GET /v1/executions/{id}/files. GET /v1/executions/{id}/conversation. DELETE /v1/executions/{id}. GET /v1/executions/{id}/stream = SSE.
POST /v1/executions/{id}/continue {answer | approve, allow_scope: once|run|always}. POST /v1/executions/{id}/followup {text}. POST /v1/executions/{id}/cancel.
POST /v1/executions/{id}/steer {text} queues a message for a live run (202), else 409 execution_finished.
WORKFLOWS: GET|POST /v1/workflows. GET|PATCH|DELETE /v1/workflows/{id}. GET|POST /v1/workflows/{id}/versions. POST /v1/workflows/{id}/versions/{versionId}/restore.
Save needs {name, kind, yaml}; kind = workflow|agent|bot|campaign. GET /v1/workflows lists every kind unless ?kind=bot (or campaign). 422 invalid_workflow lists errors, writes nothing.
SCHEDULES: POST /v1/schedules {name, schedule, workflow_id, timezone?, parameters?}; schedule = a cron, or "@once" with run_at (ISO-8601 WITH a UTC offset, in the future);
400 run_at_required | run_at_past. GET /v1/schedules. GET|PATCH|DELETE /v1/schedules/{id}. GET /v1/schedules/{id}/runs (execution_id per run).
POST /v1/schedules/{id}/run. POST /v1/schedules/{id}/pause. POST /v1/schedules/{id}/resume. POST /v1/schedules/{id}/wake {note?, in?} fires it once, now or later.
TRIGGERS (every call names schedule_id; a secret is shown once): GET|POST /v1/triggers/webhook. DELETE /v1/triggers/webhook/{token}. POST /v1/triggers/webhook/{token}/rotate.
The same four under /v1/triggers/email {allowed_from}. GET|POST /v1/triggers/hooks {url, events}. DELETE /v1/triggers/hooks/{id}.
PERMISSIONS: GET|PUT /v1/workflows/{id}/permissions {classes:{send|write|pay|delete: allow|ask|deny|hand_off}, locks, budget_usd_run, budget_usd_month}; locks need a team admin (403 lock_admin_required).
BOT DATA: GET /v1/workflows/{id}/workspace-files. GET /v1/workflows/{id}/workspace-files/download?path=. GET /v1/workflows/{id}/inbox. GET|POST /v1/workflows/{id}/bindings {channel, sender_id}.
DELETE /v1/workflows/{id}/bindings/{channel}/{sender_id}. GET /v1/channels. POST /v1/channels/pair {channel}.
GET|POST /v1/bot-groups {name, members = 2-6 of your bot ids}. PUT|DELETE /v1/bot-groups/{id}. GET /v1/bot-groups/{id}/thread.
MEMORY: GET /v1/memory/recall?q=. POST /v1/memory/remember {text, scope: memory|user}. POST /v1/memory/forget {id | text}. GET /v1/memory/runs?q= (q required). GET /v1/memory/runs/{id}.
SKILLS: GET /v1/skills. POST /v1/skills/learn {name, body} saves the body you send (it does not distill a URL). DELETE /v1/skills/{id}.
TEMPLATES: GET /v1/templates?kind=workflow|agent|bot. GET /v1/templates/{id}. POST /v1/templates/{id}/use {name?} -> 201 {workflow}.
CONNECTIONS: GET /v1/connections. POST /v1/connections/{provider} {api_key, on_behalf_of?}. DELETE /v1/connections/{provider}. POST /v1/connections/{provider}/link -> {url, expires_at}.
MODELS (OpenAI-shaped, Bearer only): GET /v1/models. POST /v1/chat/completions. POST /v1/embeddings.
TRACES: GET /v1/traces/{id}, id = the run's workflow_id.
LISTS: {workflows|executions|schedules|cards:[...], total, limit, offset, has_more}; POST /v1/schedules -> {schedule}.
ERRORS: {"error":{"code","message"}} plus an X-Error-Code header. 400 validation; 401 unauthorized; 403 refused | agent_not_active; 404 <thing>_not_found (a stranger's id too);
409 approval_required, or not_paused | execution_finished | duplicate_execution_id | name_taken; 422 invalid_params; 429 rate_limited + Retry-After; 502/503 retry once.
424 connection_required | credential_required carries error.connect {provider, method, link}: connect via POST /v1/connections/{provider} or its /link.
FENCES: 403 refused is final - surface it, never retry. 409 approval_required is a human boundary - never approve for the user. tools/{id}/execute: a human approves in Studio, then re-issue.
A paused agent reply: re-send with approved:true plus its action_token (both). A run reading needs_approval: POST /v1/executions/{id}/continue, never the original call.Verify
- Builder chat: "use plungeai_whoami to confirm my identity" → an identity card.
- Generated app (server side):
curl -H "Authorization: Bearer ozk_YOUR_KEY" https://api.plungeai.com/v1/agents→ HTTP 200 JSON. - "use plungeai_list_agents to search 'web search'" → live results from the active agent catalog.
Quirks
- v0 previews run without the project's env vars until linked/deployed — a preview 401 from the app does not mean the integration is wrong; verify with the curl check above, then set the env var on the Vercel project.
- If generation puts the fetch in a
"use client"component, regenerate with: "PlungeAI calls go through a Route Handler; the key stays server-side." - Long CNL workflows: prefer async mode
(
plungeai_execute_workflowwithmode: "async"+plungeai_get_workflow_status) over one long synchronous builder-chat call.
Where next
- Writing the Route Handler / server code that calls
https://api.plungeai.com: plungeai-api-setup. - Operating the
plungeai_*tools in chat: plungeai-mcp-setup. - Picking MCP vs the One API for a given job: choose-your-plungeai-door.
- An unlisted but MCP-capable client, or the shared connect concepts (native remote vs. stdio bridge, key hygiene): plungeai-mcp-setup.
- Full guide: https://mcp.plungeai.com/docs#3-connecting-clients (opens in a new tab)