Initial commit: Suno MCP server with 24 tools, proxy support, and opencode skill
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"suno": {
|
||||
"type": "local",
|
||||
"command": [
|
||||
"node",
|
||||
"/home/user/moder/mcp/suno-server/dist/index.js"
|
||||
],
|
||||
"env": {
|
||||
"SUNO_API_KEY": "YOUR_SUNO_API_KEY_HERE"
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: suno
|
||||
description: Используй когда пользователь просит создать музыку, текст песни, кавер, ремикс, мэшап, видео, или работать с Suno AI Music API. Покрывает генерацию музыки, тексты, разделение вокала/стемов, Persona, стили и конвертацию.
|
||||
---
|
||||
|
||||
# Suno Music API Skill
|
||||
|
||||
Этот skill описывает MCP-сервер для работы с [Suno API](https://sunoapi.org) через opencode.
|
||||
|
||||
## Лимиты и цены
|
||||
|
||||
- **Rate limit**: 20 запросов на 10 секунд.
|
||||
- **Стоимость кредита**: $0.005 USD.
|
||||
- **Кредиты не истекают**.
|
||||
- Сгенерированные файлы хранятся **15 дней**.
|
||||
|
||||
## Прокси
|
||||
|
||||
Если сервис недоступен из вашей сети, MCP сервер поддерживает прокси через переменные окружения (в порядке приоритета):
|
||||
|
||||
```
|
||||
SUNO_HTTP_PROXY=http://user:pass@proxy:8080
|
||||
HTTPS_PROXY=http://proxy:8080
|
||||
HTTP_PROXY=http://proxy:8080
|
||||
```
|
||||
|
||||
## Основные сценарии
|
||||
|
||||
### 1. Сгенерировать песню с нуля
|
||||
|
||||
```
|
||||
suno_generate_music
|
||||
prompt: "Мелодичный инди-фолк с акустической гитарой, мягким вокалом, ностальгическое настроение"
|
||||
customMode: false
|
||||
instrumental: false
|
||||
model: "V4_5ALL"
|
||||
```
|
||||
|
||||
Вернёт `taskId`. Затем нужно подождать и вызвать:
|
||||
|
||||
```
|
||||
suno_get_task_status
|
||||
taskId: "..."
|
||||
```
|
||||
|
||||
Результат: 2 трека с `audio_url`, `title`, `tags`, `duration`, `prompt`.
|
||||
|
||||
### 2. Сгенерировать песню в кастомном режиме
|
||||
|
||||
```
|
||||
suno_generate_music
|
||||
prompt: "[Verse] Ночной город, огни витрин..."
|
||||
customMode: true
|
||||
instrumental: false
|
||||
style: "Synthwave, cinematic, 80s"
|
||||
title: "Neon Rain"
|
||||
model: "V4_5"
|
||||
```
|
||||
|
||||
В `customMode: true` поля `style` и `title` обязательны. Если `instrumental: false` — ещё и `prompt` (он станет точным текстом песни).
|
||||
|
||||
### 3. Получить текст песни
|
||||
|
||||
```
|
||||
suno_generate_lyrics
|
||||
prompt: "Песня о путешествии, вдохновляющая, indie pop"
|
||||
```
|
||||
|
||||
Полученный текст можно передать в `suno_generate_music(prompt=текст, customMode=true)`.
|
||||
|
||||
### 4. Разделить вокал и музыку
|
||||
|
||||
```
|
||||
suno_separate_vocals
|
||||
taskId: "<taskId генерации>"
|
||||
audioId: "<id трека>"
|
||||
```
|
||||
|
||||
Для продвинутого разделения по инструментам:
|
||||
|
||||
```
|
||||
suno_separate_stems
|
||||
taskId: "<taskId>"
|
||||
audioId: "<id трека>"
|
||||
```
|
||||
|
||||
### 5. Сделать кавер существующего аудио
|
||||
|
||||
```
|
||||
suno_upload_and_cover
|
||||
uploadUrl: "https://example.com/my-song.mp3"
|
||||
customMode: true
|
||||
instrumental: false
|
||||
style: "Jazz"
|
||||
title: "Jazz Version"
|
||||
prompt: "[Verse] Smooth jazz arrangement..."
|
||||
model: "V4_5ALL"
|
||||
```
|
||||
|
||||
### 6. Создать персону из трека
|
||||
|
||||
```
|
||||
suno_generate_persona
|
||||
taskId: "<taskId>"
|
||||
audioId: "<id трека>"
|
||||
```
|
||||
|
||||
Бесплатно. Возвращает `personaId`, который потом можно использовать в `suno_generate_music` (`personaId` + `personaModel: style_persona`) для единого стиля.
|
||||
|
||||
### 7. Узнать промпт по готовому треку
|
||||
|
||||
```
|
||||
suno_get_task_status
|
||||
taskId: "<taskId генерации>"
|
||||
```
|
||||
|
||||
В ответе будут поля `prompt`, `title`, `tags` для каждого трека.
|
||||
|
||||
## Полный список инструментов
|
||||
|
||||
| Инструмент | Назначение | Кредиты |
|
||||
|---|---|:---:|
|
||||
| `suno_generate_music` | Генерация музыки | 12 |
|
||||
| `suno_generate_lyrics` | Генерация текста | 0.4 |
|
||||
| `suno_generate_sounds` | Звуки/амбиенты/лупы | 2.5 |
|
||||
| `suno_extend_music` | Продлить трек | 12 |
|
||||
| `suno_replace_section` | Заменить фрагмент трека | 5 |
|
||||
| `suno_add_vocals` | Добавить вокал на инструментал | 12 |
|
||||
| `suno_add_instrumental` | Добавить инструментал к вокалу | 12 |
|
||||
| `suno_upload_and_cover` | Загрузить аудио и сделать кавер | 12 |
|
||||
| `suno_upload_and_extend` | Загрузить аудио и продлить | 12 |
|
||||
| `suno_generate_mashup` | Мэшап из двух аудио | 12 |
|
||||
| `suno_separate_vocals` | Вокал + минус | 10 |
|
||||
| `suno_separate_stems` | Разделение по инструментам | 20 |
|
||||
| `suno_create_video` | Видео из трека | 2 |
|
||||
| `suno_create_cover` | Обложка трека | 0 |
|
||||
| `suno_convert_to_wav` | Конвертация в WAV | 0.4 |
|
||||
| `suno_boost_style` | Улучшить описание стиля | 0.4 |
|
||||
| `suno_generate_persona` | Создать персону из трека | 0 |
|
||||
| `suno_get_task_status` | Статус и метаданные трека | 0 |
|
||||
| `suno_get_timestamped_lyrics` | Текст с таймкодами | 0.5 |
|
||||
| `suno_get_lyrics_details` | Детали генерации текста | 0 |
|
||||
| `suno_get_wav_details` | Детали конвертации WAV | 0 |
|
||||
| `suno_get_vocal_separation_details` | Детали разделения | 0 |
|
||||
| `suno_get_video_details` | Детали видео | 0 |
|
||||
| `suno_get_credits` | Остаток кредитов | 0 |
|
||||
|
||||
## Выбор модели
|
||||
|
||||
| Модель | Длина | Когда использовать |
|
||||
|---|---|---|
|
||||
| `V4` | до 4 мин | Лучшее качество звука |
|
||||
| `V4_5` | до 8 мин | Умные промпты, быстрее |
|
||||
| `V4_5PLUS` | до 8 мин | Более насыщенный звук |
|
||||
| `V4_5ALL` | до 8 мин | Лучшая структура песни |
|
||||
| `V5` | до 8 мин | Высокая музыкальность, скорость |
|
||||
| `V5_5` | до 8 мин | Персонализированные голоса |
|
||||
|
||||
## Лимиты символов в `customMode`
|
||||
|
||||
| Поле | V4 | V4_5 / V4_5PLUS / V4_5ALL / V5 / V5_5 |
|
||||
|---|---|---|
|
||||
| `prompt` | 3000 | 5000 |
|
||||
| `style` | 200 | 1000 |
|
||||
| `title` | 80 | 100 |
|
||||
|
||||
В `customMode: false` только `prompt`, максимум 500 символов.
|
||||
|
||||
## Рекомендации по промптам
|
||||
|
||||
- Опиши жанр, настроение, инструменты, темп, энергию.
|
||||
- Для вокальных треков в `customMode` пиши текст в формате `[Verse] ... [Chorus] ...`.
|
||||
- Избегай противоречивых описаний.
|
||||
- Для повторяемого стиля используй `suno_generate_persona`.
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
| Код | Причина |
|
||||
|---|---|
|
||||
| 400 | Неверные параметры |
|
||||
| 401 | Неверный API ключ |
|
||||
| 429 | Недостаточно кредитов |
|
||||
| 430 | Слишком частые запросы |
|
||||
| 455 | Технические работы |
|
||||
|
||||
Если async-задача не завершилась за 15 минут, сервер вернёт таймаут — используй `suno_get_task_status` позже.
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Suno API HTTP client with rate limiting and polling helpers.
|
||||
*
|
||||
* Rate limit: 20 requests per 10 seconds (sunoapi.org).
|
||||
* Proxy support via SUNO_HTTP_PROXY / HTTPS_PROXY / HTTP_PROXY env vars.
|
||||
*/
|
||||
export interface SunoApiResponse<T = unknown> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
export interface SunoTrack {
|
||||
id: string;
|
||||
audio_url?: string;
|
||||
audioUrl?: string;
|
||||
source_audio_url?: string;
|
||||
stream_audio_url?: string;
|
||||
streamAudioUrl?: string;
|
||||
source_stream_audio_url?: string;
|
||||
image_url?: string;
|
||||
imageUrl?: string;
|
||||
source_image_url?: string;
|
||||
prompt?: string;
|
||||
model_name?: string;
|
||||
modelName?: string;
|
||||
title?: string;
|
||||
tags?: string;
|
||||
createTime?: string;
|
||||
duration?: number;
|
||||
}
|
||||
export interface SunoTaskData {
|
||||
taskId: string;
|
||||
parentMusicId?: string;
|
||||
param?: string;
|
||||
response?: {
|
||||
taskId?: string;
|
||||
data?: SunoTrack[];
|
||||
sunoData?: SunoTrack[];
|
||||
};
|
||||
status: string;
|
||||
type?: string;
|
||||
operationType?: 'generate' | 'extend' | 'upload_cover' | 'upload_extend';
|
||||
errorCode?: number | null;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
export declare class SunoClient {
|
||||
private readonly baseUrl;
|
||||
private readonly apiKey;
|
||||
private readonly maxRequests;
|
||||
private readonly windowMs;
|
||||
private readonly proxyAgent?;
|
||||
private requestTimestamps;
|
||||
constructor(apiKey: string);
|
||||
/**
|
||||
* Enforce rate limit: max 20 requests per 10 seconds.
|
||||
*/
|
||||
private throttle;
|
||||
request<T = unknown>(method: 'GET' | 'POST', path: string, body?: Record<string, unknown>): Promise<SunoApiResponse<T>>;
|
||||
get<T = unknown>(path: string): Promise<SunoApiResponse<T>>;
|
||||
post<T = unknown>(path: string, body: Record<string, unknown>): Promise<SunoApiResponse<T>>;
|
||||
/**
|
||||
* Poll task status until terminal state or timeout.
|
||||
*/
|
||||
pollTaskStatus(taskId: string, options?: {
|
||||
intervalMs?: number;
|
||||
maxAttempts?: number;
|
||||
statusPath?: string;
|
||||
}): Promise<SunoTaskData>;
|
||||
}
|
||||
//# sourceMappingURL=client.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,CAAC,CAAC;CACT;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;KACxB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,eAAe,CAAC;IACzE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAYD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,iBAAiB,CAAgB;gBAE7B,MAAM,EAAE,MAAM;IAQ1B;;OAEG;YACW,QAAQ;IAkBhB,OAAO,CAAC,CAAC,GAAG,OAAO,EACvB,MAAM,EAAE,KAAK,GAAG,MAAM,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAmC9B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAI3D,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAI3F;;OAEG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;QACP,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;KAChB,GACL,OAAO,CAAC,YAAY,CAAC;CA2BzB"}
|
||||
Vendored
+109
@@ -0,0 +1,109 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Suno API HTTP client with rate limiting and polling helpers.
|
||||
*
|
||||
* Rate limit: 20 requests per 10 seconds (sunoapi.org).
|
||||
* Proxy support via SUNO_HTTP_PROXY / HTTPS_PROXY / HTTP_PROXY env vars.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SunoClient = void 0;
|
||||
const undici_1 = require("undici");
|
||||
function getProxyUrl() {
|
||||
return (process.env.SUNO_HTTP_PROXY ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy);
|
||||
}
|
||||
class SunoClient {
|
||||
baseUrl = 'https://api.sunoapi.org/api/v1';
|
||||
apiKey;
|
||||
maxRequests = 20;
|
||||
windowMs = 10_000;
|
||||
proxyAgent;
|
||||
requestTimestamps = [];
|
||||
constructor(apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
const proxyUrl = getProxyUrl();
|
||||
if (proxyUrl) {
|
||||
this.proxyAgent = new undici_1.ProxyAgent(proxyUrl);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Enforce rate limit: max 20 requests per 10 seconds.
|
||||
*/
|
||||
async throttle() {
|
||||
const now = Date.now();
|
||||
this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowMs);
|
||||
if (this.requestTimestamps.length >= this.maxRequests) {
|
||||
const oldest = this.requestTimestamps[0];
|
||||
const wait = this.windowMs - (now - oldest);
|
||||
if (wait > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, wait));
|
||||
return this.throttle();
|
||||
}
|
||||
}
|
||||
this.requestTimestamps.push(now);
|
||||
}
|
||||
async request(method, path, body) {
|
||||
await this.throttle();
|
||||
const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
|
||||
const headers = {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
};
|
||||
if (this.proxyAgent) {
|
||||
fetchOptions.dispatcher = this.proxyAgent;
|
||||
}
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const text = await response.text();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
}
|
||||
catch {
|
||||
throw new Error(`Invalid JSON response from Suno API: ${text}`);
|
||||
}
|
||||
if (json.code !== 200) {
|
||||
throw new Error(`Suno API error ${json.code}: ${json.msg || 'unknown error'}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
get(path) {
|
||||
return this.request('GET', path);
|
||||
}
|
||||
post(path, body) {
|
||||
return this.request('POST', path, body);
|
||||
}
|
||||
/**
|
||||
* Poll task status until terminal state or timeout.
|
||||
*/
|
||||
async pollTaskStatus(taskId, options = {}) {
|
||||
const intervalMs = options.intervalMs ?? 15_000;
|
||||
const maxAttempts = options.maxAttempts ?? 60;
|
||||
const statusPath = options.statusPath ?? `/generate/record-info?taskId=${encodeURIComponent(taskId)}`;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
const result = await this.get(statusPath);
|
||||
const status = result.data.status;
|
||||
if (status === 'SUCCESS' ||
|
||||
status === 'FAILED' ||
|
||||
status === 'CREATE_TASK_FAILED' ||
|
||||
status === 'GENERATE_AUDIO_FAILED' ||
|
||||
status === 'CALLBACK_EXCEPTION' ||
|
||||
status === 'SENSITIVE_WORD_ERROR') {
|
||||
return result.data;
|
||||
}
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
throw new Error(`Polling timeout for task ${taskId} after ${maxAttempts} attempts`);
|
||||
}
|
||||
}
|
||||
exports.SunoClient = SunoClient;
|
||||
//# sourceMappingURL=client.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { SunoClient } from './client';
|
||||
export declare class ToolHandlers {
|
||||
private readonly client;
|
||||
constructor(client: SunoClient);
|
||||
handle(name: string, args: unknown): Promise<{
|
||||
content: {
|
||||
type: string;
|
||||
text: string;
|
||||
}[];
|
||||
}>;
|
||||
}
|
||||
//# sourceMappingURL=handlers.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAgB,MAAM,UAAU,CAAC;AA2EpD,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,UAAU;IAEzC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CAsUlG"}
|
||||
Vendored
+447
@@ -0,0 +1,447 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ToolHandlers = void 0;
|
||||
const schemas = __importStar(require("./schemas"));
|
||||
const ASYNC_POLLING_PATHS = {
|
||||
'suno_generate_music': '/generate/record-info?taskId=',
|
||||
'suno_extend_music': '/generate/record-info?taskId=',
|
||||
'suno_replace_section': '/generate/record-info?taskId=',
|
||||
'suno_upload_and_cover': '/generate/record-info?taskId=',
|
||||
'suno_upload_and_extend': '/generate/record-info?taskId=',
|
||||
'suno_add_vocals': '/generate/record-info?taskId=',
|
||||
'suno_add_instrumental': '/generate/record-info?taskId=',
|
||||
'suno_generate_mashup': '/generate/record-info?taskId=',
|
||||
'suno_separate_vocals': '/vocal-removal/record-info?taskId=',
|
||||
'suno_separate_stems': '/vocal-removal/record-info?taskId=',
|
||||
'suno_create_video': '/generate/video-info?taskId=',
|
||||
'suno_create_cover': '/generate/cover-info?taskId=',
|
||||
'suno_convert_to_wav': '/generate/wav-info?taskId=',
|
||||
'suno_generate_sounds': '/generate/record-info?taskId=',
|
||||
};
|
||||
function formatTaskResult(data) {
|
||||
const status = data.status;
|
||||
if (status !== 'SUCCESS') {
|
||||
return JSON.stringify({
|
||||
taskId: data.taskId,
|
||||
status,
|
||||
errorCode: data.errorCode,
|
||||
errorMessage: data.errorMessage,
|
||||
}, null, 2);
|
||||
}
|
||||
const response = data.response;
|
||||
const tracks = response?.data ?? response?.sunoData ?? [];
|
||||
return JSON.stringify({
|
||||
taskId: data.taskId,
|
||||
status,
|
||||
operationType: data.operationType,
|
||||
tracks: tracks.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
tags: t.tags,
|
||||
duration: t.duration,
|
||||
prompt: t.prompt,
|
||||
audio_url: t.audio_url ?? t.audioUrl,
|
||||
stream_audio_url: t.stream_audio_url ?? t.streamAudioUrl,
|
||||
image_url: t.image_url ?? t.imageUrl,
|
||||
})),
|
||||
}, null, 2);
|
||||
}
|
||||
async function runAsyncTask(client, taskName, createPath, payload) {
|
||||
const createResult = await client.post(createPath, payload);
|
||||
const taskId = createResult.data.taskId;
|
||||
const statusPathTemplate = ASYNC_POLLING_PATHS[taskName];
|
||||
if (!statusPathTemplate) {
|
||||
return JSON.stringify({ taskId, status: 'PENDING', note: 'Use suno_get_task_status to poll.' }, null, 2);
|
||||
}
|
||||
const statusData = await client.pollTaskStatus(taskId, {
|
||||
statusPath: `${statusPathTemplate}${encodeURIComponent(taskId)}`,
|
||||
});
|
||||
return formatTaskResult(statusData);
|
||||
}
|
||||
class ToolHandlers {
|
||||
client;
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
async handle(name, args) {
|
||||
let resultText;
|
||||
switch (name) {
|
||||
case 'suno_generate_music': {
|
||||
const input = schemas.GenerateMusicSchema.parse(args);
|
||||
const payload = {
|
||||
prompt: input.prompt,
|
||||
customMode: input.customMode,
|
||||
instrumental: input.instrumental,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.negativeTags !== undefined)
|
||||
payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined)
|
||||
payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined)
|
||||
payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined)
|
||||
payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined)
|
||||
payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined)
|
||||
payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined)
|
||||
payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_generate_lyrics': {
|
||||
const input = schemas.GenerateLyricsSchema.parse(args);
|
||||
const result = await this.client.post('/lyrics', {
|
||||
prompt: input.prompt,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
resultText = JSON.stringify({ taskId: result.data.taskId, status: 'PENDING' }, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_generate_sounds': {
|
||||
const input = schemas.GenerateSoundsSchema.parse(args);
|
||||
const payload = {
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.soundLoop !== undefined)
|
||||
payload.soundLoop = input.soundLoop;
|
||||
if (input.soundTempo !== undefined)
|
||||
payload.soundTempo = input.soundTempo;
|
||||
if (input.soundKey !== undefined)
|
||||
payload.soundKey = input.soundKey;
|
||||
if (input.grabLyrics !== undefined)
|
||||
payload.grabLyrics = input.grabLyrics;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/sounds', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_extend_music': {
|
||||
const input = schemas.ExtendMusicSchema.parse(args);
|
||||
const payload = {
|
||||
audioId: input.audioId,
|
||||
defaultParamFlag: input.defaultParamFlag,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined)
|
||||
payload.prompt = input.prompt;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.continueAt !== undefined)
|
||||
payload.continueAt = input.continueAt;
|
||||
if (input.negativeTags !== undefined)
|
||||
payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined)
|
||||
payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined)
|
||||
payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined)
|
||||
payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined)
|
||||
payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined)
|
||||
payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined)
|
||||
payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/extend', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_replace_section': {
|
||||
const input = schemas.ReplaceSectionSchema.parse(args);
|
||||
const payload = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
start: input.start,
|
||||
end: input.end,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.replaceAudioUrl !== undefined)
|
||||
payload.replaceAudioUrl = input.replaceAudioUrl;
|
||||
if (input.customMode !== undefined)
|
||||
payload.customMode = input.customMode;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.instrumental !== undefined)
|
||||
payload.instrumental = input.instrumental;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/replace-section', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_add_vocals': {
|
||||
const input = schemas.AddVocalsSchema.parse(args);
|
||||
const payload = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.customMode !== undefined)
|
||||
payload.customMode = input.customMode;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/add-vocals', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_add_instrumental': {
|
||||
const input = schemas.AddInstrumentalSchema.parse(args);
|
||||
const payload = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.customMode !== undefined)
|
||||
payload.customMode = input.customMode;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/add-instrumental', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_upload_and_cover': {
|
||||
const input = schemas.UploadAndCoverSchema.parse(args);
|
||||
const payload = {
|
||||
uploadUrl: input.uploadUrl,
|
||||
customMode: input.customMode,
|
||||
instrumental: input.instrumental,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined)
|
||||
payload.prompt = input.prompt;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.negativeTags !== undefined)
|
||||
payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined)
|
||||
payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined)
|
||||
payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined)
|
||||
payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined)
|
||||
payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined)
|
||||
payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined)
|
||||
payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/upload-cover', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_upload_and_extend': {
|
||||
const input = schemas.UploadAndExtendSchema.parse(args);
|
||||
const payload = {
|
||||
uploadUrl: input.uploadUrl,
|
||||
defaultParamFlag: input.defaultParamFlag,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined)
|
||||
payload.prompt = input.prompt;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.continueAt !== undefined)
|
||||
payload.continueAt = input.continueAt;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/upload-extend', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_generate_mashup': {
|
||||
const input = schemas.GenerateMashupSchema.parse(args);
|
||||
const payload = {
|
||||
uploadUrlList: input.uploadUrlList,
|
||||
customMode: input.customMode,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.instrumental !== undefined)
|
||||
payload.instrumental = input.instrumental;
|
||||
if (input.style !== undefined)
|
||||
payload.style = input.style;
|
||||
if (input.title !== undefined)
|
||||
payload.title = input.title;
|
||||
if (input.negativeTags !== undefined)
|
||||
payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined)
|
||||
payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined)
|
||||
payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined)
|
||||
payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined)
|
||||
payload.audioWeight = input.audioWeight;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/mashup', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_separate_vocals': {
|
||||
const input = schemas.SeparateVocalsSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/vocal-removal/generate', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'suno_separate_stems': {
|
||||
const input = schemas.SeparateStemsSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/vocal-removal/generate', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
type: 'split_stem',
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'suno_create_video': {
|
||||
const input = schemas.CreateVideoSchema.parse(args);
|
||||
const payload = {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.author !== undefined)
|
||||
payload.author = input.author;
|
||||
if (input.domainName !== undefined)
|
||||
payload.domainName = input.domainName;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/video', payload);
|
||||
break;
|
||||
}
|
||||
case 'suno_create_cover': {
|
||||
const input = schemas.CreateCoverSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/cover', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'suno_convert_to_wav': {
|
||||
const input = schemas.ConvertToWavSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/wav', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'suno_boost_style': {
|
||||
const input = schemas.BoostStyleSchema.parse(args);
|
||||
const result = await this.client.post('/style/generate', {
|
||||
content: input.content,
|
||||
});
|
||||
resultText = result.data.result;
|
||||
break;
|
||||
}
|
||||
case 'suno_generate_persona': {
|
||||
const input = schemas.GeneratePersonaSchema.parse(args);
|
||||
const payload = { taskId: input.taskId };
|
||||
if (input.audioId !== undefined)
|
||||
payload.audioId = input.audioId;
|
||||
const result = await this.client.post('/generate/persona', payload);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_task_status': {
|
||||
const input = schemas.GetTaskStatusSchema.parse(args);
|
||||
const data = await this.client.get(`/generate/record-info?taskId=${encodeURIComponent(input.taskId)}`);
|
||||
resultText = formatTaskResult(data.data);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_timestamped_lyrics': {
|
||||
const input = schemas.GetTimestampedLyricsSchema.parse(args);
|
||||
const result = await this.client.get(`/generate/timestamped-lyrics?taskId=${encodeURIComponent(input.taskId)}&audioId=${encodeURIComponent(input.audioId)}`);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_lyrics_details': {
|
||||
const input = schemas.GetLyricsDetailsSchema.parse(args);
|
||||
const result = await this.client.get(`/generate/lyrics-info?taskId=${encodeURIComponent(input.taskId)}`);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_wav_details': {
|
||||
const input = schemas.GetWavDetailsSchema.parse(args);
|
||||
const result = await this.client.get(`/generate/wav-info?taskId=${encodeURIComponent(input.taskId)}`);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_vocal_separation_details': {
|
||||
const input = schemas.GetVocalSeparationDetailsSchema.parse(args);
|
||||
const result = await this.client.get(`/vocal-removal/record-info?taskId=${encodeURIComponent(input.taskId)}`);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_video_details': {
|
||||
const input = schemas.GetVideoDetailsSchema.parse(args);
|
||||
const result = await this.client.get(`/generate/video-info?taskId=${encodeURIComponent(input.taskId)}`);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'suno_get_credits': {
|
||||
schemas.GetCreditsSchema.parse(args);
|
||||
const result = await this.client.get('/generate/credit');
|
||||
resultText = JSON.stringify({ credits: result.data }, null, 2);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text', text: resultText }],
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.ToolHandlers = ToolHandlers;
|
||||
//# sourceMappingURL=handlers.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const client_js_1 = require("./client.js");
|
||||
const handlers_js_1 = require("./handlers.js");
|
||||
const tools_js_1 = require("./tools.js");
|
||||
function main() {
|
||||
const apiKey = process.env.SUNO_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('SUNO_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
const client = new client_js_1.SunoClient(apiKey);
|
||||
const handlers = new handlers_js_1.ToolHandlers(client);
|
||||
const server = new index_js_1.Server({
|
||||
name: 'suno-mcp-server',
|
||||
version: '1.0.0',
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
});
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
||||
return { tools: tools_js_1.TOOLS };
|
||||
});
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const result = await handlers.handle(request.params.name, request.params.arguments ?? {});
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
server.connect(transport).catch((error) => {
|
||||
console.error('Failed to start MCP server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
main();
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,wEAAmE;AACnE,wEAAiF;AACjF,iEAI4C;AAC5C,2CAAyC;AACzC,+CAA6C;AAC7C,yCAAmC;AAEnC,SAAS,IAAI;IACX,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACxC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,sBAAU,CAAC,MAAM,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,0BAAY,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAG,IAAI,iBAAM,CACvB;QACE,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,OAAO;KACjB,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;SACV;KACF,CACF,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,iCAAsB,EAAE,KAAK,IAAI,EAAE;QAC1D,OAAO,EAAE,KAAK,EAAE,gBAAe,EAAE,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,iBAAiB,CAAC,gCAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;YAC1F,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,OAAO,EAAE,EAAE,CAAC;gBACtD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,+BAAoB,EAAE,CAAC;IAC7C,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACjD,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC"}
|
||||
Vendored
+496
@@ -0,0 +1,496 @@
|
||||
import { z } from 'zod';
|
||||
export declare const GenerateMusicSchema: z.ZodObject<{
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
negativeTags: z.ZodOptional<z.ZodString>;
|
||||
vocalGender: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
||||
styleWeight: z.ZodOptional<z.ZodNumber>;
|
||||
weirdnessConstraint: z.ZodOptional<z.ZodNumber>;
|
||||
audioWeight: z.ZodOptional<z.ZodNumber>;
|
||||
personaId: z.ZodOptional<z.ZodString>;
|
||||
personaModel: z.ZodOptional<z.ZodEnum<["style_persona", "voice_persona"]>>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
customMode: z.ZodBoolean;
|
||||
instrumental: z.ZodBoolean;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
customMode: boolean;
|
||||
instrumental: boolean;
|
||||
model: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
customMode: boolean;
|
||||
instrumental: boolean;
|
||||
model: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const GenerateLyricsSchema: z.ZodObject<{
|
||||
prompt: z.ZodString;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
prompt: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
prompt: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const GenerateSoundsSchema: z.ZodObject<{
|
||||
prompt: z.ZodString;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
soundLoop: z.ZodOptional<z.ZodBoolean>;
|
||||
soundTempo: z.ZodOptional<z.ZodNumber>;
|
||||
soundKey: z.ZodOptional<z.ZodEnum<["Any", "Cm", "C#m", "Dm", "D#m", "Em", "Fm", "F#m", "Gm", "G#m", "Am", "A#m", "Bm", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]>>;
|
||||
grabLyrics: z.ZodOptional<z.ZodBoolean>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
prompt: string;
|
||||
model: string;
|
||||
callBackUrl?: string | undefined;
|
||||
soundLoop?: boolean | undefined;
|
||||
soundTempo?: number | undefined;
|
||||
soundKey?: "Any" | "Cm" | "C#m" | "Dm" | "D#m" | "Em" | "Fm" | "F#m" | "Gm" | "G#m" | "Am" | "A#m" | "Bm" | "C" | "C#" | "D" | "D#" | "E" | "F" | "F#" | "G" | "G#" | "A" | "A#" | "B" | undefined;
|
||||
grabLyrics?: boolean | undefined;
|
||||
}, {
|
||||
prompt: string;
|
||||
model: string;
|
||||
callBackUrl?: string | undefined;
|
||||
soundLoop?: boolean | undefined;
|
||||
soundTempo?: number | undefined;
|
||||
soundKey?: "Any" | "Cm" | "C#m" | "Dm" | "D#m" | "Em" | "Fm" | "F#m" | "Gm" | "G#m" | "Am" | "A#m" | "Bm" | "C" | "C#" | "D" | "D#" | "E" | "F" | "F#" | "G" | "G#" | "A" | "A#" | "B" | undefined;
|
||||
grabLyrics?: boolean | undefined;
|
||||
}>;
|
||||
export declare const ExtendMusicSchema: z.ZodObject<{
|
||||
audioId: z.ZodString;
|
||||
defaultParamFlag: z.ZodBoolean;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
continueAt: z.ZodOptional<z.ZodNumber>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
negativeTags: z.ZodOptional<z.ZodString>;
|
||||
vocalGender: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
||||
styleWeight: z.ZodOptional<z.ZodNumber>;
|
||||
weirdnessConstraint: z.ZodOptional<z.ZodNumber>;
|
||||
audioWeight: z.ZodOptional<z.ZodNumber>;
|
||||
personaId: z.ZodOptional<z.ZodString>;
|
||||
personaModel: z.ZodOptional<z.ZodEnum<["style_persona", "voice_persona"]>>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
defaultParamFlag: boolean;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
continueAt?: number | undefined;
|
||||
}, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
defaultParamFlag: boolean;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
continueAt?: number | undefined;
|
||||
}>;
|
||||
export declare const ReplaceSectionSchema: z.ZodObject<{
|
||||
audioId: z.ZodString;
|
||||
prompt: z.ZodString;
|
||||
replaceAudioUrl: z.ZodOptional<z.ZodString>;
|
||||
start: z.ZodNumber;
|
||||
end: z.ZodNumber;
|
||||
customMode: z.ZodOptional<z.ZodBoolean>;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
instrumental: z.ZodOptional<z.ZodBoolean>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
prompt: string;
|
||||
model: string;
|
||||
audioId: string;
|
||||
start: number;
|
||||
end: number;
|
||||
customMode?: boolean | undefined;
|
||||
instrumental?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
replaceAudioUrl?: string | undefined;
|
||||
}, {
|
||||
prompt: string;
|
||||
model: string;
|
||||
audioId: string;
|
||||
start: number;
|
||||
end: number;
|
||||
customMode?: boolean | undefined;
|
||||
instrumental?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
replaceAudioUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const AddVocalsSchema: z.ZodObject<{
|
||||
audioId: z.ZodString;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
customMode: z.ZodOptional<z.ZodBoolean>;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
prompt?: string | undefined;
|
||||
customMode?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
prompt?: string | undefined;
|
||||
customMode?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const AddInstrumentalSchema: z.ZodObject<{
|
||||
audioId: z.ZodString;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
customMode: z.ZodOptional<z.ZodBoolean>;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
prompt?: string | undefined;
|
||||
customMode?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
model: string;
|
||||
audioId: string;
|
||||
prompt?: string | undefined;
|
||||
customMode?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const UploadAndCoverSchema: z.ZodObject<{
|
||||
uploadUrl: z.ZodString;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
customMode: z.ZodBoolean;
|
||||
instrumental: z.ZodBoolean;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
negativeTags: z.ZodOptional<z.ZodString>;
|
||||
vocalGender: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
||||
styleWeight: z.ZodOptional<z.ZodNumber>;
|
||||
weirdnessConstraint: z.ZodOptional<z.ZodNumber>;
|
||||
audioWeight: z.ZodOptional<z.ZodNumber>;
|
||||
personaId: z.ZodOptional<z.ZodString>;
|
||||
personaModel: z.ZodOptional<z.ZodEnum<["style_persona", "voice_persona"]>>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
customMode: boolean;
|
||||
instrumental: boolean;
|
||||
model: string;
|
||||
uploadUrl: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
customMode: boolean;
|
||||
instrumental: boolean;
|
||||
model: string;
|
||||
uploadUrl: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
personaId?: string | undefined;
|
||||
personaModel?: "style_persona" | "voice_persona" | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const UploadAndExtendSchema: z.ZodObject<{
|
||||
uploadUrl: z.ZodString;
|
||||
defaultParamFlag: z.ZodBoolean;
|
||||
prompt: z.ZodOptional<z.ZodString>;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
continueAt: z.ZodOptional<z.ZodNumber>;
|
||||
model: z.ZodEnum<[string, ...string[]]>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
model: string;
|
||||
defaultParamFlag: boolean;
|
||||
uploadUrl: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
continueAt?: number | undefined;
|
||||
}, {
|
||||
model: string;
|
||||
defaultParamFlag: boolean;
|
||||
uploadUrl: string;
|
||||
prompt?: string | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
continueAt?: number | undefined;
|
||||
}>;
|
||||
export declare const SeparateVocalsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const SeparateStemsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const CreateVideoSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
author: z.ZodOptional<z.ZodString>;
|
||||
domainName: z.ZodOptional<z.ZodString>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
author?: string | undefined;
|
||||
domainName?: string | undefined;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
author?: string | undefined;
|
||||
domainName?: string | undefined;
|
||||
}>;
|
||||
export declare const CreateCoverSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const ConvertToWavSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const BoostStyleSchema: z.ZodObject<{
|
||||
content: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
content: string;
|
||||
}, {
|
||||
content: string;
|
||||
}>;
|
||||
export declare const GeneratePersonaSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GenerateMashupSchema: z.ZodObject<{
|
||||
uploadUrlList: z.ZodArray<z.ZodString, "many">;
|
||||
customMode: z.ZodBoolean;
|
||||
instrumental: z.ZodOptional<z.ZodBoolean>;
|
||||
prompt: z.ZodString;
|
||||
style: z.ZodOptional<z.ZodString>;
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
model: z.ZodEnum<["V4", "V4_5", "V4_5PLUS", "V4_5ALL", "V5"]>;
|
||||
negativeTags: z.ZodOptional<z.ZodString>;
|
||||
vocalGender: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
||||
styleWeight: z.ZodOptional<z.ZodNumber>;
|
||||
weirdnessConstraint: z.ZodOptional<z.ZodNumber>;
|
||||
audioWeight: z.ZodOptional<z.ZodNumber>;
|
||||
callBackUrl: z.ZodOptional<z.ZodString>;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
prompt: string;
|
||||
customMode: boolean;
|
||||
model: "V4" | "V4_5" | "V4_5PLUS" | "V4_5ALL" | "V5";
|
||||
uploadUrlList: string[];
|
||||
instrumental?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}, {
|
||||
prompt: string;
|
||||
customMode: boolean;
|
||||
model: "V4" | "V4_5" | "V4_5PLUS" | "V4_5ALL" | "V5";
|
||||
uploadUrlList: string[];
|
||||
instrumental?: boolean | undefined;
|
||||
style?: string | undefined;
|
||||
title?: string | undefined;
|
||||
negativeTags?: string | undefined;
|
||||
vocalGender?: "m" | "f" | undefined;
|
||||
styleWeight?: number | undefined;
|
||||
weirdnessConstraint?: number | undefined;
|
||||
audioWeight?: number | undefined;
|
||||
callBackUrl?: string | undefined;
|
||||
}>;
|
||||
export declare const GetTaskStatusSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
taskId: string;
|
||||
}, {
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetTimestampedLyricsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
audioId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
}, {
|
||||
audioId: string;
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetLyricsDetailsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
taskId: string;
|
||||
}, {
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetWavDetailsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
taskId: string;
|
||||
}, {
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetVocalSeparationDetailsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
taskId: string;
|
||||
}, {
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetVideoDetailsSchema: z.ZodObject<{
|
||||
taskId: z.ZodString;
|
||||
}, "strict", z.ZodTypeAny, {
|
||||
taskId: string;
|
||||
}, {
|
||||
taskId: string;
|
||||
}>;
|
||||
export declare const GetCreditsSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
|
||||
export type GenerateMusicInput = z.infer<typeof GenerateMusicSchema>;
|
||||
export type GenerateLyricsInput = z.infer<typeof GenerateLyricsSchema>;
|
||||
export type GenerateSoundsInput = z.infer<typeof GenerateSoundsSchema>;
|
||||
export type ExtendMusicInput = z.infer<typeof ExtendMusicSchema>;
|
||||
export type ReplaceSectionInput = z.infer<typeof ReplaceSectionSchema>;
|
||||
export type AddVocalsInput = z.infer<typeof AddVocalsSchema>;
|
||||
export type AddInstrumentalInput = z.infer<typeof AddInstrumentalSchema>;
|
||||
export type UploadAndCoverInput = z.infer<typeof UploadAndCoverSchema>;
|
||||
export type UploadAndExtendInput = z.infer<typeof UploadAndExtendSchema>;
|
||||
export type SeparateVocalsInput = z.infer<typeof SeparateVocalsSchema>;
|
||||
export type SeparateStemsInput = z.infer<typeof SeparateStemsSchema>;
|
||||
export type CreateVideoInput = z.infer<typeof CreateVideoSchema>;
|
||||
export type CreateCoverInput = z.infer<typeof CreateCoverSchema>;
|
||||
export type ConvertToWavInput = z.infer<typeof ConvertToWavSchema>;
|
||||
export type BoostStyleInput = z.infer<typeof BoostStyleSchema>;
|
||||
export type GeneratePersonaInput = z.infer<typeof GeneratePersonaSchema>;
|
||||
export type GenerateMashupInput = z.infer<typeof GenerateMashupSchema>;
|
||||
export type GetTaskStatusInput = z.infer<typeof GetTaskStatusSchema>;
|
||||
export type GetTimestampedLyricsInput = z.infer<typeof GetTimestampedLyricsSchema>;
|
||||
export type GetLyricsDetailsInput = z.infer<typeof GetLyricsDetailsSchema>;
|
||||
export type GetWavDetailsInput = z.infer<typeof GetWavDetailsSchema>;
|
||||
export type GetVocalSeparationDetailsInput = z.infer<typeof GetVocalSeparationDetailsSchema>;
|
||||
export type GetVideoDetailsInput = z.infer<typeof GetVideoDetailsSchema>;
|
||||
//# sourceMappingURL=schemas.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+214
@@ -0,0 +1,214 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GetCreditsSchema = exports.GetVideoDetailsSchema = exports.GetVocalSeparationDetailsSchema = exports.GetWavDetailsSchema = exports.GetLyricsDetailsSchema = exports.GetTimestampedLyricsSchema = exports.GetTaskStatusSchema = exports.GenerateMashupSchema = exports.GeneratePersonaSchema = exports.BoostStyleSchema = exports.ConvertToWavSchema = exports.CreateCoverSchema = exports.CreateVideoSchema = exports.SeparateStemsSchema = exports.SeparateVocalsSchema = exports.UploadAndExtendSchema = exports.UploadAndCoverSchema = exports.AddInstrumentalSchema = exports.AddVocalsSchema = exports.ReplaceSectionSchema = exports.ExtendMusicSchema = exports.GenerateSoundsSchema = exports.GenerateLyricsSchema = exports.GenerateMusicSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const MODELS = ['V4', 'V4_5', 'V4_5PLUS', 'V4_5ALL', 'V5', 'V5_5'];
|
||||
const SOUND_MODELS = ['V5'];
|
||||
const ModelEnum = zod_1.z.enum(MODELS).describe('Model version (V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5)');
|
||||
const SoundModelEnum = zod_1.z.enum(SOUND_MODELS).describe('Sounds model (V5 only)');
|
||||
const PersonaModelEnum = zod_1.z.enum(['style_persona', 'voice_persona']).optional();
|
||||
const CallbackUrl = zod_1.z.string().optional().describe('Optional webhook URL for async completion');
|
||||
const CommonGenerateParams = {
|
||||
model: ModelEnum,
|
||||
negativeTags: zod_1.z.string().optional().describe('Styles to exclude'),
|
||||
vocalGender: zod_1.z.enum(['m', 'f']).optional().describe('Preferred vocal gender'),
|
||||
styleWeight: zod_1.z.number().min(0).max(1).optional().describe('Style weight 0.00-1.00'),
|
||||
weirdnessConstraint: zod_1.z.number().min(0).max(1).optional().describe('Weirdness constraint 0.00-1.00'),
|
||||
audioWeight: zod_1.z.number().min(0).max(1).optional().describe('Audio influence weight 0.00-1.00'),
|
||||
personaId: zod_1.z.string().optional().describe('Persona or voice ID'),
|
||||
personaModel: PersonaModelEnum.describe('Use style_persona or voice_persona'),
|
||||
callBackUrl: CallbackUrl,
|
||||
};
|
||||
// 1. Generate Music (12 credits)
|
||||
exports.GenerateMusicSchema = zod_1.z.object({
|
||||
prompt: zod_1.z.string().optional().describe('Music description / lyrics (max 500 chars non-custom, 3000-5000 custom mode; required when customMode=false or customMode=true with instrumental=false)'),
|
||||
customMode: zod_1.z.boolean().describe('Enable custom mode (requires style and title)'),
|
||||
instrumental: zod_1.z.boolean().describe('Generate instrumental without vocals'),
|
||||
style: zod_1.z.string().optional().describe('Music style/genre (required when customMode=true)'),
|
||||
title: zod_1.z.string().optional().describe('Song title (required when customMode=true)'),
|
||||
...CommonGenerateParams,
|
||||
}).strict();
|
||||
// 2. Generate Lyrics (0.4 credits)
|
||||
exports.GenerateLyricsSchema = zod_1.z.object({
|
||||
prompt: zod_1.z.string().describe('Topic or theme for lyrics'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 3. Generate Sounds (2.5 credits)
|
||||
exports.GenerateSoundsSchema = zod_1.z.object({
|
||||
prompt: zod_1.z.string().max(500).describe('Sound description (max 500 chars)'),
|
||||
model: SoundModelEnum,
|
||||
soundLoop: zod_1.z.boolean().optional().describe('Enable loop playback'),
|
||||
soundTempo: zod_1.z.number().int().min(1).max(300).optional().describe('BPM'),
|
||||
soundKey: zod_1.z.enum([
|
||||
'Any', 'Cm', 'C#m', 'Dm', 'D#m', 'Em', 'Fm', 'F#m', 'Gm', 'G#m', 'Am', 'A#m', 'Bm',
|
||||
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
|
||||
]).optional().describe('Pitch key'),
|
||||
grabLyrics: zod_1.z.boolean().optional().describe('Fetch lyric subtitles'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 4. Extend Music (12 credits)
|
||||
exports.ExtendMusicSchema = zod_1.z.object({
|
||||
audioId: zod_1.z.string().describe('ID of the source track to extend'),
|
||||
defaultParamFlag: zod_1.z.boolean().describe('Use custom params (true) or source params (false)'),
|
||||
prompt: zod_1.z.string().optional().describe('Extension description (required with custom params)'),
|
||||
style: zod_1.z.string().optional().describe('Style (required with custom params)'),
|
||||
title: zod_1.z.string().optional().describe('Title (required with custom params)'),
|
||||
continueAt: zod_1.z.number().positive().optional().describe('Start time in seconds (required with custom params)'),
|
||||
model: ModelEnum,
|
||||
negativeTags: zod_1.z.string().optional(),
|
||||
vocalGender: zod_1.z.enum(['m', 'f']).optional(),
|
||||
styleWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: zod_1.z.number().min(0).max(1).optional(),
|
||||
audioWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
personaId: zod_1.z.string().optional(),
|
||||
personaModel: PersonaModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 5. Replace Section (5 credits)
|
||||
exports.ReplaceSectionSchema = zod_1.z.object({
|
||||
audioId: zod_1.z.string().describe('ID of track to modify (or taskId if replacing within own generated track)'),
|
||||
prompt: zod_1.z.string().describe('Description for replacement section'),
|
||||
replaceAudioUrl: zod_1.z.string().optional().describe('Custom uploaded audio URL for replacement'),
|
||||
start: zod_1.z.number().nonnegative().describe('Start time in seconds'),
|
||||
end: zod_1.z.number().positive().describe('End time in seconds'),
|
||||
customMode: zod_1.z.boolean().optional().describe('Enable custom mode'),
|
||||
style: zod_1.z.string().optional().describe('Style (custom mode)'),
|
||||
title: zod_1.z.string().optional().describe('Title (custom mode)'),
|
||||
instrumental: zod_1.z.boolean().optional().describe('Instrumental (custom mode)'),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 6. Add Vocals (12 credits)
|
||||
exports.AddVocalsSchema = zod_1.z.object({
|
||||
audioId: zod_1.z.string().describe('Instrumental track ID'),
|
||||
prompt: zod_1.z.string().optional().describe('Vocal/lyrics concept'),
|
||||
customMode: zod_1.z.boolean().optional(),
|
||||
style: zod_1.z.string().optional(),
|
||||
title: zod_1.z.string().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 7. Add Instrumental (12 credits)
|
||||
exports.AddInstrumentalSchema = zod_1.z.object({
|
||||
audioId: zod_1.z.string().describe('Vocal or melody track ID'),
|
||||
prompt: zod_1.z.string().optional().describe('Desired backing arrangement'),
|
||||
customMode: zod_1.z.boolean().optional(),
|
||||
style: zod_1.z.string().optional(),
|
||||
title: zod_1.z.string().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 8. Upload and Cover (12 credits)
|
||||
exports.UploadAndCoverSchema = zod_1.z.object({
|
||||
uploadUrl: zod_1.z.string().url().describe('Public URL of audio file to cover (max 8 minutes)'),
|
||||
prompt: zod_1.z.string().optional(),
|
||||
customMode: zod_1.z.boolean(),
|
||||
instrumental: zod_1.z.boolean(),
|
||||
style: zod_1.z.string().optional(),
|
||||
title: zod_1.z.string().optional(),
|
||||
model: ModelEnum,
|
||||
negativeTags: zod_1.z.string().optional(),
|
||||
vocalGender: zod_1.z.enum(['m', 'f']).optional(),
|
||||
styleWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: zod_1.z.number().min(0).max(1).optional(),
|
||||
audioWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
personaId: zod_1.z.string().optional(),
|
||||
personaModel: PersonaModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 9. Upload and Extend (12 credits)
|
||||
exports.UploadAndExtendSchema = zod_1.z.object({
|
||||
uploadUrl: zod_1.z.string().url().describe('Public URL of audio file to extend'),
|
||||
defaultParamFlag: zod_1.z.boolean(),
|
||||
prompt: zod_1.z.string().optional(),
|
||||
style: zod_1.z.string().optional(),
|
||||
title: zod_1.z.string().optional(),
|
||||
continueAt: zod_1.z.number().positive().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 10. Separate Vocals (10 credits)
|
||||
exports.SeparateVocalsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Original generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID to separate'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 11. Separate Stems / Advanced (20 credits)
|
||||
exports.SeparateStemsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Original generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID to separate into stems'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 12. Create Music Video (2 credits)
|
||||
exports.CreateVideoSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID for the video'),
|
||||
author: zod_1.z.string().optional().describe('Artist name'),
|
||||
domainName: zod_1.z.string().optional().describe('Brand domain'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 13. Create Music Cover (0 credits)
|
||||
exports.CreateCoverSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID for cover image'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 14. Convert to WAV (0.4 credits)
|
||||
exports.ConvertToWavSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID to convert'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 15. Boost Style (0.4 credits)
|
||||
exports.BoostStyleSchema = zod_1.z.object({
|
||||
content: zod_1.z.string().describe('Short style description, e.g. "Pop, Mysterious"'),
|
||||
}).strict();
|
||||
// 16. Generate Persona (0 credits)
|
||||
exports.GeneratePersonaSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music generation task ID to derive persona from'),
|
||||
audioId: zod_1.z.string().describe('Track ID to derive persona from'),
|
||||
}).strict();
|
||||
// 17. Generate Mashup (12 credits)
|
||||
exports.GenerateMashupSchema = zod_1.z.object({
|
||||
uploadUrlList: zod_1.z.array(zod_1.z.string().url()).length(2).describe('Exactly 2 audio URLs to mash up'),
|
||||
customMode: zod_1.z.boolean(),
|
||||
instrumental: zod_1.z.boolean().optional(),
|
||||
prompt: zod_1.z.string().describe('Mashup concept / lyrics'),
|
||||
style: zod_1.z.string().optional(),
|
||||
title: zod_1.z.string().optional(),
|
||||
model: zod_1.z.enum(['V4', 'V4_5', 'V4_5PLUS', 'V4_5ALL', 'V5']),
|
||||
negativeTags: zod_1.z.string().optional(),
|
||||
vocalGender: zod_1.z.enum(['m', 'f']).optional(),
|
||||
styleWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: zod_1.z.number().min(0).max(1).optional(),
|
||||
audioWeight: zod_1.z.number().min(0).max(1).optional(),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
// 18. Get Task Status (0 credits)
|
||||
exports.GetTaskStatusSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Task ID to check'),
|
||||
}).strict();
|
||||
// 19. Get Timestamped Lyrics (0.5 credits)
|
||||
exports.GetTimestampedLyricsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music generation task ID'),
|
||||
audioId: zod_1.z.string().describe('Track ID'),
|
||||
}).strict();
|
||||
// 20. Get Lyrics Details (0 credits)
|
||||
exports.GetLyricsDetailsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Lyrics generation task ID'),
|
||||
}).strict();
|
||||
// 21. Get WAV Details (0 credits)
|
||||
exports.GetWavDetailsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('WAV conversion task ID'),
|
||||
}).strict();
|
||||
// 22. Get Vocal Separation Details (0 credits)
|
||||
exports.GetVocalSeparationDetailsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Vocal separation task ID'),
|
||||
}).strict();
|
||||
// 23. Get Video Details (0 credits)
|
||||
exports.GetVideoDetailsSchema = zod_1.z.object({
|
||||
taskId: zod_1.z.string().describe('Music video task ID'),
|
||||
}).strict();
|
||||
// 24. Get Credits (0 credits)
|
||||
exports.GetCreditsSchema = zod_1.z.object({}).strict();
|
||||
//# sourceMappingURL=schemas.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
}
|
||||
export declare const TOOLS: ToolDefinition[];
|
||||
//# sourceMappingURL=tools.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,eAAO,MAAM,KAAK,EAAE,cAAc,EAyHjC,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TOOLS = void 0;
|
||||
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
||||
const schemas_1 = require("./schemas");
|
||||
function makeTool(name, description, schema) {
|
||||
const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, {
|
||||
name,
|
||||
$refStrategy: 'none',
|
||||
});
|
||||
const definition = jsonSchema.definitions?.[name] ?? jsonSchema;
|
||||
definition.additionalProperties = false;
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
inputSchema: definition,
|
||||
};
|
||||
}
|
||||
exports.TOOLS = [
|
||||
makeTool('suno_generate_music', 'Generate music from a text prompt. Cost: 12 credits. Returns 2 tracks. Use customMode=true for full control (style, title, lyrics).', schemas_1.GenerateMusicSchema),
|
||||
makeTool('suno_generate_lyrics', 'Generate lyrics from a topic. Cost: 0.4 credits.', schemas_1.GenerateLyricsSchema),
|
||||
makeTool('suno_generate_sounds', 'Generate sound effects / ambient loops. Cost: 2.5 credits. Model V5 only.', schemas_1.GenerateSoundsSchema),
|
||||
makeTool('suno_extend_music', 'Extend an existing track. Cost: 12 credits. Set defaultParamFlag=false to reuse source parameters.', schemas_1.ExtendMusicSchema),
|
||||
makeTool('suno_replace_section', 'Replace a time segment of a track. Cost: 5 credits. Provide start/end seconds.', schemas_1.ReplaceSectionSchema),
|
||||
makeTool('suno_add_vocals', 'Generate vocals over an instrumental track. Cost: 12 credits.', schemas_1.AddVocalsSchema),
|
||||
makeTool('suno_add_instrumental', 'Generate backing music for a vocal/melody track. Cost: 12 credits.', schemas_1.AddInstrumentalSchema),
|
||||
makeTool('suno_upload_and_cover', 'Upload an audio file URL and create a cover in a new style. Cost: 12 credits. Audio must be <= 8 minutes.', schemas_1.UploadAndCoverSchema),
|
||||
makeTool('suno_upload_and_extend', 'Upload an audio file URL and extend it. Cost: 12 credits.', schemas_1.UploadAndExtendSchema),
|
||||
makeTool('suno_separate_vocals', 'Separate vocals from music. Cost: 10 credits.', schemas_1.SeparateVocalsSchema),
|
||||
makeTool('suno_separate_stems', 'Advanced stem separation (per-instrument). Cost: 20 credits.', schemas_1.SeparateStemsSchema),
|
||||
makeTool('suno_create_video', 'Create an MP4 video from a music track. Cost: 2 credits.', schemas_1.CreateVideoSchema),
|
||||
makeTool('suno_create_cover', 'Generate a cover image for a track. Cost: 0 credits.', schemas_1.CreateCoverSchema),
|
||||
makeTool('suno_convert_to_wav', 'Convert a track to WAV format. Cost: 0.4 credits.', schemas_1.ConvertToWavSchema),
|
||||
makeTool('suno_boost_style', 'Enhance a short style description into a detailed prompt. Cost: 0.4 credits.', schemas_1.BoostStyleSchema),
|
||||
makeTool('suno_generate_persona', 'Create a reusable persona from a track. Cost: 0 credits.', schemas_1.GeneratePersonaSchema),
|
||||
makeTool('suno_generate_mashup', 'Create a mashup from exactly 2 audio URLs. Cost: 12 credits.', schemas_1.GenerateMashupSchema),
|
||||
makeTool('suno_get_task_status', 'Check music/extend/cover task status and get resulting tracks including prompt, title, tags and audio_url. Cost: 0 credits.', schemas_1.GetTaskStatusSchema),
|
||||
makeTool('suno_get_timestamped_lyrics', 'Get timestamped lyrics for a track. Cost: 0.5 credits.', schemas_1.GetTimestampedLyricsSchema),
|
||||
makeTool('suno_get_lyrics_details', 'Get details of a lyrics generation task. Cost: 0 credits.', schemas_1.GetLyricsDetailsSchema),
|
||||
makeTool('suno_get_wav_details', 'Get details of a WAV conversion task. Cost: 0 credits.', schemas_1.GetWavDetailsSchema),
|
||||
makeTool('suno_get_vocal_separation_details', 'Get details of a vocal/stem separation task. Cost: 0 credits.', schemas_1.GetVocalSeparationDetailsSchema),
|
||||
makeTool('suno_get_video_details', 'Get details of a music video task. Cost: 0 credits.', schemas_1.GetVideoDetailsSchema),
|
||||
makeTool('suno_get_credits', 'Get remaining Suno API credits. Cost: 0 credits.', schemas_1.GetCreditsSchema),
|
||||
];
|
||||
//# sourceMappingURL=tools.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":";;;AAAA,2DAAqD;AACrD,uCAyBmB;AAQnB,SAAS,QAAQ,CAAC,IAAY,EAAE,WAAmB,EAAE,MAA8C;IACjG,MAAM,UAAU,GAAG,IAAA,oCAAe,EAAC,MAAa,EAAE;QAChD,IAAI;QACJ,YAAY,EAAE,MAAM;KACrB,CAAC,CAAC;IACH,MAAM,UAAU,GAAI,UAAkB,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC;IACzE,UAAU,CAAC,oBAAoB,GAAG,KAAK,CAAC;IACxC,OAAO;QACL,IAAI;QACJ,WAAW;QACX,WAAW,EAAE,UAAU;KACxB,CAAC;AACJ,CAAC;AAEY,QAAA,KAAK,GAAqB;IACrC,QAAQ,CACN,qBAAqB,EACrB,qIAAqI,EACrI,6BAAmB,CACpB;IACD,QAAQ,CACN,sBAAsB,EACtB,kDAAkD,EAClD,8BAAoB,CACrB;IACD,QAAQ,CACN,sBAAsB,EACtB,2EAA2E,EAC3E,8BAAoB,CACrB;IACD,QAAQ,CACN,mBAAmB,EACnB,oGAAoG,EACpG,2BAAiB,CAClB;IACD,QAAQ,CACN,sBAAsB,EACtB,gFAAgF,EAChF,8BAAoB,CACrB;IACD,QAAQ,CACN,iBAAiB,EACjB,+DAA+D,EAC/D,yBAAe,CAChB;IACD,QAAQ,CACN,uBAAuB,EACvB,oEAAoE,EACpE,+BAAqB,CACtB;IACD,QAAQ,CACN,uBAAuB,EACvB,2GAA2G,EAC3G,8BAAoB,CACrB;IACD,QAAQ,CACN,wBAAwB,EACxB,2DAA2D,EAC3D,+BAAqB,CACtB;IACD,QAAQ,CACN,sBAAsB,EACtB,+CAA+C,EAC/C,8BAAoB,CACrB;IACD,QAAQ,CACN,qBAAqB,EACrB,8DAA8D,EAC9D,6BAAmB,CACpB;IACD,QAAQ,CACN,mBAAmB,EACnB,0DAA0D,EAC1D,2BAAiB,CAClB;IACD,QAAQ,CACN,mBAAmB,EACnB,sDAAsD,EACtD,2BAAiB,CAClB;IACD,QAAQ,CACN,qBAAqB,EACrB,mDAAmD,EACnD,4BAAkB,CACnB;IACD,QAAQ,CACN,kBAAkB,EAClB,8EAA8E,EAC9E,0BAAgB,CACjB;IACD,QAAQ,CACN,uBAAuB,EACvB,0DAA0D,EAC1D,+BAAqB,CACtB;IACD,QAAQ,CACN,sBAAsB,EACtB,8DAA8D,EAC9D,8BAAoB,CACrB;IACD,QAAQ,CACN,sBAAsB,EACtB,6HAA6H,EAC7H,6BAAmB,CACpB;IACD,QAAQ,CACN,6BAA6B,EAC7B,wDAAwD,EACxD,oCAA0B,CAC3B;IACD,QAAQ,CACN,yBAAyB,EACzB,2DAA2D,EAC3D,gCAAsB,CACvB;IACD,QAAQ,CACN,sBAAsB,EACtB,wDAAwD,EACxD,6BAAmB,CACpB;IACD,QAAQ,CACN,mCAAmC,EACnC,+DAA+D,EAC/D,yCAA+B,CAChC;IACD,QAAQ,CACN,wBAAwB,EACxB,qDAAqD,EACrD,+BAAqB,CACtB;IACD,QAAQ,CACN,kBAAkB,EAClB,kDAAkD,EAClD,0BAAgB,CACjB;CACF,CAAC"}
|
||||
Generated
+1124
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "suno-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server for Suno API (sunoapi.org)",
|
||||
"main": "dist/index.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"suno",
|
||||
"music",
|
||||
"ai"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"undici": "^6.27.0",
|
||||
"zod": "^3.25.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Suno API HTTP client with rate limiting and polling helpers.
|
||||
*
|
||||
* Rate limit: 20 requests per 10 seconds (sunoapi.org).
|
||||
* Proxy support via SUNO_HTTP_PROXY / HTTPS_PROXY / HTTP_PROXY env vars.
|
||||
*/
|
||||
|
||||
import { ProxyAgent } from 'undici';
|
||||
|
||||
export interface SunoApiResponse<T = unknown> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface SunoTrack {
|
||||
id: string;
|
||||
audio_url?: string;
|
||||
audioUrl?: string;
|
||||
source_audio_url?: string;
|
||||
stream_audio_url?: string;
|
||||
streamAudioUrl?: string;
|
||||
source_stream_audio_url?: string;
|
||||
image_url?: string;
|
||||
imageUrl?: string;
|
||||
source_image_url?: string;
|
||||
prompt?: string;
|
||||
model_name?: string;
|
||||
modelName?: string;
|
||||
title?: string;
|
||||
tags?: string;
|
||||
createTime?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface SunoTaskData {
|
||||
taskId: string;
|
||||
parentMusicId?: string;
|
||||
param?: string;
|
||||
response?: {
|
||||
taskId?: string;
|
||||
data?: SunoTrack[];
|
||||
sunoData?: SunoTrack[];
|
||||
};
|
||||
status: string;
|
||||
type?: string;
|
||||
operationType?: 'generate' | 'extend' | 'upload_cover' | 'upload_extend';
|
||||
errorCode?: number | null;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
function getProxyUrl(): string | undefined {
|
||||
return (
|
||||
process.env.SUNO_HTTP_PROXY ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy
|
||||
);
|
||||
}
|
||||
|
||||
export class SunoClient {
|
||||
private readonly baseUrl = 'https://api.sunoapi.org/api/v1';
|
||||
private readonly apiKey: string;
|
||||
private readonly maxRequests = 20;
|
||||
private readonly windowMs = 10_000;
|
||||
private readonly proxyAgent?: ProxyAgent;
|
||||
private requestTimestamps: number[] = [];
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
const proxyUrl = getProxyUrl();
|
||||
if (proxyUrl) {
|
||||
this.proxyAgent = new ProxyAgent(proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce rate limit: max 20 requests per 10 seconds.
|
||||
*/
|
||||
private async throttle(): Promise<void> {
|
||||
const now = Date.now();
|
||||
this.requestTimestamps = this.requestTimestamps.filter(
|
||||
(ts) => now - ts < this.windowMs
|
||||
);
|
||||
|
||||
if (this.requestTimestamps.length >= this.maxRequests) {
|
||||
const oldest = this.requestTimestamps[0];
|
||||
const wait = this.windowMs - (now - oldest);
|
||||
if (wait > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, wait));
|
||||
return this.throttle();
|
||||
}
|
||||
}
|
||||
|
||||
this.requestTimestamps.push(now);
|
||||
}
|
||||
|
||||
async request<T = unknown>(
|
||||
method: 'GET' | 'POST',
|
||||
path: string,
|
||||
body?: Record<string, unknown>
|
||||
): Promise<SunoApiResponse<T>> {
|
||||
await this.throttle();
|
||||
|
||||
const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const fetchOptions: RequestInit & { dispatcher?: any } = {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
};
|
||||
if (this.proxyAgent) {
|
||||
fetchOptions.dispatcher = this.proxyAgent;
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
const text = await response.text();
|
||||
let json: SunoApiResponse<T>;
|
||||
try {
|
||||
json = JSON.parse(text) as SunoApiResponse<T>;
|
||||
} catch {
|
||||
throw new Error(`Invalid JSON response from Suno API: ${text}`);
|
||||
}
|
||||
|
||||
if (json.code !== 200) {
|
||||
throw new Error(`Suno API error ${json.code}: ${json.msg || 'unknown error'}`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
get<T = unknown>(path: string): Promise<SunoApiResponse<T>> {
|
||||
return this.request<T>('GET', path);
|
||||
}
|
||||
|
||||
post<T = unknown>(path: string, body: Record<string, unknown>): Promise<SunoApiResponse<T>> {
|
||||
return this.request<T>('POST', path, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll task status until terminal state or timeout.
|
||||
*/
|
||||
async pollTaskStatus(
|
||||
taskId: string,
|
||||
options: {
|
||||
intervalMs?: number;
|
||||
maxAttempts?: number;
|
||||
statusPath?: string;
|
||||
} = {}
|
||||
): Promise<SunoTaskData> {
|
||||
const intervalMs = options.intervalMs ?? 15_000;
|
||||
const maxAttempts = options.maxAttempts ?? 60;
|
||||
const statusPath = options.statusPath ?? `/generate/record-info?taskId=${encodeURIComponent(taskId)}`;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
const result = await this.get<SunoTaskData>(statusPath);
|
||||
const status = result.data.status;
|
||||
|
||||
if (
|
||||
status === 'SUCCESS' ||
|
||||
status === 'FAILED' ||
|
||||
status === 'CREATE_TASK_FAILED' ||
|
||||
status === 'GENERATE_AUDIO_FAILED' ||
|
||||
status === 'CALLBACK_EXCEPTION' ||
|
||||
status === 'SENSITIVE_WORD_ERROR'
|
||||
) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Polling timeout for task ${taskId} after ${maxAttempts} attempts`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
import { SunoClient, SunoTaskData } from './client';
|
||||
import * as schemas from './schemas';
|
||||
|
||||
const ASYNC_POLLING_PATHS: Record<string, string> = {
|
||||
'suno_generate_music': '/generate/record-info?taskId=',
|
||||
'suno_extend_music': '/generate/record-info?taskId=',
|
||||
'suno_replace_section': '/generate/record-info?taskId=',
|
||||
'suno_upload_and_cover': '/generate/record-info?taskId=',
|
||||
'suno_upload_and_extend': '/generate/record-info?taskId=',
|
||||
'suno_add_vocals': '/generate/record-info?taskId=',
|
||||
'suno_add_instrumental': '/generate/record-info?taskId=',
|
||||
'suno_generate_mashup': '/generate/record-info?taskId=',
|
||||
'suno_separate_vocals': '/vocal-removal/record-info?taskId=',
|
||||
'suno_separate_stems': '/vocal-removal/record-info?taskId=',
|
||||
'suno_create_video': '/generate/video-info?taskId=',
|
||||
'suno_create_cover': '/generate/cover-info?taskId=',
|
||||
'suno_convert_to_wav': '/generate/wav-info?taskId=',
|
||||
'suno_generate_sounds': '/generate/record-info?taskId=',
|
||||
};
|
||||
|
||||
function formatTaskResult(data: SunoTaskData): string {
|
||||
const status = data.status;
|
||||
if (status !== 'SUCCESS') {
|
||||
return JSON.stringify({
|
||||
taskId: data.taskId,
|
||||
status,
|
||||
errorCode: data.errorCode,
|
||||
errorMessage: data.errorMessage,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
const response = data.response;
|
||||
const tracks = response?.data ?? response?.sunoData ?? [];
|
||||
return JSON.stringify(
|
||||
{
|
||||
taskId: data.taskId,
|
||||
status,
|
||||
operationType: data.operationType,
|
||||
tracks: tracks.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
tags: t.tags,
|
||||
duration: t.duration,
|
||||
prompt: t.prompt,
|
||||
audio_url: t.audio_url ?? t.audioUrl,
|
||||
stream_audio_url: t.stream_audio_url ?? t.streamAudioUrl,
|
||||
image_url: t.image_url ?? t.imageUrl,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
async function runAsyncTask(
|
||||
client: SunoClient,
|
||||
taskName: string,
|
||||
createPath: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<string> {
|
||||
const createResult = await client.post<{ taskId: string }>(createPath, payload);
|
||||
const taskId = createResult.data.taskId;
|
||||
|
||||
const statusPathTemplate = ASYNC_POLLING_PATHS[taskName];
|
||||
if (!statusPathTemplate) {
|
||||
return JSON.stringify({ taskId, status: 'PENDING', note: 'Use suno_get_task_status to poll.' }, null, 2);
|
||||
}
|
||||
|
||||
const statusData = await client.pollTaskStatus(taskId, {
|
||||
statusPath: `${statusPathTemplate}${encodeURIComponent(taskId)}`,
|
||||
});
|
||||
|
||||
return formatTaskResult(statusData);
|
||||
}
|
||||
|
||||
export class ToolHandlers {
|
||||
constructor(private readonly client: SunoClient) {}
|
||||
|
||||
async handle(name: string, args: unknown): Promise<{ content: { type: string; text: string }[] }> {
|
||||
let resultText: string;
|
||||
|
||||
switch (name) {
|
||||
case 'suno_generate_music': {
|
||||
const input = schemas.GenerateMusicSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
prompt: input.prompt,
|
||||
customMode: input.customMode,
|
||||
instrumental: input.instrumental,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined) payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_generate_lyrics': {
|
||||
const input = schemas.GenerateLyricsSchema.parse(args);
|
||||
const result = await this.client.post<{ taskId: string }>('/lyrics', {
|
||||
prompt: input.prompt,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
resultText = JSON.stringify({ taskId: result.data.taskId, status: 'PENDING' }, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_generate_sounds': {
|
||||
const input = schemas.GenerateSoundsSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.soundLoop !== undefined) payload.soundLoop = input.soundLoop;
|
||||
if (input.soundTempo !== undefined) payload.soundTempo = input.soundTempo;
|
||||
if (input.soundKey !== undefined) payload.soundKey = input.soundKey;
|
||||
if (input.grabLyrics !== undefined) payload.grabLyrics = input.grabLyrics;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/sounds', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_extend_music': {
|
||||
const input = schemas.ExtendMusicSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
audioId: input.audioId,
|
||||
defaultParamFlag: input.defaultParamFlag,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined) payload.prompt = input.prompt;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.continueAt !== undefined) payload.continueAt = input.continueAt;
|
||||
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined) payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/extend', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_replace_section': {
|
||||
const input = schemas.ReplaceSectionSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
start: input.start,
|
||||
end: input.end,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.replaceAudioUrl !== undefined) payload.replaceAudioUrl = input.replaceAudioUrl;
|
||||
if (input.customMode !== undefined) payload.customMode = input.customMode;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.instrumental !== undefined) payload.instrumental = input.instrumental;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/replace-section', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_add_vocals': {
|
||||
const input = schemas.AddVocalsSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.customMode !== undefined) payload.customMode = input.customMode;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/add-vocals', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_add_instrumental': {
|
||||
const input = schemas.AddInstrumentalSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
audioId: input.audioId,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.customMode !== undefined) payload.customMode = input.customMode;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/add-instrumental', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_upload_and_cover': {
|
||||
const input = schemas.UploadAndCoverSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
uploadUrl: input.uploadUrl,
|
||||
customMode: input.customMode,
|
||||
instrumental: input.instrumental,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined) payload.prompt = input.prompt;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
|
||||
if (input.personaId !== undefined) payload.personaId = input.personaId;
|
||||
if (input.personaModel !== undefined) payload.personaModel = input.personaModel;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/upload-cover', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_upload_and_extend': {
|
||||
const input = schemas.UploadAndExtendSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
uploadUrl: input.uploadUrl,
|
||||
defaultParamFlag: input.defaultParamFlag,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.prompt !== undefined) payload.prompt = input.prompt;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.continueAt !== undefined) payload.continueAt = input.continueAt;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/upload-extend', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_generate_mashup': {
|
||||
const input = schemas.GenerateMashupSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
uploadUrlList: input.uploadUrlList,
|
||||
customMode: input.customMode,
|
||||
prompt: input.prompt,
|
||||
model: input.model,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.instrumental !== undefined) payload.instrumental = input.instrumental;
|
||||
if (input.style !== undefined) payload.style = input.style;
|
||||
if (input.title !== undefined) payload.title = input.title;
|
||||
if (input.negativeTags !== undefined) payload.negativeTags = input.negativeTags;
|
||||
if (input.vocalGender !== undefined) payload.vocalGender = input.vocalGender;
|
||||
if (input.styleWeight !== undefined) payload.styleWeight = input.styleWeight;
|
||||
if (input.weirdnessConstraint !== undefined) payload.weirdnessConstraint = input.weirdnessConstraint;
|
||||
if (input.audioWeight !== undefined) payload.audioWeight = input.audioWeight;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/mashup', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_separate_vocals': {
|
||||
const input = schemas.SeparateVocalsSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/vocal-removal/generate', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_separate_stems': {
|
||||
const input = schemas.SeparateStemsSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/vocal-removal/generate', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
type: 'split_stem',
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_create_video': {
|
||||
const input = schemas.CreateVideoSchema.parse(args);
|
||||
const payload: Record<string, unknown> = {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
};
|
||||
if (input.author !== undefined) payload.author = input.author;
|
||||
if (input.domainName !== undefined) payload.domainName = input.domainName;
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/video', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_create_cover': {
|
||||
const input = schemas.CreateCoverSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/cover', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_convert_to_wav': {
|
||||
const input = schemas.ConvertToWavSchema.parse(args);
|
||||
resultText = await runAsyncTask(this.client, name, '/generate/wav', {
|
||||
taskId: input.taskId,
|
||||
audioId: input.audioId,
|
||||
callBackUrl: input.callBackUrl ?? '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_boost_style': {
|
||||
const input = schemas.BoostStyleSchema.parse(args);
|
||||
const result = await this.client.post<{ result: string }>('/style/generate', {
|
||||
content: input.content,
|
||||
});
|
||||
resultText = result.data.result;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_generate_persona': {
|
||||
const input = schemas.GeneratePersonaSchema.parse(args);
|
||||
const payload: Record<string, unknown> = { taskId: input.taskId };
|
||||
if (input.audioId !== undefined) payload.audioId = input.audioId;
|
||||
const result = await this.client.post<{ personaId?: string }>('/generate/persona', payload);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_task_status': {
|
||||
const input = schemas.GetTaskStatusSchema.parse(args);
|
||||
const data = await this.client.get<SunoTaskData>(
|
||||
`/generate/record-info?taskId=${encodeURIComponent(input.taskId)}`
|
||||
);
|
||||
resultText = formatTaskResult(data.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_timestamped_lyrics': {
|
||||
const input = schemas.GetTimestampedLyricsSchema.parse(args);
|
||||
const result = await this.client.get<unknown>(
|
||||
`/generate/timestamped-lyrics?taskId=${encodeURIComponent(input.taskId)}&audioId=${encodeURIComponent(
|
||||
input.audioId
|
||||
)}`
|
||||
);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_lyrics_details': {
|
||||
const input = schemas.GetLyricsDetailsSchema.parse(args);
|
||||
const result = await this.client.get<unknown>(
|
||||
`/generate/lyrics-info?taskId=${encodeURIComponent(input.taskId)}`
|
||||
);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_wav_details': {
|
||||
const input = schemas.GetWavDetailsSchema.parse(args);
|
||||
const result = await this.client.get<unknown>(
|
||||
`/generate/wav-info?taskId=${encodeURIComponent(input.taskId)}`
|
||||
);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_vocal_separation_details': {
|
||||
const input = schemas.GetVocalSeparationDetailsSchema.parse(args);
|
||||
const result = await this.client.get<unknown>(
|
||||
`/vocal-removal/record-info?taskId=${encodeURIComponent(input.taskId)}`
|
||||
);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_video_details': {
|
||||
const input = schemas.GetVideoDetailsSchema.parse(args);
|
||||
const result = await this.client.get<unknown>(
|
||||
`/generate/video-info?taskId=${encodeURIComponent(input.taskId)}`
|
||||
);
|
||||
resultText = JSON.stringify(result.data, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'suno_get_credits': {
|
||||
schemas.GetCreditsSchema.parse(args);
|
||||
const result = await this.client.get<number>('/generate/credit');
|
||||
resultText = JSON.stringify({ credits: result.data }, null, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: resultText }],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { SunoClient } from './client.js';
|
||||
import { ToolHandlers } from './handlers.js';
|
||||
import { TOOLS } from './tools.js';
|
||||
|
||||
function main(): void {
|
||||
const apiKey = process.env.SUNO_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('SUNO_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new SunoClient(apiKey);
|
||||
const handlers = new ToolHandlers(client);
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'suno-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return { tools: TOOLS as Tool[] };
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const result = await handlers.handle(request.params.name, request.params.arguments ?? {});
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
server.connect(transport).catch((error: unknown) => {
|
||||
console.error('Failed to start MCP server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,262 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const MODELS: [string, ...string[]] = ['V4', 'V4_5', 'V4_5PLUS', 'V4_5ALL', 'V5', 'V5_5'];
|
||||
const SOUND_MODELS: [string, ...string[]] = ['V5'];
|
||||
|
||||
const ModelEnum = z.enum(MODELS).describe('Model version (V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5)');
|
||||
const SoundModelEnum = z.enum(SOUND_MODELS).describe('Sounds model (V5 only)');
|
||||
const PersonaModelEnum = z.enum(['style_persona', 'voice_persona']).optional();
|
||||
|
||||
const CallbackUrl = z.string().optional().describe('Optional webhook URL for async completion');
|
||||
|
||||
const CommonGenerateParams = {
|
||||
model: ModelEnum,
|
||||
negativeTags: z.string().optional().describe('Styles to exclude'),
|
||||
vocalGender: z.enum(['m', 'f']).optional().describe('Preferred vocal gender'),
|
||||
styleWeight: z.number().min(0).max(1).optional().describe('Style weight 0.00-1.00'),
|
||||
weirdnessConstraint: z.number().min(0).max(1).optional().describe('Weirdness constraint 0.00-1.00'),
|
||||
audioWeight: z.number().min(0).max(1).optional().describe('Audio influence weight 0.00-1.00'),
|
||||
personaId: z.string().optional().describe('Persona or voice ID'),
|
||||
personaModel: PersonaModelEnum.describe('Use style_persona or voice_persona'),
|
||||
callBackUrl: CallbackUrl,
|
||||
};
|
||||
|
||||
// 1. Generate Music (12 credits)
|
||||
export const GenerateMusicSchema = z.object({
|
||||
prompt: z.string().optional().describe('Music description / lyrics (max 500 chars non-custom, 3000-5000 custom mode; required when customMode=false or customMode=true with instrumental=false)'),
|
||||
customMode: z.boolean().describe('Enable custom mode (requires style and title)'),
|
||||
instrumental: z.boolean().describe('Generate instrumental without vocals'),
|
||||
style: z.string().optional().describe('Music style/genre (required when customMode=true)'),
|
||||
title: z.string().optional().describe('Song title (required when customMode=true)'),
|
||||
...CommonGenerateParams,
|
||||
}).strict();
|
||||
|
||||
// 2. Generate Lyrics (0.4 credits)
|
||||
export const GenerateLyricsSchema = z.object({
|
||||
prompt: z.string().describe('Topic or theme for lyrics'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 3. Generate Sounds (2.5 credits)
|
||||
export const GenerateSoundsSchema = z.object({
|
||||
prompt: z.string().max(500).describe('Sound description (max 500 chars)'),
|
||||
model: SoundModelEnum,
|
||||
soundLoop: z.boolean().optional().describe('Enable loop playback'),
|
||||
soundTempo: z.number().int().min(1).max(300).optional().describe('BPM'),
|
||||
soundKey: z.enum([
|
||||
'Any', 'Cm', 'C#m', 'Dm', 'D#m', 'Em', 'Fm', 'F#m', 'Gm', 'G#m', 'Am', 'A#m', 'Bm',
|
||||
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
|
||||
]).optional().describe('Pitch key'),
|
||||
grabLyrics: z.boolean().optional().describe('Fetch lyric subtitles'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 4. Extend Music (12 credits)
|
||||
export const ExtendMusicSchema = z.object({
|
||||
audioId: z.string().describe('ID of the source track to extend'),
|
||||
defaultParamFlag: z.boolean().describe('Use custom params (true) or source params (false)'),
|
||||
prompt: z.string().optional().describe('Extension description (required with custom params)'),
|
||||
style: z.string().optional().describe('Style (required with custom params)'),
|
||||
title: z.string().optional().describe('Title (required with custom params)'),
|
||||
continueAt: z.number().positive().optional().describe('Start time in seconds (required with custom params)'),
|
||||
model: ModelEnum,
|
||||
negativeTags: z.string().optional(),
|
||||
vocalGender: z.enum(['m', 'f']).optional(),
|
||||
styleWeight: z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: z.number().min(0).max(1).optional(),
|
||||
audioWeight: z.number().min(0).max(1).optional(),
|
||||
personaId: z.string().optional(),
|
||||
personaModel: PersonaModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 5. Replace Section (5 credits)
|
||||
export const ReplaceSectionSchema = z.object({
|
||||
audioId: z.string().describe('ID of track to modify (or taskId if replacing within own generated track)'),
|
||||
prompt: z.string().describe('Description for replacement section'),
|
||||
replaceAudioUrl: z.string().optional().describe('Custom uploaded audio URL for replacement'),
|
||||
start: z.number().nonnegative().describe('Start time in seconds'),
|
||||
end: z.number().positive().describe('End time in seconds'),
|
||||
customMode: z.boolean().optional().describe('Enable custom mode'),
|
||||
style: z.string().optional().describe('Style (custom mode)'),
|
||||
title: z.string().optional().describe('Title (custom mode)'),
|
||||
instrumental: z.boolean().optional().describe('Instrumental (custom mode)'),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 6. Add Vocals (12 credits)
|
||||
export const AddVocalsSchema = z.object({
|
||||
audioId: z.string().describe('Instrumental track ID'),
|
||||
prompt: z.string().optional().describe('Vocal/lyrics concept'),
|
||||
customMode: z.boolean().optional(),
|
||||
style: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 7. Add Instrumental (12 credits)
|
||||
export const AddInstrumentalSchema = z.object({
|
||||
audioId: z.string().describe('Vocal or melody track ID'),
|
||||
prompt: z.string().optional().describe('Desired backing arrangement'),
|
||||
customMode: z.boolean().optional(),
|
||||
style: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 8. Upload and Cover (12 credits)
|
||||
export const UploadAndCoverSchema = z.object({
|
||||
uploadUrl: z.string().url().describe('Public URL of audio file to cover (max 8 minutes)'),
|
||||
prompt: z.string().optional(),
|
||||
customMode: z.boolean(),
|
||||
instrumental: z.boolean(),
|
||||
style: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
model: ModelEnum,
|
||||
negativeTags: z.string().optional(),
|
||||
vocalGender: z.enum(['m', 'f']).optional(),
|
||||
styleWeight: z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: z.number().min(0).max(1).optional(),
|
||||
audioWeight: z.number().min(0).max(1).optional(),
|
||||
personaId: z.string().optional(),
|
||||
personaModel: PersonaModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 9. Upload and Extend (12 credits)
|
||||
export const UploadAndExtendSchema = z.object({
|
||||
uploadUrl: z.string().url().describe('Public URL of audio file to extend'),
|
||||
defaultParamFlag: z.boolean(),
|
||||
prompt: z.string().optional(),
|
||||
style: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
continueAt: z.number().positive().optional(),
|
||||
model: ModelEnum,
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 10. Separate Vocals (10 credits)
|
||||
export const SeparateVocalsSchema = z.object({
|
||||
taskId: z.string().describe('Original generation task ID'),
|
||||
audioId: z.string().describe('Track ID to separate'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 11. Separate Stems / Advanced (20 credits)
|
||||
export const SeparateStemsSchema = z.object({
|
||||
taskId: z.string().describe('Original generation task ID'),
|
||||
audioId: z.string().describe('Track ID to separate into stems'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 12. Create Music Video (2 credits)
|
||||
export const CreateVideoSchema = z.object({
|
||||
taskId: z.string().describe('Music generation task ID'),
|
||||
audioId: z.string().describe('Track ID for the video'),
|
||||
author: z.string().optional().describe('Artist name'),
|
||||
domainName: z.string().optional().describe('Brand domain'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 13. Create Music Cover (0 credits)
|
||||
export const CreateCoverSchema = z.object({
|
||||
taskId: z.string().describe('Music generation task ID'),
|
||||
audioId: z.string().describe('Track ID for cover image'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 14. Convert to WAV (0.4 credits)
|
||||
export const ConvertToWavSchema = z.object({
|
||||
taskId: z.string().describe('Music generation task ID'),
|
||||
audioId: z.string().describe('Track ID to convert'),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 15. Boost Style (0.4 credits)
|
||||
export const BoostStyleSchema = z.object({
|
||||
content: z.string().describe('Short style description, e.g. "Pop, Mysterious"'),
|
||||
}).strict();
|
||||
|
||||
// 16. Generate Persona (0 credits)
|
||||
export const GeneratePersonaSchema = z.object({
|
||||
taskId: z.string().describe('Music generation task ID to derive persona from'),
|
||||
audioId: z.string().describe('Track ID to derive persona from'),
|
||||
}).strict();
|
||||
|
||||
// 17. Generate Mashup (12 credits)
|
||||
export const GenerateMashupSchema = z.object({
|
||||
uploadUrlList: z.array(z.string().url()).length(2).describe('Exactly 2 audio URLs to mash up'),
|
||||
customMode: z.boolean(),
|
||||
instrumental: z.boolean().optional(),
|
||||
prompt: z.string().describe('Mashup concept / lyrics'),
|
||||
style: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
model: z.enum(['V4', 'V4_5', 'V4_5PLUS', 'V4_5ALL', 'V5']),
|
||||
negativeTags: z.string().optional(),
|
||||
vocalGender: z.enum(['m', 'f']).optional(),
|
||||
styleWeight: z.number().min(0).max(1).optional(),
|
||||
weirdnessConstraint: z.number().min(0).max(1).optional(),
|
||||
audioWeight: z.number().min(0).max(1).optional(),
|
||||
callBackUrl: CallbackUrl,
|
||||
}).strict();
|
||||
|
||||
// 18. Get Task Status (0 credits)
|
||||
export const GetTaskStatusSchema = z.object({
|
||||
taskId: z.string().describe('Task ID to check'),
|
||||
}).strict();
|
||||
|
||||
// 19. Get Timestamped Lyrics (0.5 credits)
|
||||
export const GetTimestampedLyricsSchema = z.object({
|
||||
taskId: z.string().describe('Music generation task ID'),
|
||||
audioId: z.string().describe('Track ID'),
|
||||
}).strict();
|
||||
|
||||
// 20. Get Lyrics Details (0 credits)
|
||||
export const GetLyricsDetailsSchema = z.object({
|
||||
taskId: z.string().describe('Lyrics generation task ID'),
|
||||
}).strict();
|
||||
|
||||
// 21. Get WAV Details (0 credits)
|
||||
export const GetWavDetailsSchema = z.object({
|
||||
taskId: z.string().describe('WAV conversion task ID'),
|
||||
}).strict();
|
||||
|
||||
// 22. Get Vocal Separation Details (0 credits)
|
||||
export const GetVocalSeparationDetailsSchema = z.object({
|
||||
taskId: z.string().describe('Vocal separation task ID'),
|
||||
}).strict();
|
||||
|
||||
// 23. Get Video Details (0 credits)
|
||||
export const GetVideoDetailsSchema = z.object({
|
||||
taskId: z.string().describe('Music video task ID'),
|
||||
}).strict();
|
||||
|
||||
// 24. Get Credits (0 credits)
|
||||
export const GetCreditsSchema = z.object({}).strict();
|
||||
|
||||
export type GenerateMusicInput = z.infer<typeof GenerateMusicSchema>;
|
||||
export type GenerateLyricsInput = z.infer<typeof GenerateLyricsSchema>;
|
||||
export type GenerateSoundsInput = z.infer<typeof GenerateSoundsSchema>;
|
||||
export type ExtendMusicInput = z.infer<typeof ExtendMusicSchema>;
|
||||
export type ReplaceSectionInput = z.infer<typeof ReplaceSectionSchema>;
|
||||
export type AddVocalsInput = z.infer<typeof AddVocalsSchema>;
|
||||
export type AddInstrumentalInput = z.infer<typeof AddInstrumentalSchema>;
|
||||
export type UploadAndCoverInput = z.infer<typeof UploadAndCoverSchema>;
|
||||
export type UploadAndExtendInput = z.infer<typeof UploadAndExtendSchema>;
|
||||
export type SeparateVocalsInput = z.infer<typeof SeparateVocalsSchema>;
|
||||
export type SeparateStemsInput = z.infer<typeof SeparateStemsSchema>;
|
||||
export type CreateVideoInput = z.infer<typeof CreateVideoSchema>;
|
||||
export type CreateCoverInput = z.infer<typeof CreateCoverSchema>;
|
||||
export type ConvertToWavInput = z.infer<typeof ConvertToWavSchema>;
|
||||
export type BoostStyleInput = z.infer<typeof BoostStyleSchema>;
|
||||
export type GeneratePersonaInput = z.infer<typeof GeneratePersonaSchema>;
|
||||
export type GenerateMashupInput = z.infer<typeof GenerateMashupSchema>;
|
||||
export type GetTaskStatusInput = z.infer<typeof GetTaskStatusSchema>;
|
||||
export type GetTimestampedLyricsInput = z.infer<typeof GetTimestampedLyricsSchema>;
|
||||
export type GetLyricsDetailsInput = z.infer<typeof GetLyricsDetailsSchema>;
|
||||
export type GetWavDetailsInput = z.infer<typeof GetWavDetailsSchema>;
|
||||
export type GetVocalSeparationDetailsInput = z.infer<typeof GetVocalSeparationDetailsSchema>;
|
||||
export type GetVideoDetailsInput = z.infer<typeof GetVideoDetailsSchema>;
|
||||
@@ -0,0 +1,170 @@
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import {
|
||||
GenerateMusicSchema,
|
||||
GenerateLyricsSchema,
|
||||
GenerateSoundsSchema,
|
||||
ExtendMusicSchema,
|
||||
ReplaceSectionSchema,
|
||||
AddVocalsSchema,
|
||||
AddInstrumentalSchema,
|
||||
UploadAndCoverSchema,
|
||||
UploadAndExtendSchema,
|
||||
SeparateVocalsSchema,
|
||||
SeparateStemsSchema,
|
||||
CreateVideoSchema,
|
||||
CreateCoverSchema,
|
||||
ConvertToWavSchema,
|
||||
BoostStyleSchema,
|
||||
GeneratePersonaSchema,
|
||||
GenerateMashupSchema,
|
||||
GetTaskStatusSchema,
|
||||
GetTimestampedLyricsSchema,
|
||||
GetLyricsDetailsSchema,
|
||||
GetWavDetailsSchema,
|
||||
GetVocalSeparationDetailsSchema,
|
||||
GetVideoDetailsSchema,
|
||||
GetCreditsSchema,
|
||||
} from './schemas';
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
}
|
||||
|
||||
function makeTool(name: string, description: string, schema: { safeParse: (v: unknown) => unknown }): ToolDefinition {
|
||||
const jsonSchema = zodToJsonSchema(schema as any, {
|
||||
name,
|
||||
$refStrategy: 'none',
|
||||
});
|
||||
const definition = (jsonSchema as any).definitions?.[name] ?? jsonSchema;
|
||||
definition.additionalProperties = false;
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
inputSchema: definition,
|
||||
};
|
||||
}
|
||||
|
||||
export const TOOLS: ToolDefinition[] = [
|
||||
makeTool(
|
||||
'suno_generate_music',
|
||||
'Generate music from a text prompt. Cost: 12 credits. Returns 2 tracks. Use customMode=true for full control (style, title, lyrics).',
|
||||
GenerateMusicSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_generate_lyrics',
|
||||
'Generate lyrics from a topic. Cost: 0.4 credits.',
|
||||
GenerateLyricsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_generate_sounds',
|
||||
'Generate sound effects / ambient loops. Cost: 2.5 credits. Model V5 only.',
|
||||
GenerateSoundsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_extend_music',
|
||||
'Extend an existing track. Cost: 12 credits. Set defaultParamFlag=false to reuse source parameters.',
|
||||
ExtendMusicSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_replace_section',
|
||||
'Replace a time segment of a track. Cost: 5 credits. Provide start/end seconds.',
|
||||
ReplaceSectionSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_add_vocals',
|
||||
'Generate vocals over an instrumental track. Cost: 12 credits.',
|
||||
AddVocalsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_add_instrumental',
|
||||
'Generate backing music for a vocal/melody track. Cost: 12 credits.',
|
||||
AddInstrumentalSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_upload_and_cover',
|
||||
'Upload an audio file URL and create a cover in a new style. Cost: 12 credits. Audio must be <= 8 minutes.',
|
||||
UploadAndCoverSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_upload_and_extend',
|
||||
'Upload an audio file URL and extend it. Cost: 12 credits.',
|
||||
UploadAndExtendSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_separate_vocals',
|
||||
'Separate vocals from music. Cost: 10 credits.',
|
||||
SeparateVocalsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_separate_stems',
|
||||
'Advanced stem separation (per-instrument). Cost: 20 credits.',
|
||||
SeparateStemsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_create_video',
|
||||
'Create an MP4 video from a music track. Cost: 2 credits.',
|
||||
CreateVideoSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_create_cover',
|
||||
'Generate a cover image for a track. Cost: 0 credits.',
|
||||
CreateCoverSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_convert_to_wav',
|
||||
'Convert a track to WAV format. Cost: 0.4 credits.',
|
||||
ConvertToWavSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_boost_style',
|
||||
'Enhance a short style description into a detailed prompt. Cost: 0.4 credits.',
|
||||
BoostStyleSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_generate_persona',
|
||||
'Create a reusable persona from a track. Cost: 0 credits.',
|
||||
GeneratePersonaSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_generate_mashup',
|
||||
'Create a mashup from exactly 2 audio URLs. Cost: 12 credits.',
|
||||
GenerateMashupSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_task_status',
|
||||
'Check music/extend/cover task status and get resulting tracks including prompt, title, tags and audio_url. Cost: 0 credits.',
|
||||
GetTaskStatusSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_timestamped_lyrics',
|
||||
'Get timestamped lyrics for a track. Cost: 0.5 credits.',
|
||||
GetTimestampedLyricsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_lyrics_details',
|
||||
'Get details of a lyrics generation task. Cost: 0 credits.',
|
||||
GetLyricsDetailsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_wav_details',
|
||||
'Get details of a WAV conversion task. Cost: 0 credits.',
|
||||
GetWavDetailsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_vocal_separation_details',
|
||||
'Get details of a vocal/stem separation task. Cost: 0 credits.',
|
||||
GetVocalSeparationDetailsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_video_details',
|
||||
'Get details of a music video task. Cost: 0 credits.',
|
||||
GetVideoDetailsSchema
|
||||
),
|
||||
makeTool(
|
||||
'suno_get_credits',
|
||||
'Get remaining Suno API credits. Cost: 0 credits.',
|
||||
GetCreditsSchema
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,93 @@
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const apiKey = process.env.SUNO_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('Usage: SUNO_API_KEY=your-key node test-real.js');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = spawn('node', ['dist/index.js'], {
|
||||
env: { ...process.env, SUNO_API_KEY: apiKey },
|
||||
cwd: __dirname,
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let initialized = false;
|
||||
let step = 0;
|
||||
|
||||
const requests = [
|
||||
{ id: 1, name: 'suno_get_credits', args: {} },
|
||||
{ id: 2, name: 'suno_boost_style', args: { content: 'Pop, mysterious' } },
|
||||
];
|
||||
|
||||
function sendRequest(id, name, args) {
|
||||
server.stdin.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method: 'tools/call',
|
||||
params: { name, arguments: args },
|
||||
}) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
server.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || !line.trim().startsWith('{')) continue;
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
if (!initialized && msg.id === 0) {
|
||||
initialized = true;
|
||||
const req = requests[step++];
|
||||
sendRequest(req.id, req.name, req.args);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.id === 1) {
|
||||
console.log('\n=== suno_get_credits ===');
|
||||
console.log(JSON.stringify(msg.result, null, 2));
|
||||
const req = requests[step++];
|
||||
sendRequest(req.id, req.name, req.args);
|
||||
} else if (msg.id === 2) {
|
||||
console.log('\n=== suno_boost_style ===');
|
||||
console.log(JSON.stringify(msg.result, null, 2));
|
||||
server.kill();
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore non-json lines
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.stderr.on('data', (data) => {
|
||||
console.error('STDERR:', data.toString().slice(0, 500));
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error('Server error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
server.stdin.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test', version: '1.0' },
|
||||
},
|
||||
}) + '\n'
|
||||
);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
console.error('Test timeout');
|
||||
server.kill();
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user