MCP Integration

How to talk to the Spriteoven MCP server, what the 14 tools do, and the operational guarantees (auth, rate limits, RLS, billing).

This document covers two audiences: integrators wiring an IDE (Claude Code, Cursor, Cline, Zed, Continue) to Spriteoven, and provider maintainers extending the server's tool surface. The split is signposted inline.

Wave 5b shipped the MCP server live; MCP-F1 and MCP-F2 grew it to 14 tools over a single Streamable HTTP endpoint, Bearer auth via Supabase JWT. The /mcp page on spriteoven.com auto-prefills the install one-liner once you're signed in — that's the canonical entry point. This doc is the textual back-up + reference for operators.

🔑 The 14 are not a list somebody typed here. They are three blocks that server/index.js composes in the augment of mcpHandler, and each block owns its own names:

block source of truth count
historical (Wave 5b) listMcpToolNames()server/lib/mcp-server.js 3
read-only (MCP-F1) READ_TOOL_NAMESserver/mcp/index.js 5
write (MCP-F2) WRITE_TOOL_NAMESserver/mcp/writeRegister.js 6

tests/053-deudafina.mjs §E1 imports those three and fails if this document's count or any tool name drifts from them. Add a tool and forget this page, and the suite says so — which is the only reason the number above can be trusted.


TL;DR — install (integrator)

Sign in to Spriteoven, then visit /mcp. Click Copy next to the claude mcp add ... block. Paste in your terminal. Restart Claude Code.

# example (the real Bearer is auto-prefilled by /mcp)
claude mcp add spriteoven https://spriteoven.com/mcp \
  --transport http \
  -H 'Authorization: Bearer <YOUR_SUPABASE_JWT>'

For Cursor / Cline / Zed / Continue, click ▾ Other clients on /mcp and copy the JSON config block — same Bearer header, same URL, just the IDE-native config shape.

The Bearer is your Supabase session JWT. It expires per Supabase TTL (default 1 hour). Re-load /mcp after re-login to get a fresh one-liner.


The tool surface — 14 tools in three blocks

Block 1 — the historical 3 (Wave 5b)

Tool Purpose Inputs Output
generate_sprite Generate a pixel-art sprite from a prompt. Saves to your library + returns the PNG inline. prompt (str, required); provider (gpt-image-2 | grok | nb2, default gpt-image-2); size (1536x512 | 512x512 | 1024x1024, default 1024x1024); quality (medium | high, default medium) image content block (PNG base64) + text (asset summary) + structuredContent ({asset_id, library_url, png_mime, dimensions, cost_usd, tags})
get_library List your library assets. RLS-scoped — you only see your own rows. filter? (free-text on name+tags); tag? (single tag overlap); limit? (1-100, default 20); offset? (default 0) text (one-line per asset) + structuredContent ({count, limit, offset, assets[{asset_id, slug, name, tags, created_at, thumbnail_url, library_url}]})
save_to_library Append tags to an existing asset. Set-union, idempotent. asset_id (UUID from generate_sprite or get_library); tags? (string[]) text (status) + structuredContent ({asset_id, tags, added, unchanged, library_url})

Block 2 — read-only (MCP-F1)

