cyy-mall-mcp 0.1.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 +85 -0
  2. package/package.json +36 -0
  3. package/src/index.js +352 -0
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # cyy-mall-mcp
2
+
3
+ 通过 **Model Context Protocol (stdio)** 将 [**cyymall-cli**](https://www.npmjs.com/package/cyymall-cli)(命令 `cyy`)暴露给 Cursor、阿里百炼 FC MCP、Claude Desktop 等宿主。
4
+
5
+ ## 原理
6
+
7
+ 本进程使用 **`@modelcontextprotocol/sdk`** 注册工具;每次工具调用通过 **`node /path/to/cyymall-cli/bin/cyy.js …`** 执行,与你在终端手工输入 `cyy …` 等价。会话仍落在 **`~/.cyymall/config.json`**(或由宿主注入的环境变量)。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install -g cyy-mall-mcp cyymall-cli
13
+ ```
14
+
15
+ 全局安装 `cyymall-cli` 可保证任意工作目录下都能 `require.resolve('cyymall-cli/bin/cyy.js')`;仅安装 `cyy-mall-mcp` 时,`cyymall-cli` 会作为依赖出现在 `node_modules` 中,同样可用。
16
+
17
+ 本地开发(与本仓库同级的 `cyymall-cli`)可在本目录:
18
+
19
+ ```bash
20
+ npm install
21
+ node src/index.js
22
+ ```
23
+
24
+ ## 运行(stdio MCP)
25
+
26
+ ```bash
27
+ npx cyy-mall-mcp
28
+ # 或
29
+ cyy-mall-mcp
30
+ ```
31
+
32
+ **注意:** MCP 使用 **stdio** 通信,请勿再向 **stdout** 打印调试信息。
33
+
34
+ ## 环境变量
35
+
36
+ | 变量 | 说明 |
37
+ |------|------|
38
+ | `CYY_BASE_URL` | API 根地址,默认 `https://dhcmall.ifoodbuy.com` |
39
+ | `CYY_PASSWORD` | 配合工具 **`auth_login`** 使用(勿把密码写进工具参数或聊天记录) |
40
+ | `CYY_MCP_ALLOW_WRITE` | 设为 `true` 时才允许 **`cart_add`**、**`order_pre_settle`**、**`order_confirm`**、**`order_quick`**(降低误下单风险) |
41
+ | 其它 `CYY_*` | 与 [cyymall-cli README](https://www.npmjs.com/package/cyymall-cli) 一致(如 `CYY_BOOTSTRAP_*`) |
42
+
43
+ ## 工具一览
44
+
45
+ | 工具名 | 说明 |
46
+ |--------|------|
47
+ | `auth_whoami` | 当前登录态 |
48
+ | `config_show` | 脱敏后的会话配置 |
49
+ | `auth_login` | 登录(需 `CYY_PASSWORD`) |
50
+ | `shop_list` / `shop_sites` / `shop_use` / `shop_use_site` | 门店与站点 |
51
+ | `product_search` | 商品搜索 |
52
+ | `api_call` | 万能 API(对应 `cyy api call`) |
53
+ | `order_pay_url` | 代付链接 |
54
+ | `cart_add` / `order_pre_settle` / `order_confirm` / `order_quick` | 需 **`CYY_MCP_ALLOW_WRITE=true`** |
55
+
56
+ ## 阿里百炼 / FC MCP 部署示例
57
+
58
+ 安装方式选 **npx**,在 **MCP 服务配置** 中可使用(按控制台实际字段调整):
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "cyy-mall": {
64
+ "command": "npx",
65
+ "args": ["-y", "cyy-mall-mcp"],
66
+ "env": {
67
+ "CYY_BASE_URL": "https://dhcmall.ifoodbuy.com",
68
+ "CYY_MCP_ALLOW_WRITE": "false"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ 首次冷启动时 `npx` 会下载依赖,可能需要 **数十秒**。需要登录时,在 FC 环境变量中配置 **`CYY_PASSWORD`**(注意安全存储与轮换),再通过对话触发 **`auth_login`**。
76
+
77
+ ## 安全说明
78
+
79
+ - 禁止在模型对话中粘贴 **npm token、商城密码、session token**。
80
+ - 生产环境应对 MCP 入口做 **鉴权与租户隔离**;本包仅为 thin wrapper。
81
+ - 下单类工具默认关闭,需显式打开 **`CYY_MCP_ALLOW_WRITE`**。
82
+
83
+ ## 许可证
84
+
85
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "cyy-mall-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that exposes 菜洋洋 cyymall-cli (cyy) as Model Context Protocol tools (stdio)",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "cyy-mall-mcp": "src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "package.json"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "start": "node src/index.js",
20
+ "prepublishOnly": "node --check src/index.js"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "cyymall",
26
+ "dhcmall",
27
+ "cyy",
28
+ "cli"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "cyymall-cli": "^0.1.0",
34
+ "zod": "^4.4.3"
35
+ }
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server: exposes cyymall-cli (`cyy`) as MCP tools via stdio.
4
+ * Requires dependency `cyymall-cli` (runs bin/cyy.js with Node).
5
+ */
6
+ import { createRequire } from "node:module";
7
+ import { spawnSync } from "node:child_process";
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import * as z from "zod/v4";
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ function resolveCyyScript() {
15
+ try {
16
+ return require.resolve("cyymall-cli/bin/cyy.js");
17
+ } catch {
18
+ throw new Error(
19
+ "Cannot find cyymall-cli. Install: npm install cyymall-cli (this package lists it as a dependency).",
20
+ );
21
+ }
22
+ }
23
+
24
+ /**
25
+ * @param {string[]} argv Arguments after `cyy`
26
+ */
27
+ function runCyy(argv) {
28
+ const script = resolveCyyScript();
29
+ const r = spawnSync(process.execPath, [script, ...argv], {
30
+ encoding: "utf8",
31
+ maxBuffer: 50 * 1024 * 1024,
32
+ windowsHide: true,
33
+ env: { ...process.env },
34
+ });
35
+ let text = (r.stdout ?? "").trim();
36
+ const err = (r.stderr ?? "").trim();
37
+ if (r.status !== 0 && err) {
38
+ text = text ? `${text}\n${err}` : err;
39
+ } else if (err && !text) {
40
+ text = err;
41
+ }
42
+ if (r.error) {
43
+ text = text ? `${text}\n${r.error.message}` : String(r.error.message);
44
+ }
45
+ return { text: text || "(empty)", ok: r.status === 0 };
46
+ }
47
+
48
+ function textResult(text, isError) {
49
+ return {
50
+ content: [{ type: "text", text }],
51
+ isError: Boolean(isError),
52
+ };
53
+ }
54
+
55
+ function allowWrite() {
56
+ return process.env.CYY_MCP_ALLOW_WRITE === "true" || process.env.CYY_MCP_ALLOW_WRITE === "1";
57
+ }
58
+
59
+ const emptyObj = z.object({});
60
+
61
+ const mcpServer = new McpServer(
62
+ {
63
+ name: "cyy-mall-mcp",
64
+ version: "0.1.0",
65
+ },
66
+ {
67
+ capabilities: { tools: {} },
68
+ instructions:
69
+ "菜洋洋商城 MCP:工具通过子进程调用 cyymall-cli(cyy)。会话默认 ~/.cyymall/config.json。购物车/预结算/确认/一键下单需 CYY_MCP_ALLOW_WRITE=true。登录密码仅通过环境变量 CYY_PASSWORD 配合 auth_login,勿在对话中传密码。",
70
+ },
71
+ );
72
+
73
+ mcpServer.registerTool(
74
+ "auth_whoami",
75
+ {
76
+ description: "GET /member/getInfo/V2 — current member info using saved session (cyy auth whoami)",
77
+ inputSchema: emptyObj,
78
+ },
79
+ async () => {
80
+ const r = runCyy(["auth", "whoami"]);
81
+ return textResult(r.text, !r.ok);
82
+ },
83
+ );
84
+
85
+ mcpServer.registerTool(
86
+ "config_show",
87
+ {
88
+ description: "Print ~/.cyymall/config.json with masked token (cyy config show)",
89
+ inputSchema: emptyObj,
90
+ },
91
+ async () => {
92
+ const r = runCyy(["config", "show"]);
93
+ return textResult(r.text, !r.ok);
94
+ },
95
+ );
96
+
97
+ mcpServer.registerTool(
98
+ "auth_login",
99
+ {
100
+ description:
101
+ "Password login (cyy auth login --phone …). Requires env CYY_PASSWORD; password must NOT be passed in tool arguments.",
102
+ inputSchema: {
103
+ phone: z.string().describe("Mobile phone"),
104
+ },
105
+ },
106
+ async ({ phone }) => {
107
+ if (!process.env.CYY_PASSWORD) {
108
+ return textResult(
109
+ "Refused: set environment variable CYY_PASSWORD on the MCP server host (do not pass password in chat).",
110
+ true,
111
+ );
112
+ }
113
+ const r = runCyy(["auth", "login", "--phone", phone]);
114
+ return textResult(r.text, !r.ok);
115
+ },
116
+ );
117
+
118
+ mcpServer.registerTool(
119
+ "shop_list",
120
+ {
121
+ description: "POST /shop/member/list — paginated shops (cyy shop list)",
122
+ inputSchema: {
123
+ page: z.string().optional().describe('pageNum default "1"'),
124
+ pageSize: z.string().optional().describe('pageSize default "20"'),
125
+ name: z.string().optional().describe("shopName filter"),
126
+ objectCode: z.string().optional().describe('objectCode default "3"'),
127
+ },
128
+ },
129
+ async (args) => {
130
+ const argv = ["shop", "list"];
131
+ if (args.page != null) argv.push("--page", String(args.page));
132
+ if (args.pageSize != null) argv.push("--page-size", String(args.pageSize));
133
+ if (args.name != null) argv.push("--name", String(args.name));
134
+ if (args.objectCode != null) argv.push("--object-code", String(args.objectCode));
135
+ const r = runCyy(argv);
136
+ return textResult(r.text, !r.ok);
137
+ },
138
+ );
139
+
140
+ mcpServer.registerTool(
141
+ "shop_sites",
142
+ {
143
+ description: "GET /shop/member/store/list — sites for a shop (cyy shop sites)",
144
+ inputSchema: {
145
+ shopId: z.string().optional().describe("Shop id; omit to use session shop_id"),
146
+ siteName: z.string().optional().describe("Optional site name filter"),
147
+ },
148
+ },
149
+ async (args) => {
150
+ const argv = ["shop", "sites"];
151
+ if (args.shopId != null && String(args.shopId).length)
152
+ argv.push("--shop-id", String(args.shopId));
153
+ if (args.siteName != null && String(args.siteName).length)
154
+ argv.push("--site-name", String(args.siteName));
155
+ const r = runCyy(argv);
156
+ return textResult(r.text, !r.ok);
157
+ },
158
+ );
159
+
160
+ mcpServer.registerTool(
161
+ "shop_use",
162
+ {
163
+ description: "Set default shop_id only in session config (cyy shop use --shop-id)",
164
+ inputSchema: {
165
+ shopId: z.string().describe("Numeric shop id from shop_list"),
166
+ },
167
+ },
168
+ async ({ shopId }) => {
169
+ const r = runCyy(["shop", "use", "--shop-id", String(shopId)]);
170
+ return textResult(r.text, !r.ok);
171
+ },
172
+ );
173
+
174
+ mcpServer.registerTool(
175
+ "shop_use_site",
176
+ {
177
+ description: "Set default site_id for current shop (cyy shop use-site --site-id)",
178
+ inputSchema: {
179
+ siteId: z.string().describe("Site id from shop_sites"),
180
+ },
181
+ },
182
+ async ({ siteId }) => {
183
+ const r = runCyy(["shop", "use-site", "--site-id", String(siteId)]);
184
+ return textResult(r.text, !r.ok);
185
+ },
186
+ );
187
+
188
+ mcpServer.registerTool(
189
+ "product_search",
190
+ {
191
+ description: "POST /app/product/getSkuList — search SKU (cyy product search)",
192
+ inputSchema: {
193
+ keyword: z.string().describe("spuName keyword"),
194
+ shopId: z.string().optional(),
195
+ siteId: z.string().optional(),
196
+ page: z.string().optional(),
197
+ pageSize: z.string().optional(),
198
+ stockFlag: z.string().optional().describe('stockFlag string e.g. "0"'),
199
+ },
200
+ },
201
+ async (args) => {
202
+ const argv = ["product", "search", "--keyword", args.keyword];
203
+ if (args.shopId != null) argv.push("--shop-id", String(args.shopId));
204
+ if (args.siteId != null) argv.push("--site-id", String(args.siteId));
205
+ if (args.page != null) argv.push("--page", String(args.page));
206
+ if (args.pageSize != null) argv.push("--page-size", String(args.pageSize));
207
+ if (args.stockFlag != null) argv.push("--stock-flag", String(args.stockFlag));
208
+ const r = runCyy(argv);
209
+ return textResult(r.text, !r.ok);
210
+ },
211
+ );
212
+
213
+ mcpServer.registerTool(
214
+ "api_call",
215
+ {
216
+ description:
217
+ "Low-level API call (cyy api call). module = DEFAULT|ORDER|PRODUCT|PLATFORM|PAYMENT|OSS|BIZ",
218
+ inputSchema: {
219
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
220
+ module: z.enum(["DEFAULT", "ORDER", "PRODUCT", "PLATFORM", "PAYMENT", "OSS", "BIZ"]),
221
+ path: z.string().describe("Path starting with /"),
222
+ bodyJson: z.string().optional().describe("JSON body string for non-GET"),
223
+ query: z.array(z.string()).optional().describe('Repeated query pairs key=value e.g. ["shopId=1"]'),
224
+ bare: z.boolean().optional().describe("Bootstrap headers only (no merged session)"),
225
+ },
226
+ },
227
+ async (args) => {
228
+ const argv = ["api", "call", "--method", args.method, "--module", args.module, "--path", args.path];
229
+ if (args.bodyJson != null && args.bodyJson.length) argv.push("--body-json", args.bodyJson);
230
+ if (args.query && args.query.length) {
231
+ for (const q of args.query) argv.push("--query", q);
232
+ }
233
+ if (args.bare) argv.push("--bare");
234
+ const r = runCyy(argv);
235
+ return textResult(r.text, !r.ok);
236
+ },
237
+ );
238
+
239
+ function gateWrite(name) {
240
+ if (!allowWrite()) {
241
+ return textResult(
242
+ `Tool "${name}" is disabled. Set env CYY_MCP_ALLOW_WRITE=true on the MCP server to enable cart/order tools.`,
243
+ true,
244
+ );
245
+ }
246
+ return null;
247
+ }
248
+
249
+ mcpServer.registerTool(
250
+ "cart_add",
251
+ {
252
+ description: "POST /app/order/cart — requires CYY_MCP_ALLOW_WRITE=true",
253
+ inputSchema: {
254
+ bodyJson: z.string().describe("Full cart JSON body"),
255
+ },
256
+ },
257
+ async ({ bodyJson }) => {
258
+ const g = gateWrite("cart_add");
259
+ if (g) return g;
260
+ const r = runCyy(["cart", "add", "--body-json", bodyJson]);
261
+ return textResult(r.text, !r.ok);
262
+ },
263
+ );
264
+
265
+ mcpServer.registerTool(
266
+ "order_pre_settle",
267
+ {
268
+ description: "POST /app/order/preSettleOrder — requires CYY_MCP_ALLOW_WRITE=true",
269
+ inputSchema: {
270
+ bodyJson: z.string(),
271
+ },
272
+ },
273
+ async ({ bodyJson }) => {
274
+ const g = gateWrite("order_pre_settle");
275
+ if (g) return g;
276
+ const r = runCyy(["order", "pre-settle", "--body-json", bodyJson]);
277
+ return textResult(r.text, !r.ok);
278
+ },
279
+ );
280
+
281
+ mcpServer.registerTool(
282
+ "order_confirm",
283
+ {
284
+ description: "POST /app/order/confirmOrder — DESTRUCTIVE; requires CYY_MCP_ALLOW_WRITE=true and user confirmation in host app",
285
+ inputSchema: {
286
+ bodyJson: z.string(),
287
+ },
288
+ },
289
+ async ({ bodyJson }) => {
290
+ const g = gateWrite("order_confirm");
291
+ if (g) return g;
292
+ const r = runCyy(["order", "confirm", "--body-json", bodyJson]);
293
+ return textResult(r.text, !r.ok);
294
+ },
295
+ );
296
+
297
+ mcpServer.registerTool(
298
+ "order_quick",
299
+ {
300
+ description:
301
+ "Search→cart→pre-settle→confirm — DESTRUCTIVE; requires CYY_MCP_ALLOW_WRITE=true",
302
+ inputSchema: {
303
+ keyword: z.string(),
304
+ quantity: z.string().optional().describe('default "1"'),
305
+ unit: z.string().optional().describe('e.g. 袋'),
306
+ shopId: z.string().optional(),
307
+ siteId: z.string().optional(),
308
+ },
309
+ },
310
+ async (args) => {
311
+ const g = gateWrite("order_quick");
312
+ if (g) return g;
313
+ const argv = [
314
+ "order",
315
+ "quick",
316
+ "--keyword",
317
+ args.keyword,
318
+ "--quantity",
319
+ args.quantity ?? "1",
320
+ "--unit",
321
+ args.unit ?? "袋",
322
+ ];
323
+ if (args.shopId != null) argv.push("--shop-id", String(args.shopId));
324
+ if (args.siteId != null) argv.push("--site-id", String(args.siteId));
325
+ const r = runCyy(argv);
326
+ return textResult(r.text, !r.ok);
327
+ },
328
+ );
329
+
330
+ mcpServer.registerTool(
331
+ "order_pay_url",
332
+ {
333
+ description: "Build H5 replacePay URL (cyy order pay-url --order-id)",
334
+ inputSchema: {
335
+ orderId: z.string(),
336
+ },
337
+ },
338
+ async ({ orderId }) => {
339
+ const r = runCyy(["order", "pay-url", "--order-id", orderId]);
340
+ return textResult(r.text, !r.ok);
341
+ },
342
+ );
343
+
344
+ async function main() {
345
+ const transport = new StdioServerTransport();
346
+ await mcpServer.connect(transport);
347
+ }
348
+
349
+ main().catch((e) => {
350
+ console.error(e);
351
+ process.exit(1);
352
+ });