mcp-ai-music 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +256 -0
  2. package/dist/index.js +836 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # AI作曲 MCP 服务
2
+
3
+ **版本 (Version):** 1.0.0
4
+
5
+ ## 描述 (Description)
6
+
7
+ `mcp-ai-music` 是一个基于 Suno4.5 API 的 AI 作曲 MCP 服务,提供音乐生成、翻唱、扩展和进度查询功能。该服务支持多种音乐风格,可以生成原创音乐、对现有音乐进行翻唱转换,以及扩展音乐长度。
8
+
9
+ ## 功能特点
10
+
11
+ - **原创音乐生成**:根据提示词和风格生成全新的音乐作品
12
+ - **音乐翻唱**:将现有音乐转换为不同风格,保留核心旋律
13
+ - **音乐扩展**:在保持原始风格的基础上延长音乐时长
14
+ - **实时进度查询**:支持轮询查询任务进度,实时了解生成状态
15
+ - **多模型支持**:支持 V3_5、V4、V4_5 多个AI模型版本
16
+ - **纯音乐模式**:支持生成无歌词的纯音乐版本
17
+
18
+ ## 环境配置
19
+
20
+ > **重要**:您需要拥有 Suno API 的密钥才能使用此服务。请将密钥配置为环境变量 `SUNO_API_KEY`。
21
+
22
+ ### 费用说明
23
+ - 生成音乐:¥0.438/次
24
+ - 翻唱音乐:¥0.438/次
25
+ - 扩展音乐:¥0.438/次
26
+ - 查询进度:免费
27
+
28
+ ## 使用方法
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "mcp-ai-music": {
34
+ "command": "node",
35
+ "args": [
36
+ "dist/index.js"
37
+ ],
38
+ "env": {
39
+ "SUNO_API_KEY": "您的Suno API密钥"
40
+ },
41
+ "autoApprove": [
42
+ "generate_music",
43
+ "cover_music",
44
+ "extend_music",
45
+ "query_progress"
46
+ ]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## 可用工具 (Available Tools)
53
+
54
+ 该服务提供以下4个工具:
55
+
56
+ ### 1. `generate_music` - 生成音乐
57
+
58
+ 生成原创音乐作品。
59
+
60
+ **输入参数:**
61
+ - `prompt` (必需): 音乐描述提示词,详细描述想要的音乐风格、情感、乐器等
62
+ - V3_5/V4 模型:最多 3000 字符
63
+ - V4_5 模型:最多 5000 字符
64
+ - `style` (可选): 音乐风格,如"古典"、"流行"、"摇滚"等
65
+ - V3_5/V4 模型:最多 200 字符
66
+ - V4_5 模型:最多 1000 字符
67
+ - `title` (可选): 音乐标题,最多 80 字符
68
+ - `instrumental` (可选): 是否生成纯音乐(无歌词),默认 false
69
+ - `model` (可选): AI模型版本,可选 "V3_5"、"V4"、"V4_5",默认 "V4_5"
70
+ - `negativeTags` (可选): 负面标签,描述不想要的音乐元素
71
+
72
+ **调用示例:**
73
+ ```json
74
+ {
75
+ "name": "generate_music",
76
+ "arguments": {
77
+ "prompt": "一段平静舒缓的钢琴曲,带有柔和的旋律,适合冥想和放松",
78
+ "style": "古典",
79
+ "title": "宁静钢琴冥想",
80
+ "instrumental": true,
81
+ "model": "V4_5",
82
+ "negativeTags": "重金属, 强节奏鼓点"
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### 2. `cover_music` - 翻唱音乐
88
+
89
+ 将现有音乐转换为新的风格,保留核心旋律。
90
+
91
+ **输入参数:**
92
+ - `uploadUrl` (必需): 要翻唱的音频文件URL,音频长度不超过2分钟
93
+ - `prompt` (必需): 翻唱风格描述
94
+ - `style` (可选): 目标音乐风格
95
+ - `title` (可选): 翻唱版本标题
96
+ - `instrumental` (可选): 是否生成纯音乐版本,默认 false
97
+ - `model` (可选): AI模型版本,默认 "V4_5"
98
+ - `negativeTags` (可选): 负面标签
99
+
100
+ **调用示例:**
101
+ ```json
102
+ {
103
+ "name": "cover_music",
104
+ "arguments": {
105
+ "uploadUrl": "https://example.com/audio.mp3",
106
+ "prompt": "将这首歌转换为爵士风格",
107
+ "style": "爵士",
108
+ "title": "爵士翻唱版",
109
+ "instrumental": false
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### 3. `extend_music` - 扩展音乐
115
+
116
+ 在保留原始音频风格的同时扩展音轨长度。
117
+
118
+ **输入参数:**
119
+ - `uploadUrl` (必需): 要扩展的音频文件URL,音频长度不超过2分钟
120
+ - `prompt` (必需): 扩展描述,如"用更多舒缓的音符延长音乐"
121
+ - `continueAt` (必需): 从音频的第几秒开始扩展,必须大于0且小于音频总时长
122
+ - `style` (可选): 保持的音乐风格
123
+ - `title` (可选): 扩展版本标题
124
+ - `instrumental` (可选): 是否生成纯音乐版本,默认 false
125
+ - `model` (可选): AI模型版本,必须与源音乐保持一致,默认 "V4_5"
126
+ - `negativeTags` (可选): 负面标签
127
+
128
+ **调用示例:**
129
+ ```json
130
+ {
131
+ "name": "extend_music",
132
+ "arguments": {
133
+ "uploadUrl": "https://example.com/audio.mp3",
134
+ "prompt": "用更多舒缓的音符延长音乐,保持原有的宁静氛围",
135
+ "continueAt": 60,
136
+ "style": "古典",
137
+ "title": "宁静钢琴延长版",
138
+ "instrumental": true
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### 4. `query_progress` - 查询进度
144
+
145
+ 查询音乐生成任务的进度状态。
146
+
147
+ > **重要提示**:由于AI生成音乐需要时间,大模型需要轮询此接口来获取任务状态和结果。建议每10-30秒查询一次,直到状态为'complete'或'failed'。
148
+
149
+ **输入参数:**
150
+ - `taskId` (必需): 音乐生成任务的ID(从其他工具的返回结果中获取)
151
+
152
+ **调用示例:**
153
+ ```json
154
+ {
155
+ "name": "query_progress",
156
+ "arguments": {
157
+ "taskId": "task_12345"
158
+ }
159
+ }
160
+ ```
161
+
162
+ **状态说明:**
163
+ - `pending`: 任务排队中
164
+ - `processing`: 正在处理中
165
+ - `text`: 文本生成完成,正在生成音频
166
+ - `first`: 第一首音乐生成完成
167
+ - `complete`: 任务完成
168
+ - `failed`: 任务失败
169
+
170
+ ## 工作流程示例
171
+
172
+ ### 生成原创音乐的完整流程:
173
+
174
+ 1. **发起生成请求**
175
+ ```json
176
+ {
177
+ "name": "generate_music",
178
+ "arguments": {
179
+ "prompt": "一首欢快的流行歌曲,适合夏天",
180
+ "style": "流行",
181
+ "title": "夏日阳光"
182
+ }
183
+ }
184
+ ```
185
+
186
+ 2. **获取任务ID**
187
+ ```json
188
+ {
189
+ "taskId": "task_abc123",
190
+ "status": "pending",
191
+ "message": "音乐生成任务已提交,请使用query_progress工具查询进度。",
192
+ "cost": "¥0.438"
193
+ }
194
+ ```
195
+
196
+ 3. **轮询查询进度**
197
+ ```json
198
+ {
199
+ "name": "query_progress",
200
+ "arguments": {
201
+ "taskId": "task_abc123"
202
+ }
203
+ }
204
+ ```
205
+
206
+ 4. **获取最终结果**
207
+ 当状态变为'complete'时,会返回包含音乐文件信息的完整结果。
208
+
209
+ ## 注意事项
210
+
211
+ 1. **API密钥安全**:请确保 `SUNO_API_KEY` 环境变量已正确设置,不要在代码中硬编码API密钥。
212
+
213
+ 2. **文件大小限制**:上传的音频文件长度不得超过2分钟。
214
+
215
+ 3. **文件保存期限**:生成的音乐文件在服务器上保留15天后会被删除。
216
+
217
+ 4. **费用控制**:每次调用生成、翻唱、扩展功能都会产生¥0.438的费用,请合理使用。
218
+
219
+ 5. **轮询间隔**:建议查询进度的间隔为10-30秒,避免过于频繁的请求。
220
+
221
+ 6. **模型兼容性**:在扩展音乐时,使用的模型版本必须与源音乐的生成模型保持一致。
222
+
223
+ 7. **字符限制**:请注意各个参数的字符长度限制,超出限制会导致请求失败。
224
+
225
+ ## 错误处理
226
+
227
+ 服务会返回详细的错误信息,包括:
228
+ - 参数验证错误
229
+ - API请求失败
230
+ - 字符长度超限
231
+ - 任务状态异常
232
+
233
+ 所有错误都会在响应中通过 `isError` 字段标识,并在 `errorMessage` 字段中提供具体的错误描述。
234
+
235
+ ## 技术实现
236
+
237
+ - 基于 Model Context Protocol (MCP) SDK 构建
238
+ - 使用 Suno4.5 API 进行音乐生成
239
+ - 支持 TypeScript 开发
240
+ - 提供完整的类型定义和输入验证
241
+
242
+ ## 开发和构建
243
+
244
+ ```bash
245
+ # 安装依赖
246
+ npm install
247
+
248
+ # 开发模式运行
249
+ npm run dev
250
+
251
+ # 构建项目
252
+ npm run build
253
+
254
+ # 启动服务
255
+ npm start
256
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,836 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
10
+ const node_fetch_1 = __importDefault(require("node-fetch"));
11
+ // API 基础配置
12
+ const API_BASE_URL = "https://api.sunoapi.org/api/v1";
13
+ // 获取API密钥的函数
14
+ function getApiKey() {
15
+ const apiKey = process.env.SUNO_API_KEY;
16
+ if (!apiKey) {
17
+ throw new Error("请设置 SUNO_API_KEY 环境变量");
18
+ }
19
+ return apiKey;
20
+ }
21
+ // 统一的服务端基地址:回调与查询都从此派生(向后兼容旧变量)
22
+ function getServiceBase() {
23
+ return (process.env.SUNO_SERVICE_BASE ||
24
+ process.env.SUNO_API_BASE ||
25
+ process.env.SUNO_CALLBACK_BASE ||
26
+ process.env.SUNO_QUERY_BASE_URL ||
27
+ process.env.SUNO_CALLBACK_URL);
28
+ }
29
+ // 构造回调地址
30
+ function buildCallbackUrl(kind) {
31
+ const base = getServiceBase();
32
+ if (!base) {
33
+ throw new Error("未配置 SUNO_SERVICE_BASE(或兼容变量),用于设置服务端基地址");
34
+ }
35
+ const pathMap = {
36
+ "generate": "/suno/callback/generate",
37
+ "upload-cover": "/suno/callback/upload-cover",
38
+ "upload-extend": "/suno/callback/upload-extend",
39
+ };
40
+ return `${base.replace(/\/$/, "")}${pathMap[kind]}`;
41
+ }
42
+ // 查询同样使用统一基地址
43
+ function getQueryBase() {
44
+ return getServiceBase();
45
+ }
46
+ // 工具输出模式定义
47
+ const MUSIC_GENERATION_OUTPUT_SCHEMA = {
48
+ type: "object",
49
+ description: "音乐生成结果的结构",
50
+ properties: {
51
+ content: {
52
+ type: "array",
53
+ description: "结果数组。成功时包含任务信息,失败时为空。",
54
+ items: {
55
+ type: "object",
56
+ properties: {
57
+ type: { type: "string", description: "内容类型" },
58
+ text: { type: "string", description: "JSON格式的任务信息" },
59
+ },
60
+ },
61
+ },
62
+ isError: { type: "boolean", description: "请求是否发生错误" },
63
+ errorMessage: { type: "string", description: "错误信息(如果 isError 为 true 时提供)" },
64
+ },
65
+ };
66
+ const PROGRESS_QUERY_OUTPUT_SCHEMA = {
67
+ type: "object",
68
+ description: "进度查询结果的结构",
69
+ properties: {
70
+ content: {
71
+ type: "array",
72
+ description: "结果数组。成功时包含进度信息,失败时为空。",
73
+ items: {
74
+ type: "object",
75
+ properties: {
76
+ type: { type: "string", description: "内容类型" },
77
+ text: { type: "string", description: "JSON格式的进度信息" },
78
+ },
79
+ },
80
+ },
81
+ isError: { type: "boolean", description: "请求是否发生错误" },
82
+ errorMessage: { type: "string", description: "错误信息(如果 isError 为 true 时提供)" },
83
+ },
84
+ };
85
+ // 工具定义
86
+ const GENERATE_MUSIC_TOOL = {
87
+ name: "generate_music",
88
+ description: "生成音乐。根据提示词和风格生成原创音乐,支持自定义模式和纯音乐模式。\n\n注意:生成需要时间,请使用query_progress工具查询进度。\n\n返回字段说明:\n- taskId: 任务ID,用于查询进度\n- status: 任务状态 (pending)\n- message: 状态描述",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ prompt: {
93
+ type: "string",
94
+ description: "音乐描述提示词,详细描述想要的音乐风格、情感、乐器等。长度限制:V3_5和V4模型3000字符,V4_5模型5000字符。",
95
+ },
96
+ style: {
97
+ type: "string",
98
+ description: "音乐风格,如'古典'、'流行'、'摇滚'等。长度限制:V3_5和V4模型200字符,V4_5模型1000字符。",
99
+ default: "",
100
+ },
101
+ title: {
102
+ type: "string",
103
+ description: "音乐标题,最多80字符。",
104
+ default: "",
105
+ },
106
+ instrumental: {
107
+ type: "boolean",
108
+ description: "是否生成纯音乐(无歌词)。",
109
+ default: false,
110
+ },
111
+ model: {
112
+ type: "string",
113
+ enum: ["V3_5", "V4", "V4_5"],
114
+ description: "使用的AI模型版本。",
115
+ default: "V4_5",
116
+ },
117
+ negativeTags: {
118
+ type: "string",
119
+ description: "负面标签,描述不想要的音乐元素。",
120
+ default: "",
121
+ },
122
+ },
123
+ required: ["prompt"],
124
+ },
125
+ // outputSchema: MUSIC_GENERATION_OUTPUT_SCHEMA,
126
+ };
127
+ const COVER_MUSIC_TOOL = {
128
+ name: "cover_music",
129
+ description: "翻唱音乐。上传音频文件并转换为新的风格,保留核心旋律。\n\n注意:生成需要时间,请使用query_progress工具查询进度。\n\n返回字段说明:\n- taskId: 任务ID,用于查询进度\n- status: 任务状态 (pending)\n- message: 状态描述",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ uploadUrl: {
134
+ type: "string",
135
+ description: "要翻唱的音频文件URL,音频长度不超过2分钟。",
136
+ },
137
+ prompt: {
138
+ type: "string",
139
+ description: "翻唱风格描述。",
140
+ },
141
+ style: {
142
+ type: "string",
143
+ description: "目标音乐风格。",
144
+ default: "",
145
+ },
146
+ title: {
147
+ type: "string",
148
+ description: "翻唱版本标题。",
149
+ default: "",
150
+ },
151
+ instrumental: {
152
+ type: "boolean",
153
+ description: "是否生成纯音乐版本。",
154
+ default: false,
155
+ },
156
+ model: {
157
+ type: "string",
158
+ enum: ["V3_5", "V4", "V4_5"],
159
+ description: "使用的AI模型版本。",
160
+ default: "V4_5",
161
+ },
162
+ negativeTags: {
163
+ type: "string",
164
+ description: "负面标签。",
165
+ default: "",
166
+ },
167
+ },
168
+ required: ["uploadUrl", "prompt"],
169
+ },
170
+ outputSchema: MUSIC_GENERATION_OUTPUT_SCHEMA,
171
+ };
172
+ const EXTEND_MUSIC_TOOL = {
173
+ name: "extend_music",
174
+ description: "扩展音乐。在保留原始音频风格的同时扩展音轨长度。\n\n注意:生成需要时间,请使用query_progress工具查询进度。\n\n返回字段说明:\n- taskId: 任务ID,用于查询进度\n- status: 任务状态 (pending)\n- message: 状态描述",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ uploadUrl: {
179
+ type: "string",
180
+ description: "要扩展的音频文件URL,音频长度不超过2分钟。",
181
+ },
182
+ prompt: {
183
+ type: "string",
184
+ description: "扩展描述,如'用更多舒缓的音符延长音乐'。",
185
+ },
186
+ style: {
187
+ type: "string",
188
+ description: "保持的音乐风格。",
189
+ default: "",
190
+ },
191
+ title: {
192
+ type: "string",
193
+ description: "扩展版本标题。",
194
+ default: "",
195
+ },
196
+ continueAt: {
197
+ type: "number",
198
+ description: "从音频的第几秒开始扩展,必须大于0且小于音频总时长。",
199
+ minimum: 1,
200
+ },
201
+ instrumental: {
202
+ type: "boolean",
203
+ description: "是否生成纯音乐版本。",
204
+ default: false,
205
+ },
206
+ model: {
207
+ type: "string",
208
+ enum: ["V3_5", "V4", "V4_5"],
209
+ description: "使用的AI模型版本,必须与源音乐保持一致。",
210
+ default: "V4_5",
211
+ },
212
+ negativeTags: {
213
+ type: "string",
214
+ description: "负面标签。",
215
+ default: "",
216
+ },
217
+ },
218
+ required: ["uploadUrl", "prompt", "continueAt"],
219
+ },
220
+ // outputSchema: MUSIC_GENERATION_OUTPUT_SCHEMA,
221
+ };
222
+ const QUERY_PROGRESS_TOOL = {
223
+ name: "query_progress",
224
+ description: "查询音乐生成进度。由于AI生成音乐需要时间,大模型需要轮询此接口来获取任务状态和结果。建议每10-30秒查询一次,直到状态为'complete'或'failed'。\n\n返回字段说明:\n- taskId: 任务ID\n- status: 任务状态 (pending/processing/complete/failed)\n- progress: 进度百分比\n- message: 状态描述\n- result: 完成时的详细结果,包含:\n • 基本信息:prompt(提示词)、modelName(模型)、title(标题)、tags(标签)、duration(时长秒数)\n • 音频链接:audioUrl(MP3下载)、streamAudioUrl(流媒体)、sourceAudioUrl(原始音频)\n • 图片链接:imageUrl(封面图)、sourceImageUrl(原始封面)\n • 其他:musicId(音乐ID)、taskCreateTime(创建时间)、callbackType(回调类型)",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ taskId: {
229
+ type: "string",
230
+ description: "音乐生成任务的ID。",
231
+ },
232
+ },
233
+ required: ["taskId"],
234
+ },
235
+ // outputSchema: PROGRESS_QUERY_OUTPUT_SCHEMA,
236
+ };
237
+ const TOOLS = [
238
+ GENERATE_MUSIC_TOOL,
239
+ COVER_MUSIC_TOOL,
240
+ EXTEND_MUSIC_TOOL,
241
+ QUERY_PROGRESS_TOOL,
242
+ ];
243
+ // 通用API请求函数
244
+ async function makeApiRequest(endpoint, method, body, retries = 3) {
245
+ for (let attempt = 1; attempt <= retries; attempt++) {
246
+ let timeoutId;
247
+ try {
248
+ const controller = new AbortController();
249
+ timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时
250
+ const config = {
251
+ method,
252
+ headers: {
253
+ 'Authorization': `Bearer ${getApiKey()}`,
254
+ 'Content-Type': 'application/json',
255
+ },
256
+ signal: controller.signal,
257
+ };
258
+ if (body && method !== 'GET') {
259
+ config.body = JSON.stringify(body);
260
+ }
261
+ console.error(`尝试第 ${attempt} 次请求: ${method} ${API_BASE_URL}${endpoint}`);
262
+ const response = await (0, node_fetch_1.default)(`${API_BASE_URL}${endpoint}`, config);
263
+ clearTimeout(timeoutId);
264
+ const result = await response.json();
265
+ console.error(`请求成功,响应数据:`, JSON.stringify(result, null, 2));
266
+ // 检查API返回的业务错误码
267
+ if (result.code && result.code !== 200) {
268
+ throw new Error(`API业务错误: ${result.code} - ${result.msg || '未知错误'}`);
269
+ }
270
+ return result;
271
+ }
272
+ catch (error) {
273
+ if (timeoutId) {
274
+ clearTimeout(timeoutId);
275
+ }
276
+ if (attempt === retries) {
277
+ // 最后一次重试失败
278
+ if (error instanceof Error && error.name === 'AbortError') {
279
+ throw new Error(`API请求超时: 请求在60秒内未完成`);
280
+ }
281
+ throw new Error(`API请求错误 (${attempt}/${retries}次重试后失败): ${error instanceof Error ? error.message : String(error)}`);
282
+ }
283
+ // 等待后重试,递增延迟时间
284
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // 指数退避,最大5秒
285
+ console.error(`第 ${attempt} 次请求失败,${delay / 1000}秒后重试: ${error instanceof Error ? error.message : String(error)}`);
286
+ await new Promise(resolve => setTimeout(resolve, delay));
287
+ }
288
+ }
289
+ }
290
+ // 直接请求完整URL(用于自有查询服务)
291
+ async function makeApiRequestRaw(url, method, body, retries = 3) {
292
+ for (let attempt = 1; attempt <= retries; attempt++) {
293
+ let timeoutId;
294
+ try {
295
+ const controller = new AbortController();
296
+ timeoutId = setTimeout(() => controller.abort(), 60000);
297
+ const config = {
298
+ method,
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ },
302
+ signal: controller.signal,
303
+ };
304
+ if (body && method !== 'GET')
305
+ config.body = JSON.stringify(body);
306
+ console.error(`尝试第 ${attempt} 次请求: ${method} ${url}`);
307
+ const response = await (0, node_fetch_1.default)(url, config);
308
+ clearTimeout(timeoutId);
309
+ if (!response.ok) {
310
+ const errorText = await response.text();
311
+ throw new Error(`API请求失败: ${response.status} ${response.statusText} - ${errorText}`);
312
+ }
313
+ const result = await response.json();
314
+ console.error(`请求成功,响应数据:`, JSON.stringify(result, null, 2));
315
+ return result;
316
+ }
317
+ catch (error) {
318
+ if (timeoutId)
319
+ clearTimeout(timeoutId);
320
+ if (attempt === retries) {
321
+ if (error instanceof Error && error.name === 'AbortError') {
322
+ throw new Error(`API请求超时: 请求在60秒内未完成`);
323
+ }
324
+ throw new Error(`API请求错误 (${attempt}/${retries}次重试后失败): ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ // 等待后重试,递增延迟时间
327
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // 指数退避,最大5秒
328
+ console.error(`第 ${attempt} 次请求失败,${delay / 1000}秒后重试: ${error instanceof Error ? error.message : String(error)}`);
329
+ await new Promise(resolve => setTimeout(resolve, delay));
330
+ }
331
+ }
332
+ }
333
+ // 生成音乐处理函数
334
+ async function handleGenerateMusic(input) {
335
+ try {
336
+ if (!input || typeof input !== 'object') {
337
+ return {
338
+ content: [],
339
+ isError: true,
340
+ errorMessage: "输入参数格式错误,预期为包含prompt字段的对象。",
341
+ };
342
+ }
343
+ const { prompt, style = "", title = "", instrumental = false, model = "V4_5", negativeTags = "" } = input;
344
+ if (!prompt || typeof prompt !== 'string') {
345
+ return {
346
+ content: [],
347
+ isError: true,
348
+ errorMessage: "prompt字段是必需的,且必须为字符串类型。",
349
+ };
350
+ }
351
+ // 验证prompt长度
352
+ const maxPromptLength = model === "V4_5" ? 5000 : 3000;
353
+ if (prompt.length > maxPromptLength) {
354
+ return {
355
+ content: [],
356
+ isError: true,
357
+ errorMessage: `prompt长度超过限制。${model}模型最大允许${maxPromptLength}字符。`,
358
+ };
359
+ }
360
+ // 验证style长度
361
+ const maxStyleLength = model === "V4_5" ? 1000 : 200;
362
+ if (style.length > maxStyleLength) {
363
+ return {
364
+ content: [],
365
+ isError: true,
366
+ errorMessage: `style长度超过限制。${model}模型最大允许${maxStyleLength}字符。`,
367
+ };
368
+ }
369
+ // 验证title长度
370
+ if (title.length > 80) {
371
+ return {
372
+ content: [],
373
+ isError: true,
374
+ errorMessage: "title长度超过限制,最大允许80字符。",
375
+ };
376
+ }
377
+ const callbackUrl = buildCallbackUrl("generate");
378
+ console.error(`使用回调地址: ${callbackUrl}`);
379
+ const requestBody = {
380
+ prompt,
381
+ style,
382
+ title,
383
+ customMode: true,
384
+ instrumental,
385
+ model,
386
+ negativeTags,
387
+ callBackUrl: callbackUrl, // 必需的参数
388
+ };
389
+ const result = await makeApiRequest('/generate', 'POST', requestBody);
390
+ const taskData = {
391
+ taskId: result.data?.taskId || result.taskId || result.id || result.task_id || "unknown",
392
+ status: "pending",
393
+ message: "音乐生成任务已提交,请使用query_progress工具查询进度。",
394
+ };
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: JSON.stringify([taskData])
400
+ },
401
+ ],
402
+ isError: false,
403
+ errorMessage: "",
404
+ };
405
+ }
406
+ catch (error) {
407
+ const errorMessage = error instanceof Error ? error.message : String(error);
408
+ console.error("生成音乐时发生错误:", errorMessage);
409
+ return {
410
+ content: [],
411
+ isError: true,
412
+ errorMessage: `生成音乐失败: ${errorMessage}`,
413
+ };
414
+ }
415
+ }
416
+ // 翻唱音乐处理函数
417
+ async function handleCoverMusic(input) {
418
+ try {
419
+ if (!input || typeof input !== 'object') {
420
+ return {
421
+ content: [],
422
+ isError: true,
423
+ errorMessage: "输入参数格式错误,预期为包含uploadUrl和prompt字段的对象。",
424
+ };
425
+ }
426
+ const { uploadUrl, prompt, style = "", title = "", instrumental = false, model = "V4_5", negativeTags = "" } = input;
427
+ if (!uploadUrl || typeof uploadUrl !== 'string') {
428
+ return {
429
+ content: [],
430
+ isError: true,
431
+ errorMessage: "uploadUrl字段是必需的,且必须为字符串类型。",
432
+ };
433
+ }
434
+ if (!prompt || typeof prompt !== 'string') {
435
+ return {
436
+ content: [],
437
+ isError: true,
438
+ errorMessage: "prompt字段是必需的,且必须为字符串类型。",
439
+ };
440
+ }
441
+ const requestBody = {
442
+ uploadUrl,
443
+ prompt,
444
+ style,
445
+ title,
446
+ customMode: true,
447
+ instrumental,
448
+ model,
449
+ negativeTags,
450
+ callBackUrl: buildCallbackUrl("upload-cover"), // 必需的参数
451
+ };
452
+ const result = await makeApiRequest('/generate/upload-cover', 'POST', requestBody);
453
+ const taskData = {
454
+ taskId: result.data?.taskId || result.taskId || result.id || result.task_id || "unknown",
455
+ status: "pending",
456
+ message: "音乐翻唱任务已提交,请使用query_progress工具查询进度。",
457
+ };
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: JSON.stringify([taskData])
463
+ },
464
+ ],
465
+ isError: false,
466
+ errorMessage: "",
467
+ };
468
+ }
469
+ catch (error) {
470
+ const errorMessage = error instanceof Error ? error.message : String(error);
471
+ console.error("翻唱音乐时发生错误:", errorMessage);
472
+ return {
473
+ content: [],
474
+ isError: true,
475
+ errorMessage: `翻唱音乐失败: ${errorMessage}`,
476
+ };
477
+ }
478
+ }
479
+ // 扩展音乐处理函数
480
+ async function handleExtendMusic(input) {
481
+ try {
482
+ if (!input || typeof input !== 'object') {
483
+ return {
484
+ content: [],
485
+ isError: true,
486
+ errorMessage: "输入参数格式错误,预期为包含uploadUrl、prompt和continueAt字段的对象。",
487
+ };
488
+ }
489
+ const { uploadUrl, prompt, style = "", title = "", continueAt, instrumental = false, model = "V4_5", negativeTags = "" } = input;
490
+ if (!uploadUrl || typeof uploadUrl !== 'string') {
491
+ return {
492
+ content: [],
493
+ isError: true,
494
+ errorMessage: "uploadUrl字段是必需的,且必须为字符串类型。",
495
+ };
496
+ }
497
+ if (!prompt || typeof prompt !== 'string') {
498
+ return {
499
+ content: [],
500
+ isError: true,
501
+ errorMessage: "prompt字段是必需的,且必须为字符串类型。",
502
+ };
503
+ }
504
+ if (typeof continueAt !== 'number' || continueAt <= 0) {
505
+ return {
506
+ content: [],
507
+ isError: true,
508
+ errorMessage: "continueAt字段是必需的,且必须为大于0的数字。",
509
+ };
510
+ }
511
+ const requestBody = {
512
+ uploadUrl,
513
+ defaultParamFlag: true,
514
+ instrumental,
515
+ prompt,
516
+ style,
517
+ title,
518
+ continueAt,
519
+ model,
520
+ negativeTags,
521
+ callBackUrl: buildCallbackUrl("upload-extend"), // 必需的参数
522
+ };
523
+ const result = await makeApiRequest('/generate/upload-extend', 'POST', requestBody);
524
+ const taskData = {
525
+ taskId: result.data?.taskId || result.taskId || result.id || result.task_id || "unknown",
526
+ status: "pending",
527
+ message: "音乐扩展任务已提交,请使用query_progress工具查询进度。",
528
+ };
529
+ return {
530
+ content: [
531
+ {
532
+ type: "text",
533
+ text: JSON.stringify([taskData])
534
+ },
535
+ ],
536
+ isError: false,
537
+ errorMessage: "",
538
+ };
539
+ }
540
+ catch (error) {
541
+ const errorMessage = error instanceof Error ? error.message : String(error);
542
+ console.error("扩展音乐时发生错误:", errorMessage);
543
+ return {
544
+ content: [],
545
+ isError: true,
546
+ errorMessage: `扩展音乐失败: ${errorMessage}`,
547
+ };
548
+ }
549
+ }
550
+ // 查询进度处理函数
551
+ async function handleQueryProgress(input) {
552
+ try {
553
+ if (!input || typeof input !== 'object') {
554
+ return {
555
+ content: [],
556
+ isError: true,
557
+ errorMessage: "输入参数格式错误,预期为包含taskId字段的对象。",
558
+ };
559
+ }
560
+ const { taskId } = input;
561
+ if (!taskId || typeof taskId !== 'string') {
562
+ return {
563
+ content: [],
564
+ isError: true,
565
+ errorMessage: "taskId字段是必需的,且必须为字符串类型。",
566
+ };
567
+ }
568
+ // 优先查询自有服务端,其次回退官方端点
569
+ const customBase = getQueryBase();
570
+ const endpoints = [
571
+ customBase ? `${customBase.replace(/\/$/, "")}/suno/task/${taskId}` : "",
572
+ `/generate/${taskId}`,
573
+ `/generate/details/${taskId}`,
574
+ `/task/${taskId}`,
575
+ ].filter(Boolean);
576
+ let result = null;
577
+ let lastError = null;
578
+ for (const ep of endpoints) {
579
+ try {
580
+ // 自有服务端点是完整URL,需要直接请求;官方端点仍走 API_BASE_URL
581
+ if (ep.startsWith('http')) {
582
+ // 直接 fetch(沿用超时与头部设置)
583
+ result = await makeApiRequestRaw(ep, 'GET');
584
+ }
585
+ else {
586
+ result = await makeApiRequest(ep, 'GET');
587
+ }
588
+ if (result)
589
+ break;
590
+ }
591
+ catch (e) {
592
+ lastError = e;
593
+ continue;
594
+ }
595
+ }
596
+ if (!result) {
597
+ throw new Error(`无法通过任何端点获取任务详情,taskId=${taskId}。${lastError instanceof Error ? lastError.message : String(lastError)}`);
598
+ }
599
+ // 使用自定义服务端的数据
600
+ if (result.code !== undefined && result.code === 0 && result.data) {
601
+ result = result.data;
602
+ }
603
+ // 解析任务状态,兼容自定义服务端的状态值
604
+ let status = result.status || 'unknown';
605
+ const progress = result.progress || 0;
606
+ // 状态值映射,兼容自定义服务端
607
+ if (status === 'completed')
608
+ status = 'complete';
609
+ if (status === 'running')
610
+ status = 'processing';
611
+ let statusMessage = "";
612
+ switch (status) {
613
+ case 'pending':
614
+ statusMessage = "任务排队中...";
615
+ break;
616
+ case 'processing':
617
+ statusMessage = "正在处理中...";
618
+ break;
619
+ case 'text':
620
+ statusMessage = "文本生成完成,正在生成音频...";
621
+ break;
622
+ case 'first':
623
+ statusMessage = "第一首音乐生成完成...";
624
+ break;
625
+ case 'complete':
626
+ statusMessage = "任务完成!";
627
+ break;
628
+ case 'failed':
629
+ statusMessage = "任务失败";
630
+ break;
631
+ default:
632
+ statusMessage = `状态: ${status}`;
633
+ }
634
+ const responseData = {
635
+ taskId,
636
+ status,
637
+ progress,
638
+ message: statusMessage,
639
+ result: status === 'complete' ? {
640
+ // 基本信息
641
+ taskId: result.taskId,
642
+ status: result.status,
643
+ prompt: result.prompt,
644
+ modelName: result.modelName,
645
+ title: result.title,
646
+ tags: result.tags,
647
+ duration: result.duration,
648
+ callbackType: result.callbackType,
649
+ taskCreateTime: result.taskCreateTime,
650
+ // 音频链接
651
+ audioUrl: result.audioUrl,
652
+ sourceAudioUrl: result.sourceAudioUrl,
653
+ streamAudioUrl: result.streamAudioUrl,
654
+ sourceStreamAudioUrl: result.sourceStreamAudioUrl,
655
+ // 图片链接
656
+ imageUrl: result.imageUrl,
657
+ sourceImageUrl: result.sourceImageUrl,
658
+ // 其他信息
659
+ musicId: result.musicId,
660
+ userUuid: result.userUuid,
661
+ errorMessage: result.errorMessage,
662
+ // 原始完整数据
663
+ rawData: result
664
+ } : null,
665
+ };
666
+ // 检查是否来自自定义服务端且返回错误
667
+ if (result.code !== undefined) {
668
+ // 自定义服务端格式:code 0=成功,其他=失败
669
+ if (result.code !== 0) {
670
+ // 如果是404错误,返回转换中状态而不是错误
671
+ if (result.data && result.data.status === 404) {
672
+ return {
673
+ content: [
674
+ {
675
+ type: "text",
676
+ text: JSON.stringify([{
677
+ taskId,
678
+ status: "pending",
679
+ progress: 0,
680
+ message: "任务正在处理中,请稍后再查询...",
681
+ result: null,
682
+ }])
683
+ },
684
+ ],
685
+ isError: false,
686
+ errorMessage: "",
687
+ };
688
+ }
689
+ return {
690
+ content: [],
691
+ isError: true,
692
+ errorMessage: `查询失败: ${result.msg || '未知错误'}`,
693
+ };
694
+ }
695
+ // 如果成功但data中有错误信息,也要处理
696
+ if (result.data && result.data.error) {
697
+ // 如果是404错误,返回转换中状态
698
+ if (result.data.status === 404) {
699
+ return {
700
+ content: [
701
+ {
702
+ type: "text",
703
+ text: JSON.stringify([{
704
+ taskId,
705
+ status: "pending",
706
+ progress: 0,
707
+ message: "任务正在处理中,请稍后再查询...",
708
+ result: null,
709
+ }])
710
+ },
711
+ ],
712
+ isError: false,
713
+ errorMessage: "",
714
+ };
715
+ }
716
+ return {
717
+ content: [],
718
+ isError: true,
719
+ errorMessage: `查询失败: ${result.data.error} - ${result.data.message || ''}`,
720
+ };
721
+ }
722
+ // 如果自定义服务端返回成功但没有实际数据,说明任务不存在或还在处理中
723
+ if (!result.data || Object.keys(result.data).length === 0) {
724
+ return {
725
+ content: [
726
+ {
727
+ type: "text",
728
+ text: JSON.stringify([{
729
+ taskId,
730
+ status: "pending",
731
+ progress: 0,
732
+ message: "任务正在处理中,请稍后再查询...",
733
+ result: null,
734
+ }])
735
+ },
736
+ ],
737
+ isError: false,
738
+ errorMessage: "",
739
+ };
740
+ }
741
+ // 使用自定义服务端的数据
742
+ result = result.data || result;
743
+ }
744
+ return {
745
+ content: [
746
+ {
747
+ type: "text",
748
+ text: JSON.stringify([responseData])
749
+ },
750
+ ],
751
+ isError: false,
752
+ errorMessage: "",
753
+ };
754
+ }
755
+ catch (error) {
756
+ const errorMessage = error instanceof Error ? error.message : String(error);
757
+ console.error("查询进度时发生错误:", errorMessage);
758
+ return {
759
+ content: [],
760
+ isError: true,
761
+ errorMessage: `查询进度失败: ${errorMessage}`,
762
+ };
763
+ }
764
+ }
765
+ // MCP 服务器配置与启动
766
+ const server = new index_js_1.Server({
767
+ name: "mcp-ai-music",
768
+ version: "1.0.0",
769
+ description: "AI作曲MCP服务 - 基于Suno4.5 API的音乐生成、翻唱、扩展和进度查询工具"
770
+ }, {
771
+ capabilities: {
772
+ tools: TOOLS.reduce((acc, tool) => {
773
+ acc[tool.name] = tool;
774
+ return acc;
775
+ }, {}),
776
+ },
777
+ });
778
+ // 注册请求处理器
779
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
780
+ return { tools: TOOLS };
781
+ });
782
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
783
+ try {
784
+ const toolName = request.params.name;
785
+ const toolInput = request.params.arguments;
786
+ console.error(`收到工具调用请求: ${toolName}, 输入: ${JSON.stringify(toolInput)}`);
787
+ switch (toolName) {
788
+ case GENERATE_MUSIC_TOOL.name:
789
+ return await handleGenerateMusic(toolInput);
790
+ case COVER_MUSIC_TOOL.name:
791
+ return await handleCoverMusic(toolInput);
792
+ case EXTEND_MUSIC_TOOL.name:
793
+ return await handleExtendMusic(toolInput);
794
+ case QUERY_PROGRESS_TOOL.name:
795
+ return await handleQueryProgress(toolInput);
796
+ default:
797
+ console.error(`未知的工具名称: ${toolName}`);
798
+ return {
799
+ content: [],
800
+ isError: true,
801
+ errorMessage: `未找到名为 '${toolName}' 的工具。`,
802
+ };
803
+ }
804
+ }
805
+ catch (error) {
806
+ const errorMessage = error instanceof Error ? error.message : String(error);
807
+ console.error("处理工具调用请求时发生严重错误:", errorMessage);
808
+ return {
809
+ content: [],
810
+ isError: true,
811
+ errorMessage: `处理请求时发生系统内部错误: ${errorMessage}`,
812
+ };
813
+ }
814
+ });
815
+ // 启动服务器
816
+ async function runServer() {
817
+ try {
818
+ const transport = new stdio_js_1.StdioServerTransport();
819
+ await server.connect(transport);
820
+ console.error("MCP AI作曲服务已启动");
821
+ console.error(`已注册 ${TOOLS.length} 个工具: ${TOOLS.map((t) => t.name).join(", ")}`);
822
+ try {
823
+ getApiKey();
824
+ console.error("环境变量 SUNO_API_KEY: 已设置");
825
+ }
826
+ catch (error) {
827
+ console.error("环境变量 SUNO_API_KEY: 未设置");
828
+ }
829
+ console.error(`服务端基地址 SUNO_SERVICE_BASE(或兼容变量): ${getQueryBase() || "未设置(回调将报错/查询走官方)"}`);
830
+ }
831
+ catch (error) {
832
+ console.error("MCP 服务启动失败:", error);
833
+ process.exit(1);
834
+ }
835
+ }
836
+ runServer();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "mcp-ai-music",
3
+ "version": "1.0.0",
4
+ "description": "AI作曲MCP服务 - 基于Suno4.5 API的音乐生成、翻唱、扩展和进度查询工具",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "start": "node dist/index.js",
9
+ "dev": "ts-node src/index.ts",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "AI",
14
+ "music",
15
+ "composition",
16
+ "suno",
17
+ "mcp",
18
+ "audio",
19
+ "generation"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC",
23
+ "bin": {
24
+ "mcp-ai-music": "dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.10.0",
32
+ "node-fetch": "^2.7.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.14.1",
36
+ "@types/node-fetch": "^2.6.12",
37
+ "typescript": "^5.8.3",
38
+ "ts-node": "^10.9.2"
39
+ }
40
+ }