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.jscomposes in theaugmentofmcpHandler, and each block owns its own names:
block source of truth count historical (Wave 5b) listMcpToolNames()—server/lib/mcp-server.js3 read-only (MCP-F1) READ_TOOL_NAMES—server/mcp/index.js5 write (MCP-F2) WRITE_TOOL_NAMES—server/mcp/writeRegister.js6
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.
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.
| 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}) |
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. |
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.
requireAuth (server/lib/authMiddleware.js)
via supabase.auth.getUser(jwt)./mcp re-renders with the fresh install one-liner.get_library cannot return another user's assets and save_to_library
on a foreign asset surfaces as ASSET_NOT_FOUND (RLS hides the row).The MCP endpoint has its own bucket (makeMcpRateLimiter in
server/lib/mcp-server.js):
req.user.id, fallback IP).{"jsonrpc":"2.0","error":{"code":-32004,"message":"rate_limited","data":{"bucket":"mcp","limit":60,"window_ms":60000}},"id":null}.RATE_LIMIT_DISABLED=1 (same convention as
server/lib/hardening.js).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:
req.user.id, regardless of
whether the call came from the web UI or MCP.generate_sprite returns PAYMENT_REQUIRED (mapped to MCP isError)
when the caller has 0 credits AND 0 wow remaining.FREE_WOW_GEN_CAP = 8) is per-user, not per-surface.get_library and save_to_library are read/write only (no AI gen) and
are not billed beyond the per-request rate-limit budget.
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:
requireAuth → vetted Supabase JWT → req.user.mcpRateLimiter → 60/min/user bucket.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.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.
server/lib/mcp-tools.js as pure DI
functions (no SDK coupling). The MCP wrapping happens in
server/lib/mcp-server.js. Adding a tool is: write the pure function,
register it in buildMcpServer, add the test in test-mcp-tools.js +
test-mcp-server.js.content[] array.
Use image blocks for binary outputs (base64), text for human-friendly
summaries, structuredContent for machine-readable returns. Errors
surface as { isError: true, content: [{type:"text", text:"[CODE] ..."}] }
so the calling agent gets a usable message instead of a 500.assets table requires project_id. For MCP-generated sprites
we lazy-create a per-user MCP Sprites project (idempotent;
ensureMcpProject in mcp-tools.js). Don't insert assets without a
project — RLS will reject them.spriteoven-assets follow the path pattern
{user_id}/{project_id}/{asset_id}/v1.png — that's the only shape
the bucket policies allow (see docs/SUPABASE-SETUP.md). Failures
are best-effort: the asset row still inserts, with storage_path:null./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.
| 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.
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).