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

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
@@ -11,9 +11,15 @@
11
11
  | 工具 | 说明 | 写操作 |
12
12
  |---|---|---|
13
13
  | `list_products` / `get_product` | 查商品 | 读 |
14
- | `create_product` / `update_product` / `deactivate_product` | 增 / 改 / 下线商品 | 写(dry_run 默认) |
14
+ | `list_sales_channels` / `list_collections` | 列渠道 / 集合 | 读 |
15
+ | `get_translations` | 查多语言翻译(核对翻译用) | 读 |
16
+ | `create_product` / `update_product` / `deactivate_product` | 增 / 改 / 下线商品(含图片、SKU、渠道、物理/海关字段、metadata 合并) | 写(dry_run 默认) |
17
+ | `update_product_prices` | 替换变体价格(多币种) | 写(dry_run 默认) |
18
+ | `upload_product_image` / `add_product_to_sales_channel` | 追加图片 / 挂渠道 | 写(dry_run 默认) |
19
+ | `create_product_content` / `get_product_content` | 详情页富内容块(hero/richtext/specs/图集/faq/video) | 写 / 读 |
20
+ | `set_product_stock` | 给商品建仓/改库存(自动建库存条目,幂等) | 写(dry_run 默认) |
15
21
  | `list_stock_locations` / `create_stock_location` | 列 / 增仓库 | 写(dry_run 默认) |
16
- | `get_inventory_levels` / `update_inventory_level` | 查 / 改库存 | 写(dry_run 默认) |
22
+ | `get_inventory_levels` / `update_inventory_level` | 查 / 改库存水平 | 写(dry_run 默认) |
17
23
  | `list_orders` / `get_order` | 查订单 | 读 |
18
24
  | `list_warehouse_tasks` / `list_return_requests` | 查履约 / 退货 | 读 |
19
25
  | `search_docs` / `ask_usage` | 基于 `docs/` 回答后台使用问题 | 读 |
