Cover Art Integration Guide
This guide is for partners and third-party sites that want to call ONCE’s generate_cover_art MCP tool to create or refine album artwork on behalf of their users. It covers authentication, request shapes, the iterative-editing flow, displaying results, billing, and error handling.
If you’re configuring an end-user MCP client (Cursor, Claude, ChatGPT), see Agent Setup instead.
What You Get
A single JSON-RPC tool that:
- Generates a 1024×1024 album cover from a text prompt using Google Gemini Nano Banana 2 (non-patron tier, available to every authenticated ONCE user).
- Optionally edits a previous generation when you pass back the prior
fileUrl— same iterative flow as the in-app generator. - Auto-crops to square and resizes to 3000×3000 PNG.
- Persists the image into the user’s private
cover-artSupabase bucket. - Returns a
fileUrlthat drops directly intorelease.cover_art_file_urlforsubmit_release.
POST https://beta.once.app/api/mcp
Authorization: Bearer <ONCE access token>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "generate_cover_art",
"arguments": {
"prompt": "Lo-fi vinyl in a sunset cafe, warm pastels, 70s poster art"
}
}
}Prerequisites
- ONCE account. Each request must be authorized as a real ONCE user — generated images are stored in that user’s bucket, billed against their rate limit, and attributed to them in the admin dashboard.
- Bearer access token. Either:
- Native MCP OAuth (recommended for direct MCP clients) — the user signs in via
https://beta.once.app/oauth/authorize. See API Reference → Authentication. - Programmatic Supabase session (recommended for server-to-server integrations on behalf of users you already authenticated) — exchange your user’s email/password for a Supabase access token. See Authentication Patterns below.
- Native MCP OAuth (recommended for direct MCP clients) — the user signs in via
- Endpoint. All MCP traffic goes to a single URL:
https://beta.once.app/api/mcp(POST, JSON-RPC 2.0).
Authentication Patterns
Pattern A — Native MCP OAuth (interactive)
Best for browser-based agents and IDE plugins. Your client triggers a 401 from /api/mcp, walks the user through /oauth/authorize + /oauth/token, and stores the resulting bearer token. Most MCP clients automate this for you.
Pattern B — Server-to-server with Supabase password grant
If you’re an integration partner with your own user account system that mirrors ONCE accounts, you can sign in once and reuse the access token until it expires (typically 1 hour).
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.ONCE_SUPABASE_URL!, // Same NEXT_PUBLIC_SUPABASE_URL ONCE uses
process.env.ONCE_SUPABASE_ANON_KEY!, // Same NEXT_PUBLIC_SUPABASE_ANON_KEY
{ auth: { persistSession: false, autoRefreshToken: false } },
);
const { data, error } = await supabase.auth.signInWithPassword({
email: userEmail,
password: userPassword,
});
if (error) throw error;
const accessToken = data.session!.access_token;Talk to the ONCE team if you need partner-specific Supabase credentials. Never embed these in a browser bundle — Pattern B is server-side only.
Provenance header (optional)
Server-to-server calls can self-identify their surface by adding provenance to the JSON-RPC arguments or using a recognized Origin header. ONCE auto-tags requests from app.aimusicdistributor.com as AIMD; everything else is MCP. See Provenance & Partner Billing.
Tool Reference: generate_cover_art
| Argument | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | 1–1500 character description of the desired artwork. When editing, describe what should change. |
baseFileUrl | string | No | Prior cover-art fileUrl to edit. Must belong to the calling user and live in the cover-art bucket. |
baseImageBase64 | string | No | Alternative to baseFileUrl. Raw base64 (with or without data: prefix). |
baseImageMimeType | string | No | MIME override for baseImageBase64 (e.g. image/png). |
releaseId | string | No | Attribute usage to a specific release (must be owned by the user). |
conversationId | string | No | Attribute usage to a chat/conversation for analytics. |
returnBase64 | boolean | No | Also return raw image base64 in the response. |
fileName | string | No | Filename hint (defaults to mcp-cover-<timestamp>.png). |
Response Shape
{
"fileUrl": "/api/files/cover-art/USER_ID/<uuid>.png",
"bucket": "cover-art",
"key": "USER_ID/<uuid>.png",
"fileName": "<uuid>.png",
"prompt": "Lo-fi vinyl in a sunset cafe, warm pastels, 70s poster art",
"model": "gemini-3.1-flash-image-preview",
"modelVariant": "nano_banana_2",
"imageSize": "1K",
"aspectRatio": "1:1",
"edited": false,
"rateLimit": { "remaining": 4 }
}edited is true whenever a base image was provided. The fileUrl is a relative path — prefix it with https://beta.once.app to fetch it.
Flows
1. Generate from scratch
async function generateCoverArt(accessToken: string, prompt: string) {
const res = await fetch('https://beta.once.app/api/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
jsonrpc: '2.0',
id: crypto.randomUUID(),
method: 'tools/call',
params: {
name: 'generate_cover_art',
arguments: { prompt },
},
}),
});
const json = await res.json();
if (json.error) throw Object.assign(new Error(json.error.message), { code: json.error.code, data: json.error.data });
// Tools return their result as a JSON string in `content[0].text`.
return JSON.parse(json.result.content[0].text);
}2. Edit a previous generation (iterative refinement)
Pass the prior fileUrl as baseFileUrl and tell Gemini what to change. The same flow as the in-app cover art generator.
const first = await generateCoverArt(token,
'Minimal flat vector vinyl record on a peach background',
);
const refined = await callMcp(token, 'generate_cover_art', {
prompt: 'Replace the peach background with deep navy and add subtle starlight specks',
baseFileUrl: first.fileUrl,
});
console.log(refined.edited); // true
console.log(refined.fileUrl); // new image URLYou can chain edits indefinitely — each call returns a fresh fileUrl that you can feed back in.
3. Use the result on a release
The returned fileUrl is already what submit_release expects:
await callMcp(token, 'submit_release', {
release: {
title: 'My Release',
primary_artist_name: 'Artist Name',
genre: 'Pop',
release_date: '2026-02-01',
cover_art_file_url: refined.fileUrl,
},
tracks: [
{
title: 'My Release',
audio_file_url: '/api/files/audio/USER_ID/track.wav',
explicit_flag: false,
writers: [{ name: 'First Last' }],
},
],
});Displaying the Image
fileUrl points to /api/files/cover-art/USER_ID/<uuid>.png, which is a private, authenticated URL backed by Supabase signed URLs. There are two recommended ways to surface it:
Option A — Authenticated proxy (recommended)
In your backend, fetch the image with the user’s bearer token and stream it through your own endpoint:
const upstream = await fetch(`https://beta.once.app${fileUrl}`, {
headers: { Authorization: `Bearer ${accessToken}` },
redirect: 'follow',
});
const buffer = Buffer.from(await upstream.arrayBuffer());
res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'image/png');
res.send(buffer);The endpoint returns a 302 redirect to a short-lived signed Supabase URL (TTL ~1 hour), which the redirect: 'follow' option resolves automatically.
Option B — returnBase64: true
For one-shot previews where you don’t want a second round trip, pass returnBase64: true and render the result as a data URL. Skip this for big galleries — it triples response size.
const result = await callMcp(token, 'generate_cover_art', {
prompt,
returnBase64: true,
});
const dataUrl = `data:image/png;base64,${result.imageBase64}`;Rate Limits
| Action | Default budget |
|---|---|
cover_art_generate | 5 requests / 2 minutes per user (configurable server-side) |
When the limit is hit you get a JSON-RPC error:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 429,
"message": "Too many cover art requests. Please wait before trying again.",
"data": null
}
}The HTTP response also returns 200 (because JSON-RPC errors live inside the body), so check error.code rather than res.status. Back off using a fixed 60–120 second delay or your own exponential backoff.
Billing
generate_cover_art itself does not charge ONCE credits — credits are debited only at submit_release time. Image generation cost is recorded in model_usage_events for partner-level reporting, attributed to:
user_id— requiredrelease_id— when you passreleaseIdconversation_id— when you passconversationId
Pass these IDs whenever you have them so revenue and cost roll up correctly in your partner dashboard.
Error Handling
error.code | Meaning | Action |
|---|---|---|
-32602 | Invalid JSON-RPC params | Check request shape |
400 | prompt missing/too long, base image not an image, etc. | Surface message to caller |
401 | Bearer token missing or invalid | Re-auth (OAuth or refresh Supabase session) |
403 | baseFileUrl belongs to a different user or wrong bucket | Use a fileUrl from this user’s prior generation |
429 | Rate limit exceeded | Back off and retry |
502 | Upstream image API failed | Retry with the same prompt |
503 | Image generation not configured on target environment | Contact ONCE — GOOGLE_API_KEY is missing |
All errors come back as JSON-RPC errors:
{ "jsonrpc": "2.0", "id": 1, "error": { "code": 400, "message": "prompt is required" } }Limitations
- Non-patron tier only. The MCP exposes Nano Banana 2 (1K). Patron-only Nano Banana Pro (4K) is not available through MCP — patrons should use the in-app generator if they need higher fidelity.
- One image per call. The tool always returns a single result. To produce variants, call it multiple times in parallel (within the rate-limit budget).
- Square only.
aspectRatiois fixed at1:1to match release cover art requirements. - English prompts work best. Prompts are forwarded verbatim to Gemini.
Quick curl Test
curl -sS -X POST https://beta.once.app/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ONCE_TOKEN" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "generate_cover_art",
"arguments": { "prompt": "neon-soaked cyberpunk cassette tape, square album cover" }
}
}' | jq -r '.result.content[0].text' | jqWhere Things Live
- MCP endpoint —
POST https://beta.once.app/api/mcp - Server card —
GET https://beta.once.app/.well-known/mcp/server-card.json - Public docs — API Reference
- Source resource (read via MCP) —
mcp://docs/agent-guide
If you need partner credentials, branded provenance, or higher rate limits for your integration, reach out to the ONCE team.