Conversation
Six endpoints proxying ElevenLabs Music API: - POST /api/music/compose — generate song from prompt or composition plan - POST /api/music/compose/detailed — generate with metadata + timestamps - POST /api/music/stream — streaming audio generation - POST /api/music/plan — create composition plan (free, no credits) - POST /api/music/video-to-music — background music from video files - POST /api/music/stem-separation — separate audio into stems Includes: - lib/elevenlabs/ module (2 HTTP clients, shared schemas, 6 validators, 6 handlers) - 6 MCP tools (compose_music, compose_music_detailed, stream_music, create_composition_plan, video_to_music, separate_stems) - 9 test files, 37 test cases — all passing - ELEVENLABS_BASE_URL constant in lib/const.ts Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughIntroduces ElevenLabs Music API integration with six new Next.js API routes ( Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant NextAPI as Next.js API Route
participant Auth as Auth System
participant Validator as Request Validator
participant ElevenLabs as ElevenLabs API
participant Client2 as Client
Client->>NextAPI: POST /api/music/compose
NextAPI->>Auth: validateAuthContext()
Auth-->>NextAPI: ✓ Account ID or ✗ NextResponse
alt Auth Failed
NextAPI-->>Client2: 401 Error Response
else Auth Success
NextAPI->>Validator: validateComposeBody(payload)
Validator-->>NextAPI: ✓ Validated Body or ✗ NextResponse
alt Validation Failed
NextAPI-->>Client2: 400 Error Response
else Validation Success
NextAPI->>ElevenLabs: callElevenLabsMusic("/v1/music", body, outputFormat)
ElevenLabs-->>NextAPI: Response (binary audio + headers)
NextAPI->>NextAPI: handleUpstreamError(response)
alt Upstream Error
NextAPI-->>Client2: 502/500 Error Response
else Upstream Success
NextAPI->>NextAPI: buildUpstreamResponse(response, defaultContentType)
NextAPI-->>Client2: 200 Response (audio + CORS headers)
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes The PR spans 27 new files with mixed complexity patterns. While many follow consistent architectural templates (validation schemas, handler factories, MCP registrations), the breadth and heterogeneity of change types—spanning API routes, upstream integration, schema definitions, and tool registration—combined with logic-dense handlers for multipart processing ( Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Extract createElevenLabsProxyHandler factory for compose/detailed/stream (3 handlers → 3 config objects, shared auth+validate+proxy+error logic) - Extract handleUpstreamError for shared upstream error handling - Extract buildUpstreamResponse for shared response construction - Extract musicGenerationBaseFields + promptOrPlanRefinements into schemas.ts (compose/detailed/stream validators now extend shared base instead of duplicating) - Use safeParseJson instead of duplicated try/catch JSON parsing - Net result: -55 lines, 0 duplicated handler logic Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (12)
lib/elevenlabs/callElevenLabsMusic.ts (1)
28-35: Same timeout consideration applies here.Like the multipart variant, this fetch has no timeout. Apply
AbortSignal.timeout()for consistency and resilience against upstream latency issues.♻️ Proposed timeout addition
return fetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", "xi-api-key": apiKey, }, body: JSON.stringify(body), + signal: AbortSignal.timeout(110_000), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/elevenlabs/callElevenLabsMusic.ts` around lines 28 - 35, The POST fetch in callElevenLabsMusic.ts lacks a timeout; wrap the request with an AbortSignal by creating a signal (e.g., AbortSignal.timeout(TIMEOUT_MS) or a passed-in timeout value) and pass it as the signal option to fetch in the callElevenLabsMusic function so the request is aborted on exceed; ensure you clear or handle the timeout appropriately (use a constant or reuse the existing timeout value used by other ElevenLabs functions) and propagate/handle the AbortError where needed.app/api/music/plan/route.ts (1)
23-25: Consider addingmaxDurationfor consistency with other music endpoints.Other music routes (compose, stream) set
maxDuration = 120. While plan creation is likely faster, adding the same config maintains consistency and protects against unexpectedly slow upstream responses.♻️ Optional: Add maxDuration
export const dynamic = "force-dynamic"; export const fetchCache = "force-no-store"; export const revalidate = 0; +export const maxDuration = 120;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/music/plan/route.ts` around lines 23 - 25, Add a maxDuration export to match other music endpoints: declare export const maxDuration = 120 alongside the existing exports (dynamic, fetchCache, revalidate) in route.ts so the plan route uses the same maximum upstream response timeout as compose/stream and keeps configs consistent.lib/elevenlabs/validateStreamBody.ts (1)
23-29: Refinement errors may produce confusingmissing_fieldsvalues.When a refinement fails (e.g., "Cannot use both prompt and composition_plan"),
firstError.pathwill be an empty array[], makingmissing_fields: []potentially confusing to API consumers.Consider conditionally including
missing_fieldsonly when the path is non-empty:♻️ Optional: Cleaner error response for refinements
if (!result.success) { const firstError = result.error.issues[0]; + const errorBody: Record<string, unknown> = { status: "error", error: firstError.message }; + if (firstError.path.length > 0) { + errorBody.missing_fields = firstError.path; + } return NextResponse.json( - { status: "error", missing_fields: firstError.path, error: firstError.message }, + errorBody, { status: 400, headers: getCorsHeaders() }, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/elevenlabs/validateStreamBody.ts` around lines 23 - 29, The current error response always includes missing_fields using firstError.path which is an empty array for refinement errors; update the response logic in validateStreamBody (the block handling if (!result.success)) to only include missing_fields when firstError.path && firstError.path.length > 0, otherwise omit missing_fields (or return a different descriptive field such as refinement_error/message) and still return the 400 NextResponse with getCorsHeaders(); locate the error handling that reads result.error.issues[0] and conditionally add the missing_fields property based on the path length.lib/elevenlabs/callElevenLabsMusicMultipart.ts (1)
28-34: Consider adding a timeout for upstream requests.The
fetchcall has no timeout configured. If ElevenLabs becomes slow or unresponsive, this could cause requests to hang indefinitely, potentially exhausting connection pools or hitting the route'smaxDurationlimit without graceful handling.Consider using
AbortSignal.timeout()to enforce a reasonable timeout:♻️ Proposed timeout addition
- return fetch(url.toString(), { + return fetch(url.toString(), { method: "POST", headers: { "xi-api-key": apiKey, }, body: formData, + signal: AbortSignal.timeout(110_000), // slightly under maxDuration });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/elevenlabs/callElevenLabsMusicMultipart.ts` around lines 28 - 34, The fetch to ElevenLabs in callElevenLabsMusicMultipart.ts currently has no timeout; update the call in the function that returns fetch(url.toString(), { ... }) to use an AbortSignal created via AbortSignal.timeout(<reasonable-ms>, /* e.g. 30000 */) and pass that signal in the fetch options (add signal: abortSignal) so upstream requests are bounded; also ensure the surrounding caller (or this function) catches an aborted/timeout error (AbortError) and logs/returns an appropriate error instead of hanging.lib/mcp/tools/music/registerComposeMusicTool.ts (1)
32-38: ResolvedaccountIdis unused (same as compose_detailed).The
accountIdis resolved but not used. If this is intentional for auth-only validation, a clarifying comment would help future maintainers understand the intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerComposeMusicTool.ts` around lines 32 - 38, The resolved accountId returned by resolveAccountId in registerComposeMusicTool is never used; either use it or make the intent explicit — update registerComposeMusicTool to either (a) use the accountId in subsequent logic (e.g., pass it into compose-related calls) or (b) add a clear inline comment above the resolveAccountId call explaining that the resolution is only for authentication/validation side-effects and the id is intentionally unused; keep resolveAccountId, getToolResultError, and compose-related identifiers intact when making this change.lib/mcp/tools/music/registerCreateCompositionPlanTool.ts (1)
48-49: JSON parsing could fail silently.If the upstream returns a non-JSON response despite
upstream.ok, theawait upstream.json()call will throw. While the outercatchhandles this, the error message "Plan creation failed" doesn't indicate a parse failure. Consider adding explicit handling or a more descriptive fallback.💡 More explicit error handling
- const plan = await upstream.json(); - return getToolResultSuccess(plan); + try { + const plan = await upstream.json(); + return getToolResultSuccess(plan); + } catch { + return getToolResultError("Failed to parse composition plan response"); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerCreateCompositionPlanTool.ts` around lines 48 - 49, The call to upstream.json() in registerCreateCompositionPlanTool (where you assign const plan = await upstream.json() and return getToolResultSuccess(plan)) can throw a parse error that is currently masked by a generic catch; update the code to explicitly handle JSON parse failures by wrapping upstream.json() in its own try/catch (or using upstream.text() fallback) and return a failed tool result with a descriptive message (e.g., "Plan creation failed: invalid JSON response" including any parse error.message), while still preserving the existing upstream.ok check and using getToolResultSuccess(plan) only when parsing succeeds.lib/mcp/tools/music/registerComposeDetailedMusicTool.ts (1)
32-38: ResolvedaccountIdis never used.The tool correctly resolves and validates the account ID using
resolveAccountId(), but theaccountIdvariable is never utilized after validation. If account resolution is purely for authentication/authorization, consider adding a comment clarifying this intent. Otherwise, if the ID should be used for logging, rate limiting, or associating the generated song with the account, that logic is missing.💡 If auth-only, add clarifying comment
if (error) return getToolResultError(error); if (!accountId) return getToolResultError("Failed to resolve account ID"); + // accountId validated for auth; not passed to upstream (ElevenLabs uses its own API key auth)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerComposeDetailedMusicTool.ts` around lines 32 - 38, The resolved accountId returned by resolveAccountId(...) is validated but never used; either (A) explicitly document that resolution is only for auth by adding a clarifying comment after the validation (near the resolveAccountId call and the getToolResultError checks) stating accountId is intentionally unused for downstream operations, or (B) actually use accountId where needed—e.g., pass accountId into the downstream song creation / persistence or logging/rate-limiting steps (functions that handle composition creation inside this tool) so the generated song is associated with the user and for audit/rate-limiter hooks; update references around resolveAccountId, accountId, and getToolResultError accordingly.lib/elevenlabs/schemas.ts (1)
20-32: Consider type-safe refinement application.The
promptOrPlanRefinementsarray uses a custom pattern withcheckfunctions andmessagestrings. While this works, consumers must manually iterate and apply these refinements using.refine(). Consider whether a helper function or a more idiomatic ZodsuperRefinepattern might reduce boilerplate at call sites.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/elevenlabs/schemas.ts` around lines 20 - 32, The current promptOrPlanRefinements array requires callers to manually iterate and call .refine(); instead export a small helper that applies these checks in a type-safe way (or attach the logic via Zod's superRefine) so consumers just call applyPromptOrPlanRefinement(schema) or schema.superRefine(...) once; update promptOrPlanRefinements to expose a function named (for example) applyPromptOrPlanRefinement that accepts a Zod object schema, uses the same checks (ensuring the runtime shape { prompt?: string | null; composition_plan?: unknown } is used) and adds the two validation rules via schema.superRefine or schema.refine internally to centralize behavior and remove boilerplate at call sites.lib/mcp/tools/music/registerStreamMusicTool.ts (1)
32-38: ResolvedaccountIdis unused—intentional or oversight?The
accountIdis resolved viaresolveAccountId()but never passed tocallElevenLabsMusic. If ElevenLabs authentication is handled separately viaELEVENLABS_API_KEY, this is fine for access control gating. However, ifaccountIdshould be logged or tracked for billing/audit purposes, it's currently being discarded.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerStreamMusicTool.ts` around lines 32 - 38, resolveAccountId() returns accountId that's never used; either use it for auditing/billing or remove the resolution. Update the code around resolveAccountId, getToolResultError and callElevenLabsMusic: if accountId should be tracked, pass accountId into callElevenLabsMusic (or include it in a log/audit event before calling callElevenLabsMusic) so the resolved ID is not discarded; if accountId is not needed for ElevenLabs calls, remove the resolveAccountId call and related error handling to avoid unnecessary work.lib/mcp/tools/music/registerVideoToMusicTool.ts (1)
54-59: Consider adding a timeout to video fetch operations.Downloading videos without a timeout could cause the handler to hang indefinitely on slow or unresponsive servers. Consider using
AbortControllerwith a reasonable timeout.💡 Suggested pattern
const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout try { const videoResponse = await fetch(url, { signal: controller.signal }); // ... } finally { clearTimeout(timeout); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerVideoToMusicTool.ts` around lines 54 - 59, The video download loop over args.video_urls in registerVideoToMusicTool can hang on slow servers—wrap each fetch in an AbortController with a per-request timeout (e.g., 30s), pass controller.signal to fetch, and clear the timeout in a finally block; on abort or non-ok responses, return a descriptive getToolResultError mentioning the URL and the error/timeout so the handler fails fast and doesn’t hang indefinitely.lib/mcp/tools/music/registerStemSeparationTool.ts (1)
75-78: Success response lacks actionable retrieval info.The success message mentions the ZIP is "available via POST /api/music/stem-separation" but this is the same endpoint that initiates the separation—not a retrieval endpoint. If there's a separate way to retrieve the stems (e.g., via a song ID or download URL), consider including that in the response.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/mcp/tools/music/registerStemSeparationTool.ts` around lines 75 - 78, The success response returned by getToolResultSuccess (in the registerStemSeparationTool handler) is misleading — it points back to the initiating POST endpoint instead of giving a way to retrieve the ZIP; update the returned payload to include actionable retrieval details (e.g., add a download_url or retrieval_id and the correct retrieval endpoint or song_id) alongside stem_variation_id so callers can directly fetch the archive; modify the object passed to getToolResultSuccess (the return in registerStemSeparationTool) to include these new fields and ensure the values are generated/filled by the stem separation flow (e.g., a presigned URL, download token, or song ID).lib/elevenlabs/videoToMusicHandler.ts (1)
21-105: Consider extracting shared logic to reduce function length and improve DRY.This handler is 84 lines, exceeding the 50-line guideline. The text field extraction logic (lines 57-68) is nearly identical to
stemSeparationHandler.ts(lines 43-52). Consider extracting:
- A shared
extractTextFields(formData, excludeKey, booleanFields)utility- Potentially a shared multipart handler factory similar to
createElevenLabsProxyHandlerThis would improve maintainability and reduce duplication across the multipart handlers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/elevenlabs/videoToMusicHandler.ts` around lines 21 - 105, videoToMusicHandler is too long and duplicates text-field parsing from stemSeparationHandler; extract a reusable helper (e.g., extractTextFields(formData: FormData, excludeKey: string, booleanFields: string[])) and use it in videoToMusicHandler before calling validateVideoToMusicBody to populate textFields, handling tags -> string[] and boolean conversion (sign_with_c2pa) inside that helper; optionally factor repeated multipart setup into a createElevenLabsProxyHandler-style factory to build upstreamForm and callElevenLabsMusicMultipart, replacing the manual loop that appends videos/tags/description/sign_with_c2pa so videoToMusicHandler becomes short and delegates parsing to extractTextFields and multipart construction to the factory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/music/compose/route.ts`:
- Around line 14-20: getCorsHeaders() currently does not expose custom response
headers, so browser clients cannot read the song-id or content-disposition set
by buildUpstreamResponse(); update getCorsHeaders() to include
"Access-Control-Expose-Headers" with the values "song-id, content-disposition"
so composeHandler (exported as POST), stemSeparationHandler and any endpoints
that use buildUpstreamResponse() can surface those headers to browser clients.
In `@lib/elevenlabs/buildUpstreamResponse.ts`:
- Line 31: The passthrough response currently hardcodes status 200 in the
Response construction inside buildUpstreamResponse, which hides the real
upstream status; change the Response creation to use upstream.status (i.e.,
replace the literal 200 with upstream.status) so the function returns the
original upstream status code (and continue to pass the constructed headers and
body as before).
- Around line 20-29: The current CORS headers returned by getCorsHeaders() don't
expose custom response headers so browser clients can't read headers like
"song-id" or "content-disposition"; update the getCorsHeaders() function
(lib/networking/getCorsHeaders.ts) to include an "Access-Control-Expose-Headers"
entry listing at least "song-id" and "content-disposition" (and any other
forwarded/custom headers you expect), so when buildUpstreamResponse.ts spreads
getCorsHeaders() into the response headers those headers become readable in
browsers while keeping the existing forwardHeaders handling intact.
In `@lib/elevenlabs/compositionPlanSchema.ts`:
- Around line 7-16: The sections array schema currently allows empty arrays;
update the z.array(...) used in the sections property of the composition plan
schema to enforce at least one element by appending .min(1) to that array
validator (i.e., modify the sections: z.array(...) declaration that wraps the
z.object({...}) so it becomes z.array(...).min(1)), ensuring the schema matches
ElevenLabs API requirement for at least one section.
In `@lib/elevenlabs/stemSeparationHandler.ts`:
- Around line 35-41: The code unsafely casts incomingForm.get("file") to File |
null; instead, check the actual type returned by incomingForm.get before
treating it as a File: retrieve the value into a variable (e.g., fileEntry =
incomingForm.get("file")), verify it's not null and is an instance of File
(using instanceof File), and if the check fails return the 400 JSON error;
update any subsequent uses to rely on the validated File variable (file) so
string values won't silently break downstream logic in stemSeparationHandler.ts.
In `@lib/elevenlabs/validateStemSeparationBody.ts`:
- Line 1: The file fails Prettier formatting; run Prettier to fix formatting
inconsistencies (e.g., run prettier --write on
lib/elevenlabs/validateStemSeparationBody.ts) and commit the reformatted file so
the import line (import { NextResponse } from "next/server";) and any other
lines conform to the project's Prettier rules; ensure no other lint/format
errors remain in validateStemSeparationBody.ts before pushing.
In `@lib/elevenlabs/videoToMusicHandler.ts`:
- Line 1: Prettier flagged formatting issues in
lib/elevenlabs/videoToMusicHandler.ts; run a formatting pass (e.g., run
`prettier --write` on that file) or apply the project's Prettier rules to fix
spacing/line breaks so the import and other declarations conform to the
project's style; after formatting, stage the updated file and re-run CI to
ensure the Prettier check passes.
- Line 35: Replace the unsafe cast of incomingForm.getAll("videos") to File[] by
explicitly filtering and validating the returned values: call
incomingForm.getAll("videos"), iterate the array and keep only entries that are
instances of File (use instanceof File), validate each File's size and type as
required, and then assign the filtered list to the videos variable; also handle
the case where no valid files remain (e.g., return an error). This change
affects the code around incomingForm.getAll("videos") and the videos variable
usage in videoToMusicHandler.
In `@lib/mcp/tools/music/registerComposeMusicTool.ts`:
- Line 1: The file has Prettier formatting issues around the import line (import
{ McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"); run the project's
formatter on this file (e.g., run prettier --write
lib/mcp/tools/music/registerComposeMusicTool.ts) or reformat the import and
entire file to match the project's Prettier config so the pipeline passes;
ensure the import statement and surrounding whitespace/line endings conform to
Prettier rules.
- Around line 11-16: Update the JSDoc for registerComposeMusicTool to accurately
describe behavior: state that it generates a song via ElevenLabs (or the
ElevenLabs TTS service), returns the song_id (or result from ElevenLabs) and
does not save audio to Supabase storage; remove or replace the incorrect "saves
audio to Supabase storage" clause and mention that the function returns a
song_id for downstream handling (referencing registerComposeMusicTool and the
compose_music tool name).
In `@lib/mcp/tools/music/registerStemSeparationTool.ts`:
- Around line 51-56: The audio download in registerStemSeparationTool.ts uses
args.audio_url directly (audioResponse = await fetch(args.audio_url)) and needs
the same SSRF protections as registerVideoToMusicTool.ts; before calling fetch
or constructing filename from args.audio_url, validate the URL with a shared URL
validation utility (e.g., isSafeUrl or validateExternalUrl) that rejects
non-HTTP(S), private/internal IPs, localhost, link-local addresses, and cloud
metadata endpoints, and return getToolResultError on validation failure; add or
import the shared validator and call it at the start of the download flow (prior
to fetch, filename extraction), ensuring all error messages include the
offending URL for diagnostics.
In `@lib/mcp/tools/music/registerVideoToMusicTool.ts`:
- Around line 54-62: The loop that downloads user-supplied URLs (iterate over
args.video_urls) currently uses fetch without scheme/IP validation or timeout;
update the code around the for (const url of args.video_urls) block to (1)
enforce HTTPS-only URLs (reject any URL not starting with https: before
fetching), (2) perform a DNS/IP resolution or parse the hostname and block
private/internal ranges (169.254.0.0/16, 127.0.0.0/8, 10.0.0.0/8,
192.168.0.0/16, etc.) before downloading, and (3) add a fetch timeout (use
AbortController with ~30s) so the fetch(url) call cannot hang; return
getToolResultError on validation failures and proceed to blob/formData.append
only after the checks pass.
---
Nitpick comments:
In `@app/api/music/plan/route.ts`:
- Around line 23-25: Add a maxDuration export to match other music endpoints:
declare export const maxDuration = 120 alongside the existing exports (dynamic,
fetchCache, revalidate) in route.ts so the plan route uses the same maximum
upstream response timeout as compose/stream and keeps configs consistent.
In `@lib/elevenlabs/callElevenLabsMusic.ts`:
- Around line 28-35: The POST fetch in callElevenLabsMusic.ts lacks a timeout;
wrap the request with an AbortSignal by creating a signal (e.g.,
AbortSignal.timeout(TIMEOUT_MS) or a passed-in timeout value) and pass it as the
signal option to fetch in the callElevenLabsMusic function so the request is
aborted on exceed; ensure you clear or handle the timeout appropriately (use a
constant or reuse the existing timeout value used by other ElevenLabs functions)
and propagate/handle the AbortError where needed.
In `@lib/elevenlabs/callElevenLabsMusicMultipart.ts`:
- Around line 28-34: The fetch to ElevenLabs in callElevenLabsMusicMultipart.ts
currently has no timeout; update the call in the function that returns
fetch(url.toString(), { ... }) to use an AbortSignal created via
AbortSignal.timeout(<reasonable-ms>, /* e.g. 30000 */) and pass that signal in
the fetch options (add signal: abortSignal) so upstream requests are bounded;
also ensure the surrounding caller (or this function) catches an aborted/timeout
error (AbortError) and logs/returns an appropriate error instead of hanging.
In `@lib/elevenlabs/schemas.ts`:
- Around line 20-32: The current promptOrPlanRefinements array requires callers
to manually iterate and call .refine(); instead export a small helper that
applies these checks in a type-safe way (or attach the logic via Zod's
superRefine) so consumers just call applyPromptOrPlanRefinement(schema) or
schema.superRefine(...) once; update promptOrPlanRefinements to expose a
function named (for example) applyPromptOrPlanRefinement that accepts a Zod
object schema, uses the same checks (ensuring the runtime shape { prompt?:
string | null; composition_plan?: unknown } is used) and adds the two validation
rules via schema.superRefine or schema.refine internally to centralize behavior
and remove boilerplate at call sites.
In `@lib/elevenlabs/validateStreamBody.ts`:
- Around line 23-29: The current error response always includes missing_fields
using firstError.path which is an empty array for refinement errors; update the
response logic in validateStreamBody (the block handling if (!result.success))
to only include missing_fields when firstError.path && firstError.path.length >
0, otherwise omit missing_fields (or return a different descriptive field such
as refinement_error/message) and still return the 400 NextResponse with
getCorsHeaders(); locate the error handling that reads result.error.issues[0]
and conditionally add the missing_fields property based on the path length.
In `@lib/elevenlabs/videoToMusicHandler.ts`:
- Around line 21-105: videoToMusicHandler is too long and duplicates text-field
parsing from stemSeparationHandler; extract a reusable helper (e.g.,
extractTextFields(formData: FormData, excludeKey: string, booleanFields:
string[])) and use it in videoToMusicHandler before calling
validateVideoToMusicBody to populate textFields, handling tags -> string[] and
boolean conversion (sign_with_c2pa) inside that helper; optionally factor
repeated multipart setup into a createElevenLabsProxyHandler-style factory to
build upstreamForm and callElevenLabsMusicMultipart, replacing the manual loop
that appends videos/tags/description/sign_with_c2pa so videoToMusicHandler
becomes short and delegates parsing to extractTextFields and multipart
construction to the factory.
In `@lib/mcp/tools/music/registerComposeDetailedMusicTool.ts`:
- Around line 32-38: The resolved accountId returned by resolveAccountId(...) is
validated but never used; either (A) explicitly document that resolution is only
for auth by adding a clarifying comment after the validation (near the
resolveAccountId call and the getToolResultError checks) stating accountId is
intentionally unused for downstream operations, or (B) actually use accountId
where needed—e.g., pass accountId into the downstream song creation /
persistence or logging/rate-limiting steps (functions that handle composition
creation inside this tool) so the generated song is associated with the user and
for audit/rate-limiter hooks; update references around resolveAccountId,
accountId, and getToolResultError accordingly.
In `@lib/mcp/tools/music/registerComposeMusicTool.ts`:
- Around line 32-38: The resolved accountId returned by resolveAccountId in
registerComposeMusicTool is never used; either use it or make the intent
explicit — update registerComposeMusicTool to either (a) use the accountId in
subsequent logic (e.g., pass it into compose-related calls) or (b) add a clear
inline comment above the resolveAccountId call explaining that the resolution is
only for authentication/validation side-effects and the id is intentionally
unused; keep resolveAccountId, getToolResultError, and compose-related
identifiers intact when making this change.
In `@lib/mcp/tools/music/registerCreateCompositionPlanTool.ts`:
- Around line 48-49: The call to upstream.json() in
registerCreateCompositionPlanTool (where you assign const plan = await
upstream.json() and return getToolResultSuccess(plan)) can throw a parse error
that is currently masked by a generic catch; update the code to explicitly
handle JSON parse failures by wrapping upstream.json() in its own try/catch (or
using upstream.text() fallback) and return a failed tool result with a
descriptive message (e.g., "Plan creation failed: invalid JSON response"
including any parse error.message), while still preserving the existing
upstream.ok check and using getToolResultSuccess(plan) only when parsing
succeeds.
In `@lib/mcp/tools/music/registerStemSeparationTool.ts`:
- Around line 75-78: The success response returned by getToolResultSuccess (in
the registerStemSeparationTool handler) is misleading — it points back to the
initiating POST endpoint instead of giving a way to retrieve the ZIP; update the
returned payload to include actionable retrieval details (e.g., add a
download_url or retrieval_id and the correct retrieval endpoint or song_id)
alongside stem_variation_id so callers can directly fetch the archive; modify
the object passed to getToolResultSuccess (the return in
registerStemSeparationTool) to include these new fields and ensure the values
are generated/filled by the stem separation flow (e.g., a presigned URL,
download token, or song ID).
In `@lib/mcp/tools/music/registerStreamMusicTool.ts`:
- Around line 32-38: resolveAccountId() returns accountId that's never used;
either use it for auditing/billing or remove the resolution. Update the code
around resolveAccountId, getToolResultError and callElevenLabsMusic: if
accountId should be tracked, pass accountId into callElevenLabsMusic (or include
it in a log/audit event before calling callElevenLabsMusic) so the resolved ID
is not discarded; if accountId is not needed for ElevenLabs calls, remove the
resolveAccountId call and related error handling to avoid unnecessary work.
In `@lib/mcp/tools/music/registerVideoToMusicTool.ts`:
- Around line 54-59: The video download loop over args.video_urls in
registerVideoToMusicTool can hang on slow servers—wrap each fetch in an
AbortController with a per-request timeout (e.g., 30s), pass controller.signal
to fetch, and clear the timeout in a finally block; on abort or non-ok
responses, return a descriptive getToolResultError mentioning the URL and the
error/timeout so the handler fails fast and doesn’t hang indefinitely.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5b6db4f3-8d8c-4f68-b9d6-01ca7b5943dc
⛔ Files ignored due to path filters (9)
lib/elevenlabs/__tests__/callElevenLabsMusic.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/callElevenLabsMusicMultipart.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/composeHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/createPlanHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/validateComposeBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/validateCreatePlanBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/validateStemSeparationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/validateStreamBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/elevenlabs/__tests__/validateVideoToMusicBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (35)
app/api/music/compose/detailed/route.tsapp/api/music/compose/route.tsapp/api/music/plan/route.tsapp/api/music/stem-separation/route.tsapp/api/music/stream/route.tsapp/api/music/video-to-music/route.tslib/const.tslib/elevenlabs/buildUpstreamResponse.tslib/elevenlabs/callElevenLabsMusic.tslib/elevenlabs/callElevenLabsMusicMultipart.tslib/elevenlabs/composeDetailedHandler.tslib/elevenlabs/composeHandler.tslib/elevenlabs/compositionPlanSchema.tslib/elevenlabs/createPlanHandler.tslib/elevenlabs/handleElevenLabsProxy.tslib/elevenlabs/handleUpstreamError.tslib/elevenlabs/outputFormats.tslib/elevenlabs/schemas.tslib/elevenlabs/stemSeparationHandler.tslib/elevenlabs/streamHandler.tslib/elevenlabs/validateComposeBody.tslib/elevenlabs/validateComposeDetailedBody.tslib/elevenlabs/validateCreatePlanBody.tslib/elevenlabs/validateStemSeparationBody.tslib/elevenlabs/validateStreamBody.tslib/elevenlabs/validateVideoToMusicBody.tslib/elevenlabs/videoToMusicHandler.tslib/mcp/tools/index.tslib/mcp/tools/music/index.tslib/mcp/tools/music/registerComposeDetailedMusicTool.tslib/mcp/tools/music/registerComposeMusicTool.tslib/mcp/tools/music/registerCreateCompositionPlanTool.tslib/mcp/tools/music/registerStemSeparationTool.tslib/mcp/tools/music/registerStreamMusicTool.tslib/mcp/tools/music/registerVideoToMusicTool.ts
| /** | ||
| * POST /api/music/compose | ||
| * | ||
| * Generate a song from a text prompt or composition plan using ElevenLabs Music AI. | ||
| * Returns binary audio. The song-id is returned in the response headers. | ||
| */ | ||
| export { composeHandler as POST }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify getCorsHeaders doesn't expose custom headers
cat lib/networking/getCorsHeaders.ts
# Check which handlers set custom headers like song-id
rg -n "song-id|content-disposition" lib/elevenlabs/Repository: recoupable/api
Length of output: 1263
🏁 Script executed:
#!/bin/bash
# Get full buildUpstreamResponse implementation
cat lib/elevenlabs/buildUpstreamResponse.ts
# Check if any route handler sets Access-Control-Expose-Headers
rg -n "Access-Control-Expose-Headers" .
# Check the composeHandler to see how it uses buildUpstreamResponse
cat lib/elevenlabs/composeHandler.tsRepository: recoupable/api
Length of output: 1676
🏁 Script executed:
#!/bin/bash
# Get full route file context
cat app/api/music/compose/route.ts
# Check test setup to understand CORS testing
head -80 lib/elevenlabs/__tests__/composeHandler.test.tsRepository: recoupable/api
Length of output: 3246
🏁 Script executed:
#!/bin/bash
# Check if there are other places where Access-Control-Expose-Headers is set or handled
rg -n "Expose-Headers|forwardHeaders" --type ts --type tsx
# Check all uses of buildUpstreamResponse to see the pattern
rg -n "buildUpstreamResponse" --type ts --type tsx -B 2 -A 2Repository: recoupable/api
Length of output: 113
🏁 Script executed:
#!/bin/bash
# Check if there are other places where Access-Control-Expose-Headers is set or handled
rg -n "Expose-Headers|forwardHeaders"
# Check all uses of buildUpstreamResponse to see the pattern
rg -n "buildUpstreamResponse" -B 2 -A 2Repository: recoupable/api
Length of output: 3257
Browser clients cannot read response headers without Access-Control-Expose-Headers in CORS response.
The endpoint correctly sets song-id in response headers, but getCorsHeaders() doesn't include Access-Control-Expose-Headers. This blocks browser clients from reading the header—the browser's CORS policy will strip it. The same issue affects stemSeparationHandler with content-disposition.
Update getCorsHeaders() to expose custom headers:
"Access-Control-Expose-Headers": "song-id, content-disposition"This affects all music endpoints using buildUpstreamResponse() (compose, video-to-music, stem-separation).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/music/compose/route.ts` around lines 14 - 20, getCorsHeaders()
currently does not expose custom response headers, so browser clients cannot
read the song-id or content-disposition set by buildUpstreamResponse(); update
getCorsHeaders() to include "Access-Control-Expose-Headers" with the values
"song-id, content-disposition" so composeHandler (exported as POST),
stemSeparationHandler and any endpoints that use buildUpstreamResponse() can
surface those headers to browser clients.
| const headers: Record<string, string> = { | ||
| ...getCorsHeaders(), | ||
| "Content-Type": contentType, | ||
| }; | ||
| if (songId) headers["song-id"] = songId; | ||
|
|
||
| for (const name of forwardHeaders) { | ||
| const value = upstream.headers.get(name); | ||
| if (value) headers[name] = value; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== getCorsHeaders implementation =="
fd -i 'getCorsHeaders.ts' | while read -r f; do
echo "--- $f ---"
sed -n '1,220p' "$f"
done
echo
echo "== References to expose headers and forwarded music headers =="
rg -n "Access-Control-Expose-Headers|song-id|content-disposition|forwardHeaders"Repository: recoupable/api
Length of output: 3016
🏁 Script executed:
rg -n "Access-Control-Expose-Headers" lib/Repository: recoupable/api
Length of output: 40
Add Access-Control-Expose-Headers to expose response headers to browser clients.
Response headers like song-id and content-disposition are being set but cannot be read by browser clients because they are not exposed via CORS. Update getCorsHeaders() in lib/networking/getCorsHeaders.ts to include Access-Control-Expose-Headers with the necessary header names (e.g., song-id, content-disposition). Without this, tests and client code expecting to read these headers will fail in browser environments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/elevenlabs/buildUpstreamResponse.ts` around lines 20 - 29, The current
CORS headers returned by getCorsHeaders() don't expose custom response headers
so browser clients can't read headers like "song-id" or "content-disposition";
update the getCorsHeaders() function (lib/networking/getCorsHeaders.ts) to
include an "Access-Control-Expose-Headers" entry listing at least "song-id" and
"content-disposition" (and any other forwarded/custom headers you expect), so
when buildUpstreamResponse.ts spreads getCorsHeaders() into the response headers
those headers become readable in browsers while keeping the existing
forwardHeaders handling intact.
| if (value) headers[name] = value; | ||
| } | ||
|
|
||
| return new Response(upstream.body, { status: 200, headers }); |
There was a problem hiding this comment.
Preserve upstream status codes on passthrough responses.
Line 31 always returns 200, which can mask legitimate upstream success statuses (for example partial content). Forward upstream.status instead.
Proposed fix
- return new Response(upstream.body, { status: 200, headers });
+ return new Response(upstream.body, { status: upstream.status, headers });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return new Response(upstream.body, { status: 200, headers }); | |
| return new Response(upstream.body, { status: upstream.status, headers }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/elevenlabs/buildUpstreamResponse.ts` at line 31, The passthrough response
currently hardcodes status 200 in the Response construction inside
buildUpstreamResponse, which hides the real upstream status; change the Response
creation to use upstream.status (i.e., replace the literal 200 with
upstream.status) so the function returns the original upstream status code (and
continue to pass the constructed headers and body as before).
| sections: z.array( | ||
| z.object({ | ||
| title: z.string(), | ||
| lyrics: z.string().nullable().optional(), | ||
| duration_ms: z.number().int().min(3000).max(600000).optional(), | ||
| positive_styles: z.array(z.string()).optional(), | ||
| negative_styles: z.array(z.string()).optional(), | ||
| audio_url: z.string().url().nullable().optional(), | ||
| }), | ||
| ), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🌐 Web query:
Does ElevenLabs Music API composition plan require at least one section?
💡 Result:
Yes, the ElevenLabs Music API composition plan requires at least one section. The composition plan is defined as a MusicPrompt object, which mandates the "sections" field as a required array of SongSection objects. Official API schema explicitly lists required fields: positive_global_styles, negative_global_styles, and sections. All examples include at least one section, and documentation describes sections as essential for defining song structure with up to 30 sections, each having a minimum duration of 3000ms. Total song duration constraints (3s to 10min) imply at least one section is necessary.
Citations:
- 1: https://elevenlabs.io/docs/eleven-api/guides/how-to/music/composition-plans
- 2: https://elevenlabs.io/docs/api-reference/music/create-composition-plan
- 3: https://elevenlabs.io/docs/api-reference/music/compose
- 4: https://elevenlabs.io/docs/api-reference/music/upload
Add .min(1) constraint to sections array to match ElevenLabs API requirements.
The ElevenLabs Music API explicitly requires at least one section in the composition plan. The current schema allows an empty sections array, which would pass validation but fail at the API. Add the minimum length constraint:
Enforce minimum one section
sections: z.array(
z.object({
title: z.string(),
lyrics: z.string().nullable().optional(),
duration_ms: z.number().int().min(3000).max(600000).optional(),
positive_styles: z.array(z.string()).optional(),
negative_styles: z.array(z.string()).optional(),
audio_url: z.string().url().nullable().optional(),
}),
- ),
+ ).min(1),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sections: z.array( | |
| z.object({ | |
| title: z.string(), | |
| lyrics: z.string().nullable().optional(), | |
| duration_ms: z.number().int().min(3000).max(600000).optional(), | |
| positive_styles: z.array(z.string()).optional(), | |
| negative_styles: z.array(z.string()).optional(), | |
| audio_url: z.string().url().nullable().optional(), | |
| }), | |
| ), | |
| sections: z.array( | |
| z.object({ | |
| title: z.string(), | |
| lyrics: z.string().nullable().optional(), | |
| duration_ms: z.number().int().min(3000).max(600000).optional(), | |
| positive_styles: z.array(z.string()).optional(), | |
| negative_styles: z.array(z.string()).optional(), | |
| audio_url: z.string().url().nullable().optional(), | |
| }), | |
| ).min(1), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/elevenlabs/compositionPlanSchema.ts` around lines 7 - 16, The sections
array schema currently allows empty arrays; update the z.array(...) used in the
sections property of the composition plan schema to enforce at least one element
by appending .min(1) to that array validator (i.e., modify the sections:
z.array(...) declaration that wraps the z.object({...}) so it becomes
z.array(...).min(1)), ensuring the schema matches ElevenLabs API requirement for
at least one section.
| const file = incomingForm.get("file") as File | null; | ||
| if (!file) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "An audio file is required in the 'file' field" }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Unsafe type assertion on form data entry.
FormData.get() returns FormDataEntryValue | null, which is File | string | null. Casting directly to File | null is unsafe—a string value would pass the null check but fail silently downstream.
🛡️ Proposed fix to validate file instance
- const file = incomingForm.get("file") as File | null;
- if (!file) {
+ const fileEntry = incomingForm.get("file");
+ if (!fileEntry || !(fileEntry instanceof File)) {
return NextResponse.json(
{ status: "error", error: "An audio file is required in the 'file' field" },
{ status: 400, headers: getCorsHeaders() },
);
}
+ const file = fileEntry;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const file = incomingForm.get("file") as File | null; | |
| if (!file) { | |
| return NextResponse.json( | |
| { status: "error", error: "An audio file is required in the 'file' field" }, | |
| { status: 400, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| const fileEntry = incomingForm.get("file"); | |
| if (!fileEntry || !(fileEntry instanceof File)) { | |
| return NextResponse.json( | |
| { status: "error", error: "An audio file is required in the 'file' field" }, | |
| { status: 400, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| const file = fileEntry; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/elevenlabs/stemSeparationHandler.ts` around lines 35 - 41, The code
unsafely casts incomingForm.get("file") to File | null; instead, check the
actual type returned by incomingForm.get before treating it as a File: retrieve
the value into a variable (e.g., fileEntry = incomingForm.get("file")), verify
it's not null and is an instance of File (using instanceof File), and if the
check fails return the 400 JSON error; update any subsequent uses to rely on the
validated File variable (file) so string values won't silently break downstream
logic in stemSeparationHandler.ts.
| ); | ||
| } | ||
|
|
||
| const videos = incomingForm.getAll("videos") as File[]; |
There was a problem hiding this comment.
Unsafe type assertion on getAll("videos") result.
FormData.getAll() returns (File | string)[], so casting directly to File[] is unsafe. A malicious or misconfigured client could send string values under the videos field, bypassing your size checks.
🛡️ Proposed fix to filter and validate file entries
- const videos = incomingForm.getAll("videos") as File[];
- if (videos.length === 0) {
+ const videoEntries = incomingForm.getAll("videos");
+ const videos = videoEntries.filter((v): v is File => v instanceof File);
+ if (videos.length === 0) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const videos = incomingForm.getAll("videos") as File[]; | |
| const videoEntries = incomingForm.getAll("videos"); | |
| const videos = videoEntries.filter((v): v is File => v instanceof File); | |
| if (videos.length === 0) { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/elevenlabs/videoToMusicHandler.ts` at line 35, Replace the unsafe cast of
incomingForm.getAll("videos") to File[] by explicitly filtering and validating
the returned values: call incomingForm.getAll("videos"), iterate the array and
keep only entries that are instances of File (use instanceof File), validate
each File's size and type as required, and then assign the filtered list to the
videos variable; also handle the case where no valid files remain (e.g., return
an error). This change affects the code around incomingForm.getAll("videos") and
the videos variable usage in videoToMusicHandler.
| @@ -0,0 +1,67 @@ | |||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | |||
There was a problem hiding this comment.
Fix Prettier formatting.
Pipeline reports a formatting issue. Run prettier --write on this file.
🧰 Tools
🪛 GitHub Actions: Format Check
[warning] 1-1: Prettier --check reported a formatting/style issue in this file. Code style issues found; run Prettier with --write to fix.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/music/registerComposeMusicTool.ts` at line 1, The file has
Prettier formatting issues around the import line (import { McpServer } from
"@modelcontextprotocol/sdk/server/mcp.js"); run the project's formatter on this
file (e.g., run prettier --write
lib/mcp/tools/music/registerComposeMusicTool.ts) or reformat the import and
entire file to match the project's Prettier config so the pipeline passes;
ensure the import statement and surrounding whitespace/line endings conform to
Prettier rules.
| /** | ||
| * Registers the compose_music MCP tool. | ||
| * Generates a song, saves audio to Supabase storage, and returns the URL. | ||
| * | ||
| * @param server - The MCP server instance. | ||
| */ |
There was a problem hiding this comment.
JSDoc description is inaccurate.
The JSDoc states "saves audio to Supabase storage," but the implementation only calls ElevenLabs and returns the song_id. The audio is not stored in Supabase. Update the documentation to match actual behavior.
📝 Suggested fix
/**
* Registers the compose_music MCP tool.
- * Generates a song, saves audio to Supabase storage, and returns the URL.
+ * Generates a song via ElevenLabs Music AI and returns the song ID.
*
* `@param` server - The MCP server instance.
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Registers the compose_music MCP tool. | |
| * Generates a song, saves audio to Supabase storage, and returns the URL. | |
| * | |
| * @param server - The MCP server instance. | |
| */ | |
| /** | |
| * Registers the compose_music MCP tool. | |
| * Generates a song via ElevenLabs Music AI and returns the song ID. | |
| * | |
| * `@param` server - The MCP server instance. | |
| */ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/music/registerComposeMusicTool.ts` around lines 11 - 16, Update
the JSDoc for registerComposeMusicTool to accurately describe behavior: state
that it generates a song via ElevenLabs (or the ElevenLabs TTS service), returns
the song_id (or result from ElevenLabs) and does not save audio to Supabase
storage; remove or replace the incorrect "saves audio to Supabase storage"
clause and mention that the function returns a song_id for downstream handling
(referencing registerComposeMusicTool and the compose_music tool name).
| const audioResponse = await fetch(args.audio_url); | ||
| if (!audioResponse.ok) { | ||
| return getToolResultError(`Failed to download audio from ${args.audio_url}`); | ||
| } | ||
| const blob = await audioResponse.blob(); | ||
| const filename = args.audio_url.split("/").pop() ?? "audio.mp3"; |
There was a problem hiding this comment.
Same SSRF risk applies here.
The audio URL download has the same SSRF vulnerability as registerVideoToMusicTool.ts. User-provided URLs should be validated to prevent access to internal networks and cloud metadata endpoints. Consider implementing a shared URL validation utility that both tools can use.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/music/registerStemSeparationTool.ts` around lines 51 - 56, The
audio download in registerStemSeparationTool.ts uses args.audio_url directly
(audioResponse = await fetch(args.audio_url)) and needs the same SSRF
protections as registerVideoToMusicTool.ts; before calling fetch or constructing
filename from args.audio_url, validate the URL with a shared URL validation
utility (e.g., isSafeUrl or validateExternalUrl) that rejects non-HTTP(S),
private/internal IPs, localhost, link-local addresses, and cloud metadata
endpoints, and return getToolResultError on validation failure; add or import
the shared validator and call it at the start of the download flow (prior to
fetch, filename extraction), ensuring all error messages include the offending
URL for diagnostics.
| for (const url of args.video_urls) { | ||
| const videoResponse = await fetch(url); | ||
| if (!videoResponse.ok) { | ||
| return getToolResultError(`Failed to download video from ${url}`); | ||
| } | ||
| const blob = await videoResponse.blob(); | ||
| const filename = url.split("/").pop() ?? "video.mp4"; | ||
| formData.append("videos", blob, filename); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's existing URL validation infrastructure in the codebase
rg -n "ssrf|validateUrl|isPublicUrl|allowedHost" --type=ts -C2Repository: recoupable/api
Length of output: 40
🏁 Script executed:
cat -n lib/mcp/tools/music/registerVideoToMusicTool.tsRepository: recoupable/api
Length of output: 4377
🏁 Script executed:
# Broader search for URL validation patterns
rg -n "url|fetch|validation|validate" --type=ts -i lib/mcp/ | head -40Repository: recoupable/api
Length of output: 5080
🏁 Script executed:
# Search for URL scheme validation or similar patterns
rg -n "https?|http|scheme|protocol|private|internal" --type=ts -C1 lib/ | grep -E "(validate|check|allow|block)" | head -20Repository: recoupable/api
Length of output: 1996
🏁 Script executed:
# Check if there are any utility files for validation
fd -t f "validat|sanitiz|url" --type f lib/Repository: recoupable/api
Length of output: 7974
SSRF vulnerability: URLs lack scheme and IP range validation.
The tool downloads videos from user-provided URLs validated only by Zod's .url() method, which checks URL format syntax but does NOT restrict schemes or IP ranges. This allows:
- Internal metadata endpoints:
http://169.254.169.254/(AWS/cloud metadata) - Private network hosts:
http://localhost:8080/,http://192.168.x.x/ - Data exfiltration via DNS or response timing
Additionally, missing timeout on fetch (line 55) risks hanging requests and DoS attacks.
Recommendations:
- Restrict to
https://scheme only - Block private/internal IP ranges (169.254.x.x, 10.x.x.x, 192.168.x.x, 127.x.x.x)
- Add timeout to fetch calls (e.g., 30 seconds)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/mcp/tools/music/registerVideoToMusicTool.ts` around lines 54 - 62, The
loop that downloads user-supplied URLs (iterate over args.video_urls) currently
uses fetch without scheme/IP validation or timeout; update the code around the
for (const url of args.video_urls) block to (1) enforce HTTPS-only URLs (reject
any URL not starting with https: before fetching), (2) perform a DNS/IP
resolution or parse the hostname and block private/internal ranges
(169.254.0.0/16, 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, etc.) before
downloading, and (3) add a fetch timeout (use AbortController with ~30s) so the
fetch(url) call cannot hang; return getToolResultError on validation failures
and proceed to blob/formData.append only after the checks pass.
Summary
/api/music/Endpoints
POST /api/music/composePOST /api/music/compose/detailedPOST /api/music/streamPOST /api/music/planPOST /api/music/video-to-musicPOST /api/music/stem-separationMCP Tools
compose_music,compose_music_detailed,stream_music,create_composition_plan,video_to_music,separate_stemsFiles
lib/elevenlabs/— 2 HTTP clients, shared schemas, 6 validators, 6 handlerslib/mcp/tools/music/— 6 tool registrations + indexTest plan
ELEVENLABS_API_KEYto Vercel env vars before testingMade with Cursor
Summary by CodeRabbit