Five tools that never spend a credit. READ_TOOL_NAMES, in the fixed order tools/list returns them (the spec asks for determinism so clients can cache and the model's prompt-cache does not churn):

Tool Purpose
list_folders The caller's folder tree.
list_assets The caller's assets, RLS-scoped, filterable.
get_asset One asset's card, including the formats its family can export to.
list_styles The style ladder, with the reason a family has none.
get_credits Balance + WOW gens remaining, before promising the user anything.

Block 3 — write (MCP-F2)

Six tools, of which 4 generate_* — one per family that publishes a declarative price function. WRITE_TOOL_NAMES:

Tool Scope Notes
generate_fx generate 🔴 spends credits
generate_deco generate 🔴 spends credits
generate_uikit generate 🔴 spends credits
generate_item_set generate 🔴 spends credits
get_job read generation is asynchronous — the four above return a job_id, and this resolves it
export_asset read signed URL to a .zip, never bytes; costs nothing

🔑 The consent pattern: a generate_* called without confirm_token spends nothing — it returns the exact price, the balance before and after, and a token signed over the parameters. The agent is meant to show that price to its user and only then call again with the same parameters plus the token. The quote lives 300 s.

personajes and mapas have no generate_*, and the reason is measured: their endpoints bill per generation inside the worker and publish no declarative price function, so no exact quote can be given before enqueuing — and this surface's whole consent pattern rests on the price being exact. list_generators carries that reason where the agent can read it.

The remaining tools from the spike are deferred to Ciclo 8 backlog (create_version, bulk_generate, batch_status, export_unity, export_aseprite, palette_swap, tilemap_export, animate_sprite). Promotion gate: ≥30 unique MCP installs in 4 weeks post-launch (per spike §6.3). See EVIDENCE/cycle7/wave5/track-r/SPIKE-MCP-SERVER.md.


Auth model


Rate limits

The MCP endpoint has its own bucket (makeMcpRateLimiter in server/lib/mcp-server.js):


Billing

generate_sprite runs through the same generation path as the REST /api/sprite-lab/<provider> endpoints. Credits and the WOW 8-gen lifecycle apply identically — the MCP surface does not bypass billing. Specifically:

get_library and save_to_library are read/write only (no AI gen) and are not billed beyond the per-request rate-limit budget.


Endpoint reference

GET /api/mcp/info (auth required)

Returns the live install payload the /mcp UI prefills. Echoes back the caller's Bearer in the rendered install_command so the user can copy-paste a single line.

{
  "status": "live",
  "wave": "5b",
  "server_url": "https://spriteoven.com/mcp",
  "transport": "http",
  "bearer_token": "<echoed Supabase JWT>",
  "install_command": "claude mcp add spriteoven https://spriteoven.com/mcp --transport http -H 'Authorization: Bearer <JWT>'",
  "clients_supported": ["Claude Code", "Cursor", "Continue", "Cline", "Zed"],
  "tools": ["generate_sprite", "get_library", "save_to_library",
            "list_folders", "list_assets", "get_asset", "list_styles", "get_credits",
            "generate_fx", "generate_deco", "generate_uikit", "generate_item_set",
            "get_job", "export_asset"],
  "rate_limit": { "max": 60, "window_ms": 60000, "scope": "per-user" },
  "notes": "Bearer token = your Supabase session JWT. Re-login to refresh."
}

server_url is computed from SOV_PUBLIC_URL env (preferred) → request origin (x-forwarded-proto + x-forwarded-host honored) → http://localhost:PORT fallback.

POST /mcp (auth required, MCP Streamable HTTP)

Stateless transport (sessionIdGenerator: undefined) per MCP spec 2025-03-26. Each request:

  1. requireAuth → vetted Supabase JWT → req.user.
  2. mcpRateLimiter → 60/min/user bucket.
  3. mcpHandler → builds per-request McpServer + StreamableHTTPServerTransport, wires the historical 3 with a deps bag (sb = RLS-scoped Supabase client; storage = same; generate = provider dispatch; uploadPng = spriteoven-assets bucket put; recordEvent = recordSingleEvent to mcp_tool_invoked / mcp_tool_failed), then its augment hook adds the 5 read-only (registerReadOnlyTools) and the 6 write (registerWriteTools) — in that order, deliberately, so an already-installed client never sees tools/list reshuffled.
  4. Transport calls handleRequest(req, res, req.body) — Express's express.json() parsed the body upstream.

GET/DELETE/PATCH on /mcp are not wired. The HTML wrapper at GET /mcp is registered separately near the static-page block.


Provider-side notes (extending the surface)


Telemetry events (Track O /api/events table)

Event Source Properties
mcp_doc_viewed /mcp page first paint wave, page, state ('pending' | 'live'), referrer, ts_client
mcp_install_clicked /mcp Copy button click surface ('cli' | 'json'), client, clients_supported
mcp_tool_invoked server-side, every successful tool/call surface:'mcp', tool, plus tool-specific (provider, size, count, added, ...)
mcp_tool_failed server-side, every isError tool/call surface:'mcp', tool, code (e.g. EMPTY_PROMPT, ASSET_NOT_FOUND, GENERATION_FAILED)

Aggregate via getAdminMetrics (/api/admin/metrics) per Track O O.4 spec. Promotion gate (≥30 installs / 4 weeks) reads mcp_install_clicked distinct user count.


Tests

Suite What it covers Run
test-mcp-tools.js Pure tool functions with FakeSb (29 tests) node test-mcp-tools.js
test-mcp-server.js Real MCP SDK Client → handler roundtrip via in-process Express + FakeSb (13 tests) node test-mcp-server.js
test-mcp-info.js Server spawn + auth gating + page rendering (9 tests) node test-mcp-info.js
scripts/cycle7/smoke-wave5-mcp-real.mjs Real Spriteoven + real Claude Code SDK + manually-pasted JWT SOV_TEST_JWT=<jwt> node scripts/cycle7/smoke-wave5-mcp-real.mjs
scripts/cycle7/smoke-wave5-mcp-auto.mjs Mints a throwaway Supabase user via service_role + runs the full wire end-to-end (5 tests) node scripts/cycle7/smoke-wave5-mcp-auto.mjs

The base npm test (33 tests) is unchanged — MCP suites are dedicated runners following the scripts/cycle7/test-wave5-*.mjs convention.


Operational notes — prompt caching OpenAI

Spriteoven chain-edit reusa la misma reference image entre llamadas. OpenAI aplica image-input caching automático (75% discount $8 → $2 /M tokens) cuando los bytes de la reference son idénticos. Para hit cache hit consistente:

Text-input caching NO aplica a Spriteoven porque los prompts (~150-200 tokens) están por debajo del threshold mínimo OpenAI (1024 tokens). NO recomendable inflar prompts solo para hit threshold (ROI < 1% costo total).

Referencia técnica: EVIDENCE/cycle7/research/gpt-image-2-pricing-deep-dive.md (commit d04e42c).