@eldment/meting-mcp 1.6.1 → 1.6.3

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.
@@ -0,0 +1,9 @@
1
+ # 致谢
2
+
3
+ `Meting-MCP` 基于原项目 **[metowolf/Meting](https://github.com/metowolf/Meting)** 构建
4
+
5
+ - 原项目:`metowolf/Meting`
6
+ - 原作者:`metowolf` / `METO`
7
+ - 原始许可证:[MIT](./LICENSE)
8
+
9
+ 本仓库在保留原项目信息与许可证声明的前提下,将 Meting 核心封装为 MCP Server
package/README.md CHANGED
@@ -1,77 +1,76 @@
1
- # @eldment/meting-mcp
1
+ # Meting-MCP
2
2
 
3
- `@eldment/meting-mcp` 基于原版 **Meting Node.js** 核心实现,只保留适合 MCP Server 发布与运行的形态。底层支持这些平台:
3
+ `Meting-MCP` 是基于 **[metowolf/Meting](https://github.com/metowolf/Meting)** 构建的 MCP Server,支持 [网易云音乐](https://music.163.com/)(`netease`)、[腾讯音乐](https://y.qq.com/)(`tencent`)、[酷狗音乐](https://www.kugou.com/)(`kugou`)、[千千音乐](https://music.taihe.com/)(`baidu`)、[酷我音乐](https://www.kuwo.cn/)(`kuwo`) 等音乐平台,提供搜索、歌曲、专辑、歌手、歌单、播放链接、歌词、封面等能力
4
4
 
5
- - `netease`
6
- - `tencent`
7
- - `kugou`
8
- - `baidu`
9
- - `kuwo`
5
+ ## 提供的 MCP 工具
10
6
 
11
- ## 功能概览
12
-
13
- - 基于原版 Meting 的统一音乐接口
14
- - 通过 stdio 暴露为 MCP Server
15
- - 支持搜索、歌曲、专辑、歌手、歌单、播放链接、歌词、封面等操作
16
- - 所有 MCP 工具默认返回格式化后的 JSON 文本
17
-
18
- ## 安装
19
-
20
- ```bash
21
- npm install
22
- ```
23
-
24
- 发布后可直接运行:
25
-
26
- ```bash
27
- npx @eldment/meting-mcp
28
- ```
7
+ - `platforms`: 列出当前支持的音乐平台及其平台代号
8
+ - `search`: 按关键字在指定平台搜索歌曲、专辑、歌手或其他资源
9
+ - `song`: 按歌曲 ID 获取歌曲详情
10
+ - `album`: 按专辑 ID 获取专辑详情
11
+ - `artist`: 按歌手 ID 获取歌手信息或作品列表
12
+ - `playlist`: 按歌单 ID 获取歌单内容
13
+ - `url`: 按歌曲 ID 获取可播放链接
14
+ - `lyric`: 按歌曲 ID 获取歌词内容
15
+ - `pic`: 按图片或资源 ID 获取封面图链接
29
16
 
30
17
  ## MCP 接入
31
18
 
32
- 示例配置:
19
+ Claude 示例配置:
33
20
 
34
21
  ```json
35
22
  {
36
23
  "mcpServers": {
37
24
  "meting": {
38
25
  "command": "npx",
39
- "args": ["-y", "@eldment/meting-mcp"]
26
+ "args": ["-y", "@eldment/meting-mcp@latest"],
27
+ "env": {
28
+ "METING_NETEASE_COOKIE": "...",
29
+ "METING_TENCENT_COOKIE": "..."
30
+ },
31
+ "timeout": 60000
40
32
  }
41
33
  }
42
34
  }
43
35
  ```
44
36
 
45
- 提供的 MCP 工具:
37
+ Codex 示例配置:
38
+
39
+ ```toml
40
+ [mcp_servers.meting]
41
+ type = "stdio"
42
+ command = "npx"
43
+ args = [
44
+ "-y",
45
+ "@eldment/meting-mcp@latest",
46
+ ]
47
+ env = {
48
+ METING_NETEASE_COOKIE = "...",
49
+ METING_TENCENT_COOKIE = "...",
50
+ }
51
+ tool_timeout_sec = 60
52
+ disabled = false
53
+ ```
46
54
 
47
- - `platforms`
48
- - `search`
49
- - `song`
50
- - `album`
51
- - `artist`
52
- - `playlist`
53
- - `url`
54
- - `lyric`
55
- - `pic`
55
+ ## Cookie 配置
56
56
 
57
- ## 工具参数
57
+ MCP 运行时会优先从环境变量读取 cookie,再回退到工具输入参数传入的 cookie,优先级如下:
58
58
 
59
- 所有工具都支持:
59
+ 1. `METING_<PLATFORM>_COOKIE`
60
+ 2. `METING_COOKIE`
61
+ 3. MCP 工具调用时传入的 `cookie`
60
62
 
61
- - `platform`: `netease`、`tencent`、`kugou`、`baidu`、`kuwo`
62
- - `cookie`: 可选,对应平台 Cookie
63
+ 当前支持的环境变量:
63
64
 
64
- 各工具额外参数:
65
+ - `METING_NETEASE_COOKIE`
66
+ - `METING_TENCENT_COOKIE`
67
+ - `METING_KUGOU_COOKIE`
68
+ - `METING_BAIDU_COOKIE`
69
+ - `METING_KUWO_COOKIE`
70
+ - `METING_COOKIE`(兜底变量)
65
71
 
66
- - `search`: `keyword`,可选 `type`、`page`、`limit`
67
- - `song`: `id`
68
- - `album`: `id`
69
- - `artist`: `id`,可选 `limit`
70
- - `playlist`: `id`
71
- - `url`: `id`,可选 `br`
72
- - `lyric`: `id`
73
- - `pic`: `id`,可选 `size`
72
+ 如果只需要给某一个平台带 cookie,优先使用对应的平台变量;如果想统一兜底,可以只设置 `METING_COOKIE`
74
73
 
75
- ## 版权与致谢
74
+ ---
76
75
 
77
- 底层核心来自原项目 [metowolf/Meting](https://github.com/metowolf/Meting),遵循 [MIT](./LICENSE) License。
76
+ 关键词: MCP Server | Model Context Protocol | Music API | Node.js MCP | AI Tool Integration | NetEase Cloud Music | Tencent QQ Music | KuGou Music | Baidu Music | Kuwo Music | Lyrics API | Playlist API
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
1
  {
2
2
  "name": "@eldment/meting-mcp",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "MCP server wrapper for the Meting multi-platform music API core.",
5
5
  "type": "module",
6
6
  "main": "./src/mcp-server.js",
7
- "bin": {
8
- "meting-mcp": "./src/index.js"
9
- },
7
+ "bin": "./src/index.js",
10
8
  "exports": {
11
9
  ".": "./src/mcp-server.js"
12
10
  },
13
11
  "files": [
14
12
  "src",
15
13
  "README.md",
14
+ "ACKNOWLEDGEMENTS.md",
16
15
  "LICENSE"
17
16
  ],
18
17
  "scripts": {
19
18
  "start": "node src/index.js",
20
19
  "lint": "node --check src/index.js && node --check src/mcp-server.js && node --check src/meting.js && node --check src/providers/index.js && node --check test/test.js && node --check test/example.js",
20
+ "format": "prettier --write .",
21
+ "format:check": "prettier --check .",
21
22
  "test": "node test/test.js",
22
23
  "example": "node test/example.js",
23
24
  "build": "npm pack --dry-run",
@@ -60,5 +61,8 @@
60
61
  "dependencies": {
61
62
  "@modelcontextprotocol/sdk": "^1.27.1",
62
63
  "zod": "^4.3.6"
64
+ },
65
+ "devDependencies": {
66
+ "prettier": "^3.6.2"
63
67
  }
64
68
  }
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CreateMcpServer, serviceMetadata } from "./mcp-server.js";
@@ -12,7 +12,7 @@ function GetHelpText() {
12
12
  "Usage:",
13
13
  " meting-mcp Start the MCP stdio server",
14
14
  " meting-mcp --help Show help",
15
- " meting-mcp --version Show version"
15
+ " meting-mcp --version Show version",
16
16
  ].join("\n");
17
17
  }
18
18
 
@@ -47,7 +47,9 @@ async function Main() {
47
47
  await StartServer();
48
48
  }
49
49
 
50
- Main().catch(error => {
51
- process.stderr.write(`meting-mcp failed to start: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
50
+ Main().catch((error) => {
51
+ process.stderr.write(
52
+ `meting-mcp failed to start: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
53
+ );
52
54
  process.exit(1);
53
55
  });
package/src/mcp-server.js CHANGED
@@ -4,7 +4,7 @@ import Meting from "./meting.js";
4
4
 
5
5
  export const serviceMetadata = Object.freeze({
6
6
  name: "meting-mcp",
7
- version: "1.6.1"
7
+ version: "1.6.2",
8
8
  });
9
9
 
10
10
  const platformCatalog = Object.freeze([
@@ -12,15 +12,36 @@ const platformCatalog = Object.freeze([
12
12
  { name: "Tencent Music", code: "tencent" },
13
13
  { name: "KuGou Music", code: "kugou" },
14
14
  { name: "Baidu Music", code: "baidu" },
15
- { name: "Kuwo Music", code: "kuwo" }
15
+ { name: "Kuwo Music", code: "kuwo" },
16
16
  ]);
17
17
 
18
18
  const platformSchema = z.enum(Meting.getSupportedPlatforms());
19
19
 
20
- function CreateClient(platform, cookie) {
20
+ const cookieEnvNames = Object.freeze({
21
+ netease: "METING_NETEASE_COOKIE",
22
+ tencent: "METING_TENCENT_COOKIE",
23
+ kugou: "METING_KUGOU_COOKIE",
24
+ baidu: "METING_BAIDU_COOKIE",
25
+ kuwo: "METING_KUWO_COOKIE",
26
+ });
27
+
28
+ function ReadEnvValue(name) {
29
+ const value = process.env[name];
30
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
31
+ }
32
+
33
+ export function ResolveCookie(platform, inputCookie) {
34
+ const platformCookie = ReadEnvValue(cookieEnvNames[platform]);
35
+ const fallbackCookie = ReadEnvValue("METING_COOKIE");
36
+ return platformCookie ?? fallbackCookie ?? inputCookie;
37
+ }
38
+
39
+ function CreateClient(platform, inputCookie) {
21
40
  const meting = new Meting(platform);
22
41
  meting.format(true);
23
42
 
43
+ const cookie = ResolveCookie(platform, inputCookie);
44
+
24
45
  if (cookie) {
25
46
  meting.cookie(cookie);
26
47
  }
@@ -46,9 +67,9 @@ function CreateTextResult(result, isError = false) {
46
67
  content: [
47
68
  {
48
69
  type: "text",
49
- text: JSON.stringify(result, null, 2)
50
- }
51
- ]
70
+ text: JSON.stringify(result, null, 2),
71
+ },
72
+ ],
52
73
  };
53
74
  }
54
75
 
@@ -57,7 +78,7 @@ function CreateOperationResult(operation, platform, data) {
57
78
  ok: true,
58
79
  operation,
59
80
  platform,
60
- data
81
+ data,
61
82
  };
62
83
  }
63
84
 
@@ -66,15 +87,20 @@ function CreateOperationError(operation, platform, error) {
66
87
  ok: false,
67
88
  operation,
68
89
  platform,
69
- message: error instanceof Error ? error.message : String(error)
90
+ message: error instanceof Error ? error.message : String(error),
70
91
  };
71
92
  }
72
93
 
73
94
  function WithCommonInput(extraSchema) {
74
95
  return {
75
96
  platform: platformSchema.describe("Music platform code."),
76
- cookie: z.string().optional().describe("Optional platform cookie."),
77
- ...extraSchema
97
+ cookie: z
98
+ .string()
99
+ .optional()
100
+ .describe(
101
+ "Optional platform cookie. Lower priority than METING_<PLATFORM>_COOKIE and METING_COOKIE."
102
+ ),
103
+ ...extraSchema,
78
104
  };
79
105
  }
80
106
 
@@ -84,9 +110,9 @@ function RegisterTool(server, toolName, description, inputSchema, handler) {
84
110
  {
85
111
  title: toolName,
86
112
  description,
87
- inputSchema
113
+ inputSchema,
88
114
  },
89
- async input => {
115
+ async (input) => {
90
116
  try {
91
117
  const data = await handler(input);
92
118
  return CreateTextResult(CreateOperationResult(toolName, input.platform, data));
@@ -104,12 +130,12 @@ export function CreateMcpServer() {
104
130
  "platforms",
105
131
  {
106
132
  title: "platforms",
107
- description: "List supported music platforms."
133
+ description: "List supported music platforms.",
108
134
  },
109
135
  async () => {
110
136
  return CreateTextResult({
111
137
  ok: true,
112
- data: platformCatalog
138
+ data: platformCatalog,
113
139
  });
114
140
  }
115
141
  );
@@ -120,11 +146,16 @@ export function CreateMcpServer() {
120
146
  "Search songs, albums or artists on a specific platform.",
121
147
  WithCommonInput({
122
148
  keyword: z.string().min(1).describe("Search keyword."),
123
- type: z.number().int().positive().optional().describe("Optional platform-specific search type."),
149
+ type: z
150
+ .number()
151
+ .int()
152
+ .positive()
153
+ .optional()
154
+ .describe("Optional platform-specific search type."),
124
155
  page: z.number().int().positive().optional().describe("Optional page number."),
125
- limit: z.number().int().positive().max(100).optional().describe("Optional page size.")
156
+ limit: z.number().int().positive().max(100).optional().describe("Optional page size."),
126
157
  }),
127
- async input => {
158
+ async (input) => {
128
159
  const meting = CreateClient(input.platform, input.cookie);
129
160
  const options = {};
130
161
 
@@ -149,9 +180,9 @@ export function CreateMcpServer() {
149
180
  "song",
150
181
  "Get song detail by id.",
151
182
  WithCommonInput({
152
- id: z.string().min(1).describe("Song id.")
183
+ id: z.string().min(1).describe("Song id."),
153
184
  }),
154
- async input => {
185
+ async (input) => {
155
186
  const meting = CreateClient(input.platform, input.cookie);
156
187
  return ParseResult(await meting.song(input.id));
157
188
  }
@@ -162,9 +193,9 @@ export function CreateMcpServer() {
162
193
  "album",
163
194
  "Get album detail by id.",
164
195
  WithCommonInput({
165
- id: z.string().min(1).describe("Album id.")
196
+ id: z.string().min(1).describe("Album id."),
166
197
  }),
167
- async input => {
198
+ async (input) => {
168
199
  const meting = CreateClient(input.platform, input.cookie);
169
200
  return ParseResult(await meting.album(input.id));
170
201
  }
@@ -176,9 +207,9 @@ export function CreateMcpServer() {
176
207
  "Get artist songs by id.",
177
208
  WithCommonInput({
178
209
  id: z.string().min(1).describe("Artist id."),
179
- limit: z.number().int().positive().max(200).optional().describe("Optional result size.")
210
+ limit: z.number().int().positive().max(200).optional().describe("Optional result size."),
180
211
  }),
181
- async input => {
212
+ async (input) => {
182
213
  const meting = CreateClient(input.platform, input.cookie);
183
214
  return ParseResult(await meting.artist(input.id, input.limit));
184
215
  }
@@ -189,9 +220,9 @@ export function CreateMcpServer() {
189
220
  "playlist",
190
221
  "Get playlist detail by id.",
191
222
  WithCommonInput({
192
- id: z.string().min(1).describe("Playlist id.")
223
+ id: z.string().min(1).describe("Playlist id."),
193
224
  }),
194
- async input => {
225
+ async (input) => {
195
226
  const meting = CreateClient(input.platform, input.cookie);
196
227
  return ParseResult(await meting.playlist(input.id));
197
228
  }
@@ -203,10 +234,11 @@ export function CreateMcpServer() {
203
234
  "Get playable song url by id.",
204
235
  WithCommonInput({
205
236
  id: z.string().min(1).describe("Song id."),
206
- br: z.number().int().positive().optional().describe("Optional bitrate.")
237
+ br: z.number().int().positive().optional().describe("Optional bitrate."),
207
238
  }),
208
- async input => {
239
+ async (input) => {
209
240
  const meting = CreateClient(input.platform, input.cookie);
241
+ WithCommonInput;
210
242
  return ParseResult(await meting.url(input.id, input.br));
211
243
  }
212
244
  );
@@ -216,9 +248,9 @@ export function CreateMcpServer() {
216
248
  "lyric",
217
249
  "Get song lyric by id.",
218
250
  WithCommonInput({
219
- id: z.string().min(1).describe("Song id.")
251
+ id: z.string().min(1).describe("Song id."),
220
252
  }),
221
- async input => {
253
+ async (input) => {
222
254
  const meting = CreateClient(input.platform, input.cookie);
223
255
  return ParseResult(await meting.lyric(input.id));
224
256
  }
@@ -230,9 +262,9 @@ export function CreateMcpServer() {
230
262
  "Get cover or picture url by id.",
231
263
  WithCommonInput({
232
264
  id: z.string().min(1).describe("Picture id."),
233
- size: z.number().int().positive().optional().describe("Optional picture size.")
265
+ size: z.number().int().positive().optional().describe("Optional picture size."),
234
266
  }),
235
- async input => {
267
+ async (input) => {
236
268
  const meting = CreateClient(input.platform, input.cookie);
237
269
  return ParseResult(await meting.pic(input.id, input.size));
238
270
  }
package/src/meting.js CHANGED
@@ -7,12 +7,12 @@
7
7
  * Released under the MIT license
8
8
  */
9
9
 
10
- import { URLSearchParams } from 'url';
11
- import ProviderFactory from './providers/index.js';
10
+ import { URLSearchParams } from "url";
11
+ import ProviderFactory from "./providers/index.js";
12
12
 
13
13
  class Meting {
14
- constructor(server = 'netease') {
15
- this.VERSION = '__VERSION__'; // 在构建时由 rollup 替换为实际版本号
14
+ constructor(server = "netease") {
15
+ this.VERSION = "__VERSION__"; // 在构建时由 rollup 替换为实际版本号
16
16
  this.raw = null;
17
17
  this.info = null;
18
18
  this.error = null;
@@ -30,7 +30,7 @@ class Meting {
30
30
  // 设置音乐平台
31
31
  site(server) {
32
32
  if (!ProviderFactory.isSupported(server)) {
33
- server = 'netease'; // 默认使用网易云音乐
33
+ server = "netease"; // 默认使用网易云音乐
34
34
  }
35
35
 
36
36
  this.server = server;
@@ -42,7 +42,7 @@ class Meting {
42
42
 
43
43
  // 设置 Cookie
44
44
  cookie(cookie) {
45
- this.header['Cookie'] = cookie;
45
+ this.header["Cookie"] = cookie;
46
46
  return this;
47
47
  }
48
48
 
@@ -61,15 +61,15 @@ class Meting {
61
61
  // HTTP 请求方法 - 使用 fetch API
62
62
  async _curl(url, payload = null, headerOnly = false) {
63
63
  const requestOptions = {
64
- method: payload ? 'POST' : 'GET',
65
- headers: { ...this.header }
64
+ method: payload ? "POST" : "GET",
65
+ headers: { ...this.header },
66
66
  };
67
67
 
68
68
  // 处理请求体
69
69
  if (payload) {
70
- if (typeof payload === 'object' && !Buffer.isBuffer(payload) && typeof payload !== 'string') {
70
+ if (typeof payload === "object" && !Buffer.isBuffer(payload) && typeof payload !== "string") {
71
71
  payload = new URLSearchParams(payload).toString();
72
- requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
72
+ requestOptions.headers["Content-Type"] = "application/x-www-form-urlencoded";
73
73
  }
74
74
  requestOptions.body = payload;
75
75
  }
@@ -83,38 +83,38 @@ class Meting {
83
83
  const makeRequest = async () => {
84
84
  try {
85
85
  const response = await fetch(url, requestOptions);
86
-
86
+
87
87
  clearTimeout(timeoutId);
88
-
88
+
89
89
  // 存储响应信息
90
90
  this.info = {
91
91
  statusCode: response.status,
92
- headers: Object.fromEntries(response.headers.entries())
92
+ headers: Object.fromEntries(response.headers.entries()),
93
93
  };
94
94
 
95
95
  // 获取响应数据
96
96
  const data = await response.text();
97
97
  this.raw = data;
98
98
  this.error = null;
99
- this.status = '';
100
-
99
+ this.status = "";
100
+
101
101
  return this;
102
102
  } catch (err) {
103
103
  clearTimeout(timeoutId);
104
-
104
+
105
105
  // 处理错误
106
- if (err.name === 'AbortError') {
107
- this.error = 'TIMEOUT';
108
- this.status = 'Request timeout';
106
+ if (err.name === "AbortError") {
107
+ this.error = "TIMEOUT";
108
+ this.status = "Request timeout";
109
109
  } else {
110
110
  this.error = err.code || err.name;
111
111
  this.status = err.message;
112
112
  }
113
-
113
+
114
114
  // 重试机制
115
115
  if (retries > 0) {
116
116
  retries--;
117
- await new Promise(resolve => setTimeout(resolve, 1000));
117
+ await new Promise((resolve) => setTimeout(resolve, 1000));
118
118
  return makeRequest();
119
119
  } else {
120
120
  return this;
@@ -125,7 +125,6 @@ class Meting {
125
125
  return await makeRequest();
126
126
  }
127
127
 
128
-
129
128
  // ========== 公共 API 方法 ==========
130
129
 
131
130
  // 搜索功能