Skip to main content
Glama

generate_music_suno

Create custom songs by providing lyrics, style, and title, or generate music from a description. Returns an audio URL in HTML format for easy playback and download, accessible through the Suno API on the Suno-MCP server.

Instructions

Generates a song using the Suno API. Provide lyrics, style, and title for custom mode, or a description for inspiration mode. Returns the audio URL upon completion. Polling for results may take a few minutes.

When returning an audio URL, please use the following HTML format for user convenience:

<audio controls>
  <source src="YOUR_AUDIO_URL_HERE" type="audio/mpeg">
</audio>
<br>
<a href="YOUR_AUDIO_URL_HERE" download="SONG_TITLE.mp3">
  点击这里下载喵!
</a>

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
continue_atNoOptional. Time in seconds from which to continue the song. Requires 'task_id' and 'continue_clip_id'.
continue_clip_idNoOptional. Clip ID of the song part to continue. Requires 'task_id' and 'continue_at'.
gpt_description_promptNoOptional. Description for inspiration mode. If provided, 'prompt', 'tags', and 'title' are not strictly required by the user but might be used by the API. Example: 'A cheerful upbeat song about a sunny day.'
make_instrumentalNoOptional. Whether to generate instrumental music. Defaults to false.
mvNoOptional. Model version. Defaults to 'chirp-v4'.
promptNoLyrics content. Required for custom mode. Example: '[Verse 1]\nUnder the starry sky...'
tagsNoMusic style tags, comma-separated. Required for custom mode. Example: 'acoustic, folk, pop'
task_idNoOptional. Task ID of a previous song to continue. If provided, 'continue_at' and 'continue_clip_id' are also required.
titleNoSong title. Required for custom mode. Example: 'Starry Night Serenade'

