Skip to main content

Shopify 向けカスタム MCP サーバーの構築

既存の MCP サーバーは一般的なユースケースをカバーしていますが、Shopify のビジネスにはそれぞれ固有のニーズがあります。在庫データと独自の需要予測モデルを組み合わせた MCP サーバーが必要かもしれません。あるいは、商品作成時に特定のビジネスルールを強制するサーバー。または Shopify を社内 ERP システムと統合するサーバーが必要かもしれません。

このガイドでは、公式 TypeScript SDK を使用してカスタム MCP サーバーをゼロから構築し、Shopify Admin API ツールを実装し、チーム向けに公開する方法を説明します。

カスタム構築と既存サーバーの使い分け

以下の場合にカスタム MCP サーバーを構築します:

  • 既存のサーバーがユースケースをカバーしていない:Shopify データと社内システムを組み合わせたツールが必要
  • ビジネスロジックの強制が必要:商品作成が特定の命名規則、価格設定ルール、承認ワークフローに従う必要がある
  • セキュリティ要件が求める場合:どの API 操作を公開するかを正確に制御し、使用状況を監査する必要がある
  • ドメイン固有の抽象化が必要:生の CRUD ではなく、ビジネスプロセスをエンコードした「create_seasonal_collection」のようなツールが欲しい
  • パフォーマンス最適化:汎用サーバーがサポートしない方法で Shopify API 呼び出しをバッチ処理またはキャッシュする必要がある

以下の場合は既存のサーバーを使用します:

  • 標準的な CRUD 操作で十分な場合
  • プロトタイピングや探索をしている場合
  • コミュニティサーバーが積極的にメンテナンスされ、ニーズをカバーしている場合
80/20 ルール

コミュニティの shopify-mcp サーバーを 80% のニーズ(標準操作)に使用し、ビジネス固有の 20% にはカスタムサーバーを構築しましょう。これらは同時に実行できます -- Claude Code は複数の MCP サーバーをサポートしています。

MCP SDK のセットアップ(TypeScript)

プロジェクトの初期化

新しい MCP サーバープロジェクトを作成
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
TypeScript を初期化
npx tsc --init

tsconfig.json を更新します:

tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}

package.json を更新します:

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"
}
}

基本的なサーバー構造

src/index.ts
#!/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 クエリとビジネスロジック(カスタム閾値)を組み合わせます:

src/tools/inventory.ts(index.ts に追加)
// 低在庫商品を検索するツールを登録
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:ビジネスルールに基づく商品作成

このツールは会社の商品作成基準を強制します:

src/tools/products.ts(index.ts に追加)
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 ストアの設定を理解するのに役立つ読み取り専用のコンテキストを提供します:

src/resources.ts(index.ts に追加)
// 静的リソース:ストア設定
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),
}],
};
}
);

プロンプトの追加

プロンプトは、一般的なワークフローのための再利用可能な会話スターターを提供します:

src/prompts.ts(index.ts に追加)
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 ホストに接続せずにサーバーと対話できるビジュアルテストツールです。

インストールと実行

MCP Inspector でテスト
npx @modelcontextprotocol/inspector tsx src/index.ts -- --token=shpat_xxx --domain=store.myshopify.com

これにより、以下のことができる Web インターフェースが開きます:

  1. 利用可能なツールの表示:登録されたすべてのツールとそのスキーマを確認
  2. ツール呼び出しのテスト:パラメータを入力してツールを実行
  3. レスポンスの検査:各ツールからの JSON レスポンスを確認
  4. リソースの確認:登録されたリソースを閲覧して読み取り
  5. プロンプトのテスト:プロンプトを実行して生成されたメッセージを確認
接続前にテストする

MCP サーバーを Claude Code に接続する前に、必ず Inspector でテストしてください。AI 会話のオーバーヘッドなしに、スキーマエラー、認証の問題、ランタイムエラーを検出できます。

ユニットテスト

ツールロジックの適切なテストを作成します:

src/__tests__/tools.test.ts
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 に公開したくない場合、チームメンバーはリポジトリから直接使用できます:

Git から直接
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 でデプロイできます:

src/index-sse.ts
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");
});
Dockerfile
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 サーバーを保護してください

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 サーバーを構築するのは簡単です。主要なステップは以下の通りです:

  1. MCP SDK と Shopify API クライアントでプロジェクトを初期化する
  2. ビジネスロジックと Shopify API 呼び出しをカプセル化したツールを実装する
  3. 静的設定と動的ストアデータのためのリソースを追加する
  4. 一般的なワークフローテンプレートのためのプロンプトを追加する
  5. MCP Inspector とユニットテストでテストする
  6. チームのニーズに応じて npm、Git、または Docker でデプロイする

この投資はすぐに回収できます -- 5-10 個の適切に設計されたツールを持つカスタム MCP サーバーは、ビジネスルールを組み込んだ状態で AI アシスタントに Shopify ストアへの直接的かつ制御されたアクセスを提供することで、毎週数時間の繰り返し作業を節約できます。