@eldment/meting-mcp 1.6.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 METO <i@i-meto.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @eldment/meting-mcp
2
+
3
+ `@eldment/meting-mcp` 基于原版 **Meting Node.js** 核心实现,只保留适合 MCP Server 发布与运行的形态。底层支持这些平台:
4
+
5
+ - `netease`
6
+ - `tencent`
7
+ - `kugou`
8
+ - `baidu`
9
+ - `kuwo`
10
+
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
+ ```
29
+
30
+ ## MCP 接入
31
+
32
+ 示例配置:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "meting": {
38
+ "command": "npx",
39
+ "args": ["-y", "@eldment/meting-mcp"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ 提供的 MCP 工具:
46
+
47
+ - `platforms`
48
+ - `search`
49
+ - `song`
50
+ - `album`
51
+ - `artist`
52
+ - `playlist`
53
+ - `url`
54
+ - `lyric`
55
+ - `pic`
56
+
57
+ ## 工具参数
58
+
59
+ 所有工具都支持:
60
+
61
+ - `platform`: `netease`、`tencent`、`kugou`、`baidu`、`kuwo`
62
+ - `cookie`: 可选,对应平台 Cookie
63
+
64
+ 各工具额外参数:
65
+
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`
74
+
75
+ ## 版权与致谢
76
+
77
+ 底层核心来自原项目 [metowolf/Meting](https://github.com/metowolf/Meting),遵循 [MIT](./LICENSE) License。
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@eldment/meting-mcp",
3
+ "version": "1.6.1",
4
+ "description": "MCP server wrapper for the Meting multi-platform music API core.",
5
+ "type": "module",
6
+ "main": "./src/mcp-server.js",
7
+ "bin": {
8
+ "meting-mcp": "./src/index.js"
9
+ },
10
+ "exports": {
11
+ ".": "./src/mcp-server.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "start": "node src/index.js",
20
+ "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",
21
+ "test": "node test/test.js",
22
+ "example": "node test/example.js",
23
+ "build": "npm pack --dry-run",
24
+ "verify": "npm run lint && npm test && npm run build",
25
+ "prepublishOnly": "npm run verify"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "music",
30
+ "api",
31
+ "netease",
32
+ "tencent",
33
+ "kugou",
34
+ "baidu",
35
+ "kuwo",
36
+ "nodejs",
37
+ "music-api",
38
+ "search",
39
+ "lyrics"
40
+ ],
41
+ "author": {
42
+ "name": "ELDment",
43
+ "url": "https://github.com/ELDment"
44
+ },
45
+ "license": "MIT",
46
+ "homepage": "https://github.com/ELDment/Meting-MCP",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/ELDment/Meting-MCP.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/ELDment/Meting-MCP/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.27.1",
62
+ "zod": "^4.3.6"
63
+ }
64
+ }
package/src/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CreateMcpServer, serviceMetadata } from "./mcp-server.js";
5
+
6
+ function GetHelpText() {
7
+ return [
8
+ "meting-mcp",
9
+ "",
10
+ "MCP server wrapper for the Meting multi-platform music API core.",
11
+ "",
12
+ "Usage:",
13
+ " meting-mcp Start the MCP stdio server",
14
+ " meting-mcp --help Show help",
15
+ " meting-mcp --version Show version"
16
+ ].join("\n");
17
+ }
18
+
19
+ async function StartServer() {
20
+ const server = CreateMcpServer();
21
+ const transport = new StdioServerTransport();
22
+
23
+ await server.connect(transport);
24
+
25
+ const Shutdown = async () => {
26
+ await server.close();
27
+ process.exit(0);
28
+ };
29
+
30
+ process.once("SIGINT", Shutdown);
31
+ process.once("SIGTERM", Shutdown);
32
+ }
33
+
34
+ async function Main() {
35
+ const argumentsList = process.argv.slice(2);
36
+
37
+ if (argumentsList.includes("--help") || argumentsList.includes("-h")) {
38
+ process.stdout.write(`${GetHelpText()}\n`);
39
+ return;
40
+ }
41
+
42
+ if (argumentsList.includes("--version") || argumentsList.includes("-v")) {
43
+ process.stdout.write(`${serviceMetadata.version}\n`);
44
+ return;
45
+ }
46
+
47
+ await StartServer();
48
+ }
49
+
50
+ Main().catch(error => {
51
+ process.stderr.write(`meting-mcp failed to start: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
52
+ process.exit(1);
53
+ });
@@ -0,0 +1,242 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod/v4";
3
+ import Meting from "./meting.js";
4
+
5
+ export const serviceMetadata = Object.freeze({
6
+ name: "meting-mcp",
7
+ version: "1.6.1"
8
+ });
9
+
10
+ const platformCatalog = Object.freeze([
11
+ { name: "NetEase Cloud Music", code: "netease" },
12
+ { name: "Tencent Music", code: "tencent" },
13
+ { name: "KuGou Music", code: "kugou" },
14
+ { name: "Baidu Music", code: "baidu" },
15
+ { name: "Kuwo Music", code: "kuwo" }
16
+ ]);
17
+
18
+ const platformSchema = z.enum(Meting.getSupportedPlatforms());
19
+
20
+ function CreateClient(platform, cookie) {
21
+ const meting = new Meting(platform);
22
+ meting.format(true);
23
+
24
+ if (cookie) {
25
+ meting.cookie(cookie);
26
+ }
27
+
28
+ return meting;
29
+ }
30
+
31
+ function ParseResult(rawValue) {
32
+ if (typeof rawValue !== "string") {
33
+ return rawValue;
34
+ }
35
+
36
+ try {
37
+ return JSON.parse(rawValue);
38
+ } catch {
39
+ return { raw: rawValue };
40
+ }
41
+ }
42
+
43
+ function CreateTextResult(result, isError = false) {
44
+ return {
45
+ isError,
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: JSON.stringify(result, null, 2)
50
+ }
51
+ ]
52
+ };
53
+ }
54
+
55
+ function CreateOperationResult(operation, platform, data) {
56
+ return {
57
+ ok: true,
58
+ operation,
59
+ platform,
60
+ data
61
+ };
62
+ }
63
+
64
+ function CreateOperationError(operation, platform, error) {
65
+ return {
66
+ ok: false,
67
+ operation,
68
+ platform,
69
+ message: error instanceof Error ? error.message : String(error)
70
+ };
71
+ }
72
+
73
+ function WithCommonInput(extraSchema) {
74
+ return {
75
+ platform: platformSchema.describe("Music platform code."),
76
+ cookie: z.string().optional().describe("Optional platform cookie."),
77
+ ...extraSchema
78
+ };
79
+ }
80
+
81
+ function RegisterTool(server, toolName, description, inputSchema, handler) {
82
+ server.registerTool(
83
+ toolName,
84
+ {
85
+ title: toolName,
86
+ description,
87
+ inputSchema
88
+ },
89
+ async input => {
90
+ try {
91
+ const data = await handler(input);
92
+ return CreateTextResult(CreateOperationResult(toolName, input.platform, data));
93
+ } catch (error) {
94
+ return CreateTextResult(CreateOperationError(toolName, input.platform, error), true);
95
+ }
96
+ }
97
+ );
98
+ }
99
+
100
+ export function CreateMcpServer() {
101
+ const server = new McpServer(serviceMetadata);
102
+
103
+ server.registerTool(
104
+ "platforms",
105
+ {
106
+ title: "platforms",
107
+ description: "List supported music platforms."
108
+ },
109
+ async () => {
110
+ return CreateTextResult({
111
+ ok: true,
112
+ data: platformCatalog
113
+ });
114
+ }
115
+ );
116
+
117
+ RegisterTool(
118
+ server,
119
+ "search",
120
+ "Search songs, albums or artists on a specific platform.",
121
+ WithCommonInput({
122
+ keyword: z.string().min(1).describe("Search keyword."),
123
+ type: z.number().int().positive().optional().describe("Optional platform-specific search type."),
124
+ page: z.number().int().positive().optional().describe("Optional page number."),
125
+ limit: z.number().int().positive().max(100).optional().describe("Optional page size.")
126
+ }),
127
+ async input => {
128
+ const meting = CreateClient(input.platform, input.cookie);
129
+ const options = {};
130
+
131
+ if (input.type !== undefined) {
132
+ options.type = input.type;
133
+ }
134
+
135
+ if (input.page !== undefined) {
136
+ options.page = input.page;
137
+ }
138
+
139
+ if (input.limit !== undefined) {
140
+ options.limit = input.limit;
141
+ }
142
+
143
+ return ParseResult(await meting.search(input.keyword, options));
144
+ }
145
+ );
146
+
147
+ RegisterTool(
148
+ server,
149
+ "song",
150
+ "Get song detail by id.",
151
+ WithCommonInput({
152
+ id: z.string().min(1).describe("Song id.")
153
+ }),
154
+ async input => {
155
+ const meting = CreateClient(input.platform, input.cookie);
156
+ return ParseResult(await meting.song(input.id));
157
+ }
158
+ );
159
+
160
+ RegisterTool(
161
+ server,
162
+ "album",
163
+ "Get album detail by id.",
164
+ WithCommonInput({
165
+ id: z.string().min(1).describe("Album id.")
166
+ }),
167
+ async input => {
168
+ const meting = CreateClient(input.platform, input.cookie);
169
+ return ParseResult(await meting.album(input.id));
170
+ }
171
+ );
172
+
173
+ RegisterTool(
174
+ server,
175
+ "artist",
176
+ "Get artist songs by id.",
177
+ WithCommonInput({
178
+ id: z.string().min(1).describe("Artist id."),
179
+ limit: z.number().int().positive().max(200).optional().describe("Optional result size.")
180
+ }),
181
+ async input => {
182
+ const meting = CreateClient(input.platform, input.cookie);
183
+ return ParseResult(await meting.artist(input.id, input.limit));
184
+ }
185
+ );
186
+
187
+ RegisterTool(
188
+ server,
189
+ "playlist",
190
+ "Get playlist detail by id.",
191
+ WithCommonInput({
192
+ id: z.string().min(1).describe("Playlist id.")
193
+ }),
194
+ async input => {
195
+ const meting = CreateClient(input.platform, input.cookie);
196
+ return ParseResult(await meting.playlist(input.id));
197
+ }
198
+ );
199
+
200
+ RegisterTool(
201
+ server,
202
+ "url",
203
+ "Get playable song url by id.",
204
+ WithCommonInput({
205
+ id: z.string().min(1).describe("Song id."),
206
+ br: z.number().int().positive().optional().describe("Optional bitrate.")
207
+ }),
208
+ async input => {
209
+ const meting = CreateClient(input.platform, input.cookie);
210
+ return ParseResult(await meting.url(input.id, input.br));
211
+ }
212
+ );
213
+
214
+ RegisterTool(
215
+ server,
216
+ "lyric",
217
+ "Get song lyric by id.",
218
+ WithCommonInput({
219
+ id: z.string().min(1).describe("Song id.")
220
+ }),
221
+ async input => {
222
+ const meting = CreateClient(input.platform, input.cookie);
223
+ return ParseResult(await meting.lyric(input.id));
224
+ }
225
+ );
226
+
227
+ RegisterTool(
228
+ server,
229
+ "pic",
230
+ "Get cover or picture url by id.",
231
+ WithCommonInput({
232
+ id: z.string().min(1).describe("Picture id."),
233
+ size: z.number().int().positive().optional().describe("Optional picture size.")
234
+ }),
235
+ async input => {
236
+ const meting = CreateClient(input.platform, input.cookie);
237
+ return ParseResult(await meting.pic(input.id, input.size));
238
+ }
239
+ );
240
+
241
+ return server;
242
+ }
package/src/meting.js ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Meting music framework - Node.js version (重构版本)
3
+ * https://i-meto.com
4
+ * https://github.com/metowolf/Meting
5
+ *
6
+ * Copyright 2019, METO Sheel <i@i-meto.com>
7
+ * Released under the MIT license
8
+ */
9
+
10
+ import { URLSearchParams } from 'url';
11
+ import ProviderFactory from './providers/index.js';
12
+
13
+ class Meting {
14
+ constructor(server = 'netease') {
15
+ this.VERSION = '__VERSION__'; // 在构建时由 rollup 替换为实际版本号
16
+ this.raw = null;
17
+ this.info = null;
18
+ this.error = null;
19
+ this.status = null;
20
+ this.temp = {};
21
+
22
+ this.server = null;
23
+ this.provider = null;
24
+ this.isFormat = false;
25
+ this.header = {};
26
+
27
+ this.site(server);
28
+ }
29
+
30
+ // 设置音乐平台
31
+ site(server) {
32
+ if (!ProviderFactory.isSupported(server)) {
33
+ server = 'netease'; // 默认使用网易云音乐
34
+ }
35
+
36
+ this.server = server;
37
+ this.provider = ProviderFactory.create(server, this);
38
+ this.header = this.provider.getHeaders();
39
+
40
+ return this;
41
+ }
42
+
43
+ // 设置 Cookie
44
+ cookie(cookie) {
45
+ this.header['Cookie'] = cookie;
46
+ return this;
47
+ }
48
+
49
+ // 设置数据格式化
50
+ format(format = true) {
51
+ this.isFormat = format;
52
+ return this;
53
+ }
54
+
55
+ // 执行 API 请求的主方法
56
+ async _exec(api) {
57
+ // 让 Provider 自己处理完整的请求流程
58
+ return await this.provider.executeRequest(api, this);
59
+ }
60
+
61
+ // HTTP 请求方法 - 使用 fetch API
62
+ async _curl(url, payload = null, headerOnly = false) {
63
+ const requestOptions = {
64
+ method: payload ? 'POST' : 'GET',
65
+ headers: { ...this.header }
66
+ };
67
+
68
+ // 处理请求体
69
+ if (payload) {
70
+ if (typeof payload === 'object' && !Buffer.isBuffer(payload) && typeof payload !== 'string') {
71
+ payload = new URLSearchParams(payload).toString();
72
+ requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
73
+ }
74
+ requestOptions.body = payload;
75
+ }
76
+
77
+ // 添加超时控制
78
+ const controller = new AbortController();
79
+ const timeoutId = setTimeout(() => controller.abort(), 20000);
80
+ requestOptions.signal = controller.signal;
81
+
82
+ let retries = 3;
83
+ const makeRequest = async () => {
84
+ try {
85
+ const response = await fetch(url, requestOptions);
86
+
87
+ clearTimeout(timeoutId);
88
+
89
+ // 存储响应信息
90
+ this.info = {
91
+ statusCode: response.status,
92
+ headers: Object.fromEntries(response.headers.entries())
93
+ };
94
+
95
+ // 获取响应数据
96
+ const data = await response.text();
97
+ this.raw = data;
98
+ this.error = null;
99
+ this.status = '';
100
+
101
+ return this;
102
+ } catch (err) {
103
+ clearTimeout(timeoutId);
104
+
105
+ // 处理错误
106
+ if (err.name === 'AbortError') {
107
+ this.error = 'TIMEOUT';
108
+ this.status = 'Request timeout';
109
+ } else {
110
+ this.error = err.code || err.name;
111
+ this.status = err.message;
112
+ }
113
+
114
+ // 重试机制
115
+ if (retries > 0) {
116
+ retries--;
117
+ await new Promise(resolve => setTimeout(resolve, 1000));
118
+ return makeRequest();
119
+ } else {
120
+ return this;
121
+ }
122
+ }
123
+ };
124
+
125
+ return await makeRequest();
126
+ }
127
+
128
+
129
+ // ========== 公共 API 方法 ==========
130
+
131
+ // 搜索功能
132
+ async search(keyword, option = {}) {
133
+ const api = this.provider.search(keyword, option);
134
+ return await this._exec(api);
135
+ }
136
+
137
+ // 获取歌曲详情
138
+ async song(id) {
139
+ const api = this.provider.song(id);
140
+ return await this._exec(api);
141
+ }
142
+
143
+ // 获取专辑信息
144
+ async album(id) {
145
+ const api = this.provider.album(id);
146
+ return await this._exec(api);
147
+ }
148
+
149
+ // 获取艺术家作品
150
+ async artist(id, limit = 50) {
151
+ const api = this.provider.artist(id, limit);
152
+ return await this._exec(api);
153
+ }
154
+
155
+ // 获取播放列表
156
+ async playlist(id) {
157
+ const api = this.provider.playlist(id);
158
+ return await this._exec(api);
159
+ }
160
+
161
+ // 获取音频播放链接
162
+ async url(id, br = 320) {
163
+ this.temp.br = br;
164
+ const api = this.provider.url(id, br);
165
+ return await this._exec(api);
166
+ }
167
+
168
+ // 获取歌词
169
+ async lyric(id) {
170
+ const api = this.provider.lyric(id);
171
+ return await this._exec(api);
172
+ }
173
+
174
+ // 获取封面图片
175
+ async pic(id, size = 300) {
176
+ return await this.provider.pic(id, size);
177
+ }
178
+
179
+ // ========== 静态方法 ==========
180
+
181
+ // 获取支持的平台列表
182
+ static getSupportedPlatforms() {
183
+ return ProviderFactory.getSupportedPlatforms();
184
+ }
185
+
186
+ // 检查平台是否支持
187
+ static isSupported(platform) {
188
+ return ProviderFactory.isSupported(platform);
189
+ }
190
+ }
191
+
192
+ export default Meting;