为 Shopify 构建自定义 MCP 服务器
现有的 MCP 服务器涵盖了常见的使用场景,但每个 Shopify 商家都有独特的需求。也许您需要一个将库存数据与专有需求预测模型相结合的 MCP 服务器。或者是一个在创建产品时强制执行特定业务规则的服务器。又或者是一个将 Shopify 与内部 ERP 系统集成的服务器。
本指南将引导您使用官方 TypeScript SDK 从零开始构建自定义 MCP 服务器、实现 Shopify Admin API 工具,以及为团队发布该服务器。
何时构建自定义服务器与使用现有服务器
在以下情况下构建自定义 MCP 服务器:
- 现有服务器不涵盖您的用例:您需要的工具结合了 Shopify 数据与内部系统
- 您需要业务逻辑强制执行:产品创建必须遵循特定的命名规范、定价规则或审批工作流
- 安全需求要求如此:您需要精确控制暴露哪些 API 操作并审计其使用情况
- 您想要领域特定的抽象层:不使用原始的 CRUD,而是要像「create_seasonal_collection」这样编码您业务流程的工具
- 性能优化:您需要以通用服务器不支持的方式批处理或缓存 Shopify API 调用
在以下情况下使用现有服务器:
- 标准 CRUD 操作已足够
- 您正在做原型或进行探索
- 社区服务器持续维护且涵盖您的需求
使用社区 shopify-mcp 服务器来满足 80% 的需求(标准操作),并为您业务中独特的 20% 构建自定义服务器。它们可以同时运行 -- Claude Code 支持多个 MCP 服务器。
MCP SDK 设置(TypeScript)
项目初始化
mkdir shopify-custom-mcp
cd shopify-custom-mcp
npm init -y
npm install @modelcontextprotocol/sdk @shopify/shopify-api zod
npm install -D typescript @types/node tsx
npx tsc --init
更新您的 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
更新 package.json:
{
"name": "shopify-custom-mcp",
"version": "1.0.0",
"type": "module",
"bin": {
"shopify-custom-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
}
}
基本服务器结构
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 解析命令行参数
const args = process.argv.slice(2);
const accessToken = args.find(a => a.startsWith("--token="))?.split("=")[1]
|| process.env.SHOPIFY_ACCESS_TOKEN;
const storeDomain = args.find(a => a.startsWith("--domain="))?.split("=")[1]
|| process.env.SHOPIFY_STORE_DOMAIN;
if (!accessToken || !storeDomain) {
console.error("Usage: shopify-custom-mcp --token=shpat_xxx --domain=store.myshopify.com");
process.exit(1);
}
// 创建 MCP 服务器
const server = new McpServer({
name: "shopify-custom-mcp",
version: "1.0.0",
});
// Shopify API 辅助函数
async function shopifyGraphQL(query: string, variables?: Record<string, unknown>) {
const response = await fetch(
`https://${storeDomain}/admin/api/2025-01/graphql.json`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": accessToken!,
},
body: JSON.stringify({ query, variables }),
}
);
if (!response.ok) {
throw new Error(`Shopify API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// --- 在此注册工具、资源和提示 ---
// (请参阅以下章节)
// 使用 stdio 传输启动服务器
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Shopify Custom MCP server running on stdio");
}
main().catch(console.error);
实现 Shopify Admin API 工具
工具 1:获取低库存产品
此工具结合 Shopify 查询与业务逻辑(自定义阈值):
// 注册一个查找低库存产品的工具
server.tool(
"get_low_inventory_products",
"Find products where total inventory is below a threshold",
{
threshold: z.number().default(10).describe("Inventory threshold (default: 10)"),
location_id: z.string().optional().describe("Filter by location ID (optional)"),
},
async ({ threshold, location_id }) => {
const query = `
query GetProducts($first: Int!, $after: String) {
products(first: 50, after: $after) {
edges {
node {
id
title
totalInventory
variants(first: 100) {
edges {
node {
id
title
inventoryQuantity
sku
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
// 分页遍历所有产品
let allProducts: any[] = [];
let hasNextPage = true;
let cursor: string | null = null;
while (hasNextPage) {
const result = await shopifyGraphQL(query, {
first: 50,
after: cursor,
});
const products = result.data.products.edges.map((e: any) => e.node);
allProducts.push(...products);
hasNextPage = result.data.products.pageInfo.hasNextPage;
cursor = result.data.products.pageInfo.endCursor;
}
// 按阈值筛选
const lowInventory = allProducts
.filter((p: any) => p.totalInventory < threshold)
.map((p: any) => ({
id: p.id,
title: p.title,
totalInventory: p.totalInventory,
variants: p.variants.edges.map((v: any) => ({
title: v.node.title,
sku: v.node.sku,
quantity: v.node.inventoryQuantity,
})),
}));
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
threshold,
count: lowInventory.length,
products: lowInventory,
}, null, 2),
},
],
};
}
);
工具 2:按业务规则创建产品
此工具强制执行您公司的产品创建标准:
server.tool(
"create_standard_product",
"Create a product following company standards (auto-generates SKU, applies naming conventions, sets required metafields)",
{
title: z.string().describe("Product title"),
category: z.enum(["apparel", "accessories", "home", "electronics"]).describe("Product category"),
base_price: z.number().positive().describe("Base price in dollars"),
description: z.string().describe("Product description"),
vendor: z.string().optional().describe("Vendor name (defaults to company name)"),
},
async ({ title, category, base_price, description, vendor }) => {
// 业务规则:标题必须为首字母大写格式
const formattedTitle = title
.split(" ")
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
// 业务规则:按类别自动生成 SKU 前缀
const skuPrefixes: Record<string, string> = {
apparel: "APR",
accessories: "ACC",
home: "HOM",
electronics: "ELC",
};
const skuPrefix = skuPrefixes[category];
const skuNumber = Date.now().toString(36).toUpperCase();
const sku = `${skuPrefix}-${skuNumber}`;
// 业务规则:最小描述长度
if (description.length < 50) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({
error: "Description must be at least 50 characters for SEO compliance",
currentLength: description.length,
}),
}],
isError: true,
};
}
// 业务规则:价格必须在标准梯度内
const validPrices = [19.99, 29.99, 39.99, 49.99, 59.99, 79.99, 99.99, 149.99, 199.99];
const nearestPrice = validPrices.reduce((prev, curr) =>
Math.abs(curr - base_price) < Math.abs(prev - base_price) ? curr : prev
);
const mutation = `
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
variants(first: 1) {
edges {
node {
id
sku
price
}
}
}
}
userErrors {
field
message
}
}
}
`;
const result = await shopifyGraphQL(mutation, {
input: {
title: formattedTitle,
descriptionHtml: `<p>${description}</p>`,
vendor: vendor || "Your Company Name",
productType: category,
tags: [category, "new-arrival"],
variants: [{
price: nearestPrice.toString(),
sku,
inventoryManagement: "SHOPIFY",
}],
metafields: [
{
namespace: "custom",
key: "category",
value: category,
type: "single_line_text_field",
},
{
namespace: "custom",
key: "created_via",
value: "mcp-server",
type: "single_line_text_field",
},
],
},
});
const { productCreate } = result.data;
if (productCreate.userErrors.length > 0) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({ errors: productCreate.userErrors }),
}],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: "Product created successfully",
product: productCreate.product,
appliedRules: {
titleFormatted: formattedTitle !== title,
skuGenerated: sku,
priceAdjusted: nearestPrice !== base_price
? `Adjusted from $${base_price} to nearest tier $${nearestPrice}`
: "No adjustment needed",
},
}, null, 2),
}],
};
}
);
添加资源
资源提供只读的上下文,帮助 AI 了解您 Shopify 商店的配置:
// 静态资源:商店配置
server.resource(
"store-config",
"shopify://store/config",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
domain: storeDomain,
apiVersion: "2025-01",
categories: ["apparel", "accessories", "home", "electronics"],
priceTiers: [19.99, 29.99, 39.99, 49.99, 59.99, 79.99, 99.99, 149.99, 199.99],
skuPrefixes: {
apparel: "APR",
accessories: "ACC",
home: "HOM",
electronics: "ELC",
},
businessRules: {
minDescriptionLength: 50,
requiredMetafields: ["custom.category"],
defaultVendor: "Your Company Name",
},
}, null, 2),
}],
})
);
// 动态资源:商店摘要(实时获取)
server.resource(
"store-summary",
"shopify://store/summary",
async (uri) => {
const result = await shopifyGraphQL(`{
shop {
name
email
currencyCode
primaryDomain { url }
}
products(first: 1) { edges { cursor } }
orders(first: 1) { edges { cursor } }
}`);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result.data, null, 2),
}],
};
}
);
添加提示
提示为常见工作流提供可重复使用的对话起始模板:
server.prompt(
"weekly_inventory_report",
"Generate a weekly inventory status report",
{
threshold: z.string().default("10").describe("Low stock threshold"),
},
async ({ threshold }) => ({
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Generate a weekly inventory report for our Shopify store.
1. Use the get_low_inventory_products tool with threshold ${threshold}
2. Organize results by category
3. For each low-stock product, include:
- Product title and SKU
- Current inventory level
- How far below threshold it is
4. Prioritize products with 0 inventory (out of stock) at the top
5. Include a summary with total products checked, products below threshold, and out-of-stock count
6. Format as a clean Markdown table`,
},
},
],
})
);
server.prompt(
"new_product_launch",
"Guided workflow for launching a new product",
{
product_name: z.string().describe("Name of the product to launch"),
},
async ({ product_name }) => ({
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Help me launch a new product: "${product_name}"
Walk me through this checklist:
1. Create the product using create_standard_product with proper category and pricing
2. Verify the product was created correctly
3. Check that SKU was generated and metafields were set
4. Suggest collections to add it to based on the category
5. Draft a product description if one wasn't provided (minimum 50 chars)
6. Recommend initial inventory levels based on similar products
Ask me for any required information before proceeding.`,
},
},
],
})
);
使用 MCP Inspector 进行测试
MCP Inspector 是一个可视化测试工具,让您可以在不连接 AI 主机的情况下与服务器交互。
安装与运行
npx @modelcontextprotocol/inspector tsx src/index.ts -- --token=shpat_xxx --domain=store.myshopify.com
这会打开一个网页界面,您可以:
- 查看可用工具:查看所有已注册工具及其结构描述
- 测试工具调用:填入参数并执行工具
- 检查响应:查看每个工具的 JSON 响应
- 检查资源:浏览和读取已注册的资源
- 测试提示:执行提示并查看生成的消息
在将 MCP 服务器连接到 Claude Code 之前,请务必先使用 Inspector 进行测试。这可以在不需要 AI 对话开销的情况下捕获结构描述错误、认证问题和运行时错误。
单元测试
为您的工具逻辑编写正式的测试:
import { describe, it, expect, vi } from 'vitest';
// 将业务逻辑与 MCP 层分开测试
describe('Product creation rules', () => {
it('formats titles to title case', () => {
const input = "organic COTTON t-shirt";
const result = input
.split(" ")
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
expect(result).toBe("Organic Cotton T-shirt");
});
it('generates correct SKU prefix for category', () => {
const prefixes: Record<string, string> = {
apparel: "APR",
accessories: "ACC",
home: "HOM",
electronics: "ELC",
};
expect(prefixes["apparel"]).toBe("APR");
expect(prefixes["electronics"]).toBe("ELC");
});
it('snaps price to nearest tier', () => {
const tiers = [19.99, 29.99, 39.99, 49.99];
const input = 35;
const nearest = tiers.reduce((prev, curr) =>
Math.abs(curr - input) < Math.abs(prev - input) ? curr : prev
);
expect(nearest).toBe(39.99);
});
it('rejects short descriptions', () => {
const description = "A nice shirt";
expect(description.length).toBeLessThan(50);
});
});
发布
为您的团队发布(npm)
npm run build
npm publish
团队成员安装:
claude mcp add your-custom-mcp -- npx -- -y shopify-custom-mcp --token=shpat_xxx --domain=store.myshopify.com
为您的团队发布(Git)
如果您不想发布到 npm,团队成员可以直接从您的仓库使用:
claude mcp add custom-shopify -- npx -- tsx /path/to/shopify-custom-mcp/src/index.ts -- --token=shpat_xxx --domain=store.myshopify.com
Docker 部署(SSE 传输)
对于共享团队服务器,您可以将传输修改为 SSE 并使用 Docker 部署:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
const server = new McpServer({ name: "shopify-custom-mcp", version: "1.0.0" });
// ... 注册工具、资源、提示(与上面相同)...
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
// 处理传入消息
});
app.listen(3100, () => {
console.log("MCP SSE server running on port 3100");
});
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/
EXPOSE 3100
CMD ["node", "dist/index-sse.js"]
如果您将 MCP 服务器部署为网络服务(SSE 或 HTTP),它可以直接访问您 Shopify 商店的 Admin API。请务必:
- 在认证后端运行(API 密钥、OAuth 或 VPN)
- 在生产环境中使用 HTTPS
- 记录所有工具调用以供审计
- 限制传入连接速率
- 不要在没有认证的情况下暴露到公共互联网
完整项目结构
shopify-custom-mcp/
src/
index.ts # 主入口点(服务器 + 工具 + 资源 + 提示)
__tests__/
tools.test.ts # 业务逻辑的单元测试
dist/ # 编译输出
package.json
tsconfig.json
README.md
对于较大的服务器,将工具拆分为单独的文件:
shopify-custom-mcp/
src/
index.ts # 服务器设置和传输
shopify-client.ts # 共用 Shopify API 辅助工具
tools/
inventory.ts # 库存相关工具
products.ts # 产品相关工具
orders.ts # 订单相关工具
resources/
store-config.ts # 静态商店配置
store-summary.ts # 动态商店数据
prompts/
reports.ts # 报告生成提示
workflows.ts # 工作流提示
__tests__/
inventory.test.ts
products.test.ts
总结
使用 TypeScript SDK 为 Shopify 构建自定义 MCP 服务器非常简单直接。关键步骤如下:
- 初始化项目,包含 MCP SDK 和 Shopify API 客户端
- 实现工具,封装您的业务逻辑和 Shopify API 调用
- 添加资源,用于静态配置和动态商店数据
- 添加提示,用于常见的工作流模板
- 测试,使用 MCP Inspector 和单元测试
- 部署,根据您团队的需求通过 npm、Git 或 Docker
这项投资很快就会回本 -- 一个具有 5-10 个精心设计工具的自定义 MCP 服务器,可以通过给予 AI 助手对您 Shopify 商店的直接、受控访问(同时内置您的业务规则),每周节省数小时的重复性工作。