Implementation Reference

  • The primary handler function for the 'generate_music_suno' tool. It validates input arguments, constructs the payload, submits the music generation request to the Suno API, polls for completion, and returns the resulting audio URL or errors appropriately.
    private async handleGenerateMusicTool(args: any) { // Changed unknown to any for now, validation is done by isValidSunoMusicRequestArgs
        if (!isValidSunoMusicRequestArgs(args)) {
            console.error("Invalid args received for generate_music_suno:", args);
            throw new McpError(ErrorCode.InvalidParams, "主人! Input parameters are invalid nya~! Please check the requirements for prompt, tags, title or gpt_description_prompt, and continuation parameters. (>_<)");
        }
    
        const payload: any = {
            prompt: args.prompt,
            tags: args.tags,
            title: args.title,
            mv: args.mv || "chirp-v4", // Default model updated to v4
            make_instrumental: args.make_instrumental || false,
        };
    
        if (args.gpt_description_prompt) {
            payload.gpt_description_prompt = args.gpt_description_prompt;
            // As per API docs, if gpt_description_prompt is used, prompt/tags/title are not "required" for that mode.
            // However, the API might still use them if provided. We send what's given.
            // If they are empty, we might need to remove them or send empty strings based on API behavior.
            // For now, send them as is.
            if (!args.prompt) delete payload.prompt;
            if (!args.tags) delete payload.tags;
            if (!args.title) delete payload.title;
        } else {
             // Ensure these are present if not in gpt_description_mode
            if (!payload.prompt || !payload.tags || !payload.title) {
                 throw new McpError(ErrorCode.InvalidParams, "主人!For custom mode, 'prompt', 'tags', and 'title' are all required nya~!");
            }
        }
    
    
        if (args.task_id && args.continue_at !== undefined && args.continue_clip_id) {
            payload.task_id = args.task_id;
            payload.continue_at = args.continue_at;
            payload.continue_clip_id = args.continue_clip_id;
        }
    
    
        console.log("Sending payload to Suno API:", JSON.stringify(payload));
    
        try {
            // 1. Submit music generation task
            const submitResponse = await this.sunoApiAxiosInstance.post<SunoApiSubmitResponse>(
                SUNO_API_CONFIG.ENDPOINTS.SUBMIT_MUSIC,
                payload
            );
    
            console.log("Received submit response from Suno API:", submitResponse.data);
    
            if (submitResponse.data.code !== "success" || typeof submitResponse.data.data !== 'string' || submitResponse.data.data.trim() === '') {
                throw new McpError(ErrorCode.InternalError, `Suno API submission failed: ${submitResponse.data.message || 'No task ID string returned.'}`);
            }
    
            const taskId: string = submitResponse.data.data;
            // The check for !taskId is now more robust due to the typeof and trim check above.
            // A simple truthiness check for taskId can still be useful.
            if (!taskId) {
                 throw new McpError(ErrorCode.InternalError, `Suno API submission failed: No task_id found in response (after direct assignment).`);
            }
            console.log(`Music generation task submitted. Task ID: ${taskId}. Polling for results...`);
    
            // 2. Poll for task status
            let attempts = 0;
            while (attempts < SUNO_API_CONFIG.MAX_POLLING_ATTEMPTS) {
                attempts++;
                await new Promise(resolve => setTimeout(resolve, SUNO_API_CONFIG.POLLING_INTERVAL_MS));
    
                console.log(`Polling attempt ${attempts} for task ${taskId}...`);
                const fetchResponse = await this.sunoApiAxiosInstance.get<SunoApiFetchResponse>(
                    `${SUNO_API_CONFIG.ENDPOINTS.FETCH_TASK}${taskId}`
                );
    
                console.log(`Received fetch response for task ${taskId}:`, fetchResponse.data);
    
                // Check if the fetch was successful and if the task data is present
                if (fetchResponse.data.code !== "success" || !fetchResponse.data.data) {
                    console.warn(`Polling for task ${taskId}: API returned code ${fetchResponse.data.code} or no task data. Message: ${fetchResponse.data.message}`);
                    if (attempts >= SUNO_API_CONFIG.MAX_POLLING_ATTEMPTS / 2 && fetchResponse.data.code !== "success") {
                         console.error(`Task ${taskId} still not showing success code after ${attempts} attempts. Last code: ${fetchResponse.data.code}`);
                    }
                    // Continue polling
                } else {
                    // Directly use fetchResponse.data.data as taskDetails since it's now a single object
                    const taskDetails: SunoApiResponseData = fetchResponse.data.data;
    
                    // Ensure the fetched task_id matches the one we are polling for, as a sanity check
                    if (taskDetails.task_id !== taskId) {
                        console.warn(`Polling for task ${taskId}: Mismatched task_id in response (${taskDetails.task_id}). Continuing poll.`);
                        // Decide if this should be an error or just continue polling. For now, continue.
                    } else {
                        if (taskDetails.status === "COMPLETE" || taskDetails.status === "IN_PROGRESS") {
                            // For a single task object, taskDetails.data is SunoAudioData[]
                            if (taskDetails.data && taskDetails.data.length > 0 && taskDetails.data[0].audio_url) {
                                const audioUrl = taskDetails.data[0].audio_url;
                                console.log(`Task ${taskId} complete! Audio URL: ${audioUrl}`);
                                const resultText: TextContent = {
                                    type: "text",
                                    text: `Song generated! You can listen to it here: ${audioUrl}`
                                };
                                if (taskDetails.data[0].title) {
                                    resultText.text += `\nTitle: ${taskDetails.data[0].title}`;
                                }
                                if (taskDetails.data[0].metadata?.tags) {
                                    resultText.text += `\nStyle: ${taskDetails.data[0].metadata.tags}`;
                                }
                                if (taskDetails.data[0].image_url) {
                                    resultText.text += `\nImage: ${taskDetails.data[0].image_url}`;
                                }
                                return { content: [resultText] };
                            } else if (taskDetails.status === "COMPLETE" && (!taskDetails.data || taskDetails.data.length === 0 || !taskDetails.data[0].audio_url)) {
                                throw new McpError(ErrorCode.InternalError, `Suno Task ${taskId} is COMPLETE but no audio_url was found.`);
                            }
                            // If IN_PROGRESS but no audio_url yet, continue polling
                        } else if (taskDetails.status === "FAILED") {
                            throw new McpError(ErrorCode.InternalError, `Suno Task ${taskId} failed: ${taskDetails.fail_reason || 'Unknown reason'}`);
                        }
                        // Other statuses like PENDING, SUBMITTED: continue polling
                        console.log(`Task ${taskId} status: ${taskDetails.status}. Progress: ${taskDetails.progress || 'N/A'}`);
                    }
                }
            }
    
            throw new McpError(ErrorCode.InternalError, `Suno Task ${taskId} timed out after ${attempts} polling attempts. (Used InternalError as Timeout code was not available)`);
    
        } catch (error: unknown) { // Typed error
            console.error("Error calling Suno API:", error instanceof Error ? error.message : error);
            if (axios.isAxiosError(error)) { // AxiosError type guard handles error.response
                const apiError = error.response?.data as any; // Assuming data can be anything
                const status = error.response?.status;
                const message = apiError?.message || apiError?.error?.message || (typeof apiError === 'string' ? apiError : (error as AxiosError).message);
                return {
                    content: [{
                        type: "text",
                        text: `Waaah! (つД`)・゚・ Suno API error (Status ${status}): ${message}`
                    }],
                    isError: true,
                };
            }
            if (error instanceof McpError) throw error; // Re-throw McpError
            throw new McpError(ErrorCode.InternalError, `Meow~ An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
  • index.ts:81-130 (registration)
    Tool registration in the ListToolsRequestSchema handler, defining the tool's name, description, and detailed input schema for MCP protocol compliance.
    {
        name: "generate_music_suno",
        description: "Generates a song using the Suno API. Provide lyrics, style, and title for custom mode, or a description for inspiration mode. Returns the audio URL upon completion. Polling for results may take a few minutes.\n\nWhen returning an audio URL, please use the following HTML format for user convenience:\n```html\n<audio controls>\n  <source src=\"YOUR_AUDIO_URL_HERE\" type=\"audio/mpeg\">\n</audio>\n<br>\n<a href=\"YOUR_AUDIO_URL_HERE\" download=\"SONG_TITLE.mp3\">\n  点击这里下载喵!\n</a>\n```",
        inputSchema: {
            type: "object",
            properties: {
                prompt: {
                    type: "string",
                    description: "Lyrics content. Required for custom mode. Example: '[Verse 1]\\nUnder the starry sky...' "
                },
                tags: {
                    type: "string",
                    description: "Music style tags, comma-separated. Required for custom mode. Example: 'acoustic, folk, pop'"
                },
                title: {
                    type: "string",
                    description: "Song title. Required for custom mode. Example: 'Starry Night Serenade'"
                },
                mv: {
                    type: "string",
                    enum: ["chirp-v3-0", "chirp-v3-5", "chirp-v4"],
                    description: "Optional. Model version. Defaults to 'chirp-v4'."
                },
                make_instrumental: {
                    type: "boolean",
                    description: "Optional. Whether to generate instrumental music. Defaults to false."
                },
                gpt_description_prompt: {
                    type: "string",
                    description: "Optional. Description for inspiration mode. If provided, 'prompt', 'tags', and 'title' are not strictly required by the user but might be used by the API. Example: 'A cheerful upbeat song about a sunny day.'"
                },
                task_id: {
                    type: "string",
                    description: "Optional. Task ID of a previous song to continue. If provided, 'continue_at' and 'continue_clip_id' are also required."
                },
                continue_at: {
                    type: "number",
                    description: "Optional. Time in seconds from which to continue the song. Requires 'task_id' and 'continue_clip_id'."
                },
                continue_clip_id: {
                    type: "string",
                    description: "Optional. Clip ID of the song part to continue. Requires 'task_id' and 'continue_at'."
                }
            },
            // If gpt_description_prompt is not provided, then prompt, tags, and title are required.
            // This complex dependency is better handled in the validation logic.
            // For schema, we list them and then validate.
            required: [] // Validation logic will handle conditional requirements
        }
    }
  • types.ts:6-58 (schema)
    TypeScript interface defining the structure and types for the tool's input arguments, used for validation and typing.
    export interface SunoMusicRequestArgs {
        /**
         * Lyrics content. Required for custom mode.
         * @example "[Verse 1]\nUnder the starry sky, with a guitar in hand, I sing an old song from my homeland"
         */
        prompt: string;
    
        /**
         * Music style tags, comma-separated. Required for custom mode.
         * @example "acoustic, folk, spanish"
         */
        tags: string;
    
        /**
         * Song title. Required for custom mode.
         * @example "Homeland Song"
         */
        title: string;
    
        /**
         * Model version. Optional.
         * @enum ["chirp-v3-0", "chirp-v3-5", "chirp-v4"]
         * @default "chirp-v4"
         */
        mv?: "chirp-v3-0" | "chirp-v3-5" | "chirp-v4";
    
        /**
         * Whether to generate instrumental music. Optional.
         * @default false
         */
        make_instrumental?: boolean;
    
        /**
         * Optional. Description for inspiration mode.
         * @example "A sad song about a rainy day"
         */
        gpt_description_prompt?: string;
    
         /**
         * Optional. Task ID to continue from.
         */
        task_id?: string;
    
        /**
         * Optional. Time in seconds to continue from.
         */
        continue_at?: number;
    
        /**
         * Optional. Clip ID to continue from.
         */
        continue_clip_id?: string;
    }
  • Helper function that validates the input arguments against the SunoMusicRequestArgs schema, enforcing conditional requirements for custom vs inspiration modes and continuation parameters.
    export function isValidSunoMusicRequestArgs(args: any): args is SunoMusicRequestArgs {
        if (!args || typeof args !== 'object') return false;
    
        // Custom mode: prompt, tags, title are required
        if (!args.gpt_description_prompt) {
            if (typeof args.prompt !== 'string' || args.prompt.trim() === '') return false;
            if (typeof args.tags !== 'string' || args.tags.trim() === '') return false;
            if (typeof args.title !== 'string' || args.title.trim() === '') return false;
        } else { // Inspiration mode: gpt_description_prompt is required
            if (typeof args.gpt_description_prompt !== 'string' || args.gpt_description_prompt.trim() === '') return false;
            // In inspiration mode, prompt, tags, title might be optional or not used by the API directly
        }
    
    
        if (args.mv !== undefined && !["chirp-v3-0", "chirp-v3-5", "chirp-v4"].includes(args.mv)) return false;
        if (args.make_instrumental !== undefined && typeof args.make_instrumental !== 'boolean') return false;
    
        // Validate continuation parameters if present
        const hasTaskId = args.task_id !== undefined && typeof args.task_id === 'string' && args.task_id.trim() !== '';
        const hasContinueAt = args.continue_at !== undefined && typeof args.continue_at === 'number' && args.continue_at >= 0;
        const hasContinueClipId = args.continue_clip_id !== undefined && typeof args.continue_clip_id === 'string' && args.continue_clip_id.trim() !== '';
    
        if (hasTaskId || hasContinueAt || hasContinueClipId) {
            // If any continuation param is present, all three must be present
            if (!(hasTaskId && hasContinueAt && hasContinueClipId)) {
                return false;
            }
        }
    
        return true;
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: the tool returns an audio URL, polling may take minutes, and it includes a specific HTML format for user convenience. However, it doesn't cover potential errors, rate limits, or authentication needs, leaving some gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the core purpose, followed by usage notes and output details. The HTML format section is lengthy but serves a practical purpose. Some sentences could be more concise, but overall it's efficient with minimal waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (9 parameters, no output schema, no annotations), the description does a good job covering the tool's purpose, modes, and output handling. It lacks details on error cases or advanced usage scenarios, but for a generative tool with rich schema coverage, it's reasonably complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 9 parameters thoroughly. The description adds minimal value by mentioning lyrics, style, and title for custom mode and description for inspiration mode, but doesn't provide additional syntax or format details beyond what the schema provides. Baseline 3 is appropriate when schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool 'Generates a song using the Suno API' with specific verbs ('generates') and resources ('song'), and mentions two modes (custom and inspiration). However, it doesn't distinguish from any siblings since none exist, so it can't achieve a perfect 5 for sibling differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage by mentioning two modes (custom vs inspiration) and their required inputs, but it doesn't explicitly state when to choose one mode over the other or provide broader contextual guidance. No alternatives are mentioned, but since there are no sibling tools, this is less critical.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lioensky/MCP-Suno'

If you have feedback or need assistance with the MCP directory API, please join our Discord server