@sea-ecommerce/admin-mcp-server 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  | `update_product_prices` | 替换变体价格(多币种) | 写(dry_run 默认) |
18
18
  | `upload_product_image` / `add_product_to_sales_channel` | 追加图片 / 挂渠道 | 写(dry_run 默认) |
19
19
  | `create_product_content` / `get_product_content` | 详情页富内容块(hero/richtext/specs/图集/faq/video) | 写 / 读 |
20
- | `set_product_stock` | 给商品建仓/改库存(自动建库存条目,幂等) | 写(dry_run 默认) |
20
+ | `set_product_stock` | 给商品建仓/改库存(自动建库存条目 + 关联变体,幂等) | 写(dry_run 默认) |
21
21
  | `list_stock_locations` / `create_stock_location` | 列 / 增仓库 | 写(dry_run 默认) |
22
22
  | `get_inventory_levels` / `update_inventory_level` | 查 / 改库存水平 | 写(dry_run 默认) |
23
23
  | `list_orders` / `get_order` | 查订单 | 读 |
@@ -21567,6 +21567,23 @@ function loadConfig() {
21567
21567
  requiredLocales
21568
21568
  };
21569
21569
  }
21570
+ var LOCALE_CANON = {
21571
+ en: "en-US",
21572
+ "en-us": "en-US",
21573
+ "en-US": "en-US",
21574
+ zh: "zh-CN",
21575
+ "zh-cn": "zh-CN",
21576
+ "zh-CN": "zh-CN",
21577
+ ms: "ms-MY",
21578
+ "ms-my": "ms-MY",
21579
+ "ms-MY": "ms-MY",
21580
+ id: "id-ID",
21581
+ "id-id": "id-ID",
21582
+ "id-ID": "id-ID"
21583
+ };
21584
+ function normalizeLocale(input) {
21585
+ return LOCALE_CANON[input] ?? LOCALE_CANON[input.trim().toLowerCase()] ?? null;
21586
+ }
21570
21587
 
21571
21588
  // src/auth.ts
21572
21589
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -21858,6 +21875,19 @@ var MedusaAdminClient = class {
21858
21875
  body
21859
21876
  );
21860
21877
  }
