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;
    }
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