@@ -21718,6 +21718,17 @@ var MedusaAdminClient = class {
21718
21718
  async batchTranslations(create, update = []) {
21719
21719
  return this.request("POST", "/admin/translations/batch", { create, update });
21720
21720
  }
21721
+ /**
21722
+ * 查询翻译(GET /admin/translations)——用于核对多语言是否写入成功。
21723
+ * ?reference=product&reference_id=<productId>[&locale_code=zh]
21724
+ */
21725
+ listTranslations(params = {}) {
21726
+ return this.request("GET", `/admin/translations${buildQuery(params)}`);
21727
+ }
21728
+ /** 列出集合(GET /admin/collections),用于按名称找 collection_id。 */
21729
+ listCollections(params = {}) {
21730
+ return this.request("GET", `/admin/collections${buildQuery(params)}`);
21731
+ }
21721
21732
  /**
21722
21733
  * 把一组商品加入销售渠道(POST /admin/sales-channels/{id}/products)。
21723
21734
  * ⚠️ 注意:Medusa v2 该端点挂在「渠道」下,不是商品下;body 为 { add:[productIds], remove:[] }。
@@ -21806,25 +21817,66 @@ var MedusaAdminClient = class {
21806
21817
  createStockLocation(body) {
21807
21818
  return this.request("POST", "/admin/stock-locations", body);
21808
21819
  }
21809
- /** 某仓库下的库存条目及库存水平 */
21810
- listInventoryItems(locationId, params = {}) {
21820
+ /**
21821
+ * 库存条目列表(GET /admin/inventory-items,Medusa 2.21 契约)。
21822
+ * ⚠️ 2.21 没有 GET /admin/stock-locations/{id}/inventory-items 路由;
21823
+ * 按仓库过滤用查询参数:`location_levels.location_id=<sloc_xxx>`(点号写法)。
21824
+ * 常用参数:q(匹配 sku/title)、sku(精确)、location_levels.location_id。
21825
+ */
21826
+ listInventoryItems(params = {}) {
21827
+ return this.request("GET", `/admin/inventory-items${buildQuery(params)}`);
21828
+ }
21829
+ /**
21830
+ * 创建库存条目(POST /admin/inventory-items)。
21831
+ * body: { sku, title?, weight?, length?, height?, width?, requires_shipping?, ... },
21832
+ * 可附带 location_levels: [{ location_id, stocked_quantity }] 一步建仓。
21833
+ * sku 有唯一约束(未删除项),同 sku 重复创建会 409。
21834
+ */
21835
+ createInventoryItem(body) {
21836
+ return this.request("POST", "/admin/inventory-items", body);
21837
+ }
21838
+ /** 某库存条目已有的库存水平(GET /admin/inventory-items/{id}/location-levels[?location_id=]) */
21839
+ getInventoryItemLevels(inventoryItemId, params = {}) {
21811
21840
  return this.request(
21812
21841
  "GET",
21813
- `/admin/stock-locations/${encodeURIComponent(locationId)}/inventory-items${buildQuery(params)}`
21842
+ `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels${buildQuery(params)}`
21814
21843
  );
21815
21844
  }
21816
- /**
21817
- * 设置/更新某库存条目在指定仓库的库存水平。
21818
- * ⚠️ 实施时按 Medusa 2.21 官方契约核对端点与字段
21819
- * (v2 常见为 POST /admin/inventory-items/{id}/location-levels,body 含 location_id + stocked_quantity)。
21820
- */
21821
- updateInventoryLevel(inventoryItemId, body) {
21845
+ /** 新建某库存条目在某仓库的库存水平(POST .../location-levels,body: { location_id, stocked_quantity? }) */
21846
+ createInventoryItemLevel(inventoryItemId, body) {
21822
21847
  return this.request(
21823
21848
  "POST",
21824
21849
  `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels`,
21825
21850
  body
21826
21851
  );
21827
21852
  }
21853
+ /** 更新某库存条目在指定仓库的库存水平(POST .../location-levels/{location_id},body: { stocked_quantity? }) */
21854
+ updateInventoryItemLevel(inventoryItemId, locationId, body) {
21855
+ return this.request(
21856
+ "POST",
21857
+ `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels/${encodeURIComponent(locationId)}`,
21858
+ body
21859
+ );
21860
+ }
21861
+ /**
21862
+ * 幂等地设置某库存条目在指定仓库的库存数量:
21863
+ * 已有 (item, location) 水平 → 更新;没有 → 创建。
21864
+ */
21865
+ async upsertInventoryLevel(inventoryItemId, locationId, stockedQuantity) {
21866
+ const existing = await this.getInventoryItemLevels(inventoryItemId, {
21867
+ location_id: locationId
21868
+ });
21869
+ const found = (existing?.inventory_levels || []).find((l) => l.location_id === locationId);
21870
+ if (found) {
21871
+ await this.updateInventoryItemLevel(inventoryItemId, locationId, { stocked_quantity: stockedQuantity });
21872
+ return { created: false, stocked_quantity: stockedQuantity };
21873
+ }
21874
+ await this.createInventoryItemLevel(inventoryItemId, {
21875
+ location_id: locationId,
21876
+ stocked_quantity: stockedQuantity
21877
+ });
21878
+ return { created: true, stocked_quantity: stockedQuantity };
21879
+ }
21828
21880
  // ─── 订单 ───
21829
21881
  listOrders(params = {}) {
21830
21882
  return this.request("GET", `/admin/orders${buildQuery(params)}`);
@@ -21841,6 +21893,180 @@ var MedusaAdminClient = class {
21841
21893
  }
21842
21894
  };
21843
21895
 
21896
+ // src/tools/stock.ts
21897
+ async function applyProductStock(ctx2, opts) {
21898
+ const { client: client2 } = ctx2;
21899
+ const prod = await client2.getProduct(opts.productId);
21900
+ const product = prod?.product ?? prod;
21901
+ if (!product?.variants?.length) throw new Error(`\u5546\u54C1 ${opts.productId} \u6CA1\u6709\u53D8\u4F53\uFF0C\u65E0\u6CD5\u5EFA\u4ED3`);
21902
+ const variant = opts.sku ? product.variants.find((v) => v.sku === opts.sku) : product.variants[0];
21903
+ if (!variant) throw new Error(`\u627E\u4E0D\u5230\u76EE\u6807\u53D8\u4F53\uFF08sku=${opts.sku || "\u7B2C\u4E00\u4E2A"}\uFF09`);
21904
+ let location = null;
21905
+ if (opts.locationId) {
21906
+ const locs = await client2.listStockLocations({ limit: 100 });
21907
+ location = (locs?.stock_locations || []).find((l) => l.id === opts.locationId);
21908
+ if (!location) throw new Error(`\u627E\u4E0D\u5230\u4ED3\u5E93 ${opts.locationId}`);
21909
+ } else {
21910
+ const locs = await client2.listStockLocations({ limit: 100 });
21911
+ const list = locs?.stock_locations || [];
21912
+ location = list.find((l) => /main warehouse/i.test(l.name)) || list[0];
21913
+ if (!location) throw new Error("\u6CA1\u6709\u4EFB\u4F55\u53EF\u7528\u4ED3\u5E93\uFF0C\u8BF7\u5148 create_stock_location");
21914
+ }
21915
+ let iitem = null;
21916
+ if (variant.sku) {
21917
+ 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({
21923
+ sku: variant.sku,
21924
+ title: variant.title || void 0
21925
+ });
21926
+ iitem = created?.inventory_item ?? created;
21927
+ }
21928
+ const level = await client2.upsertInventoryLevel(iitem.id, location.id, opts.quantity);
21929
+ if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21930
+ await client2.updateProduct(product.id, {
21931
+ variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21932
+ });
21933
+ }
21934
+ return {
21935
+ product_id: product.id,
21936
+ variant_id: variant.id,
21937
+ sku: variant.sku,
21938
+ location_id: location.id,
21939
+ 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
21944
+ };
21945
+ }
21946
+ var stockTools = [
21947
+ {
21948
+ name: "list_stock_locations",
21949
+ description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
21950
+ schema: {
21951
+ q: external_exports.string().optional(),
21952
+ limit: external_exports.number().int().positive().max(100).default(50)
21953
+ },
21954
+ async run(input, { client: client2 }) {
21955
+ return client2.listStockLocations({ q: input.q, limit: input.limit });
21956
+ }
21957
+ },
21958
+ {
21959
+ name: "create_stock_location",
21960
+ description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
21961
+ schema: {
21962
+ name: external_exports.string(),
21963
+ address: external_exports.object({
21964
+ address_1: external_exports.string().optional(),
21965
+ address_2: external_exports.string().optional(),
21966
+ city: external_exports.string().optional(),
21967
+ country_code: external_exports.string().optional(),
21968
+ province: external_exports.string().optional(),
21969
+ postal_code: external_exports.string().optional(),
21970
+ phone: external_exports.string().optional()
21971
+ }).optional(),
21972
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
21973
+ confirm: external_exports.boolean().default(false)
21974
+ },
21975
+ async run(input, { client: client2 }) {
21976
+ const payload = { name: input.name };
21977
+ if (input.address) payload.address = input.address;
21978
+ if (input.metadata) payload.metadata = input.metadata;
21979
+ if (!input.confirm) return { dry_run: true, will_create: payload };
21980
+ return client2.createStockLocation(payload);
21981
+ }
21982
+ },
21983
+ {
21984
+ name: "get_inventory_levels",
21985
+ description: `\u67E5\u8BE2\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08GET /admin/inventory-items\uFF0CMedusa 2.21 \u5951\u7EA6\uFF09\u3002
21986
+ location_id \u53EF\u9009\uFF1A\u4F20\u4E86\u5219\u53EA\u56DE\u8BE5\u4ED3\u5E93\u4E0B\u6709\u6C34\u5E73\u7684\u6761\u76EE\uFF1B\u4E0D\u4F20\u5219\u5217\u5168\u90E8\u3002q \u6309 sku/title \u6A21\u7CCA\u8FC7\u6EE4\u3002`,
21987
+ schema: {
21988
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219\u4E0D\u9650\u4ED3\u5E93"),
21989
+ q: external_exports.string().optional(),
21990
+ limit: external_exports.number().int().positive().max(100).default(50)
21991
+ },
21992
+ async run(input, { client: client2 }) {
21993
+ const params = { limit: input.limit };
21994
+ if (input.q) params.q = input.q;
21995
+ if (input.location_id) params["location_levels.location_id"] = input.location_id;
21996
+ return client2.listInventoryItems(params);
21997
+ }
21998
+ },
21999
+ {
22000
+ name: "set_product_stock",
22001
+ description: `\u7ED9\u5546\u54C1\u5EFA\u4ED3/\u6539\u5E93\u5B58\uFF08\u5E42\u7B49\uFF09\uFF1A\u786E\u4FDD\u53D8\u4F53\u5BF9\u5E94\u7684\u5E93\u5B58\u6761\u76EE\u5B58\u5728\uFF08\u7F3A\u5931\u5219\u6309\u53D8\u4F53 SKU \u81EA\u52A8\u521B\u5EFA\uFF09\uFF0C
22002
+ \u518D\u628A\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\u8BBE\u4E3A quantity\uFF0C\u5E76\u628A\u53D8\u4F53 manage_inventory \u7F6E\u4E3A true\u3002
22003
+ \u26A0\uFE0F \u4E0D\u6307\u5B9A location_id \u65F6\u9ED8\u8BA4\u5199\u5165\u540D\u79F0\u542B "Main Warehouse" \u7684\u4ED3\u5E93\uFF08\u5426\u5219\u53D6\u7B2C\u4E00\u4E2A\u4ED3\u5E93\uFF09\u3002
22004
+ \u26A0\uFE0F \u672A\u5EFA\u4ED3\uFF08manage_inventory=false \u6216 0 \u5E93\u5B58\uFF09\u7684\u5546\u54C1\u5728\u524D\u7AEF\u8868\u73B0\u4E3A\u300C\u7F3A\u8D27\u300D\uFF0C\u6B63\u5F0F\u4E0A\u67B6\u524D\u5FC5\u987B\u5EFA\u4ED3\u3002
22005
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\uFF1Bconfirm=true \u624D\u5199\u5165\u3002`,
22006
+ schema: {
22007
+ product_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22008
+ sku: external_exports.string().optional().describe("\u76EE\u6807\u53D8\u4F53 SKU\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u53D8\u4F53"),
22009
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A"),
22010
+ quantity: external_exports.number().int().nonnegative().describe("\u8BE5\u4ED3\u5E93\u5E93\u5B58\u6570\u91CF\uFF08\u7EDD\u5BF9\u503C\uFF0C\u975E\u589E\u91CF\uFF09"),
22011
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22012
+ confirm: external_exports.boolean().default(false)
22013
+ },
22014
+ async run(input, ctx2) {
22015
+ const plan = {
22016
+ product_id: input.product_id,
22017
+ sku: input.sku || "(\u7B2C\u4E00\u4E2A\u53D8\u4F53)",
22018
+ location_id: input.location_id || "(Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93)",
22019
+ quantity: input.quantity,
22020
+ manage_inventory: input.manage_inventory
22021
+ };
22022
+ if (!input.confirm) {
22023
+ return { dry_run: true, will_set_stock: plan };
22024
+ }
22025
+ try {
22026
+ const result = await applyProductStock(ctx2, {
22027
+ productId: input.product_id,
22028
+ sku: input.sku,
22029
+ locationId: input.location_id,
22030
+ quantity: input.quantity,
22031
+ manageInventory: input.manage_inventory
22032
+ });
22033
+ return { set_stock: result };
22034
+ } catch (e) {
22035
+ return `\u9519\u8BEF\uFF1A\u5EFA\u4ED3\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
22036
+ }
22037
+ }
22038
+ },
22039
+ {
22040
+ name: "update_inventory_level",
22041
+ description: `\u4FEE\u6539\u67D0\u5E93\u5B58\u6761\u76EE\u5728\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\uFF08\u7EDD\u5BF9\u503C\uFF0C\u5E42\u7B49 upsert\uFF09\u3002
22042
+ \u76F4\u63A5\u7ED9\u5546\u54C1\u5EFA\u4ED3\u8BF7\u4F18\u5148\u7528 set_product_stock\uFF08\u4F1A\u81EA\u52A8\u627E/\u5EFA\u5E93\u5B58\u6761\u76EE\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
22043
+ schema: {
22044
+ inventory_item_id: external_exports.string(),
22045
+ location_id: external_exports.string(),
22046
+ stocked_quantity: external_exports.number().int().nonnegative(),
22047
+ confirm: external_exports.boolean().default(false)
22048
+ },
22049
+ async run(input, { client: client2 }) {
22050
+ if (!input.confirm) {
22051
+ return {
22052
+ dry_run: true,
22053
+ will_update: {
22054
+ inventory_item_id: input.inventory_item_id,
22055
+ location_id: input.location_id,
22056
+ stocked_quantity: input.stocked_quantity
22057
+ }
22058
+ };
22059
+ }
22060
+ const level = await client2.upsertInventoryLevel(
22061
+ input.inventory_item_id,
22062
+ input.location_id,
22063
+ input.stocked_quantity
22064
+ );
22065
+ return { updated: { ...level, inventory_item_id: input.inventory_item_id, location_id: input.location_id } };
22066
+ }
22067
+ }
22068
+ ];
22069
+
21844
22070
  // src/tools/products.ts
21845
22071
  var PRODUCT_STATUS = [
21846
22072
  "draft",
@@ -21884,6 +22110,66 @@ var productTools = [
21884
22110
  return client2.getProduct(input.id_or_handle);
21885
22111
  }
21886
22112
  },
22113
+ {
22114
+ name: "list_sales_channels",
22115
+ description: "\u5217\u51FA\u9500\u552E\u6E20\u9053\uFF08GET /admin/sales-channels\uFF09\u3002\u65B0\u589E\u5546\u54C1\u524D\u7528\u5B83\u786E\u8BA4\u6E20\u9053 id/\u540D\u79F0\u3002",
22116
+ schema: {
22117
+ q: external_exports.string().optional(),
22118
+ limit: external_exports.number().int().positive().max(100).default(50)
22119
+ },
22120
+ async run(input, { client: client2 }) {
22121
+ return client2.listSalesChannels({ q: input.q, limit: input.limit });
22122
+ }
22123
+ },
22124
+ {
22125
+ name: "list_collections",
22126
+ description: "\u5217\u51FA\u96C6\u5408\uFF08GET /admin/collections\uFF09\uFF0C\u7528\u4E8E\u628A\u5546\u54C1\u6302\u8FDB\u96C6\u5408\uFF08\u5982\u6D3B\u52A8\u96C6\u5408\uFF09\u3002",
22127
+ schema: {
22128
+ q: external_exports.string().optional(),
22129
+ limit: external_exports.number().int().positive().max(100).default(50)
22130
+ },
22131
+ async run(input, { client: client2 }) {
22132
+ return client2.listCollections({ q: input.q, limit: input.limit });
22133
+ }
22134
+ },
22135
+ {
22136
+ name: "get_translations",
22137
+ description: `\u8BFB\u53D6\u67D0\u5B9E\u4F53\u7684\u591A\u8BED\u8A00\u7FFB\u8BD1\uFF08GET /admin/translations\uFF09\u2014\u2014\u6838\u5BF9\u5546\u54C1\u56DB\u8BED\u8A00\u662F\u5426\u5199\u5165\u6210\u529F\u7684\u552F\u4E00\u53EF\u9760\u9014\u5F84
22138
+ \uFF08get_product \u9ED8\u8BA4\u5B57\u6BB5\u4E0D\u542B translations\uFF09\u3002reference \u4E00\u822C\u4E3A product\uFF0Creference_id \u4F20\u5546\u54C1 id \u6216 handle\u3002
22139
+ \u8FD4\u56DE\u6BCF\u6761 { locale_code, title, description }\u3002`,
22140
+ schema: {
22141
+ reference: external_exports.string().default("product"),
22142
+ reference_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22143
+ locale_code: external_exports.string().optional().describe("\u53EA\u53D6\u67D0\u4E00\u8BED\u8A00\uFF0C\u5982 zh"),
22144
+ limit: external_exports.number().int().positive().max(100).default(50)
22145
+ },
22146
+ async run(input, { client: client2 }) {
22147
+ let refId = input.reference_id;
22148
+ if (!/^[a-z]+_[a-z0-9]+$/i.test(refId)) {
22149
+ const prod = await client2.getProduct(refId).catch(() => null);
22150
+ const id = prod?.product?.id || prod?.id;
22151
+ if (!id) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230 ${input.reference} "${refId}"\uFF08\u8BF7\u4F20 id \u6216\u6709\u6548 handle\uFF09`;
22152
+ refId = id;
22153
+ }
22154
+ const params = {
22155
+ reference: input.reference,
22156
+ reference_id: refId,
22157
+ limit: input.limit
22158
+ };
22159
+ if (input.locale_code) params.locale_code = input.locale_code;
22160
+ const data = await client2.listTranslations(params);
22161
+ const rows = (data?.translations || []).map((t) => {
22162
+ const tr = t.translations || {};
22163
+ return {
22164
+ locale_code: t.locale_code,
22165
+ title: tr.title ?? null,
22166
+ description: tr.description ? tr.description.slice(0, 120) + "\u2026" : null,
22167
+ has_full_description: Boolean(tr.description)
22168
+ };
22169
+ });
22170
+ return { reference: input.reference, reference_id: input.reference_id, translations: rows, count: rows.length };
22171
+ }
22172
+ },
21887
22173
  {
21888
22174
  name: "create_product",
21889
22175
  description: `\u5728\u540E\u53F0\u589E\u52A0\u4E00\u4E2A\u5546\u54C1\u3002
@@ -21891,7 +22177,10 @@ var productTools = [
21891
22177
  \u591A\u8BED\u8A00\uFF08\xA716.2\uFF09\uFF1Atranslations \u5FC5\u987B\u5305\u542B en/zh/ms/id \u56DB\u79CD\u8BED\u8A00\u7684 title\uFF1B\u7F3A\u5931\u65F6\u9700 confirm=true \u4E14 acknowledge_missing_languages=true\u3002
21892
22178
  \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002
21893
22179
  \u56FE\u7247\uFF1Aimages \u53EF\u4F20\u672C\u5730\u6587\u4EF6\u8DEF\u5F84\uFF08\u81EA\u52A8\u4E0A\u4F20\u5230\u6587\u4EF6\u670D\u52A1\uFF09\u6216\u56FE\u7247 URL\uFF0C\u4EC5\u5728 confirm=true \u65F6\u624D\u771F\u6B63\u4E0A\u4F20/\u6302\u63A5\u3002
21894
- SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \u53EF\u5B58\u4EFB\u610F\u952E\u503C\uFF08\u5982\u89C4\u683C\u53C2\u6570\u3001\u6765\u6E90\u94FE\u63A5\uFF09\u3002`,
22180
+ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \u53EF\u5B58\u4EFB\u610F\u952E\u503C\uFF08\u5982\u89C4\u683C\u53C2\u6570\u3001\u6765\u6E90\u94FE\u63A5\uFF09\u3002
22181
+ \u6E20\u9053\uFF1Asales_channels \u4F20\u6E20\u9053 id \u6216\u540D\u79F0\uFF08\u5982 "MY Storefront"\uFF09\uFF0C\u521B\u5EFA\u5373\u6302\u8F7D\uFF0C\u5426\u5219\u524D\u7AEF\u4E0D\u53EF\u89C1\u3002
22182
+ \u5E93\u5B58\uFF1Amanage_inventory \u9ED8\u8BA4 true\uFF1B\u6B63\u5F0F\u4E0A\u67B6\u5FC5\u987B\u7ED9 initial_stock\uFF08\u5982 {quantity:100}\uFF0C\u9ED8\u8BA4\u4E3B\u4ED3\uFF09\uFF0C\u5426\u5219 0 \u5E93\u5B58=\u524D\u7AEF\u7F3A\u8D27\u3002
22183
+ \u7269\u7406/\u6D77\u5173\u5B57\u6BB5\uFF1Aweight(\u514B)/length/height/width(\u5398\u7C73)/origin_country/hs_code/mid_code/material \u5747\u53EF\u9009\u3002`,
21895
22184
  schema: {
21896
22185
  title: external_exports.string(),
21897
22186
  handle: external_exports.string().optional().describe("\u4E0D\u586B\u5219\u7528 title \u5F52\u4E00\u5316\u751F\u6210"),
@@ -21909,6 +22198,21 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21909
22198
  images: external_exports.array(external_exports.string()).optional().describe("\u672C\u5730\u56FE\u7247\u8DEF\u5F84\u6216\u56FE\u7247 URL\uFF0Cconfirm=true \u65F6\u4E0A\u4F20\u5E76\u6302\u5230\u5546\u54C1"),
21910
22199
  sku: external_exports.string().optional().describe("\u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\u7684 SKU"),
21911
22200
  metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u4EFB\u610F\u952E\u503C\uFF0C\u5982\u89C4\u683C\u53C2\u6570 / source_url"),
22201
+ sales_channels: external_exports.array(external_exports.string()).optional().describe('\u9500\u552E\u6E20\u9053 id \u6216\u540D\u79F0\uFF08\u5982 "MY Storefront"\uFF09\uFF0C\u521B\u5EFA\u5373\u6302\u8F7D\uFF0C\u5426\u5219\u524D\u7AEF\u4E0D\u53EF\u89C1'),
22202
+ collection_id: external_exports.string().optional().describe('\u96C6\u5408 id \u6216\u540D\u79F0\uFF08\u5982 "\u53CC11\u6D3B\u52A8"\uFF09'),
22203
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22204
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22205
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22206
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22207
+ origin_country: external_exports.string().optional().describe("\u539F\u4EA7\u56FD ISO \u7801\uFF0C\u5982 KR/MY"),
22208
+ hs_code: external_exports.string().optional(),
22209
+ mid_code: external_exports.string().optional(),
22210
+ material: external_exports.string().optional(),
22211
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22212
+ initial_stock: external_exports.object({
22213
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93"),
22214
+ quantity: external_exports.number().int().nonnegative()
22215
+ }).optional().describe("\u5EFA\u5E93\u5B58\uFF1A\u6309\u53D8\u4F53 SKU \u81EA\u52A8\u521B\u5EFA\u5E93\u5B58\u6761\u76EE\u5E76\u5199\u5165\u8BE5\u4ED3\u5E93\u6570\u91CF"),
21912
22216
  confirm: external_exports.boolean().default(false),
21913
22217
  acknowledge_missing_languages: external_exports.boolean().default(false)
21914
22218
  },
@@ -21922,6 +22226,29 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21922
22226
  const missing = config3.requiredLocales.filter(
21923
22227
  (l) => !provided.includes(l) || !input.translations?.[l]?.title
21924
22228
  );
22229
+ let channelIds = [];
22230
+ if (input.sales_channels?.length) {
22231
+ const ch = await client2.listSalesChannels({ limit: 100 });
22232
+ const list = ch?.sales_channels || [];
22233
+ channelIds = input.sales_channels.map((name) => list.find((c) => c.id === name || c.name === name)?.id).filter(Boolean);
22234
+ const unknown2 = input.sales_channels.filter(
22235
+ (name) => !list.some((c) => c.id === name || c.name === name)
22236
+ );
22237
+ if (unknown2.length) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u9500\u552E\u6E20\u9053\uFF1A${unknown2.join(", ")}\uFF08\u7528 list_sales_channels \u67E5\u770B\u53EF\u7528\u6E20\u9053\uFF09`;
22238
+ }
22239
+ let collectionId = input.collection_id;
22240
+ if (collectionId && !collectionId.startsWith("pcol_")) {
22241
+ const cols = await client2.listCollections({ limit: 100 });
22242
+ const hit = (cols?.collections || []).find((c) => c.id === collectionId || c.title === collectionId);
22243
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${collectionId}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22244
+ collectionId = hit.id;
22245
+ }
22246
+ const warnings = [];
22247
+ if (input.manage_inventory !== false && !input.initial_stock) {
22248
+ warnings.push(
22249
+ "manage_inventory=true \u4F46\u672A\u7ED9 initial_stock\uFF1A\u521B\u5EFA\u540E\u4E3A 0 \u5E93\u5B58\uFF0C\u524D\u7AEF\u5C06\u663E\u793A\u7F3A\u8D27\u3002\u6B63\u5F0F\u4E0A\u67B6\u524D\u5FC5\u987B\u8C03\u7528 set_product_stock \u5EFA\u4ED3\u3002"
22250
+ );
22251
+ }
21925
22252
  if (!input.confirm) {
21926
22253
  return {
21927
22254
  dry_run: true,
@@ -21935,8 +22262,14 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21935
22262
  sku: input.sku,
21936
22263
  images: input.images,
21937
22264
  metadata: input.metadata,
22265
+ sales_channels: channelIds,
22266
+ collection_id: collectionId || null,
22267
+ weight: input.weight,
22268
+ manage_inventory: input.manage_inventory,
22269
+ initial_stock: input.initial_stock,
21938
22270
  missing_translations: missing
21939
22271
  },
22272
+ warnings,
21940
22273
  note: "\u672A confirm\uFF0C\u4EC5\u9884\u89C8\u3002\u786E\u8BA4\u8BF7\u8BBE confirm=true\u3002"
21941
22274
  };
21942
22275
  }
@@ -21961,41 +22294,66 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21961
22294
  };
21962
22295
  if (input.metadata) payload.metadata = input.metadata;
21963
22296
  if (imageUrls.length) payload.images = imageUrls.map((url) => ({ url }));
22297
+ if (channelIds.length) payload.sales_channels = channelIds.map((id) => ({ id }));
22298
+ if (collectionId) payload.collection_id = collectionId;
22299
+ for (const k of ["weight", "length", "height", "width"]) {
22300
+ if (input[k] != null) payload[k] = input[k];
22301
+ }
22302
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22303
+ if (input[k]) payload[k] = input[k];
22304
+ }
21964
22305
  const variantPrices = input.prices && input.prices.length ? input.prices.map((p) => ({ amount: p.amount, currency_code: p.currency_code })) : input.price != null ? [{ amount: input.price, currency_code: input.currency_code }] : void 0;
21965
22306
  if (variantPrices) {
21966
22307
  const variant = {
21967
22308
  title: "Default",
21968
22309
  options: { Default: "Default" },
21969
22310
  prices: variantPrices,
21970
- manage_inventory: false
22311
+ manage_inventory: input.manage_inventory
21971
22312
  };
21972
22313
  if (input.sku) variant.sku = input.sku;
21973
22314
  payload.variants = [variant];
21974
22315
  }
21975
22316
  const data = await client2.createProduct(payload);
22317
+ const productId = data?.product?.id || data?.id;
21976
22318
  let translationResult = null;
21977
22319
  if (input.translations && Object.keys(input.translations).length) {
21978
- const productId = data?.product?.id || data?.id;
21979
- if (productId) {
21980
- const create = Object.entries(input.translations).map(([locale, t]) => ({
21981
- reference: "product",
21982
- reference_id: productId,
21983
- locale_code: locale,
21984
- translations: { title: t?.title, description: t?.description }
21985
- }));
21986
- try {
21987
- translationResult = await client2.batchTranslations(create);
21988
- } catch (e) {
21989
- return { created: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
21990
- }
22320
+ const create = Object.entries(input.translations).map(([locale, t]) => ({
22321
+ reference: "product",
22322
+ reference_id: productId,
22323
+ locale_code: locale,
22324
+ translations: { title: t?.title, description: t?.description }
22325
+ }));
22326
+ try {
22327
+ translationResult = await client2.batchTranslations(create);
22328
+ } catch (e) {
22329
+ return { created: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
21991
22330
  }
21992
22331
  }
21993
- return { created: data, translations: translationResult, warning_missing_languages: missing };
22332
+ let stock = null;
22333
+ if (input.initial_stock && productId) {
22334
+ try {
22335
+ stock = await applyProductStock(
22336
+ { client: client2, config: config3 },
22337
+ {
22338
+ productId,
22339
+ sku: input.sku,
22340
+ locationId: input.initial_stock.location_id,
22341
+ quantity: input.initial_stock.quantity,
22342
+ manageInventory: input.manage_inventory
22343
+ }
22344
+ );
22345
+ } catch (e) {
22346
+ warnings.push(`\u5EFA\u5E93\u5B58\u5931\u8D25\uFF08\u5546\u54C1\u5DF2\u521B\u5EFA\uFF0C\u8BF7\u8865\u8C03 set_product_stock\uFF09\u2014\u2014 ${e?.message || e}`);
22347
+ }
22348
+ }
22349
+ return { created: data, translations: translationResult, stock, warnings, warning_missing_languages: missing };
21994
22350
  }
21995
22351
  },
21996
22352
  {
21997
22353
  name: "update_product",
21998
- description: "\u4FEE\u6539\u5546\u54C1\u5B57\u6BB5\uFF08\u6807\u9898/handle/\u72B6\u6001/\u63CF\u8FF0/\u591A\u8BED\u8A00\uFF09\u3002Handle \u81EA\u52A8\u5F52\u4E00\u5316\u3002\u9ED8\u8BA4 dry_run\u3002",
22354
+ description: `\u4FEE\u6539\u5546\u54C1\u5B57\u6BB5\uFF08\u6807\u9898/handle/\u72B6\u6001/\u63CF\u8FF0/\u591A\u8BED\u8A00/\u7269\u7406\u5C3A\u5BF8/\u6D77\u5173/\u96C6\u5408/metadata\uFF09\u3002Handle \u81EA\u52A8\u5F52\u4E00\u5316\u3002
22355
+ weight \u5355\u4F4D\u514B\uFF1Blength/height/width \u5355\u4F4D\u5398\u7C73\uFF1Borigin_country \u7528 ISO \u7801\uFF08\u5982 KR\uFF09\u3002
22356
+ metadata \u4E3A**\u5408\u5E76**\u8BED\u4E49\uFF1A\u65B0\u952E\u8986\u76D6\u540C\u540D\u952E\u3001\u672A\u63D0\u53CA\u7684\u65E7\u952E\u4FDD\u7559\uFF08\u4E0D\u652F\u6301\u5220\u9664\u952E\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
21999
22357
  schema: {
22000
22358
  id: external_exports.string(),
22001
22359
  title: external_exports.string().optional(),
@@ -22004,6 +22362,16 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
22004
22362
  subtitle: external_exports.string().optional(),
22005
22363
  description: external_exports.string().optional(),
22006
22364
  translations: external_exports.record(external_exports.string(), translationShape).optional(),
22365
+ collection_id: external_exports.string().optional().describe("\u96C6\u5408 id \u6216\u540D\u79F0"),
22366
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22367
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22368
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22369
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22370
+ origin_country: external_exports.string().optional(),
22371
+ hs_code: external_exports.string().optional(),
22372
+ mid_code: external_exports.string().optional(),
22373
+ material: external_exports.string().optional(),
22374
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u5408\u5E76\u8FDB\u73B0\u6709 metadata\uFF08\u4E0D\u6574\u4F53\u8986\u76D6\uFF09"),
22007
22375
  confirm: external_exports.boolean().default(false)
22008
22376
  },
22009
22377
  async run(input, { client: client2 }) {
@@ -22017,6 +22385,27 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
22017
22385
  if (input.status) patch.status = input.status;
22018
22386
  if (input.subtitle) patch.subtitle = input.subtitle;
22019
22387
  if (input.description) patch.description = input.description;
22388
+ if (input.collection_id) {
22389
+ let cid = input.collection_id;
22390
+ if (!cid.startsWith("pcol_")) {
22391
+ const cols = await client2.listCollections({ limit: 100 });
22392
+ const hit = (cols?.collections || []).find((c) => c.id === cid || c.title === cid);
22393
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${cid}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22394
+ cid = hit.id;
22395
+ }
22396
+ patch.collection_id = cid;
22397
+ }
22398
+ for (const k of ["weight", "length", "height", "width"]) {
22399
+ if (input[k] != null) patch[k] = input[k];
22400
+ }
22401
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22402
+ if (input[k]) patch[k] = input[k];
22403
+ }
22404
+ if (input.metadata && Object.keys(input.metadata).length) {
22405
+ const cur = await client2.getProduct(input.id).catch(() => null);
22406
+ const existingMeta = (cur?.product ?? cur)?.metadata || {};
22407
+ patch.metadata = { ...existingMeta, ...input.metadata };
22408
+ }
22020
22409
  if (!input.confirm) return { dry_run: true, will_update: patch };
22021
22410
  const data = await client2.updateProduct(input.id, patch);
22022
22411
  let translationResult = null;
@@ -22211,77 +22600,6 @@ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4
22211
22600
  }
22212
22601
  ];
22213
22602
 
22214
- // src/tools/stock.ts
22215
- var stockTools = [
22216
- {
22217
- name: "list_stock_locations",
22218
- description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
22219
- schema: {
22220
- q: external_exports.string().optional(),
22221
- limit: external_exports.number().int().positive().max(100).default(50)
22222
- },
22223
- async run(input, { client: client2 }) {
22224
- return client2.listStockLocations({ q: input.q, limit: input.limit });
22225
- }
22226
- },
22227
- {
22228
- name: "create_stock_location",
22229
- description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
22230
- schema: {
22231
- name: external_exports.string(),
22232
- address: external_exports.object({
22233
- address_1: external_exports.string().optional(),
22234
- address_2: external_exports.string().optional(),
22235
- city: external_exports.string().optional(),
22236
- country_code: external_exports.string().optional(),
22237
- province: external_exports.string().optional(),
22238
- postal_code: external_exports.string().optional(),
22239
- phone: external_exports.string().optional()
22240
- }).optional(),
22241
- metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
22242
- confirm: external_exports.boolean().default(false)
22243
- },
22244
- async run(input, { client: client2 }) {
22245
- const payload = { name: input.name };
22246
- if (input.address) payload.address = input.address;
22247
- if (input.metadata) payload.metadata = input.metadata;
22248
- if (!input.confirm) return { dry_run: true, will_create: payload };
22249
- return client2.createStockLocation(payload);
22250
- }
22251
- },
22252
- {
22253
- name: "get_inventory_levels",
22254
- description: "\u67E5\u8BE2\u67D0\u4ED3\u5E93\u4E0B\u7684\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08\u6309 location_id\uFF09\u3002",
22255
- schema: {
22256
- location_id: external_exports.string(),
22257
- q: external_exports.string().optional(),
22258
- limit: external_exports.number().int().positive().max(100).default(50)
22259
- },
22260
- async run(input, { client: client2 }) {
22261
- return client2.listInventoryItems(input.location_id, { q: input.q, limit: input.limit });
22262
- }
22263
- },
22264
- {
22265
- name: "update_inventory_level",
22266
- description: `\u4FEE\u6539\u67D0\u5E93\u5B58\u6761\u76EE\u5728\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\uFF08stocked_quantity\uFF09\u3002
22267
- \u26A0\uFE0F \u7AEF\u70B9/\u5B57\u6BB5\u4EE5 Medusa 2.21 \u5951\u7EA6\u4E3A\u51C6\uFF08POST /admin/inventory-items/{id}/location-levels\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
22268
- schema: {
22269
- inventory_item_id: external_exports.string(),
22270
- location_id: external_exports.string(),
22271
- stocked_quantity: external_exports.number().int().nonnegative(),
22272
- confirm: external_exports.boolean().default(false)
22273
- },
22274
- async run(input, { client: client2 }) {
22275
- const payload = {
22276
- location_id: input.location_id,
22277
- stocked_quantity: input.stocked_quantity
22278
- };
22279
- if (!input.confirm) return { dry_run: true, will_update: payload };
22280
- return client2.updateInventoryLevel(input.inventory_item_id, payload);
22281
- }
22282
- }
22283
- ];
22284
-
22285
22603
  // src/tools/ops.ts
22286
22604
  var opsTools = [
22287
22605
  {
@@ -22461,7 +22779,7 @@ var config2 = loadConfig();
22461
22779
  var auth = new AuthManager(config2);
22462
22780
  var client = new MedusaAdminClient(config2, auth);
22463
22781
  var ctx = { client, config: config2 };
22464
- var server = new McpServer({ name: "derlux-admin", version: "0.1.1" });
22782
+ var server = new McpServer({ name: "derlux-admin", version: "0.1.2" });
22465
22783
  for (const def of toolDefs) {
22466
22784
  server.tool(def.name, def.description, def.schema, async (args) => {
22467
22785
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sea-ecommerce/admin-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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
@@ -31,6 +31,8 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
31
31
  - 商品标题/描述的**翻译**(`create_product` 的 `translations`、后端 `/admin/translations/batch`):用**简码** `en` / `zh` / `ms` / `id`。store `/store/products?locale=zh` 即回退到 zh 翻译。
32
32
  - 商品详情页**富内容块**(`create_product_content` 的 `locale`):必须用**区域码** `en-US` / `zh-CN` / `ms-MY` / `id-ID`(与前端 `useProductContent` 的 localeMap 一致)。用简码会导致该语言详情页读不到内容。
33
33
  8. **详情页「富内容」区靠 `create_product_content`,不是 `description`**:storefront 详情页下方大块内容由 `product_content` 模块渲染(`richtext`/`specs`/`image_gallery` 等)。只填 `product.description` 不会显示在那里——录入完整商品时必须再建一份 `product_content`(建议每语言一份)。`get_product` 默认不含该字段,核对请用 `get_product_content`。
34
+ 9. **不建库存 = 前端缺货**:变体 `manage_inventory` 默认 `true`,但新商品默认 0 库存——`create_product` 时必须给 `initial_stock`(如 `{quantity:100}`,默认写 Main Warehouse),或对已有商品调 `set_product_stock`,否则前台显示缺货、无法下单。
35
+ 10. **多语言核对用 `get_translations`**:`get_product` 默认字段**不含** translations,不要凭它为空就判断没翻译。用 `get_translations`(reference_id=商品id或handle) 读取实际写入的各语言 title/description。
34
36
 
35
37
  ## 工具速查表
36
38
 
@@ -38,8 +40,12 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
38
40
  |---|---|---|---|
39
41
  | `list_products` | 读 | 否 | 直接返回 |
40
42
  | `get_product` | 读 | 否 | 直接返回 |
43
+ | `list_sales_channels` | 读 | 否 | 直接返回(渠道 id/名称,建商品前确认用)|
44
+ | `list_collections` | 读 | 否 | 直接返回 |
45
+ | `get_translations` | 读 | 否 | 直接返回(某商品各语言的 title/description,多语言核对唯一可靠途径)|
41
46
  | `list_stock_locations` | 读 | 否 | 直接返回 |
42
- | `get_inventory_levels` | 读 | 否 | 直接返回(按 location_id 列库存条目与水平)|
47
+ | `get_inventory_levels` | 读 | 否 | 直接返回(location_id 可选:按仓库列库存条目与水平;q 按 sku/title 过滤)|
48
+ | `set_product_stock` | 写 | 是(`confirm=true` 才写)| 幂等建仓:自动找/建库存条目 + 写仓库数量 + manage_inventory;`confirm=false` 仅预览 |
43
49
  | `list_orders` | 读 | 否 | 直接返回 |
44
50
  | `get_order` | 读 | 否 | 直接返回 |
45
51
  | `list_warehouse_tasks` | 读 | 否 | 直接返回(自定义端点)|
@@ -63,12 +69,18 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
63
69
  - `q`(string,可选)关键词;`status`(enum:`draft|published|proposed|rejected|archived`,可选)状态过滤;`handle`(string,可选);`limit`(int 1–100,默认 20)。
64
70
  - **`get_product`**:按商品 id 或 handle 获取详情。
65
71
  - `id_or_handle`(string,**必填**)。
72
+ - **`list_sales_channels`**:列出销售渠道(含 id / 名称 / 启用状态)。
73
+ - `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
74
+ - **`list_collections`**:列出集合(如「双11活动」)。
75
+ - `q`(string,可选);`limit`(int 1–100,默认 50)。
76
+ - **`get_translations`**:读取多语言翻译(GET /admin/translations)。
77
+ - `reference_id`(string,**必填**,商品 id 或 handle);`reference`(默认 `product`);`locale_code`(可选,如 `zh`);`limit`(默认 50)。
66
78
 
67
79
  ### 读 —— 仓库与库存
68
80
  - **`list_stock_locations`**:列出仓库(StockLocation)。
69
81
  - `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
70
- - **`get_inventory_levels`**:列出某仓库下的库存条目及库存水平。
71
- - `location_id`(string,**必填**);`q`(string,可选);`limit`(int 1–100,默认 50)。
82
+ - **`get_inventory_levels`**:列出库存条目及库存水平(Medusa 2.21 走 `GET /admin/inventory-items`)。
83
+ - `location_id`(string,**可选**:只回该仓库有水平的条目);`q`(string,可选,按 sku/title 过滤);`limit`(int 1–100,默认 50)。
72
84
 
73
85
  ### 读 —— 订单 / 履约 / 退货
74
86
  - **`list_orders`**:查询订单列表。
@@ -88,10 +100,12 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
88
100
 
89
101
  ### 写 —— 商品
90
102
  - **`create_product`**:新增商品。
91
- - `title`(string,**必填**);`handle`(string,可选,缺省由 title 归一化);`status`(enum,默认 `draft`);`subtitle`(可选);`description`(可选);`price`(number ≥0,主货币单位,**切勿 ×100**);`currency_code`(默认 `MYR`);`translations`(`record<locale,{title?,description?}>`,建议覆盖 `en/zh/ms/id`);`images`(string[],可选,本地图片路径或图片 URL,工具自动上传并挂接);`sku`(string,可选,写入默认变体 SKU);`metadata`(`record<string,string>`,可选,存规格参数/来源链接等任意键值);`confirm`(bool,默认 false);`acknowledge_missing_languages`(bool,默认 false)。
92
- - 行为:`confirm=false` 返回预览(含缺语言清单);`confirm=true` 才真正创建;缺语言时须同时 `acknowledge_missing_languages=true`。创建时**自动**补上 Medusa v2 必需的 `options`(单一 Default)、把图片上传到文件服务并挂接、把 `sku` 写入默认变体、把 `translations` 写入 v2.21 专用的 `/admin/translations/batch` 端点。
93
- - **`update_product`**:修改商品字段(标题/handle/状态/描述/多语言)。
94
- - `id`(string,**必填**);`title`/`handle`/`status`/`subtitle`/`description`/`translations`(均可选);`confirm`(默认 false)。`confirm=false` 返回预览。
103
+ - `title`(string,**必填**);`handle`(string,可选,缺省由 title 归一化);`status`(enum,默认 `draft`);`subtitle`(可选);`description`(可选);`price`(number ≥0,主货币单位,**切勿 ×100**);`currency_code`(默认 `MYR`);`prices`(`{currency_code,amount}[]`,可选,多币种,优先于单一 price);`translations`(`record<locale,{title?,description?}>`,建议覆盖 `en/zh/ms/id`);`images`(string[],可选,本地图片路径或图片 URL,工具自动上传并挂接);`sku`(string,可选,写入默认变体 SKU);`metadata`(`record<string,string>`,可选,存规格参数/来源链接等任意键值);`sales_channels`(string[],可选,渠道 id 或名称如 `"MY Storefront"`,创建即挂载);`collection_id`(string,可选,集合 id 或名称);`weight`/`length`/`height`/`width`(number,可选,克/厘米);`origin_country`/`hs_code`/`mid_code`/`material`(string,可选);`manage_inventory`(bool,**默认 true**);`initial_stock`(`{location_id?, quantity}`,可选,建库存;**正式上架必给**,见硬规则 9);`confirm`(bool,默认 false);`acknowledge_missing_languages`(bool,默认 false)。
104
+ - 行为:`confirm=false` 返回预览(含缺语言清单、缺货告警);`confirm=true` 才真正创建;缺语言时须同时 `acknowledge_missing_languages=true`。创建时**自动**补上 Medusa v2 必需的 `options`(单一 Default)、图片上传并挂接、`sku` 写入默认变体、`translations` 写入 v2.21 专用的 `/admin/translations/batch` 端点、按 `initial_stock` 建库存条目并写仓库。
105
+ - **`update_product`**:修改商品字段(标题/handle/状态/描述/多语言/物理尺寸/海关/集合/metadata)。
106
+ - `id`(string,**必填**);`title`/`handle`/`status`/`subtitle`/`description`/`translations`/`collection_id`(集合 id 或名称)/`weight`/`length`/`height`/`width`/`origin_country`/`hs_code`/`mid_code`/`material`/`metadata`(均可选)。
107
+ - `metadata` 为**合并**语义:新键覆盖同名键、未提及的旧键保留(不支持删除键)。改库存请用 `set_product_stock`,改价格请用 `update_product_prices`。
108
+ - `confirm`(默认 false)。`confirm=false` 返回合并后的完整预览。
95
109
  - **`deactivate_product`**:下架商品(status 置 `archived`)。
96
110
  - `id`(string,**必填**);`confirm`(默认 false)。`confirm=false` 返回预览。
97
111
  - **`upload_product_image`**:给**已存在**的商品追加图片(与已有图片合并,不覆盖)。
@@ -114,39 +128,42 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
114
128
  ### 写 —— 仓库与库存
115
129
  - **`create_stock_location`**:新增仓库(StockLocation)。
116
130
  - `name`(string,**必填**);`address`(可选:`{address_1?,address_2?,city?,country_code?,province?,postal_code?,phone?}`);`metadata`(`record<string,string>`,可选);`confirm`(默认 false)。
117
- - **`update_inventory_level`**:修改某库存条目在指定仓库的库存数量(`stocked_quantity`)。
118
- - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。底层走 `POST /admin/inventory-items/{id}/location-levels`,字段/端点以 Medusa 2.21 契约为准。
131
+ - **`set_product_stock`**:给商品建仓/改库存(**运营最常用的库存工具**,幂等)。
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。重复调用安全。
134
+ - **`update_inventory_level`**:直接改某**库存条目**(`inventory_item_id`)在某仓库的数量(upsert,幂等)。
135
+ - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。已知 `inventory_item_id` 时用;否则优先 `set_product_stock`。
119
136
 
120
137
  ## 推荐操作流程(SOP)
121
138
 
122
139
  **创建商品**
123
- 1. 先收集:标题、handle(或告知由标题生成)、价格(主单位)、四语言标题、状态、图片、规格、详情页富文本内容(按语言)。
124
- 2. 调用 `create_product`(`confirm=false`)→ 展示预览与「缺语言」告警。
125
- 3. 提醒运营补全缺失语言;确认无误后调用 `create_product`(`confirm=true`,必要时 `acknowledge_missing_languages=true`)。
126
- 4. **`add_product_to_sales_channel`(`confirm=true`)** → 把商品加入销售渠道,否则前端看不到。
127
- 5. **`create_product_content`(`confirm=true`,逐语言 `en-US/zh-CN/ms-MY/id-ID`)** → 写详情页富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。否则详情页主体空白。
128
- 6. 把返回的新商品 id/handle 告知运营。
140
+ 1. 先收集:标题、handle(或告知由标题生成)、价格(主单位)、四语言标题、状态、图片、规格、重量、初始库存数量、目标渠道、详情页富文本内容(按语言)。
141
+ 2. 调用 `create_product`(`confirm=false`)→ 展示预览与「缺语言 / 缺货」告警。
142
+ 3. 提醒运营补全缺失语言与 `initial_stock`;确认无误后调用 `create_product`(`confirm=true`,`sales_channels` 里带上目标渠道,必要时 `acknowledge_missing_languages=true`)。创建即完成:图片上传、SKU、翻译、渠道挂载、建库存。
143
+ 4. **`create_product_content`(`confirm=true`,逐语言 `en-US/zh-CN/ms-MY/id-ID`)** → 写详情页富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。否则详情页主体空白。
144
+ 5. 用 `get_translations` + `get_product_content` 核对多语言与富内容;把新商品 id/handle 告知运营。
129
145
 
130
146
  **修改 / 下架商品**
131
147
  1. `get_product` 或 `list_products` 确认目标 id。
132
148
  2. `update_product` / `deactivate_product`(`confirm=false`)→ 展示预览。
133
149
  3. 确认后同名工具 `confirm=true` 执行。
134
150
 
135
- **调整库存**
136
- 1. `list_stock_locations` 拿到 `location_id`;`get_inventory_levels` 拿到 `inventory_item_id`。
137
- 2. `update_inventory_level`(`confirm=false`)→ 展示将写入的数量。
138
- 3. 确认后 `confirm=true` 执行。
151
+ **建仓 / 调整库存**
152
+ 1. 直接 `set_product_stock`(`product_id` + `quantity`,`confirm=false` 预览 → 确认后 `confirm=true`)——自动处理库存条目与仓库。
153
+ 2. 查现状:`get_inventory_levels`(`location_id` 可选、`q` 按 sku 过滤)。
154
+ 3. 已知 `inventory_item_id` 的精细调整:`update_inventory_level`(upsert)。
139
155
 
140
156
  **查询类**:直接调用对应读工具即可,无需 `confirm`。
141
157
 
142
158
  ## 常见任务示例(运营怎么说 / 你怎么调)
143
159
 
144
160
  - 「下架 handle 为 `summer-serum` 的商品」→ `get_product`(id_or_handle=`summer-serum`) 取 id → `deactivate_product`(id, confirm=false 预览 → 确认后 confirm=true)。
145
- - 「给马来西亚仓的 A 商品加 50 件库存」→ `list_stock_locations` 找 MY 仓 → `get_inventory_levels`(location_id) 找 A 的 inventory_item_id → `update_inventory_level`(confirm=false 预览 → 确认后 confirm=true)。
161
+ - 「给 A 商品补 100 件库存」→ `set_product_stock`(product_id=A 的 id, quantity=100, confirm=false 预览 → 确认后 confirm=true;默认写 Main Warehouse,指定仓传 location_id)。
146
162
  - 「查一下最近 10 个待发货订单」→ `list_orders`(status=`pending`, limit=10)。
147
163
  - 「后台怎么新建优惠券」→ `ask_usage`(question=`如何新建优惠券`)。
148
164
  - 「列出所有退货请求」→ `list_return_requests`()。
149
- - 「把这份抓取的 Lazada 商品录入:标题 DR.CYJ Hair Filler、价格 420 MYR、4 张本地图 `/Users/fire/temp/images/product_1..4.webp`、SKU `4820...`、规格存 metadata、并补全中/马来/印尼语翻译」→ 先整理数据,再 `create_product`(`confirm=false` 看预览含 images/sku/metadata/缺语言清单 → 确认后 `confirm=true`,必要时 `acknowledge_missing_languages=true`)。工具会自动上传图片、写 SKU/metadata、并把四语言简码翻译写入翻译端点。随后 `add_product_to_sales_channel`(`confirm=true`) 让前端可见,再 `create_product_content`(`locale=en-US/zh-CN/ms-MY/id-ID`, `confirm=true`) 逐语言写富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。
165
+ - 「把这份抓取的 Lazada 商品录入:标题 DR.CYJ Hair Filler、价格 420 MYR、4 张本地图、SKU `4820...`、规格存 metadata、补全中/马来/印尼语翻译」→ 先整理数据,`create_product`(`confirm=false` 看预览含 images/sku/metadata/缺语言/缺货告警 → 确认后 `confirm=true`,带 `sales_channels: ["MY Storefront"]`、`initial_stock: {quantity: 100}`、`weight: 150`,必要时 `acknowledge_missing_languages=true`)。工具会自动上传图片、写 SKU/metadata/物理字段、四语言翻译、渠道挂载并建库存。再 `create_product_content`(`locale=en-US/zh-CN/ms-MY/id-ID`, `confirm=true`) 逐语言写富内容块(richtext 描述 + specs 规格 + image_gallery 图集),最后 `get_translations` + `get_product_content` 核对。
166
+ - 「核对某商品多语言是否齐全」→ `get_translations`(reference_id=商品 id 或 handle),看 4 条 locale 是否都有 title/description。
150
167
  - 「让这个商品在前端能搜到」→ `get_product` 取 id → `add_product_to_sales_channel`(`product_ids=[id]`, `confirm=true`)。
151
168
  - 「补全某商品中文详情页内容」→ `create_product_content`(`product_id`, `locale=zh-CN`, `blocks=[{type:'richtext',data:{html:'...'}},{type:'specs',data:{rows:[...]}}]`, `confirm=true`)。可用 `get_product_content` 先核对现状。
152
169
 
@@ -189,9 +206,10 @@ agent 客户端(Codex / Claude Desktop / Cursor / Workbuddy / CodeBuddy 等)
189
206
  - 登录走**用户账号**而非 API Key(现有 RBAC 依赖 `user.metadata.rbac_roles`)。
190
207
  - 金额以**主货币单位**存储(见上「硬规则 2」),不要换算成分。
191
208
  - **Medusa v2.21 差异(重要)**:
192
- - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心。
193
- - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,想核对翻译请用 `ask_usage` 或后台「多语言」面板,不要凭 `get_product` 返回为空就误判没写入。
209
+ - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心;**更新**商品时 `options` 字段被移除(2.16+),工具不会误传。
210
+ - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,**核对翻译请用 `get_translations`**,不要凭 `get_product` 返回为空就误判没写入。
194
211
  - 图片走 `/admin/uploads` 上传后再挂到商品(无独立 `/admin/products/{id}/images` 端点)。
195
- - 库存写入的端点/字段以 Medusa 2.21 契约为准;自定义端点 `warehouse-tasks` / `return-requests` 为本项目扩展。
212
+ - 库存查询/写入端点已按 2.21 契约校准(`/admin/inventory-items` + `location-levels`,sku 唯一约束);`set_product_stock` 全程幂等可重放。
213
+ - 自定义端点 `warehouse-tasks` / `return-requests` / `product-contents` 为本项目扩展。
196
214
  - 文档问答(`search_docs` / `ask_usage`)依赖知识源目录;npx 形态下默认 `./docs`,若问不到相关内容,提示运营换关键词或查后台手册。
197
215
  - 用受限的 `csr` 账号即可完成日常运营;敏感动作由后端 RBAC 拦截。