21878
+ /**
21879
+ * 把库存条目关联到变体(POST /admin/products/{pid}/variants/{vid}/inventory-items)。
21880
+ * ⚠️ 关联是「变体能查到库存」的关键:只有关联后,变体的 manage_inventory 才指向该条目,
21881
+ * 且设置 manage_inventory=true 时 Medusa 不会因「无关联条目」而另建同 SKU 条目(撞唯一约束)。
21882
+ * 重复关联会报已存在,调用方应 try/catch 吞掉「已关联」类错误。
21883
+ */
21884
+ linkVariantInventoryItem(productId, variantId, inventoryItemId) {
21885
+ return this.request(
21886
+ "POST",
21887
+ `/admin/products/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}/inventory-items`,
21888
+ { inventory_item_id: inventoryItemId, required_quantity: 1 }
21889
+ );
21890
+ }
21861
21891
  /**
21862
21892
  * 幂等地设置某库存条目在指定仓库的库存数量:
21863
21893
  * 已有 (item, location) 水平 → 更新;没有 → 创建。
@@ -21912,35 +21942,65 @@ async function applyProductStock(ctx2, opts) {
21912
21942
  location = list.find((l) => /main warehouse/i.test(l.name)) || list[0];
21913
21943
  if (!location) throw new Error("\u6CA1\u6709\u4EFB\u4F55\u53EF\u7528\u4ED3\u5E93\uFF0C\u8BF7\u5148 create_stock_location");
21914
21944
  }
21915
- let iitem = null;
21916
- if (variant.sku) {
21945
+ const findItem = async () => {
21946
+ if (!variant.sku) return null;
21917
21947
  const items = await client2.listInventoryItems({ sku: variant.sku, limit: 10 });
21918
- iitem = (items?.inventory_items || []).find((i) => i.sku === variant.sku) || null;
21919
- }
21920
- if (!iitem) {
21921
- if (!variant.sku) throw new Error(`\u53D8\u4F53\u6CA1\u6709 SKU\uFF0C\u65E0\u6CD5\u5173\u8054\u5E93\u5B58\u6761\u76EE\uFF08\u8BF7\u5148 update_product \u8865 sku\uFF09`);
21922
- const created = await client2.createInventoryItem({
21948
+ return (items?.inventory_items || []).find((i) => i.sku === variant.sku) || null;
21949
+ };
21950
+ if (opts.manageInventory) {
21951
+ if (!variant.sku) {
21952
+ throw new Error(`\u53D8\u4F53\u6CA1\u6709 SKU\uFF0C\u65E0\u6CD5\u5173\u8054\u5E93\u5B58\u6761\u76EE\uFF08\u8BF7\u5148 update_product \u8865 sku\uFF09`);
21953
+ }
21954
+ let iitem2 = await findItem();
21955
+ if (!iitem2) {
21956
+ const created = await client2.createInventoryItem({
21957
+ sku: variant.sku,
21958
+ title: variant.title || void 0
21959
+ });
21960
+ iitem2 = created?.inventory_item ?? created;
21961
+ }
21962
+ try {
21963
+ await client2.linkVariantInventoryItem(product.id, variant.id, iitem2.id);
21964
+ } catch {
21965
+ }
21966
+ if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21967
+ await client2.updateProduct(product.id, {
21968
+ variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21969
+ });
21970
+ }
21971
+ iitem2 = await findItem() || iitem2;
21972
+ const level = await client2.upsertInventoryLevel(iitem2.id, location.id, opts.quantity);
21973
+ return {
21974
+ product_id: product.id,
21975
+ variant_id: variant.id,
21923
21976
  sku: variant.sku,
21924
- title: variant.title || void 0
21925
- });
21926
- iitem = created?.inventory_item ?? created;
21977
+ location_id: location.id,
21978
+ location_name: location.name,
21979
+ inventory_item_id: iitem2.id,
21980
+ manage_inventory: opts.manageInventory,
21981
+ stocked_quantity: level.stocked_quantity,
21982
+ level_created: level.created
21983
+ };
21927
21984
  }
21928
- const level = await client2.upsertInventoryLevel(iitem.id, location.id, opts.quantity);
21929
21985
  if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21930
21986
  await client2.updateProduct(product.id, {
21931
21987
  variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21932
21988
  });
21933
21989
  }
21990
+ const iitem = await findItem();
21991
+ if (iitem) {
21992
+ await client2.upsertInventoryLevel(iitem.id, location.id, 0);
21993
+ }
21934
21994
  return {
21935
21995
  product_id: product.id,
21936
21996
  variant_id: variant.id,
21937
21997
  sku: variant.sku,
21938
21998
  location_id: location.id,
21939
21999
  location_name: location.name,
21940
- inventory_item_id: iitem.id,
21941
- manage_inventory: opts.manageInventory,
21942
- stocked_quantity: level.stocked_quantity,
21943
- level_created: level.created
22000
+ inventory_item_id: iitem?.id ?? null,
22001
+ manage_inventory: false,
22002
+ stocked_quantity: 0,
22003
+ note: "\u5DF2\u5173\u95ED\u5E93\u5B58\u7BA1\u7406\u5E76\u6E05\u96F6\u5E93\u5B58"
21944
22004
  };
21945
22005
  }
21946
22006
  var stockTools = [
@@ -22530,15 +22590,19 @@ metadata \u4E3A**\u5408\u5E76**\u8BED\u4E49\uFF1A\u65B0\u952E\u8986\u76D6\u540C\
22530
22590
  {
22531
22591
  name: "get_product_content",
22532
22592
  description: `\u8BFB\u53D6\u5546\u54C1\u8BE6\u60C5\u9875\u5BCC\u5185\u5BB9\u5757\uFF08GET /admin/product-contents\uFF09\u3002
22533
- locale \u7528\u533A\u57DF\u7801\uFF1Aen-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF useProductContent \u7684 localeMap \u4E00\u81F4\uFF09\u3002
22593
+ locale \u63A5\u53D7\u533A\u57DF\u7801 en-US / zh-CN / ms-MY / id-ID\uFF08\u63A8\u8350\uFF0C\u4E0E\u524D\u7AEF\u4E00\u81F4\uFF09\u6216\u77ED\u7801 en / zh / ms / id\uFF08\u81EA\u52A8\u5F52\u4E00\u5316\u4E3A\u533A\u57DF\u7801\uFF09\u3002
22534
22594
  \u8FD4\u56DE { product_content: { id, product_id, locale, status, blocks(\u5DF2\u89E3\u6790\u6570\u7EC4) } | null }\u3002`,
22535
22595
  schema: {
22536
22596
  product_id: external_exports.string(),
22537
- locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
22597
+ locale: external_exports.string().default("en-US").describe("\u533A\u57DF\u7801 en-US/zh-CN/ms-MY/id-ID\uFF0C\u6216\u77ED\u7801 en/zh/ms/id"),
22538
22598
  status: external_exports.enum(["draft", "published"]).default("published")
22539
22599
  },
22540
22600
  async run(input, { client: client2 }) {
22541
- const data = await client2.getProductContent(input.product_id, input.locale, input.status);
22601
+ const locale = normalizeLocale(input.locale);
22602
+ if (!locale) {
22603
+ return `\u9519\u8BEF\uFF1Alocale "${input.locale}" \u4E0D\u53D7\u652F\u6301\uFF0C\u8BF7\u7528 en-US / zh-CN / ms-MY / id-ID\uFF08\u6216\u77ED\u7801 en/zh/ms/id\uFF09\u3002`;
22604
+ }
22605
+ const data = await client2.getProductContent(input.product_id, locale, input.status);
22542
22606
  const pc = data?.product_content;
22543
22607
  if (!pc) return { product_content: null };
22544
22608
  let blocks = [];
@@ -22561,11 +22625,11 @@ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4
22561
22625
  - video: data.{ url, poster? }\uFF08\u652F\u6301 YouTube\uFF09
22562
22626
  - faq: data.{ items: {question,answer}[] }
22563
22627
  - specs: data.{ rows: {label,value}[] }
22564
- \u26A0\uFE0F locale \u5FC5\u987B\u7528\u533A\u57DF\u7801 en-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF\u4E00\u81F4\uFF09\uFF1B\u5546\u54C1\u6807\u9898/\u63CF\u8FF0\u7684\u7FFB\u8BD1\u7528\u77ED\u7801 en/zh/ms/id\uFF0C\u4E8C\u8005\u4E0D\u540C\u3002
22628
+ \u26A0\uFE0F locale \u7528\u533A\u57DF\u7801 en-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF\u4E00\u81F4\uFF0C\u63A8\u8350\uFF09\uFF1B\u4E5F\u63A5\u53D7\u77ED\u7801 en/zh/ms/id\uFF08\u81EA\u52A8\u5F52\u4E00\u5316\u4E3A\u533A\u57DF\u7801\uFF09\u3002\u5546\u54C1\u6807\u9898/\u63CF\u8FF0\u7684\u7FFB\u8BD1\u7528\u77ED\u7801\uFF0C\u4E8C\u8005\u4E0D\u540C\u3002
22565
22629
  \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
22566
22630
  schema: {
22567
22631
  product_id: external_exports.string(),
22568
- locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
22632
+ locale: external_exports.string().default("en-US").describe("\u533A\u57DF\u7801 en-US/zh-CN/ms-MY/id-ID\uFF0C\u6216\u77ED\u7801 en/zh/ms/id"),
22569
22633
  status: external_exports.enum(["draft", "published"]).default("published"),
22570
22634
  blocks: external_exports.array(
22571
22635
  external_exports.object({
@@ -22577,12 +22641,16 @@ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4
22577
22641
  confirm: external_exports.boolean().default(false)
22578
22642
  },
22579
22643
  async run(input, { client: client2 }) {
22644
+ const locale = normalizeLocale(input.locale);
22645
+ if (!locale) {
22646
+ return `\u9519\u8BEF\uFF1Alocale "${input.locale}" \u4E0D\u53D7\u652F\u6301\uFF0C\u8BF7\u7528 en-US / zh-CN / ms-MY / id-ID\uFF08\u6216\u77ED\u7801 en/zh/ms/id\uFF09\u3002`;
22647
+ }
22580
22648
  if (!input.confirm) {
22581
22649
  return {
22582
22650
  dry_run: true,
22583
22651
  will_create: {
22584
22652
  product_id: input.product_id,
22585
- locale: input.locale,
22653
+ locale,
22586
22654
  status: input.status,
22587
22655
  block_count: input.blocks.length,
22588
22656
  types: input.blocks.map((b) => b.type)
@@ -22591,7 +22659,7 @@ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4
22591
22659
  }
22592
22660
  const data = await client2.createProductContent({
22593
22661
  product_id: input.product_id,
22594
- locale: input.locale,
22662
+ locale,
22595
22663
  status: input.status,
22596
22664
  blocks: input.blocks
22597
22665
  });
@@ -22779,7 +22847,7 @@ var config2 = loadConfig();
22779
22847
  var auth = new AuthManager(config2);
22780
22848
  var client = new MedusaAdminClient(config2, auth);
22781
22849
  var ctx = { client, config: config2 };
22782
- var server = new McpServer({ name: "derlux-admin", version: "0.1.2" });
22850
+ var server = new McpServer({ name: "derlux-admin", version: "0.1.4" });
22783
22851
  for (const def of toolDefs) {
22784
22852
  server.tool(def.name, def.description, def.schema, async (args) => {
22785
22853
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sea-ecommerce/admin-mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "让运营同学用本地 AI agent(Codex / Workbuddy / Claude / Cursor / CodeBuddy)运营 Medusa 后台的 MCP server。",
package/skill/SKILL.md CHANGED
@@ -29,7 +29,7 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
29
29
  6. **商品必须挂销售渠道才在前端可见**:新建商品默认**不在任何销售渠道**,storefront(按 publishable key → 渠道过滤)拉不到它,详情页表现成「空/404」。录入后用 `add_product_to_sales_channel`(`confirm=true`)把它加入渠道(默认第一个启用渠道;马来西亚站用 `sc_01M4K4F0D6HBHZ0NH6WB7H5KPT`「MY Storefront」)。
30
30
  7. **两套 locale 码,别混**:
31
31
  - 商品标题/描述的**翻译**(`create_product` 的 `translations`、后端 `/admin/translations/batch`):用**简码** `en` / `zh` / `ms` / `id`。store `/store/products?locale=zh` 即回退到 zh 翻译。
32
- - 商品详情页**富内容块**(`create_product_content` 的 `locale`):必须用**区域码** `en-US` / `zh-CN` / `ms-MY` / `id-ID`(与前端 `useProductContent` 的 localeMap 一致)。用简码会导致该语言详情页读不到内容。
32
+ - 商品详情页**富内容块**(`create_product_content` / `get_product_content` 的 `locale`):推荐用**区域码** `en-US` / `zh-CN` / `ms-MY` / `id-ID`(与前端 `useProductContent` 的 localeMap 一致);**简码 `en`/`zh`/`ms`/`id` 也会被工具自动归一化为区域码**,两种写法都能命中,但请统一用区域码避免歧义。
33
33
  8. **详情页「富内容」区靠 `create_product_content`,不是 `description`**:storefront 详情页下方大块内容由 `product_content` 模块渲染(`richtext`/`specs`/`image_gallery` 等)。只填 `product.description` 不会显示在那里——录入完整商品时必须再建一份 `product_content`(建议每语言一份)。`get_product` 默认不含该字段,核对请用 `get_product_content`。
34
34
  9. **不建库存 = 前端缺货**:变体 `manage_inventory` 默认 `true`,但新商品默认 0 库存——`create_product` 时必须给 `initial_stock`(如 `{quantity:100}`,默认写 Main Warehouse),或对已有商品调 `set_product_stock`,否则前台显示缺货、无法下单。
35
35
  10. **多语言核对用 `get_translations`**:`get_product` 默认字段**不含** translations,不要凭它为空就判断没翻译。用 `get_translations`(reference_id=商品id或handle) 读取实际写入的各语言 title/description。
@@ -130,7 +130,9 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
130
130
  - `name`(string,**必填**);`address`(可选:`{address_1?,address_2?,city?,country_code?,province?,postal_code?,phone?}`);`metadata`(`record<string,string>`,可选);`confirm`(默认 false)。
131
131
  - **`set_product_stock`**:给商品建仓/改库存(**运营最常用的库存工具**,幂等)。
132
132
  - `product_id`(string,**必填**,商品 id 或 handle);`sku`(string,可选,目标变体;省略取第一个变体);`location_id`(string,可选,省略=Main Warehouse/第一个仓库);`quantity`(int ≥0,**必填**,绝对值非增量);`manage_inventory`(bool,默认 true);`confirm`(默认 false)。
133
- - 行为:按变体 SKU 查找库存条目(缺失则自动 `POST /admin/inventory-items` 创建)→ 幂等写 (条目, 仓库) 库存水平(有则更新、无则创建)→ 确保变体 `manage_inventory` 为 true。重复调用安全。
133
+ - 行为(`manage_inventory=true`):按变体 SKU 查库存条目(缺失则自动创建)→ **关联条目到变体**(`POST .../variants/{id}/inventory-items`,已关联则忽略)→ 置变体 `manage_inventory=true` → 幂等写 (条目, 仓库) 库存水平。重复调用安全。
134
+ - ⚠️ 「关联」是变体能查到库存的关键:只建条目不关联,前台仍显示缺货。本工具已自动处理关联,运营无需关心。
135
+ - (`manage_inventory=false`)关闭库存管理并把库存清零。
134
136
  - **`update_inventory_level`**:直接改某**库存条目**(`inventory_item_id`)在某仓库的数量(upsert,幂等)。
135
137
  - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。已知 `inventory_item_id` 时用;否则优先 `set_product_stock`。
136
138
 
@@ -209,7 +211,7 @@ agent 客户端(Codex / Claude Desktop / Cursor / Workbuddy / CodeBuddy 等)
209
211
  - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心;**更新**商品时 `options` 字段被移除(2.16+),工具不会误传。
210
212
  - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,**核对翻译请用 `get_translations`**,不要凭 `get_product` 返回为空就误判没写入。
211
213
  - 图片走 `/admin/uploads` 上传后再挂到商品(无独立 `/admin/products/{id}/images` 端点)。
212
- - 库存查询/写入端点已按 2.21 契约校准(`/admin/inventory-items` + `location-levels`,sku 唯一约束);`set_product_stock` 全程幂等可重放。
214
+ - 库存查询/写入端点已按 2.21 契约校准(`/admin/inventory-items` + `location-levels`,sku 唯一约束);`set_product_stock` 会**关联条目↔变体**并幂等可重放(变体只有关联到库存条目,前台才显示有货;只建条目不关联会撞唯一约束或前台缺货,工具已自动处理)。
213
215
  - 自定义端点 `warehouse-tasks` / `return-requests` / `product-contents` 为本项目扩展。
214
216
  - 文档问答(`search_docs` / `ask_usage`)依赖知识源目录;npx 形态下默认 `./docs`,若问不到相关内容,提示运营换关键词或查后台手册。
215
217
  - 用受限的 `csr` 账号即可完成日常运营;敏感动作由后端 RBAC 拦截。