Shopify Python MCP Server
- src
- shopify_py_mcp
import asyncio
import os
import json
import shopify
import time
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio
# Shopify API設定
SHOP_URL = os.environ.get("SHOPIFY_SHOP_URL", "")
API_KEY = os.environ.get("SHOPIFY_API_KEY", "")
API_PASSWORD = os.environ.get("SHOPIFY_API_PASSWORD", "")
API_VERSION = os.environ.get("SHOPIFY_API_VERSION", "2025-01")
# Shopify APIの初期化
def initialize_shopify_api():
shop_url = f"https://{API_KEY}:{API_PASSWORD}@{SHOP_URL}/admin/api/{API_VERSION}"
shopify.ShopifyResource.set_site(shop_url)
server = Server("shopify-py-mcp")
def get_all_shopify_products(total_limit=None, per_page_limit=250):
"""
ShopifyAPIライブラリを使用して複数ページにわたる商品一覧を取得する関数
Parameters:
total_limit (int): 取得する総商品数(Noneの場合はすべての商品を取得)
per_page_limit (int): 1回のリクエストあたりの商品数(最大250)
Returns:
list: 商品のリスト
"""
# 1ページあたりの上限を250に制限
per_page_limit = min(per_page_limit, 250)
all_products = []
next_page_url = None
try:
while True:
# 既に十分な商品が取得されているか確認
if total_limit is not None and len(all_products) >= total_limit:
break
# 残りの取得数を計算
current_limit = per_page_limit
if total_limit is not None:
current_limit = min(per_page_limit, total_limit - len(all_products))
if current_limit <= 0:
break
# 商品一覧の取得
if next_page_url:
# next_page_urlからpage_infoを抽出
page_info = extract_page_info(next_page_url)
products = shopify.Product.find(
limit=current_limit, page_info=page_info
)
else:
products = shopify.Product.find(limit=current_limit)
# 結果が空の場合は終了
if not products:
break
# 取得した商品を追加
all_products.extend(products)
# レスポンスヘッダーからページネーション情報を取得
response_headers = shopify.ShopifyResource.connection.response.headers
link_header = response_headers.get("Link", "")
# 次のページURLを抽出
next_page_url = extract_next_page_url(link_header)
if not next_page_url:
break
# レート制限を避けるために少し待機
time.sleep(0.5)
except Exception as e:
print(f"エラーが発生しました: {e}")
# total_limitが指定されている場合、指定した数だけ返す
if total_limit is not None:
return all_products[:total_limit]
return all_products
def extract_next_page_url(link_header):
"""
Linkヘッダーから次のページのURLを抽出する
Parameters:
link_header (str): レスポンスのLinkヘッダー
Returns:
str: 次のページのURL(存在しない場合はNone)
"""
if not link_header:
return None
links = link_header.split(",")
for link in links:
parts = link.split(";")
if len(parts) != 2:
continue
url = parts[0].strip().strip("<>")
rel = parts[1].strip()
if 'rel="next"' in rel:
return url
return None
def extract_page_info(next_page_url):
"""
URLからpage_infoパラメータを抽出する
Parameters:
next_page_url (str): 次のページのURL
Returns:
str: page_infoパラメータ
"""
import urllib.parse
parsed_url = urllib.parse.urlparse(next_page_url)
query_params = urllib.parse.parse_qs(parsed_url.query)
if "page_info" in query_params:
return query_params["page_info"][0]
return None
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
利用可能なツールのリストを返します。
各ツールはJSON Schemaを使用して引数を指定します。
"""
return [
types.Tool(
name="list_products",
description="商品一覧を取得する",
inputSchema={
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "取得する商品数(最大250)",
"minimum": 1,
"maximum": 250,
"default": 50,
},
},
},
),
types.Tool(
name="get_product",
description="商品の詳細情報を取得する",
inputSchema={
"type": "object",
"properties": {
"product_id": {"type": "number", "description": "商品ID"}
},
"required": ["product_id"],
},
),
types.Tool(
name="create_product",
description="新しい商品を作成する",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string", "description": "商品名"},
"body_html": {
"type": "string",
"description": "商品の説明(HTML形式)",
},
"vendor": {"type": "string", "description": "ベンダー名"},
"product_type": {"type": "string", "description": "商品タイプ"},
"tags": {"type": "string", "description": "タグ(カンマ区切り)"},
"status": {
"type": "string",
"description": "ステータス",
"enum": ["active", "draft", "archived"],
"default": "active",
},
"variants": {
"type": "array",
"description": "バリエーション",
"items": {
"type": "object",
"properties": {
"price": {"type": "string", "description": "価格"},
"sku": {"type": "string", "description": "SKU"},
"inventory_quantity": {
"type": "number",
"description": "在庫数",
},
"option1": {
"type": "string",
"description": "オプション1の値",
},
"option2": {
"type": "string",
"description": "オプション2の値",
},
"option3": {
"type": "string",
"description": "オプション3の値",
},
},
"required": ["price"],
},
},
"options": {
"type": "array",
"description": "オプション",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "オプション名",
},
"position": {
"position": "number",
"description": "オプション順番",
},
"values": {
"type": "array",
"description": "オプション値",
"items": {"type": "string"},
},
},
"required": ["name", "position", "values"],
},
},
"images": {
"type": "array",
"description": "画像",
"items": {
"type": "object",
"properties": {
"src": {"type": "string", "description": "画像URL"},
"alt": {
"type": "string",
"description": "代替テキスト",
},
},
"required": ["src"],
},
},
},
"required": ["title"],
},
),
types.Tool(
name="update_product",
description="商品を更新する",
inputSchema={
"type": "object",
"properties": {
"product_id": {"type": "number", "description": "商品ID"},
"title": {"type": "string", "description": "商品名"},
"body_html": {
"type": "string",
"description": "商品の説明(HTML形式)",
},
"vendor": {"type": "string", "description": "ベンダー名"},
"product_type": {"type": "string", "description": "商品タイプ"},
"tags": {"type": "string", "description": "タグ(カンマ区切り)"},
"status": {
"type": "string",
"description": "ステータス",
"enum": ["active", "draft", "archived"],
},
"variants": {
"type": "array",
"description": "バリエーション",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "バリエーションID",
},
"price": {"type": "string", "description": "価格"},
"sku": {"type": "string", "description": "SKU"},
"inventory_quantity": {
"type": "number",
"description": "在庫数",
},
"option1": {
"type": "string",
"description": "オプション1の値",
},
"option2": {
"type": "string",
"description": "オプション2の値",
},
"option3": {
"type": "string",
"description": "オプション3の値",
},
},
},
},
"options": {
"type": "array",
"description": "オプション",
"items": {
"type": "object",
"properties": {
"id": {"type": "number", "description": "オプションID"},
"name": {
"type": "string",
"description": "オプション名",
},
"position": {
"position": "number",
"description": "オプション順番",
},
"values": {
"type": "array",
"description": "オプション値",
"items": {"type": "string"},
},
},
"required": ["name", "values"],
},
},
"images": {
"type": "array",
"description": "画像",
"items": {
"type": "object",
"properties": {
"id": {"type": "number", "description": "画像ID"},
"src": {"type": "string", "description": "画像URL"},
"alt": {
"type": "string",
"description": "代替テキスト",
},
},
"required": ["src"],
},
},
},
"required": ["product_id"],
},
),
types.Tool(
name="delete_product",
description="商品を削除する",
inputSchema={
"type": "object",
"properties": {
"product_id": {"type": "number", "description": "商品ID"}
},
"required": ["product_id"],
},
),
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
ツール実行リクエストを処理します。
"""
try:
initialize_shopify_api()
if name == "list_products":
return await handle_list_products(arguments or {})
elif name == "get_product":
return await handle_get_product(arguments or {})
elif name == "create_product":
return await handle_create_product(arguments or {})
elif name == "update_product":
return await handle_update_product(arguments or {})
elif name == "delete_product":
return await handle_delete_product(arguments or {})
else:
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
return [
types.TextContent(
type="text",
text=f"エラーが発生しました: {str(e)}",
)
]
async def handle_list_products(arguments: dict) -> list[types.TextContent]:
"""商品一覧を取得する"""
limit = int(arguments.get("limit", 50))
products = get_all_shopify_products(
total_limit=limit, per_page_limit=250 # 1回のリクエストで最大250件取得
)
result = []
for product in products:
result.append(
{
"id": product.id,
"title": product.title,
"vendor": product.vendor,
"product_type": product.product_type,
"created_at": product.created_at,
"updated_at": product.updated_at,
"status": product.status,
"variants_count": len(product.variants),
"images_count": len(product.images),
}
)
return [
types.TextContent(
type="text",
text=json.dumps(result, indent=2, ensure_ascii=False),
)
]
async def handle_get_product(arguments: dict) -> list[types.TextContent]:
"""商品の詳細情報を取得する"""
product_id = arguments.get("product_id")
if not product_id:
raise ValueError("product_id is required")
product = shopify.Product.find(product_id)
# 商品情報を整形
result = {
"id": product.id,
"title": product.title,
"body_html": product.body_html,
"vendor": product.vendor,
"product_type": product.product_type,
"created_at": product.created_at,
"updated_at": product.updated_at,
"status": product.status,
"tags": product.tags,
"variants": [],
"options": [],
"images": [],
}
# バリエーション情報
for variant in product.variants:
result["variants"].append(
{
"id": variant.id,
"title": variant.title,
"price": variant.price,
"sku": variant.sku,
"inventory_quantity": variant.inventory_quantity,
"option1": variant.option1,
"option2": variant.option2,
"option3": variant.option3,
}
)
# オプション情報
for option in product.options:
result["options"].append(
{"id": option.id, "name": option.name, "values": option.values}
)
# 画像情報
for image in product.images:
result["images"].append({"id": image.id, "src": image.src, "alt": image.alt})
return [
types.TextContent(
type="text",
text=json.dumps(result, indent=2, ensure_ascii=False),
)
]
async def handle_create_product(arguments: dict) -> list[types.TextContent]:
"""新しい商品を作成する"""
# 必須パラメータのチェック
title = arguments.get("title")
if not title:
raise ValueError("title is required")
# 商品オブジェクトの作成
product = shopify.Product()
product.title = title
# オプションパラメータの設定
if "body_html" in arguments:
product.body_html = arguments["body_html"]
if "vendor" in arguments:
product.vendor = arguments["vendor"]
if "product_type" in arguments:
product.product_type = arguments["product_type"]
if "tags" in arguments:
product.tags = arguments["tags"]
if "status" in arguments:
product.status = arguments["status"]
# オプションの設定
if "options" in arguments and arguments["options"]:
options = []
for option_data in arguments["options"]:
option = shopify.Option()
option.name = option_data["name"]
option.position = option_data["position"]
option.values = option_data["values"]
options.append(option)
product.options = options
# バリエーションの設定
if "variants" in arguments and arguments["variants"]:
variants = []
for variant_data in arguments["variants"]:
variant = shopify.Variant()
if "price" in variant_data:
variant.price = variant_data["price"]
if "sku" in variant_data:
variant.sku = variant_data["sku"]
if "inventory_quantity" in variant_data:
variant.inventory_quantity = variant_data["inventory_quantity"]
if "option1" in variant_data:
variant.option1 = variant_data["option1"]
if "option2" in variant_data:
variant.option2 = variant_data["option2"]
if "option3" in variant_data:
variant.option3 = variant_data["option3"]
variants.append(variant)
product.variants = variants
# 画像の設定
if "images" in arguments and arguments["images"]:
for image_data in arguments["images"]:
image = shopify.Image()
image.src = image_data["src"]
if "alt" in image_data:
image.alt = image_data["alt"]
product.images.append(image)
# 商品の保存
product.save()
return [
types.TextContent(
type="text",
text=json.dumps(
{
"success": True,
"product_id": product.id,
"message": f"商品「{product.title}」が作成されました",
},
indent=2,
ensure_ascii=False,
),
)
]
async def handle_update_product(arguments: dict) -> list[types.TextContent]:
"""商品を更新する"""
# 必須パラメータのチェック
product_id = arguments.get("product_id")
if not product_id:
raise ValueError("product_id is required")
# 商品の取得
product = shopify.Product.find(product_id)
# 商品情報の更新
if "title" in arguments:
product.title = arguments["title"]
if "body_html" in arguments:
product.body_html = arguments["body_html"]
if "vendor" in arguments:
product.vendor = arguments["vendor"]
if "product_type" in arguments:
product.product_type = arguments["product_type"]
if "tags" in arguments:
product.tags = arguments["tags"]
if "status" in arguments:
product.status = arguments["status"]
# バリエーションの更新
if "variants" in arguments and arguments["variants"]:
for variant_data in arguments["variants"]:
# バリエーションIDがある場合は既存のバリエーションを更新
if "id" in variant_data:
for variant in product.variants:
if variant.id == variant_data["id"]:
if "price" in variant_data:
variant.price = variant_data["price"]
if "sku" in variant_data:
variant.sku = variant_data["sku"]
if "inventory_quantity" in variant_data:
variant.inventory_quantity = variant_data[
"inventory_quantity"
]
if "option1" in variant_data:
variant.option1 = variant_data["option1"]
if "option2" in variant_data:
variant.option2 = variant_data["option2"]
if "option3" in variant_data:
variant.option3 = variant_data["option3"]
# バリエーションIDがない場合は新しいバリエーションを追加
else:
variant = shopify.Variant()
variant.product_id = product.id
if "price" in variant_data:
variant.price = variant_data["price"]
if "sku" in variant_data:
variant.sku = variant_data["sku"]
if "inventory_quantity" in variant_data:
variant.inventory_quantity = variant_data["inventory_quantity"]
if "option1" in variant_data:
variant.option1 = variant_data["option1"]
if "option2" in variant_data:
variant.option2 = variant_data["option2"]
if "option3" in variant_data:
variant.option3 = variant_data["option3"]
product.variants.append(variant)
# オプションの更新
if "options" in arguments and arguments["options"]:
for option_data in arguments["options"]:
# オプションIDがある場合は既存のオプションを更新
if "id" in option_data:
for option in product.options:
if option.id == option_data["id"]:
option.name = option_data["name"]
option.values = option_data["values"]
# オプションIDがない場合は新しいオプションを追加
else:
option = shopify.Option()
option.product_id = product.id
option.name = option_data["name"]
option.values = option_data["values"]
product.options.append(option)
# 画像の更新
if "images" in arguments and arguments["images"]:
for image_data in arguments["images"]:
# 画像IDがある場合は既存の画像を更新
if "id" in image_data:
for image in product.images:
if image.id == image_data["id"]:
image.src = image_data["src"]
if "alt" in image_data:
image.alt = image_data["alt"]
# 画像IDがない場合は新しい画像を追加
else:
image = shopify.Image()
image.product_id = product.id
image.src = image_data["src"]
if "alt" in image_data:
image.alt = image_data["alt"]
product.images.append(image)
# 商品の保存
product.save()
return [
types.TextContent(
type="text",
text=json.dumps(
{
"success": True,
"product_id": product.id,
"message": f"商品「{product.title}」が更新されました",
},
indent=2,
ensure_ascii=False,
),
)
]
async def handle_delete_product(arguments: dict) -> list[types.TextContent]:
"""商品を削除する"""
# 必須パラメータのチェック
product_id = arguments.get("product_id")
if not product_id:
raise ValueError("product_id is required")
# 商品の取得
product = shopify.Product.find(product_id)
# 商品名を保存
product_title = product.title
# 商品の削除
product.destroy()
return [
types.TextContent(
type="text",
text=json.dumps(
{
"success": True,
"message": f"商品「{product_title}」が削除されました",
},
indent=2,
ensure_ascii=False,
),
)
]
async def main():
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="shopify-py-mcp",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)