@sea-ecommerce/admin-mcp-server 0.1.0 → 0.1.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/dist/derlux-admin-mcp.js +341 -19
- package/package.json +3 -2
- package/skill/SKILL.md +197 -0
package/dist/derlux-admin-mcp.js
CHANGED
|
@@ -21638,6 +21638,15 @@ var AuthManager = class {
|
|
|
21638
21638
|
};
|
|
21639
21639
|
|
|
21640
21640
|
// src/client.ts
|
|
21641
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
21642
|
+
function mimeOf(path) {
|
|
21643
|
+
const ext = path.split(".").pop()?.toLowerCase() || "";
|
|
21644
|
+
if (ext === "webp") return "image/webp";
|
|
21645
|
+
if (ext === "png") return "image/png";
|
|
21646
|
+
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
21647
|
+
if (ext === "gif") return "image/gif";
|
|
21648
|
+
return "application/octet-stream";
|
|
21649
|
+
}
|
|
21641
21650
|
var MedusaAdminError = class extends Error {
|
|
21642
21651
|
constructor(status, message) {
|
|
21643
21652
|
super(message);
|
|
@@ -21685,12 +21694,111 @@ var MedusaAdminClient = class {
|
|
|
21685
21694
|
getProduct(id) {
|
|
21686
21695
|
return this.request("GET", `/admin/products/${encodeURIComponent(id)}`);
|
|
21687
21696
|
}
|
|
21697
|
+
/** 列出销售渠道(用于默认渠道解析) */
|
|
21698
|
+
listSalesChannels(params = {}) {
|
|
21699
|
+
return this.request("GET", `/admin/sales-channels${buildQuery(params)}`);
|
|
21700
|
+
}
|
|
21688
21701
|
createProduct(body) {
|
|
21689
21702
|
return this.request("POST", "/admin/products", body);
|
|
21690
21703
|
}
|
|
21691
21704
|
updateProduct(id, body) {
|
|
21692
21705
|
return this.request("POST", `/admin/products/${encodeURIComponent(id)}`, body);
|
|
21693
21706
|
}
|
|
21707
|
+
/**
|
|
21708
|
+
* 整体替换某变体的价格(多币种)。Medusa v2 价格挂在「变体」上,
|
|
21709
|
+
* POST /admin/products/{id} 携带 variants:[{id, prices}] 即整体替换该变体价格(不会累加)。
|
|
21710
|
+
* prices 数组形如 [{ currency_code, amount }, ...],amount 为主货币单位(元),切勿 ×100。
|
|
21711
|
+
*/
|
|
21712
|
+
updateProductPrices(productId, variantId, prices) {
|
|
21713
|
+
return this.request("POST", `/admin/products/${encodeURIComponent(productId)}`, {
|
|
21714
|
+
variants: [{ id: variantId, prices }]
|
|
21715
|
+
});
|
|
21716
|
+
}
|
|
21717
|
+
/** 批量写入翻译(POST /admin/translations/batch)。reference 一般为 "product"。 */
|
|
21718
|
+
async batchTranslations(create, update = []) {
|
|
21719
|
+
return this.request("POST", "/admin/translations/batch", { create, update });
|
|
21720
|
+
}
|
|
21721
|
+
/**
|
|
21722
|
+
* 把一组商品加入销售渠道(POST /admin/sales-channels/{id}/products)。
|
|
21723
|
+
* ⚠️ 注意:Medusa v2 该端点挂在「渠道」下,不是商品下;body 为 { add:[productIds], remove:[] }。
|
|
21724
|
+
* 商品只有挂到渠道,storefront(按 publishable key → 渠道)才能查到,否则详情页拉不到。
|
|
21725
|
+
*/
|
|
21726
|
+
addProductsToSalesChannel(channelId, productIds) {
|
|
21727
|
+
return this.request("POST", `/admin/sales-channels/${encodeURIComponent(channelId)}/products`, {
|
|
21728
|
+
add: productIds,
|
|
21729
|
+
remove: []
|
|
21730
|
+
});
|
|
21731
|
+
}
|
|
21732
|
+
// ─── 商品详情页富内容(product_content 模块)───
|
|
21733
|
+
/**
|
|
21734
|
+
* 读取某商品某语言的富内容块(GET /admin/product-contents)。
|
|
21735
|
+
* locale 用区域码:en-US / zh-CN / ms-MY / id-ID(与 storefront useProductContent 的 localeMap 一致)。
|
|
21736
|
+
* 返回 { product_content: { id, product_id, locale, status, blocks(JSON 字符串) } | null }。
|
|
21737
|
+
*/
|
|
21738
|
+
getProductContent(productId, locale = "en-US", status = "published") {
|
|
21739
|
+
return this.request(
|
|
21740
|
+
"GET",
|
|
21741
|
+
`/admin/product-contents${buildQuery({ product_id: productId, locale, status })}`
|
|
21742
|
+
);
|
|
21743
|
+
}
|
|
21744
|
+
/**
|
|
21745
|
+
* 创建/更新某商品某语言的富内容块(POST /admin/product-contents)。
|
|
21746
|
+
* body: { product_id, locale, status?, blocks };blocks 为 ContentBlock[] 数组
|
|
21747
|
+
* (type ∈ hero/richtext/image_gallery/video/faq/specs)。按 (product_id, locale) upsert。
|
|
21748
|
+
*/
|
|
21749
|
+
createProductContent(body) {
|
|
21750
|
+
return this.request("POST", "/admin/product-contents", body);
|
|
21751
|
+
}
|
|
21752
|
+
/**
|
|
21753
|
+
* 上传一组文件到 Medusa 文件服务(POST /admin/uploads,multipart 字段名 files),
|
|
21754
|
+
* 返回上传后的公开 URL 列表。
|
|
21755
|
+
*/
|
|
21756
|
+
async uploadFiles(files) {
|
|
21757
|
+
if (!files.length) return [];
|
|
21758
|
+
const form = new FormData();
|
|
21759
|
+
for (const f of files) {
|
|
21760
|
+
form.append("files", new Blob([new Uint8Array(f.buffer)], { type: f.mimetype }), f.filename);
|
|
21761
|
+
}
|
|
21762
|
+
const doPost = async () => {
|
|
21763
|
+
const res = await fetch(`${this.config.backendUrl}/admin/uploads`, {
|
|
21764
|
+
method: "POST",
|
|
21765
|
+
headers: this.auth.headers(),
|
|
21766
|
+
body: form
|
|
21767
|
+
});
|
|
21768
|
+
if (res.status === 401) {
|
|
21769
|
+
this.auth.clear();
|
|
21770
|
+
await this.auth.login();
|
|
21771
|
+
return doPost();
|
|
21772
|
+
}
|
|
21773
|
+
if (!res.ok) {
|
|
21774
|
+
const text = await res.text();
|
|
21775
|
+
throw new MedusaAdminError(res.status, text || res.statusText);
|
|
21776
|
+
}
|
|
21777
|
+
return await res.json();
|
|
21778
|
+
};
|
|
21779
|
+
const data = await doPost();
|
|
21780
|
+
return (data.files || []).map((f) => f.url);
|
|
21781
|
+
}
|
|
21782
|
+
/**
|
|
21783
|
+
* 把一组「本地文件路径 或 图片 URL」解析为可挂到商品的 URL:
|
|
21784
|
+
* - http(s) URL 原样透传;
|
|
21785
|
+
* - 本地路径读取后走 /admin/uploads 上传,返回 URL。
|
|
21786
|
+
*/
|
|
21787
|
+
async uploadProductAssets(pathsOrUrls) {
|
|
21788
|
+
const urls = [];
|
|
21789
|
+
const toUpload = [];
|
|
21790
|
+
for (const p of pathsOrUrls) {
|
|
21791
|
+
if (/^https?:\/\//i.test(p)) {
|
|
21792
|
+
urls.push(p);
|
|
21793
|
+
continue;
|
|
21794
|
+
}
|
|
21795
|
+
if (!existsSync2(p)) throw new MedusaAdminError(0, `\u56FE\u7247\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
|
|
21796
|
+
const buf = readFileSync2(p);
|
|
21797
|
+
toUpload.push({ filename: p.split("/").pop() || "image", mimetype: mimeOf(p), buffer: buf });
|
|
21798
|
+
}
|
|
21799
|
+
if (toUpload.length) urls.push(...await this.uploadFiles(toUpload));
|
|
21800
|
+
return urls;
|
|
21801
|
+
}
|
|
21694
21802
|
// ─── 仓库(StockLocation)/ 库存 ───
|
|
21695
21803
|
listStockLocations(params = {}) {
|
|
21696
21804
|
return this.request("GET", `/admin/stock-locations${buildQuery(params)}`);
|
|
@@ -21781,7 +21889,9 @@ var productTools = [
|
|
|
21781
21889
|
description: `\u5728\u540E\u53F0\u589E\u52A0\u4E00\u4E2A\u5546\u54C1\u3002
|
|
21782
21890
|
\u89C4\u5219\uFF08\xA716.1\uFF09\uFF1AHandle \u53EA\u80FD\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26(-)\uFF0C\u5DE5\u5177\u4F1A\u81EA\u52A8\u5F52\u4E00\u5316\uFF0C\u975E\u6CD5\u5219\u62A5\u9519\u3002
|
|
21783
21891
|
\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
|
|
21784
|
-
\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002
|
|
21892
|
+
\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002
|
|
21893
|
+
\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`,
|
|
21785
21895
|
schema: {
|
|
21786
21896
|
title: external_exports.string(),
|
|
21787
21897
|
handle: external_exports.string().optional().describe("\u4E0D\u586B\u5219\u7528 title \u5F52\u4E00\u5316\u751F\u6210"),
|
|
@@ -21792,9 +21902,13 @@ var productTools = [
|
|
|
21792
21902
|
// 并非最小单位(分)。官方 ESLint 规则 prices-in-major-units 明确:
|
|
21793
21903
|
// "Medusa stores prices in major units",期望 19.99 而非 1999。
|
|
21794
21904
|
// 本项目 seed 与 storefront 均遵循此官方约定,故直接透传,不要 ×100。
|
|
21795
|
-
price: external_exports.number().nonnegative().optional().describe("\u4E3B\u8D27\u5E01\u5355\u4F4D\u91D1\u989D\uFF0C\u5982 200.51 \u5373 RM200.51\uFF1BMedusa \u4EE5\u4E3B\u5355\u4F4D\u5B58\u50A8\uFF0C\u5207\u52FF\u6362\u7B97"),
|
|
21905
|
+
price: external_exports.number().nonnegative().optional().describe("\u4E3B\u8D27\u5E01\u5355\u4F4D\u91D1\u989D\uFF0C\u5982 200.51 \u5373 RM200.51\uFF1BMedusa \u4EE5\u4E3B\u5355\u4F4D\u5B58\u50A8\uFF0C\u5207\u52FF\u6362\u7B97\u3002\u4E0E prices \u4E8C\u9009\u4E00\uFF0Cprices \u4F18\u5148"),
|
|
21796
21906
|
currency_code: external_exports.string().default("MYR"),
|
|
21907
|
+
prices: external_exports.array(external_exports.object({ currency_code: external_exports.string(), amount: external_exports.number().nonnegative() })).optional().describe('\u591A\u5E01\u79CD\u4EF7\u683C\uFF08\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF09\uFF0C\u5982 [{currency_code:"myr",amount:420},{currency_code:"sgd",amount:127}]\u3002\u4E1C\u5357\u4E9A\u5E97\u901A\u5E38\u9700\u8986\u76D6 myr/sgd/idr/thb/php/cny\u3002\u4F18\u5148\u4E8E\u5355\u4E00 price'),
|
|
21797
21908
|
translations: external_exports.record(external_exports.string(), translationShape).optional(),
|
|
21909
|
+
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
|
+
sku: external_exports.string().optional().describe("\u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\u7684 SKU"),
|
|
21911
|
+
metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u4EFB\u610F\u952E\u503C\uFF0C\u5982\u89C4\u683C\u53C2\u6570 / source_url"),
|
|
21798
21912
|
confirm: external_exports.boolean().default(false),
|
|
21799
21913
|
acknowledge_missing_languages: external_exports.boolean().default(false)
|
|
21800
21914
|
},
|
|
@@ -21817,6 +21931,10 @@ var productTools = [
|
|
|
21817
21931
|
status: input.status,
|
|
21818
21932
|
price: input.price,
|
|
21819
21933
|
currency_code: input.currency_code,
|
|
21934
|
+
prices: input.prices,
|
|
21935
|
+
sku: input.sku,
|
|
21936
|
+
images: input.images,
|
|
21937
|
+
metadata: input.metadata,
|
|
21820
21938
|
missing_translations: missing
|
|
21821
21939
|
},
|
|
21822
21940
|
note: "\u672A confirm\uFF0C\u4EC5\u9884\u89C8\u3002\u786E\u8BA4\u8BF7\u8BBE confirm=true\u3002"
|
|
@@ -21825,25 +21943,54 @@ var productTools = [
|
|
|
21825
21943
|
if (missing.length > 0 && !input.acknowledge_missing_languages) {
|
|
21826
21944
|
return `\u9519\u8BEF\uFF1A\u7F3A\u5C11\u4EE5\u4E0B\u8BED\u8A00\u7684 title\uFF1A${missing.join(", ")}\u3002\u5982\u786E\u8BA4\u4ECD\u8981\u521B\u5EFA\uFF0C\u8BF7\u8BBE acknowledge_missing_languages=true\u3002`;
|
|
21827
21945
|
}
|
|
21946
|
+
let imageUrls = [];
|
|
21947
|
+
if (input.images?.length) {
|
|
21948
|
+
try {
|
|
21949
|
+
imageUrls = await client2.uploadProductAssets(input.images);
|
|
21950
|
+
} catch (e) {
|
|
21951
|
+
return `\u9519\u8BEF\uFF1A\u56FE\u7247\u4E0A\u4F20\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
|
|
21952
|
+
}
|
|
21953
|
+
}
|
|
21828
21954
|
const payload = {
|
|
21829
21955
|
title: input.title,
|
|
21830
21956
|
handle,
|
|
21831
21957
|
status: input.status,
|
|
21832
21958
|
subtitle: input.subtitle,
|
|
21833
|
-
description: input.description
|
|
21959
|
+
description: input.description,
|
|
21960
|
+
options: [{ title: "Default", values: ["Default"] }]
|
|
21834
21961
|
};
|
|
21835
|
-
if (input.
|
|
21836
|
-
if (
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
|
|
21962
|
+
if (input.metadata) payload.metadata = input.metadata;
|
|
21963
|
+
if (imageUrls.length) payload.images = imageUrls.map((url) => ({ url }));
|
|
21964
|
+
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
|
+
if (variantPrices) {
|
|
21966
|
+
const variant = {
|
|
21967
|
+
title: "Default",
|
|
21968
|
+
options: { Default: "Default" },
|
|
21969
|
+
prices: variantPrices,
|
|
21970
|
+
manage_inventory: false
|
|
21971
|
+
};
|
|
21972
|
+
if (input.sku) variant.sku = input.sku;
|
|
21973
|
+
payload.variants = [variant];
|
|
21844
21974
|
}
|
|
21845
21975
|
const data = await client2.createProduct(payload);
|
|
21846
|
-
|
|
21976
|
+
let translationResult = null;
|
|
21977
|
+
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
|
+
}
|
|
21991
|
+
}
|
|
21992
|
+
}
|
|
21993
|
+
return { created: data, translations: translationResult, warning_missing_languages: missing };
|
|
21847
21994
|
}
|
|
21848
21995
|
},
|
|
21849
21996
|
{
|
|
@@ -21870,9 +22017,58 @@ var productTools = [
|
|
|
21870
22017
|
if (input.status) patch.status = input.status;
|
|
21871
22018
|
if (input.subtitle) patch.subtitle = input.subtitle;
|
|
21872
22019
|
if (input.description) patch.description = input.description;
|
|
21873
|
-
if (input.translations) patch.translations = input.translations;
|
|
21874
22020
|
if (!input.confirm) return { dry_run: true, will_update: patch };
|
|
21875
|
-
|
|
22021
|
+
const data = await client2.updateProduct(input.id, patch);
|
|
22022
|
+
let translationResult = null;
|
|
22023
|
+
if (input.translations && Object.keys(input.translations).length) {
|
|
22024
|
+
const create = Object.entries(input.translations).map(([locale, t]) => ({
|
|
22025
|
+
reference: "product",
|
|
22026
|
+
reference_id: input.id,
|
|
22027
|
+
locale_code: locale,
|
|
22028
|
+
translations: { title: t?.title, description: t?.description }
|
|
22029
|
+
}));
|
|
22030
|
+
try {
|
|
22031
|
+
translationResult = await client2.batchTranslations(create);
|
|
22032
|
+
} catch (e) {
|
|
22033
|
+
return { updated: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
|
|
22034
|
+
}
|
|
22035
|
+
}
|
|
22036
|
+
return { updated: data, translations: translationResult };
|
|
22037
|
+
}
|
|
22038
|
+
},
|
|
22039
|
+
{
|
|
22040
|
+
name: "update_product_prices",
|
|
22041
|
+
description: `\u8BBE\u7F6E/\u66FF\u6362\u67D0\u5546\u54C1\u7684\uFF08\u53D8\u4F53\uFF09\u4EF7\u683C\uFF0C\u652F\u6301\u591A\u5E01\u79CD\u3002Medusa \u4EF7\u683C\u6302\u5728\u300C\u53D8\u4F53\u300D\u4E0A\uFF0C\u5199\u5165\u5373\u6574\u4F53\u66FF\u6362\u8BE5\u53D8\u4F53\u7684\u5168\u90E8\u4EF7\u683C\uFF08\u4E0D\u4F1A\u7D2F\u52A0\u65E7\u4EF7\uFF09\u3002
|
|
22042
|
+
\u4EF7\u683C\u4EE5\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF08\u5143\uFF09\u5B58\u50A8\uFF0C\u5207\u52FF \xD7100\u3002\u4E1C\u5357\u4E9A\u8DE8\u5883\u5E97\u901A\u5E38\u9700\u8986\u76D6 myr/sgd/idr/thb/php/cny\u3002
|
|
22043
|
+
\u4E0D\u6307\u5B9A variant_id \u65F6\u4F5C\u7528\u4E8E\u7B2C\u4E00\u4E2A\u53D8\u4F53\uFF08\u5355 SKU \u5546\u54C1\u5373\u552F\u4E00\u53D8\u4F53\uFF09\u3002\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
|
|
22044
|
+
schema: {
|
|
22045
|
+
product_id: external_exports.string(),
|
|
22046
|
+
prices: external_exports.array(external_exports.object({ currency_code: external_exports.string(), amount: external_exports.number().nonnegative() })).describe('\u591A\u5E01\u79CD\u4EF7\u683C\u6570\u7EC4\uFF08\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF09\uFF0C\u5982 [{currency_code:"myr",amount:420},{currency_code:"sgd",amount:127}]'),
|
|
22047
|
+
variant_id: external_exports.string().optional().describe("\u76EE\u6807\u53D8\u4F53 id\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u53D8\u4F53"),
|
|
22048
|
+
confirm: external_exports.boolean().default(false)
|
|
22049
|
+
},
|
|
22050
|
+
async run(input, { client: client2 }) {
|
|
22051
|
+
if (!input.confirm) {
|
|
22052
|
+
return {
|
|
22053
|
+
dry_run: true,
|
|
22054
|
+
product_id: input.product_id,
|
|
22055
|
+
will_set_prices: input.prices,
|
|
22056
|
+
variant_id: input.variant_id || "(\u7B2C\u4E00\u4E2A\u53D8\u4F53)"
|
|
22057
|
+
};
|
|
22058
|
+
}
|
|
22059
|
+
const prod = await client2.getProduct(input.product_id).catch(() => null);
|
|
22060
|
+
const product = prod?.product ?? prod;
|
|
22061
|
+
if (!product) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u5546\u54C1 ${input.product_id}`;
|
|
22062
|
+
const variants = product.variants || [];
|
|
22063
|
+
if (!variants.length) return `\u9519\u8BEF\uFF1A\u5546\u54C1\u6CA1\u6709\u53D8\u4F53\uFF0C\u65E0\u6CD5\u8BBE\u7F6E\u4EF7\u683C\u3002`;
|
|
22064
|
+
const target = input.variant_id ? variants.find((v) => v.id === input.variant_id) : variants[0];
|
|
22065
|
+
if (!target) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u76EE\u6807\u53D8\u4F53\uFF08\u6307\u5B9A ${input.variant_id} \u6216\u7B2C\u4E00\u4E2A\u53D8\u4F53\uFF09\u3002`;
|
|
22066
|
+
const data = await client2.updateProductPrices(
|
|
22067
|
+
input.product_id,
|
|
22068
|
+
target.id,
|
|
22069
|
+
input.prices.map((p) => ({ amount: p.amount, currency_code: p.currency_code }))
|
|
22070
|
+
);
|
|
22071
|
+
return { updated: data };
|
|
21876
22072
|
}
|
|
21877
22073
|
},
|
|
21878
22074
|
{
|
|
@@ -21886,6 +22082,132 @@ var productTools = [
|
|
|
21886
22082
|
if (!input.confirm) return { dry_run: true, will_set_status: "archived" };
|
|
21887
22083
|
return client2.updateProduct(input.id, { status: "archived" });
|
|
21888
22084
|
}
|
|
22085
|
+
},
|
|
22086
|
+
{
|
|
22087
|
+
name: "upload_product_image",
|
|
22088
|
+
description: `\u7ED9\u5DF2\u5B58\u5728\u7684\u5546\u54C1\u8FFD\u52A0\u56FE\u7247\u3002\u652F\u6301\u672C\u5730\u6587\u4EF6\u8DEF\u5F84\uFF08\u81EA\u52A8\u4E0A\u4F20\u5230\u6587\u4EF6\u670D\u52A1\uFF09\u6216\u56FE\u7247 URL\u3002
|
|
22089
|
+
\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\uFF1Bconfirm=true \u624D\u4E0A\u4F20\u5E76\u6302\u5230\u5546\u54C1\uFF08\u4E0E\u73B0\u6709\u56FE\u7247\u5408\u5E76\uFF0C\u4E0D\u4F1A\u8986\u76D6\u5DF2\u6709\u56FE\u7247\uFF09\u3002`,
|
|
22090
|
+
schema: {
|
|
22091
|
+
product_id: external_exports.string(),
|
|
22092
|
+
images: external_exports.array(external_exports.string()).describe("\u672C\u5730\u56FE\u7247\u8DEF\u5F84\u6216\u56FE\u7247 URL"),
|
|
22093
|
+
confirm: external_exports.boolean().default(false)
|
|
22094
|
+
},
|
|
22095
|
+
async run(input, { client: client2 }) {
|
|
22096
|
+
if (!input.confirm) {
|
|
22097
|
+
return { dry_run: true, product_id: input.product_id, will_upload: input.images };
|
|
22098
|
+
}
|
|
22099
|
+
let urls = [];
|
|
22100
|
+
try {
|
|
22101
|
+
urls = await client2.uploadProductAssets(input.images);
|
|
22102
|
+
} catch (e) {
|
|
22103
|
+
return `\u9519\u8BEF\uFF1A\u56FE\u7247\u4E0A\u4F20\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
|
|
22104
|
+
}
|
|
22105
|
+
const existing = await client2.getProduct(input.product_id).catch(() => null);
|
|
22106
|
+
const prod = existing?.product ?? existing ?? {};
|
|
22107
|
+
const existingUrls = (prod.images || []).map((i) => i.url).filter(Boolean);
|
|
22108
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...existingUrls, ...urls]));
|
|
22109
|
+
const data = await client2.updateProduct(input.product_id, {
|
|
22110
|
+
images: merged.map((u) => ({ url: u }))
|
|
22111
|
+
});
|
|
22112
|
+
return { updated: data };
|
|
22113
|
+
}
|
|
22114
|
+
},
|
|
22115
|
+
{
|
|
22116
|
+
name: "add_product_to_sales_channel",
|
|
22117
|
+
description: `\u628A\u5546\u54C1\u52A0\u5165\u9500\u552E\u6E20\u9053\uFF0C\u4F7F\u5176\u5728\u524D\u7AEF\u5546\u57CE\uFF08\u6309 publishable key \u2192 \u6E20\u9053\u8FC7\u6EE4\uFF09\u53EF\u89C1\u3002
|
|
22118
|
+
\u26A0\uFE0F Medusa v2 \u7AEF\u70B9\u6302\u5728\u6E20\u9053\u4E0B\uFF1APOST /admin/sales-channels/{id}/products\uFF0C\u800C\u975E\u5546\u54C1\u4E0B\u3002
|
|
22119
|
+
\u4E0D\u6307\u5B9A channel_id \u65F6\u9ED8\u8BA4\u52A0\u5165\u7B2C\u4E00\u4E2A\u542F\u7528\u7684\u6E20\u9053\u3002\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
|
|
22120
|
+
schema: {
|
|
22121
|
+
product_ids: external_exports.array(external_exports.string()).describe("\u8981\u52A0\u5165\u6E20\u9053\u7684\u5546\u54C1 id \u5217\u8868"),
|
|
22122
|
+
channel_id: external_exports.string().optional().describe("\u9500\u552E\u6E20\u9053 id\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u542F\u7528\u7684\u6E20\u9053"),
|
|
22123
|
+
confirm: external_exports.boolean().default(false)
|
|
22124
|
+
},
|
|
22125
|
+
async run(input, { client: client2, config: config3 }) {
|
|
22126
|
+
if (!input.confirm) {
|
|
22127
|
+
return { dry_run: true, will_add: input.product_ids, channel_id: input.channel_id || "(\u9ED8\u8BA4\u7B2C\u4E00\u4E2A\u542F\u7528\u6E20\u9053)" };
|
|
22128
|
+
}
|
|
22129
|
+
let channelId = input.channel_id;
|
|
22130
|
+
if (!channelId) {
|
|
22131
|
+
const ch = await client2.listSalesChannels?.();
|
|
22132
|
+
const list = ch?.sales_channels || [];
|
|
22133
|
+
const firstEnabled = list.find((c) => !c.is_disabled) || list[0];
|
|
22134
|
+
if (!firstEnabled) return "\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u53EF\u7528\u7684\u9500\u552E\u6E20\u9053\u3002";
|
|
22135
|
+
channelId = firstEnabled.id;
|
|
22136
|
+
}
|
|
22137
|
+
const data = await client2.addProductsToSalesChannel(channelId, input.product_ids);
|
|
22138
|
+
return { added_to_channel: channelId, result: data };
|
|
22139
|
+
}
|
|
22140
|
+
},
|
|
22141
|
+
{
|
|
22142
|
+
name: "get_product_content",
|
|
22143
|
+
description: `\u8BFB\u53D6\u5546\u54C1\u8BE6\u60C5\u9875\u5BCC\u5185\u5BB9\u5757\uFF08GET /admin/product-contents\uFF09\u3002
|
|
22144
|
+
locale \u7528\u533A\u57DF\u7801\uFF1Aen-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF useProductContent \u7684 localeMap \u4E00\u81F4\uFF09\u3002
|
|
22145
|
+
\u8FD4\u56DE { product_content: { id, product_id, locale, status, blocks(\u5DF2\u89E3\u6790\u6570\u7EC4) } | null }\u3002`,
|
|
22146
|
+
schema: {
|
|
22147
|
+
product_id: external_exports.string(),
|
|
22148
|
+
locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
|
|
22149
|
+
status: external_exports.enum(["draft", "published"]).default("published")
|
|
22150
|
+
},
|
|
22151
|
+
async run(input, { client: client2 }) {
|
|
22152
|
+
const data = await client2.getProductContent(input.product_id, input.locale, input.status);
|
|
22153
|
+
const pc = data?.product_content;
|
|
22154
|
+
if (!pc) return { product_content: null };
|
|
22155
|
+
let blocks = [];
|
|
22156
|
+
try {
|
|
22157
|
+
blocks = pc.blocks ? JSON.parse(pc.blocks) : [];
|
|
22158
|
+
} catch {
|
|
22159
|
+
blocks = [];
|
|
22160
|
+
}
|
|
22161
|
+
return { product_content: { ...pc, blocks } };
|
|
22162
|
+
}
|
|
22163
|
+
},
|
|
22164
|
+
{
|
|
22165
|
+
name: "create_product_content",
|
|
22166
|
+
description: `\u521B\u5EFA/\u66F4\u65B0\u5546\u54C1\u8BE6\u60C5\u9875\u5BCC\u5185\u5BB9\u5757\uFF08POST /admin/product-contents\uFF0C\u6309 product_id+locale upsert\uFF09\u3002
|
|
22167
|
+
storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4E9B\u5757\u6E32\u67D3\u2014\u2014\u4EC5\u586B product.description \u4E0D\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\uFF0C\u5FC5\u987B\u5EFA\u5185\u5BB9\u5757\uFF0C\u5426\u5219\u8BE6\u60C5\u9875\u4E3B\u4F53\u7A7A\u767D\u3002
|
|
22168
|
+
\u533A\u5757\u7C7B\u578B type \u2208 hero / richtext / image_gallery / video / faq / specs\uFF1A
|
|
22169
|
+
- richtext: data.{ html }\uFF08\u76F4\u63A5\u6E32\u67D3 HTML\uFF0C\u53EF\u542B <p>/<h2>/<ul> \u7B49\uFF09
|
|
22170
|
+
- image_gallery: data.{ images:string[], caption? }
|
|
22171
|
+
- hero: data.{ image?, title?, subtitle? }
|
|
22172
|
+
- video: data.{ url, poster? }\uFF08\u652F\u6301 YouTube\uFF09
|
|
22173
|
+
- faq: data.{ items: {question,answer}[] }
|
|
22174
|
+
- specs: data.{ rows: {label,value}[] }
|
|
22175
|
+
\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
|
|
22176
|
+
\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
|
|
22177
|
+
schema: {
|
|
22178
|
+
product_id: external_exports.string(),
|
|
22179
|
+
locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
|
|
22180
|
+
status: external_exports.enum(["draft", "published"]).default("published"),
|
|
22181
|
+
blocks: external_exports.array(
|
|
22182
|
+
external_exports.object({
|
|
22183
|
+
id: external_exports.string().optional(),
|
|
22184
|
+
type: external_exports.enum(["hero", "richtext", "image_gallery", "video", "faq", "specs"]),
|
|
22185
|
+
data: external_exports.record(external_exports.string(), external_exports.any())
|
|
22186
|
+
})
|
|
22187
|
+
).describe("ContentBlock[] \u6570\u7EC4"),
|
|
22188
|
+
confirm: external_exports.boolean().default(false)
|
|
22189
|
+
},
|
|
22190
|
+
async run(input, { client: client2 }) {
|
|
22191
|
+
if (!input.confirm) {
|
|
22192
|
+
return {
|
|
22193
|
+
dry_run: true,
|
|
22194
|
+
will_create: {
|
|
22195
|
+
product_id: input.product_id,
|
|
22196
|
+
locale: input.locale,
|
|
22197
|
+
status: input.status,
|
|
22198
|
+
block_count: input.blocks.length,
|
|
22199
|
+
types: input.blocks.map((b) => b.type)
|
|
22200
|
+
}
|
|
22201
|
+
};
|
|
22202
|
+
}
|
|
22203
|
+
const data = await client2.createProductContent({
|
|
22204
|
+
product_id: input.product_id,
|
|
22205
|
+
locale: input.locale,
|
|
22206
|
+
status: input.status,
|
|
22207
|
+
blocks: input.blocks
|
|
22208
|
+
});
|
|
22209
|
+
return { created: data };
|
|
22210
|
+
}
|
|
21889
22211
|
}
|
|
21890
22212
|
];
|
|
21891
22213
|
|
|
@@ -22012,11 +22334,11 @@ var opsTools = [
|
|
|
22012
22334
|
];
|
|
22013
22335
|
|
|
22014
22336
|
// src/resources/docs.ts
|
|
22015
|
-
import { readdirSync, readFileSync as
|
|
22337
|
+
import { readdirSync, readFileSync as readFileSync3, existsSync as existsSync3, statSync } from "node:fs";
|
|
22016
22338
|
import { join as join2 } from "node:path";
|
|
22017
22339
|
var cache = null;
|
|
22018
22340
|
function walk(dir, out = []) {
|
|
22019
|
-
if (!
|
|
22341
|
+
if (!existsSync3(dir)) return out;
|
|
22020
22342
|
for (const entry of readdirSync(dir)) {
|
|
22021
22343
|
const full = join2(dir, entry);
|
|
22022
22344
|
try {
|
|
@@ -22028,7 +22350,7 @@ function walk(dir, out = []) {
|
|
|
22028
22350
|
return out;
|
|
22029
22351
|
}
|
|
22030
22352
|
function parseFile(path) {
|
|
22031
|
-
const raw =
|
|
22353
|
+
const raw = readFileSync3(path, "utf8");
|
|
22032
22354
|
const lines = raw.split("\n");
|
|
22033
22355
|
const title = lines.find((l) => l.startsWith("# "))?.slice(2).trim() || path;
|
|
22034
22356
|
const sections = [];
|
|
@@ -22139,7 +22461,7 @@ var config2 = loadConfig();
|
|
|
22139
22461
|
var auth = new AuthManager(config2);
|
|
22140
22462
|
var client = new MedusaAdminClient(config2, auth);
|
|
22141
22463
|
var ctx = { client, config: config2 };
|
|
22142
|
-
var server = new McpServer({ name: "derlux-admin", version: "0.1.
|
|
22464
|
+
var server = new McpServer({ name: "derlux-admin", version: "0.1.1" });
|
|
22143
22465
|
for (const def of toolDefs) {
|
|
22144
22466
|
server.tool(def.name, def.description, def.schema, async (args) => {
|
|
22145
22467
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sea-ecommerce/admin-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "让运营同学用本地 AI agent(Codex / Workbuddy / Claude / Cursor / CodeBuddy)运营 Medusa 后台的 MCP server。",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"skill"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "tsx src/index.ts",
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: derlux-admin-mcp-ops
|
|
3
|
+
description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERLUX Store 后台的指南。覆盖商品创建/修改/下架、仓库与库存调整、订单/退货/仓库作业查询、后台使用问答。当用户要让 agent 创建、修改或下架商品、调整某仓库库存、查询订单或退货、或询问「后台怎么操作某功能」时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DERLUX 运营后台 MCP 操作指南(derlux-admin)
|
|
7
|
+
|
|
8
|
+
> 本文件随 npm 包 `@sea-ecommerce/admin-mcp-server` 一同发布,运营/agent 可经 npm 公开文件链接读取,
|
|
9
|
+
> 例如:`https://unpkg.com/@sea-ecommerce/admin-mcp-server/skill/SKILL.md`
|
|
10
|
+
> 或 `https://cdn.jsdelivr.net/npm/@sea-ecommerce/admin-mcp-server/skill/SKILL.md`
|
|
11
|
+
|
|
12
|
+
## 你是谁 / 这个 skill 是做什么的
|
|
13
|
+
|
|
14
|
+
你已接入一个名为 **`derlux-admin`** 的 MCP server。它把 DERLUX Store(面向马来西亚/印尼的东南亚电商,法律主体「香港德樂仕股份有限公司」)Medusa 后台的运营动作封装成一批语义化工具(tool),由你直接调用,代替运营同学在后台界面手动点选。
|
|
15
|
+
|
|
16
|
+
关键前提(务必记牢):
|
|
17
|
+
|
|
18
|
+
- **认证复用真实后台管理员账号**(须持有 `csr` 或 `super_admin` 角色),权限受现有 RBAC 约束。敏感动作若账号无权限,后端会直接拦截。
|
|
19
|
+
- **所有写操作默认是「预览(dry_run)」**,只有显式设 `confirm=true` 才会真正落到后台。这是给运营的兜底保护,不要跳过预览。
|
|
20
|
+
- 每次调用都会写入审计日志 `~/.derlux/mcp-audit.log`,操作可追溯。
|
|
21
|
+
|
|
22
|
+
## ⚠️ 必须遵守的硬规则(每次调用写工具前自查)
|
|
23
|
+
|
|
24
|
+
1. **预览优先,确认后执行**:任何写工具先以 `confirm=false`(默认)跑一次,把返回的预览读给运营确认;得到明确确认后,再用 `confirm=true` 真正执行。绝不在未确认时直接写库。
|
|
25
|
+
2. **价格单位 = 主货币单位(元),不要 ×100**:金额直接透传主单位,例如 `200.51` 就是 RM200.51 / MYR200.51。Medusa v2.21 以主单位存储金额,**绝不要写成 20051**。
|
|
26
|
+
3. **Handle 自动归一化**:商品 `handle` 会被自动转小写、空格/下划线/点→连字符、剔除其余非法字符。归一化后仍非法(空或全部被剔除)工具会直接报错,你无需手工处理,但应避免给无意义输入;未填时由 `title` 自动生成。
|
|
27
|
+
4. **多语言必填**:`create_product` / `update_product` 的 `translations` 应覆盖 `en` / `zh` / `ms` / `id` 四种语言的 `title`(建议也带 `description`)。缺语言时:预览会告警;真正写入需 `confirm=true` **且** `acknowledge_missing_languages=true`。创建前主动提醒运营补全翻译,不要默默放行。工具会**自动**把翻译写入 Medusa v2.21 专用的 `/admin/translations/batch` 端点(商品创建体本身不接受 `translations` 字段),你无需手动调用该端点。
|
|
28
|
+
5. **审计可追溯**:无需你处理,但要知道每次动作都在 `~/.derlux/mcp-audit.log` 留痕。
|
|
29
|
+
6. **商品必须挂销售渠道才在前端可见**:新建商品默认**不在任何销售渠道**,storefront(按 publishable key → 渠道过滤)拉不到它,详情页表现成「空/404」。录入后用 `add_product_to_sales_channel`(`confirm=true`)把它加入渠道(默认第一个启用渠道;马来西亚站用 `sc_01M4K4F0D6HBHZ0NH6WB7H5KPT`「MY Storefront」)。
|
|
30
|
+
7. **两套 locale 码,别混**:
|
|
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 一致)。用简码会导致该语言详情页读不到内容。
|
|
33
|
+
8. **详情页「富内容」区靠 `create_product_content`,不是 `description`**:storefront 详情页下方大块内容由 `product_content` 模块渲染(`richtext`/`specs`/`image_gallery` 等)。只填 `product.description` 不会显示在那里——录入完整商品时必须再建一份 `product_content`(建议每语言一份)。`get_product` 默认不含该字段,核对请用 `get_product_content`。
|
|
34
|
+
|
|
35
|
+
## 工具速查表
|
|
36
|
+
|
|
37
|
+
| 工具 | 类型 | 写库? | 默认行为 |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `list_products` | 读 | 否 | 直接返回 |
|
|
40
|
+
| `get_product` | 读 | 否 | 直接返回 |
|
|
41
|
+
| `list_stock_locations` | 读 | 否 | 直接返回 |
|
|
42
|
+
| `get_inventory_levels` | 读 | 否 | 直接返回(按 location_id 列库存条目与水平)|
|
|
43
|
+
| `list_orders` | 读 | 否 | 直接返回 |
|
|
44
|
+
| `get_order` | 读 | 否 | 直接返回 |
|
|
45
|
+
| `list_warehouse_tasks` | 读 | 否 | 直接返回(自定义端点)|
|
|
46
|
+
| `list_return_requests` | 读 | 否 | 直接返回(自定义端点)|
|
|
47
|
+
| `search_docs` | 读 | 否 | 检索后台知识库 |
|
|
48
|
+
| `ask_usage` | 读 | 否 | 回答「后台怎么操作」类问题 |
|
|
49
|
+
| `create_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
|
|
50
|
+
| `update_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
|
|
51
|
+
| `deactivate_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,置 `archived` |
|
|
52
|
+
| `upload_product_image` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,给已有商品追加图片 |
|
|
53
|
+
| `add_product_to_sales_channel` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,把商品加入销售渠道使其在前端可见 |
|
|
54
|
+
| `create_product_content` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,写商品详情页富内容块(hero/richtext/image_gallery/video/faq/specs)|
|
|
55
|
+
| `get_product_content` | 读 | 否 | 读取某商品某语言的富内容块 |
|
|
56
|
+
| `create_stock_location` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
|
|
57
|
+
| `update_inventory_level` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
|
|
58
|
+
|
|
59
|
+
## 各工具参数与说明
|
|
60
|
+
|
|
61
|
+
### 读 —— 商品
|
|
62
|
+
- **`list_products`**:查询商品列表。
|
|
63
|
+
- `q`(string,可选)关键词;`status`(enum:`draft|published|proposed|rejected|archived`,可选)状态过滤;`handle`(string,可选);`limit`(int 1–100,默认 20)。
|
|
64
|
+
- **`get_product`**:按商品 id 或 handle 获取详情。
|
|
65
|
+
- `id_or_handle`(string,**必填**)。
|
|
66
|
+
|
|
67
|
+
### 读 —— 仓库与库存
|
|
68
|
+
- **`list_stock_locations`**:列出仓库(StockLocation)。
|
|
69
|
+
- `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
|
|
70
|
+
- **`get_inventory_levels`**:列出某仓库下的库存条目及库存水平。
|
|
71
|
+
- `location_id`(string,**必填**);`q`(string,可选);`limit`(int 1–100,默认 50)。
|
|
72
|
+
|
|
73
|
+
### 读 —— 订单 / 履约 / 退货
|
|
74
|
+
- **`list_orders`**:查询订单列表。
|
|
75
|
+
- `q`(string,可选);`status`(string,可选);`limit`(int 1–100,默认 20)。
|
|
76
|
+
- **`get_order`**:按订单 id 或 `display_id` 获取详情。
|
|
77
|
+
- `id`(string,**必填**)。
|
|
78
|
+
- **`list_warehouse_tasks`**:查询仓库作业任务(自定义端点 `/admin/warehouse-tasks`)。
|
|
79
|
+
- `order_id`(string,可选);`stage`(string,可选);`limit`(int 1–200,默认 200)。
|
|
80
|
+
- **`list_return_requests`**:查询退货/售后请求(自定义端点 `/admin/return-requests`)。
|
|
81
|
+
- `status`(string,可选);`limit`(int 1–100,默认 50)。
|
|
82
|
+
|
|
83
|
+
### 读 —— 后台知识问答
|
|
84
|
+
- **`search_docs`**:在知识库(docs)中检索相关段落,返回文件、章节与摘要。
|
|
85
|
+
- `query`(string,**必填**);`limit`(int 1–20,默认 5)。
|
|
86
|
+
- **`ask_usage`**:回答「后台怎么操作」类问题,返回最相关段落与出处文件。
|
|
87
|
+
- `question`(string,**必填**);`limit`(int 1–10,默认 5)。若搜不到,提示运营换关键词或查后台手册。
|
|
88
|
+
|
|
89
|
+
### 写 —— 商品
|
|
90
|
+
- **`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` 返回预览。
|
|
95
|
+
- **`deactivate_product`**:下架商品(status 置 `archived`)。
|
|
96
|
+
- `id`(string,**必填**);`confirm`(默认 false)。`confirm=false` 返回预览。
|
|
97
|
+
- **`upload_product_image`**:给**已存在**的商品追加图片(与已有图片合并,不覆盖)。
|
|
98
|
+
- `product_id`(string,**必填**);`images`(string[],本地图片路径或图片 URL);`confirm`(默认 false)。`confirm=false` 仅预览;`confirm=true` 才上传并挂接。
|
|
99
|
+
- **`add_product_to_sales_channel`**:把商品加入销售渠道,使其在前端商城可见。
|
|
100
|
+
- `product_ids`(string[],**必填**,商品 id 列表);`channel_id`(string,可选,省略则取第一个启用渠道);`confirm`(默认 false)。`confirm=false` 仅预览。
|
|
101
|
+
- **`get_product_content`**:读取商品详情页富内容块。
|
|
102
|
+
- `product_id`(string,**必填**);`locale`(`en-US` / `zh-CN` / `ms-MY` / `id-ID`,默认 `en-US`);`status`(`draft` / `published`,默认 `published`)。返回 `{ product_content: { id, product_id, locale, status, blocks(已解析数组) } | null }`。
|
|
103
|
+
- **`create_product_content`**:创建/更新商品详情页富内容块(按 `product_id` + `locale` upsert)。**这是填充详情页主体内容的工具**——只填 `description` 详情页大块区域仍是空白。
|
|
104
|
+
- `product_id`(string,**必填**);`locale`(区域码,默认 `en-US`,见硬规则 7);`status`(默认 `published`);`blocks`(ContentBlock[],**必填**);`confirm`(默认 false)。
|
|
105
|
+
- 区块 `type` 与 `data`:
|
|
106
|
+
- `richtext`:`data.{ html }`(直接渲染 HTML,可含 `<p>/<h2>/<ul>`)
|
|
107
|
+
- `image_gallery`:`data.{ images:string[], caption? }`
|
|
108
|
+
- `hero`:`data.{ image?, title?, subtitle? }`
|
|
109
|
+
- `video`:`data.{ url, poster? }`(支持 YouTube)
|
|
110
|
+
- `faq`:`data.{ items: {question,answer}[] }`
|
|
111
|
+
- `specs`:`data.{ rows: {label,value}[] }`
|
|
112
|
+
- `confirm=false` 返回预览(含块数量与类型);`confirm=true` 才写入。
|
|
113
|
+
|
|
114
|
+
### 写 —— 仓库与库存
|
|
115
|
+
- **`create_stock_location`**:新增仓库(StockLocation)。
|
|
116
|
+
- `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 契约为准。
|
|
119
|
+
|
|
120
|
+
## 推荐操作流程(SOP)
|
|
121
|
+
|
|
122
|
+
**创建商品**
|
|
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 告知运营。
|
|
129
|
+
|
|
130
|
+
**修改 / 下架商品**
|
|
131
|
+
1. `get_product` 或 `list_products` 确认目标 id。
|
|
132
|
+
2. `update_product` / `deactivate_product`(`confirm=false`)→ 展示预览。
|
|
133
|
+
3. 确认后同名工具 `confirm=true` 执行。
|
|
134
|
+
|
|
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` 执行。
|
|
139
|
+
|
|
140
|
+
**查询类**:直接调用对应读工具即可,无需 `confirm`。
|
|
141
|
+
|
|
142
|
+
## 常见任务示例(运营怎么说 / 你怎么调)
|
|
143
|
+
|
|
144
|
+
- 「下架 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)。
|
|
146
|
+
- 「查一下最近 10 个待发货订单」→ `list_orders`(status=`pending`, limit=10)。
|
|
147
|
+
- 「后台怎么新建优惠券」→ `ask_usage`(question=`如何新建优惠券`)。
|
|
148
|
+
- 「列出所有退货请求」→ `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 图集)。
|
|
150
|
+
- 「让这个商品在前端能搜到」→ `get_product` 取 id → `add_product_to_sales_channel`(`product_ids=[id]`, `confirm=true`)。
|
|
151
|
+
- 「补全某商品中文详情页内容」→ `create_product_content`(`product_id`, `locale=zh-CN`, `blocks=[{type:'richtext',data:{html:'...'}},{type:'specs',data:{rows:[...]}}]`, `confirm=true`)。可用 `get_product_content` 先核对现状。
|
|
152
|
+
|
|
153
|
+
## 给运营的一次性安装配置
|
|
154
|
+
|
|
155
|
+
agent 客户端(Codex / Claude Desktop / Cursor / Workbuddy / CodeBuddy 等)的 MCP 配置里加一条即可(机器只需 Node ≥ 18,无需 clone 仓库、无需 build):
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"derlux-admin": {
|
|
161
|
+
"command": "npx",
|
|
162
|
+
"args": ["-y", "@sea-ecommerce/admin-mcp-server"],
|
|
163
|
+
"env": {
|
|
164
|
+
"MEDUSA_BACKEND_URL": "https://admin.internal.derlux.com",
|
|
165
|
+
"DERLUX_ADMIN_EMAIL": "你的后台账号",
|
|
166
|
+
"DERLUX_ADMIN_PASSWORD": "你的密码"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 环境变量说明
|
|
174
|
+
|
|
175
|
+
| 变量 | 必填 | 默认 | 说明 |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `MEDUSA_BACKEND_URL` | 否 | `http://localhost:9000` | 后端地址(生产填内部域名)|
|
|
178
|
+
| `DERLUX_ADMIN_EMAIL` | **是** | — | 后台管理员账号(须持 `csr`/`super_admin`)|
|
|
179
|
+
| `DERLUX_ADMIN_PASSWORD` | **是** | — | 该账号密码 |
|
|
180
|
+
| `DERLUX_AUTH_PATH` | 否 | `/auth/user/emailpass` | 登录端点 |
|
|
181
|
+
| `DERLUX_SESSION_FILE` | 否 | `~/.derlux/mcp-session` | 会话持久化文件(权限 0600)|
|
|
182
|
+
| `DERLUX_AUDIT_FILE` | 否 | `~/.derlux/mcp-audit.log` | 审计日志 |
|
|
183
|
+
| `DERLUX_DOCS_DIR` | 否 | `./docs` | 问答知识源目录(npx 形态下通常留默认或指向本地 docs 副本)|
|
|
184
|
+
| `DERLUX_REQUIRED_LOCALES` | 否 | `en,zh,ms,id` | 商品必须覆盖的语言 |
|
|
185
|
+
|
|
186
|
+
## 已知限制 / 注意事项
|
|
187
|
+
|
|
188
|
+
- 本 MCP **不改动任何数据库 schema**,仅复用现有 `/admin/*` 接口;运营动作本身按既有逻辑写库。
|
|
189
|
+
- 登录走**用户账号**而非 API Key(现有 RBAC 依赖 `user.metadata.rbac_roles`)。
|
|
190
|
+
- 金额以**主货币单位**存储(见上「硬规则 2」),不要换算成分。
|
|
191
|
+
- **Medusa v2.21 差异(重要)**:
|
|
192
|
+
- 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心。
|
|
193
|
+
- 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,想核对翻译请用 `ask_usage` 或后台「多语言」面板,不要凭 `get_product` 返回为空就误判没写入。
|
|
194
|
+
- 图片走 `/admin/uploads` 上传后再挂到商品(无独立 `/admin/products/{id}/images` 端点)。
|
|
195
|
+
- 库存写入的端点/字段以 Medusa 2.21 契约为准;自定义端点 `warehouse-tasks` / `return-requests` 为本项目扩展。
|
|
196
|
+
- 文档问答(`search_docs` / `ask_usage`)依赖知识源目录;npx 形态下默认 `./docs`,若问不到相关内容,提示运营换关键词或查后台手册。
|
|
197
|
+
- 用受限的 `csr` 账号即可完成日常运营;敏感动作由后端 RBAC 拦截。
|