MCP
Connect Claude, Cursor, and Codex to Cavuno via the hosted Model Context Protocol server at mcp.cavuno.com.
A
JThe Cavuno MCP server is a hosted Cloudflare Worker at mcp.cavuno.com. It lets AI agents like Claude, Cursor, and Codex drive the Cavuno API without any per-endpoint MCP tool registration — there is no npm package to install, just an HTTPS URL.
The server exposes exactly two tools, regardless of how many REST endpoints exist:
search— runs JavaScript against the Cavuno OpenAPI spec for endpoint and schema discovery.execute— runs JavaScript that calls the Cavuno API via a sandboxed loopback binding. Multiple calls can be chained in one execution; only the final return value flows back to the model.
This is the Cloudflare "code mode" pattern: LLMs are demonstrably better at writing code than at composing opaque tool calls, and code mode keeps the MCP context window flat (~1 K tokens) regardless of API surface size.
Connect a client
Add Cavuno to your MCP-aware editor. On first connect the client walks the OAuth 2.1 flow — you'll see a browser prompt to sign into Cavuno and approve the requested scopes. After that the client caches the token until it expires.
Claude Desktop
Edit claude_desktop_config.json:
1234567{"mcpServers": {"cavuno": {"url": "https://mcp.cavuno.com/mcp"}}}
Cursor
Edit ~/.cursor/mcp.json:
1234567{"mcpServers": {"cavuno": {"url": "https://mcp.cavuno.com/mcp"}}}
Codex
Append to your Codex YAML config:
12[mcp_servers.cavuno]url = "https://mcp.cavuno.com/mcp"
Claude Code
1claude mcp add cavuno --url https://mcp.cavuno.com/mcp
Authentication
Two paths are supported. OAuth is the right choice for interactive use; an API key is the right choice for CI / automation that doesn't have a browser.
OAuth 2.1 (default)
The MCP spec runs the standard OAuth authorization-code flow with PKCE. On first connect, the client:
- Fetches
https://mcp.cavuno.com/.well-known/oauth-protected-resourceand discovers the authorization server (api.cavuno.com/v1/oauth). - Dynamically registers itself at
/v1/oauth/register. - Opens a browser to
/v1/oauth/authorizefor user consent. The minted JWT is bound to the Cavuno MCP resource (aud=https://mcp.cavuno.com). - Sends every subsequent request with
Authorization: Bearer <jwt>.
The worker verifies the JWT locally (issuer, audience, signature via JWKS) before invoking any tool — there's no extra round-trip on every call.
API key (CI / scripts)
Mint a key in the Cavuno dashboard at Settings → API keys. Pass it as the bearer token — MCP clients that support custom request headers can attach it directly:
12345678910{"mcpServers": {"cavuno": {"url": "https://mcp.cavuno.com/mcp","headers": {"Authorization": "Bearer cavuno_live_xxxxxxxxxxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"}}}}
API keys (cavuno_live_* / cavuno_test_*) skip the OAuth flow entirely — the worker passes them through unchanged.
The two tools
Both tools take a single argument: code, the source of an async () => … arrow function. The function runs in a fresh Cloudflare Dynamic Worker isolate per call — there is no shared state across invocations.
search
123456789// Tool: search// Args: { code: string } // an async arrow function as a string//// Globals available inside the sandbox:// spec — the full Cavuno OpenAPI document with $refs pre-resolved//// Network access: blocked. Use `spec` for everything.async () => Object.keys(spec.paths).slice(0, 10)
execute
123456789101112131415// Tool: execute// Args: { code: string } // an async arrow function as a string//// Globals available inside the sandbox:// cavuno.request({ method, path, body?, query?, headers? })// -> { status: number, ok: boolean, data: unknown }//// Network access: only the loopback binding above. The agent's bearer// token is held by the parent worker and injected on every call —// never include an Authorization header in the code you write.async () => {const r = await cavuno.request({ method: 'GET', path: '/usage' });return r.data;}
The spec object that search sees is the same OpenAPI document published at api.cavuno.com/v1/openapi.json — also visible in the interactive API reference. If you can find an endpoint in Scalar, you can call it from execute.
Sandbox & limits
- Outbound network from inside the sandbox is blocked (
globalOutbound: null). The only way out iscavuno.request(…); callingfetch(…)throws. - No access to environment variables, file system, or persistent storage. Each invocation starts in a fresh isolate.
console.log/warn/erroris captured and returned alongside the function's return value, so multi-step scripts can print intermediate state for debugging.- CPU and wall-clock budgets are bounded by the underlying Cloudflare Workers limits — keep individual
executecalls under a few seconds. - File uploads (
/v1/companies/{id}/logo,/v1/media) are not exposed via MCP today — they need a filesystem the worker can't reach. Use thecavunoCLI for those.
Errors
When user code throws (or returns a rejected promise), the response is a structured error envelope rather than a tool failure. The captured console output is included so the agent can self-diagnose without re-running:
12345678{"ok": false,"error": "TypeError: Cannot read properties of null (reading 'id')","stack": "TypeError: Cannot read properties of null...","logs": [{ "level": "log", "args": ["company:", null] }]}
API-level errors (4xx / 5xx) come back from cavuno.request as { status, ok: false, data: { error: { code, message, requestId, details? } } } — the agent can branch on data.error.code rather than parsing strings. See the error-handling recipe below.
Cookbook
Worked examples — real arrow functions you can paste into the MCP Inspector or your agent client to feel out how search and execute compose.
Discover the job-related endpoints
When an agent needs to figure out what /jobs operations exist, it filters the OpenAPI spec inside search rather than asking another tool. No network — just JS over the resolved spec.
Tool: search
1async () => Object.keys(spec.paths).filter((p) => p.startsWith('/jobs'))
Inspect the body schema for POST /jobs
Agents use search to learn the required fields before calling execute. The OpenAPI document has all $refs pre-resolved so a single navigation reaches the leaf schema.
Tool: search
123456789async () => {const op = spec.paths['/jobs'].post;const schema = op.requestBody.content['application/json'].schema;return {required: schema.required,employmentType: schema.properties.employmentType.enum,remoteOption: schema.properties.remoteOption.enum,};}
Read the account's active-job quota
A trivial single-call read. The response goes directly back to the agent as the return value of the arrow function.
Tool: execute
1234async () => {const r = await cavuno.request({ method: 'GET', path: '/usage' });return r.data;}
List the published jobs and project a few fields
Project before returning so only what the agent needs flows back into the model context. Avoids stuffing the whole job objects into tokens when only titles are needed.
Tool: execute
12345678async () => {const r = await cavuno.request({method: 'GET',path: '/jobs',query: { status: 'published', limit: 25 },});return r.data.data.map((j) => ({ id: j.id, title: j.title, slug: j.slug }));}
Find-or-create a company, create a job, and publish it
Three calls chained inside a single execute. Only the final summary returns to the LLM — the intermediate company + job objects never enter the model context.
Tool: execute
12345678910111213141516171819202122232425async () => {const company = await cavuno.request({method: 'POST',path: '/companies/find-or-create',body: { name: 'Acme', website: 'https://acme.com' },});if (!company.ok) return { step: 'company', error: company.data };const job = await cavuno.request({method: 'POST',path: '/jobs',body: {companyId: company.data.id,title: 'Senior Platform Engineer',description: 'Build and run our deployment platform.',applicationUrl: 'https://acme.com/jobs/senior-platform-engineer',employmentType: 'full_time',remoteOption: 'remote',remotePermits: [{ type: 'worldwide', value: 'worldwide' }],remoteTimezones: [{ type: 'all', value: 'all' }],status: 'published',},});return { ok: job.ok, status: job.status, links: job.data.links };}
Iterate every published job for a company
Cursor-based pagination is a single loop inside execute. The agent only sees the aggregated result — each page is consumed without re-prompting the model.
Tool: execute
123456789101112131415async () => {const out = [];let cursor = null;do {const r = await cavuno.request({method: 'GET',path: '/companies/p17ek8gas8t94v9fvxabq6cbbd863qfa/jobs',query: { status: 'published', limit: 50, cursor },});if (!r.ok) throw new Error('list failed: ' + r.status);for (const job of r.data.data) out.push({ id: job.id, title: job.title });cursor = r.data.hasMore ? r.data.nextCursor : null;} while (cursor);return { count: out.length, jobs: out };}
Branch on the structured error envelope
Every Cavuno API error returns { error: { code, message, requestId, details? } }. Agents can switch on code to decide whether to retry, prompt the user, or escalate.
Tool: execute
1234567891011121314async () => {const r = await cavuno.request({method: 'POST',path: '/jobs',body: { /* intentionally invalid */ title: 'x' },});if (r.ok) return { ok: true };return {ok: false,code: r.data?.error?.code,message: r.data?.error?.message,requestId: r.data?.error?.requestId,};}
Use console.log while writing a script
Both search and execute capture console.log/warn/error and return the captured lines alongside the result. Useful for debugging multi-step flows without rerunning.
Tool: execute
12345async () => {const r = await cavuno.request({ method: 'GET', path: '/jobs', query: { limit: 1 } });console.log('first job:', r.data.data[0]?.title);return r.data.data.length;}