@akson/mcp-shopify
Version:
A comprehensive Model Context Protocol (MCP) server for Shopify Admin API integration
1,309 lines (1,211 loc) • 36.2 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Shopify Admin API configuration
const accessTokenArg = process.argv.find((arg) => arg.startsWith('--accessToken='));
const domainArg = process.argv.find((arg) => arg.startsWith('--domain='));
const SHOPIFY_ACCESS_TOKEN =
process.env.SHOPIFY_ACCESS_TOKEN ||
(accessTokenArg ? accessTokenArg.split('=')[1] : undefined);
const SHOPIFY_DOMAIN =
process.env.SHOPIFY_DOMAIN ||
(domainArg ? domainArg.split('=')[1] : undefined);
if (!SHOPIFY_ACCESS_TOKEN || !SHOPIFY_DOMAIN) {
console.error('Error: SHOPIFY_ACCESS_TOKEN and SHOPIFY_DOMAIN are required');
console.error('Usage: mcp-shopify --accessToken=YOUR_TOKEN --domain=YOUR_STORE.myshopify.com');
console.error('Or set environment variables: SHOPIFY_ACCESS_TOKEN and SHOPIFY_DOMAIN');
process.exit(1);
}
const SHOPIFY_API_URL = `https://${SHOPIFY_DOMAIN}/admin/api/2023-10/`;
class ShopifyMCPServer {
constructor() {
this.server = new Server(
{
name: 'mcp-shopify',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// Product Management
{
name: 'list_products',
description: 'List products from your Shopify store',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of products (default: 10)',
default: 10,
},
product_type: { type: 'string', description: 'Filter by product type' },
vendor: { type: 'string', description: 'Filter by vendor' },
status: {
type: 'string',
description: 'Filter by status (active, archived, draft)',
enum: ['active', 'archived', 'draft'],
},
collection_id: {
type: 'string',
description: 'Filter by collection ID',
},
},
},
},
{
name: 'get_product',
description: 'Get detailed information about a specific product',
inputSchema: {
type: 'object',
properties: {
product_id: {
type: 'string',
description: 'The product ID',
required: true,
},
},
required: ['product_id'],
},
},
{
name: 'search_products',
description: 'Search products by title or SKU',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query', required: true },
limit: {
type: 'number',
description: 'Number of results (default: 10)',
default: 10,
},
},
required: ['query'],
},
},
{
name: 'get_product_variants',
description: 'Get all variants for a specific product',
inputSchema: {
type: 'object',
properties: {
product_id: {
type: 'string',
description: 'The product ID',
required: true,
},
},
required: ['product_id'],
},
},
{
name: 'get_inventory_levels',
description: 'Get inventory levels for products',
inputSchema: {
type: 'object',
properties: {
location_id: { type: 'string', description: 'Location ID (optional)' },
limit: {
type: 'number',
description: 'Number of items (default: 10)',
default: 10,
},
},
},
},
// Order Management
{
name: 'list_orders',
description: 'List orders from your Shopify store',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of orders (default: 10)',
default: 10,
},
status: {
type: 'string',
description: 'Filter by status',
enum: ['open', 'closed', 'cancelled', 'any'],
},
financial_status: {
type: 'string',
description: 'Filter by payment status',
enum: [
'authorized',
'pending',
'paid',
'partially_paid',
'refunded',
'voided',
'partially_refunded',
'unpaid',
],
},
fulfillment_status: {
type: 'string',
description: 'Filter by fulfillment status',
enum: [
'shipped',
'partial',
'unshipped',
'unfulfilled',
'fulfilled',
],
},
created_at_min: {
type: 'string',
description: 'Orders created after this date (ISO 8601)',
},
created_at_max: {
type: 'string',
description: 'Orders created before this date (ISO 8601)',
},
},
},
},
{
name: 'get_order',
description: 'Get detailed information about a specific order',
inputSchema: {
type: 'object',
properties: {
order_id: {
type: 'string',
description: 'The order ID',
required: true,
},
},
required: ['order_id'],
},
},
{
name: 'get_order_transactions',
description: 'Get transactions for a specific order',
inputSchema: {
type: 'object',
properties: {
order_id: {
type: 'string',
description: 'The order ID',
required: true,
},
},
required: ['order_id'],
},
},
// Customer Management
{
name: 'list_customers',
description: 'List customers from your Shopify store',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of customers (default: 10)',
default: 10,
},
created_at_min: {
type: 'string',
description: 'Customers created after this date',
},
created_at_max: {
type: 'string',
description: 'Customers created before this date',
},
},
},
},
{
name: 'get_customer',
description: 'Get detailed information about a specific customer',
inputSchema: {
type: 'object',
properties: {
customer_id: {
type: 'string',
description: 'The customer ID',
required: true,
},
},
required: ['customer_id'],
},
},
{
name: 'search_customers',
description: 'Search customers by name, email, or phone',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query', required: true },
limit: {
type: 'number',
description: 'Number of results (default: 10)',
default: 10,
},
},
required: ['query'],
},
},
{
name: 'get_customer_orders',
description: 'Get all orders for a specific customer',
inputSchema: {
type: 'object',
properties: {
customer_id: {
type: 'string',
description: 'The customer ID',
required: true,
},
limit: {
type: 'number',
description: 'Number of orders (default: 10)',
default: 10,
},
},
required: ['customer_id'],
},
},
// Analytics & Reports
{
name: 'get_shop_info',
description: 'Get general information about the shop',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'get_sales_summary',
description: 'Get sales summary and statistics',
inputSchema: {
type: 'object',
properties: {
date_from: { type: 'string', description: 'Start date (ISO 8601)' },
date_to: { type: 'string', description: 'End date (ISO 8601)' },
},
},
},
{
name: 'get_best_sellers',
description: 'Analyze best-selling products',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of top products (default: 10)',
default: 10,
},
days: {
type: 'number',
description: 'Number of days to analyze (default: 30)',
default: 30,
},
},
},
},
{
name: 'get_locations',
description: 'Get all shop locations',
inputSchema: { type: 'object', properties: {} },
},
// Collections
{
name: 'list_collections',
description: 'List all collections (smart and custom)',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of collections (default: 10)',
default: 10,
},
collection_type: {
type: 'string',
description: 'Type of collection',
enum: ['smart', 'custom'],
},
},
},
},
{
name: 'get_collection_products',
description: 'Get all products in a specific collection',
inputSchema: {
type: 'object',
properties: {
collection_id: {
type: 'string',
description: 'The collection ID',
required: true,
},
limit: {
type: 'number',
description: 'Number of products (default: 10)',
default: 10,
},
},
required: ['collection_id'],
},
},
// Discounts & Price Rules
{
name: 'list_discounts',
description: 'List all discount codes',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of discounts (default: 10)',
default: 10,
},
},
},
},
{
name: 'get_discount',
description: 'Get details about a specific discount',
inputSchema: {
type: 'object',
properties: {
discount_id: {
type: 'string',
description: 'The discount ID',
required: true,
},
},
required: ['discount_id'],
},
},
// Metafields
{
name: 'get_product_metafields',
description: 'Get metafields for a product',
inputSchema: {
type: 'object',
properties: {
product_id: {
type: 'string',
description: 'The product ID',
required: true,
},
},
required: ['product_id'],
},
},
// Fulfillment
{
name: 'list_fulfillment_services',
description: 'List all fulfillment services',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'get_shipping_zones',
description: 'Get all shipping zones and rates',
inputSchema: { type: 'object', properties: {} },
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
// Product Management
case 'list_products':
return await this.listProducts(args);
case 'get_product':
return await this.getProduct(args);
case 'search_products':
return await this.searchProducts(args);
case 'get_product_variants':
return await this.getProductVariants(args);
case 'get_inventory_levels':
return await this.getInventoryLevels(args);
// Order Management
case 'list_orders':
return await this.listOrders(args);
case 'get_order':
return await this.getOrder(args);
case 'get_order_transactions':
return await this.getOrderTransactions(args);
// Customer Management
case 'list_customers':
return await this.listCustomers(args);
case 'get_customer':
return await this.getCustomer(args);
case 'search_customers':
return await this.searchCustomers(args);
case 'get_customer_orders':
return await this.getCustomerOrders(args);
// Analytics & Reports
case 'get_shop_info':
return await this.getShopInfo();
case 'get_sales_summary':
return await this.getSalesSummary(args);
case 'get_best_sellers':
return await this.getBestSellers(args);
case 'get_locations':
return await this.getLocations();
// Collections
case 'list_collections':
return await this.listCollections(args);
case 'get_collection_products':
return await this.getCollectionProducts(args);
// Discounts
case 'list_discounts':
return await this.listDiscounts(args);
case 'get_discount':
return await this.getDiscount(args);
// Metafields
case 'get_product_metafields':
return await this.getProductMetafields(args);
// Fulfillment
case 'list_fulfillment_services':
return await this.listFulfillmentServices();
case 'get_shipping_zones':
return await this.getShippingZones();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
};
}
});
}
async shopifyRequest(endpoint, options = {}) {
const fetch = (await import('node-fetch')).default;
const url = `${SHOPIFY_API_URL}${endpoint}`;
const response = await fetch(url, {
headers: {
'X-Shopify-Access-Token': SHOPIFY_ACCESS_TOKEN,
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Shopify API error: ${response.status} ${response.statusText} - ${errorText}`
);
}
return await response.json();
}
// Product Management Tools
async listProducts(args = {}) {
const params = new URLSearchParams();
if (args.limit) params.append('limit', args.limit);
if (args.product_type) params.append('product_type', args.product_type);
if (args.vendor) params.append('vendor', args.vendor);
if (args.status) params.append('status', args.status);
if (args.collection_id) params.append('collection_id', args.collection_id);
const data = await this.shopifyRequest(`products.json?${params}`);
return {
content: [
{
type: 'text',
text:
`Found ${data.products.length} products:\n\n` +
data.products
.map(
(product) =>
`• ${product.title} (ID: ${product.id})\n` +
` Type: ${product.product_type || 'N/A'}\n` +
` Vendor: ${product.vendor}\n` +
` Status: ${product.status}\n` +
` Variants: ${product.variants.length}\n` +
` Price: ${product.variants[0]?.price || 'N/A'} ${product.variants[0]?.currency_code || ''}`
)
.join('\n\n'),
},
],
};
}
async getProduct(args) {
const data = await this.shopifyRequest(`products/${args.product_id}.json`);
const product = data.product;
return {
content: [
{
type: 'text',
text:
`Product: ${product.title}\n` +
`ID: ${product.id}\n` +
`Status: ${product.status}\n` +
`Type: ${product.product_type || 'N/A'}\n` +
`Vendor: ${product.vendor}\n` +
`Created: ${new Date(product.created_at).toLocaleDateString()}\n` +
`Tags: ${product.tags}\n` +
`\nVariants (${product.variants.length}):\n` +
product.variants
.map(
(v) =>
` - ${v.title || 'Default'}: ${v.price} ${v.currency_code || ''} (SKU: ${v.sku || 'N/A'}, Stock: ${v.inventory_quantity})`
)
.join('\n') +
`\n\nDescription: ${product.body_html?.replace(/<[^>]*>/g, '').slice(0, 200)}...`,
},
],
};
}
async searchProducts(args) {
const data = await this.shopifyRequest(
`products.json?title=${encodeURIComponent(args.query)}&limit=${args.limit || 10}`
);
if (data.products.length === 0) {
// Try searching in product listings
const searchData = await this.shopifyRequest(
`products/search.json?query=${encodeURIComponent(args.query)}&limit=${args.limit || 10}`
).catch(() => null);
if (searchData?.products) {
data.products = searchData.products;
}
}
return {
content: [
{
type: 'text',
text:
`Found ${data.products.length} products matching "${args.query}":\n\n` +
data.products
.map(
(product) =>
`• ${product.title} (ID: ${product.id})\n` +
` Type: ${product.product_type || 'N/A'}\n` +
` Price: ${product.variants[0]?.price || 'N/A'}`
)
.join('\n\n'),
},
],
};
}
async getProductVariants(args) {
const data = await this.shopifyRequest(`products/${args.product_id}.json`);
const product = data.product;
return {
content: [
{
type: 'text',
text:
`Variants for "${product.title}" (${product.variants.length} total):\n\n` +
product.variants
.map(
(v, i) =>
`${i + 1}. ${v.title || 'Default Variant'}\n` +
` ID: ${v.id}\n` +
` SKU: ${v.sku || 'N/A'}\n` +
` Price: ${v.price} ${v.currency_code || ''}\n` +
` Weight: ${v.weight} ${v.weight_unit}\n` +
` Inventory: ${v.inventory_quantity} units\n` +
` Barcode: ${v.barcode || 'N/A'}`
)
.join('\n\n'),
},
],
};
}
async getInventoryLevels(args = {}) {
const data = await this.shopifyRequest(`inventory_levels.json?limit=${args.limit || 10}`);
return {
content: [
{
type: 'text',
text:
`Inventory Levels:\n\n` +
data.inventory_levels
.map(
(level) =>
`• Location ${level.location_id}\n` +
` Inventory Item: ${level.inventory_item_id}\n` +
` Available: ${level.available}\n` +
` Updated: ${new Date(level.updated_at).toLocaleDateString()}`
)
.join('\n\n'),
},
],
};
}
// Order Management Tools
async listOrders(args = {}) {
const params = new URLSearchParams();
if (args.limit) params.append('limit', args.limit);
if (args.status) params.append('status', args.status);
if (args.financial_status) params.append('financial_status', args.financial_status);
if (args.fulfillment_status) params.append('fulfillment_status', args.fulfillment_status);
if (args.created_at_min) params.append('created_at_min', args.created_at_min);
if (args.created_at_max) params.append('created_at_max', args.created_at_max);
const data = await this.shopifyRequest(`orders.json?${params}`);
return {
content: [
{
type: 'text',
text:
`Found ${data.orders.length} orders:\n\n` +
data.orders
.map(
(order) =>
`• Order #${order.order_number} (ID: ${order.id})\n` +
` Customer: ${order.customer?.first_name || 'Guest'} ${order.customer?.last_name || ''}\n` +
` Total: ${order.total_price} ${order.currency}\n` +
` Items: ${order.line_items.length}\n` +
` Payment: ${order.financial_status}\n` +
` Fulfillment: ${order.fulfillment_status || 'unfulfilled'}\n` +
` Date: ${new Date(order.created_at).toLocaleDateString()}`
)
.join('\n\n'),
},
],
};
}
async getOrder(args) {
const data = await this.shopifyRequest(`orders/${args.order_id}.json`);
const order = data.order;
return {
content: [
{
type: 'text',
text:
`Order #${order.order_number}\n` +
`ID: ${order.id}\n` +
`Status: ${order.financial_status} / ${order.fulfillment_status || 'unfulfilled'}\n` +
`Customer: ${order.customer?.first_name || 'Guest'} ${order.customer?.last_name || ''} (${order.email})\n` +
`Total: ${order.total_price} ${order.currency}\n` +
`Subtotal: ${order.subtotal_price}\n` +
`Tax: ${order.total_tax}\n` +
`Shipping: ${order.shipping_lines.map((s) => s.price).reduce((a, b) => a + parseFloat(b), 0)}\n` +
`Created: ${new Date(order.created_at).toLocaleDateString()}\n` +
`\nItems (${order.line_items.length}):\n` +
order.line_items
.map(
(item) => ` - ${item.title} x${item.quantity} @ ${item.price} each`
)
.join('\n') +
`\n\nShipping Address:\n` +
(order.shipping_address
? ` ${order.shipping_address.name}\n` +
` ${order.shipping_address.address1}\n` +
` ${order.shipping_address.city}, ${order.shipping_address.province_code} ${order.shipping_address.zip}\n` +
` ${order.shipping_address.country}`
: 'N/A'),
},
],
};
}
async getOrderTransactions(args) {
const data = await this.shopifyRequest(`orders/${args.order_id}/transactions.json`);
return {
content: [
{
type: 'text',
text:
`Transactions for Order ${args.order_id}:\n\n` +
data.transactions
.map(
(t) =>
`• ${t.kind} - ${t.status}\n` +
` Amount: ${t.amount} ${t.currency}\n` +
` Gateway: ${t.gateway}\n` +
` Date: ${new Date(t.created_at).toLocaleDateString()}\n` +
` ID: ${t.id}`
)
.join('\n\n'),
},
],
};
}
// Customer Management Tools
async listCustomers(args = {}) {
const params = new URLSearchParams();
if (args.limit) params.append('limit', args.limit);
if (args.created_at_min) params.append('created_at_min', args.created_at_min);
if (args.created_at_max) params.append('created_at_max', args.created_at_max);
const data = await this.shopifyRequest(`customers.json?${params}`);
return {
content: [
{
type: 'text',
text:
`Found ${data.customers.length} customers:\n\n` +
data.customers
.map(
(customer) =>
`• ${customer.first_name} ${customer.last_name} (ID: ${customer.id})\n` +
` Email: ${customer.email}\n` +
` Phone: ${customer.phone || 'N/A'}\n` +
` Orders: ${customer.orders_count}\n` +
` Total Spent: ${customer.total_spent} ${customer.currency || ''}\n` +
` Created: ${new Date(customer.created_at).toLocaleDateString()}`
)
.join('\n\n'),
},
],
};
}
async getCustomer(args) {
const data = await this.shopifyRequest(`customers/${args.customer_id}.json`);
const customer = data.customer;
return {
content: [
{
type: 'text',
text:
`Customer: ${customer.first_name} ${customer.last_name}\n` +
`ID: ${customer.id}\n` +
`Email: ${customer.email}\n` +
`Phone: ${customer.phone || 'N/A'}\n` +
`State: ${customer.state}\n` +
`Orders: ${customer.orders_count}\n` +
`Total Spent: ${customer.total_spent} ${customer.currency || ''}\n` +
`Average Order: ${customer.orders_count > 0 ? (parseFloat(customer.total_spent) / customer.orders_count).toFixed(2) : '0'}\n` +
`Tags: ${customer.tags}\n` +
`Marketing: ${customer.accepts_marketing ? 'Yes' : 'No'}\n` +
`Created: ${new Date(customer.created_at).toLocaleDateString()}\n` +
`Last Order: ${customer.last_order_id ? new Date(customer.updated_at).toLocaleDateString() : 'N/A'}`,
},
],
};
}
async searchCustomers(args) {
const data = await this.shopifyRequest(
`customers/search.json?query=${encodeURIComponent(args.query)}&limit=${args.limit || 10}`
);
return {
content: [
{
type: 'text',
text:
`Found ${data.customers.length} customers matching "${args.query}":\n\n` +
data.customers
.map(
(customer) =>
`• ${customer.first_name} ${customer.last_name}\n` +
` Email: ${customer.email}\n` +
` Orders: ${customer.orders_count}\n` +
` Total Spent: ${customer.total_spent}`
)
.join('\n\n'),
},
],
};
}
async getCustomerOrders(args) {
const data = await this.shopifyRequest(
`customers/${args.customer_id}/orders.json?limit=${args.limit || 10}`
);
return {
content: [
{
type: 'text',
text:
`Orders for Customer ${args.customer_id} (${data.orders.length} orders):\n\n` +
data.orders
.map(
(order) =>
`• Order #${order.order_number}\n` +
` Total: ${order.total_price} ${order.currency}\n` +
` Status: ${order.financial_status} / ${order.fulfillment_status || 'unfulfilled'}\n` +
` Date: ${new Date(order.created_at).toLocaleDateString()}`
)
.join('\n\n'),
},
],
};
}
// Analytics & Reports Tools
async getShopInfo() {
const data = await this.shopifyRequest('shop.json');
const shop = data.shop;
return {
content: [
{
type: 'text',
text:
`Shop Information:\n\n` +
`Name: ${shop.name}\n` +
`Domain: ${shop.domain}\n` +
`Email: ${shop.email}\n` +
`Currency: ${shop.currency}\n` +
`Money Format: ${shop.money_format}\n` +
`Country: ${shop.country_name}\n` +
`Timezone: ${shop.iana_timezone}\n` +
`Plan: ${shop.plan_name}\n` +
`Created: ${new Date(shop.created_at).toLocaleDateString()}\n` +
`Password Enabled: ${shop.password_enabled ? 'Yes' : 'No'}\n` +
`Tax Shipping: ${shop.tax_shipping ? 'Yes' : 'No'}\n` +
`Taxes Included: ${shop.taxes_included ? 'Yes' : 'No'}`,
},
],
};
}
async getSalesSummary(args = {}) {
const params = new URLSearchParams();
params.append('status', 'any');
params.append('limit', '250');
if (args.date_from) params.append('created_at_min', args.date_from);
if (args.date_to) params.append('created_at_max', args.date_to);
const data = await this.shopifyRequest(`orders.json?${params}`);
// Calculate summary statistics
let totalRevenue = 0;
let totalOrders = 0;
let totalItems = 0;
const dailySales = {};
const productSales = {};
data.orders.forEach((order) => {
if (order.financial_status === 'paid') {
totalRevenue += parseFloat(order.total_price);
totalOrders++;
const date = new Date(order.created_at).toLocaleDateString();
dailySales[date] = (dailySales[date] || 0) + parseFloat(order.total_price);
order.line_items.forEach((item) => {
totalItems += item.quantity;
if (!productSales[item.product_id]) {
productSales[item.product_id] = {
title: item.title,
quantity: 0,
revenue: 0,
};
}
productSales[item.product_id].quantity += item.quantity;
productSales[item.product_id].revenue += parseFloat(item.price) * item.quantity;
});
}
});
const avgOrderValue = totalOrders > 0 ? (totalRevenue / totalOrders).toFixed(2) : '0';
const topProducts = Object.entries(productSales)
.sort((a, b) => b[1].revenue - a[1].revenue)
.slice(0, 5);
return {
content: [
{
type: 'text',
text:
`Sales Summary:\n\n` +
`Period: ${args.date_from || 'All time'} to ${args.date_to || 'Present'}\n` +
`Total Revenue: ${totalRevenue.toFixed(2)} ${shop.currency || ''}\n` +
`Total Orders: ${totalOrders}\n` +
`Total Items Sold: ${totalItems}\n` +
`Average Order Value: ${avgOrderValue} ${shop.currency || ''}\n` +
`\nTop 5 Products by Revenue:\n` +
topProducts
.map(
([, data], i) =>
`${i + 1}. ${data.title}\n Revenue: ${data.revenue.toFixed(2)} (${data.quantity} units)`
)
.join('\n\n'),
},
],
};
}
async getBestSellers(args = {}) {
const days = args.days || 30;
const limit = args.limit || 10;
const dateFrom = new Date();
dateFrom.setDate(dateFrom.getDate() - days);
const params = new URLSearchParams();
params.append('status', 'any');
params.append('limit', '250');
params.append('created_at_min', dateFrom.toISOString());
const data = await this.shopifyRequest(`orders.json?${params}`);
// Aggregate product sales
const productSales = {};
let totalItemsSold = 0;
data.orders.forEach((order) => {
if (order.financial_status === 'paid' || order.financial_status === 'partially_paid') {
order.line_items.forEach((item) => {
const key = `${item.product_id}_${item.variant_id}`;
if (!productSales[key]) {
productSales[key] = {
product_id: item.product_id,
variant_id: item.variant_id,
title: item.title,
variant_title: item.variant_title,
quantity: 0,
revenue: 0,
sku: item.sku,
};
}
productSales[key].quantity += item.quantity;
productSales[key].revenue += parseFloat(item.price) * item.quantity;
totalItemsSold += item.quantity;
});
}
});
const sortedProducts = Object.values(productSales)
.sort((a, b) => b.quantity - a.quantity)
.slice(0, limit);
return {
content: [
{
type: 'text',
text:
`Best Sellers (Last ${days} days):\n\n` +
sortedProducts
.map(
(product, i) =>
`${i + 1}. ${product.title}${product.variant_title ? ` - ${product.variant_title}` : ''}\n` +
` Quantity: ${product.quantity} units (${((product.quantity / totalItemsSold) * 100).toFixed(1)}% of sales)\n` +
` Revenue: ${product.revenue.toFixed(2)}\n` +
` SKU: ${product.sku || 'N/A'}`
)
.join('\n\n') +
`\n\nTotal items sold in period: ${totalItemsSold}`,
},
],
};
}
async getLocations() {
const data = await this.shopifyRequest('locations.json');
return {
content: [
{
type: 'text',
text:
`Shop Locations (${data.locations.length}):\n\n` +
data.locations
.map(
(location) =>
`• ${location.name} (ID: ${location.id})\n` +
` Active: ${location.active ? 'Yes' : 'No'}\n` +
` Address: ${location.address1}, ${location.city}\n` +
` ${location.province}, ${location.country} ${location.zip}\n` +
` Phone: ${location.phone || 'N/A'}`
)
.join('\n\n'),
},
],
};
}
// Collections Tools
async listCollections(args = {}) {
let collections = [];
// Get custom collections
const customData = await this.shopifyRequest(
`custom_collections.json?limit=${args.limit || 10}`
);
collections = collections.concat(
customData.custom_collections.map((c) => ({ ...c, type: 'custom' }))
);
// Get smart collections
const smartData = await this.shopifyRequest(
`smart_collections.json?limit=${args.limit || 10}`
);
collections = collections.concat(
smartData.smart_collections.map((c) => ({ ...c, type: 'smart' }))
);
// Filter by type if specified
if (args.collection_type) {
collections = collections.filter((c) => c.type === args.collection_type);
}
return {
content: [
{
type: 'text',
text:
`Collections (${collections.length}):\n\n` +
collections
.map(
(collection) =>
`• ${collection.title} (ID: ${collection.id})\n` +
` Type: ${collection.type}\n` +
` Handle: ${collection.handle}\n` +
` Published: ${collection.published_at ? 'Yes' : 'No'}\n` +
` Sort Order: ${collection.sort_order || 'manual'}`
)
.join('\n\n'),
},
],
};
}
async getCollectionProducts(args) {
const data = await this.shopifyRequest(
`collections/${args.collection_id}/products.json?limit=${args.limit || 10}`
);
return {
content: [
{
type: 'text',
text:
`Products in Collection ${args.collection_id} (${data.products.length}):\n\n` +
data.products
.map(
(product) =>
`• ${product.title}\n` +
` Type: ${product.product_type || 'N/A'}\n` +
` Vendor: ${product.vendor}\n` +
` Price: ${product.variants[0]?.price || 'N/A'}`
)
.join('\n\n'),
},
],
};
}
// Discount Tools
async listDiscounts(args = {}) {
const data = await this.shopifyRequest(`price_rules.json?limit=${args.limit || 10}`);
return {
content: [
{
type: 'text',
text:
`Discounts/Price Rules (${data.price_rules.length}):\n\n` +
data.price_rules
.map(
(rule) =>
`• ${rule.title} (ID: ${rule.id})\n` +
` Type: ${rule.value_type} ${rule.value}\n` +
` Target: ${rule.target_type} (${rule.target_selection})\n` +
` Usage Limit: ${rule.usage_limit || 'Unlimited'}\n` +
` Active: ${new Date() >= new Date(rule.starts_at) && (!rule.ends_at || new Date() <= new Date(rule.ends_at)) ? 'Yes' : 'No'}`
)
.join('\n\n'),
},
],
};
}
async getDiscount(args) {
const data = await this.shopifyRequest(`price_rules/${args.discount_id}.json`);
const rule = data.price_rule;
// Get associated discount codes
const codesData = await this.shopifyRequest(
`price_rules/${args.discount_id}/discount_codes.json`
);
return {
content: [
{
type: 'text',
text:
`Discount: ${rule.title}\n` +
`ID: ${rule.id}\n` +
`Value: ${rule.value_type === 'percentage' ? `${rule.value}%` : `${rule.value} ${rule.currency}`}\n` +
`Target: ${rule.target_type} (${rule.target_selection})\n` +
`Customer Selection: ${rule.customer_selection}\n` +
`Usage Limit: ${rule.usage_limit || 'Unlimited'}\n` +
`Once Per Customer: ${rule.once_per_customer ? 'Yes' : 'No'}\n` +
`Starts: ${new Date(rule.starts_at).toLocaleDateString()}\n` +
`Ends: ${rule.ends_at ? new Date(rule.ends_at).toLocaleDateString() : 'Never'}\n` +
`\nDiscount Codes:\n` +
codesData.discount_codes
.map((code) => ` - ${code.code} (Uses: ${code.usage_count})`)
.join('\n'),
},
],
};
}
// Metafields Tools
async getProductMetafields(args) {
const data = await this.shopifyRequest(`products/${args.product_id}/metafields.json`);
if (data.metafields.length === 0) {
return {
content: [
{
type: 'text',
text: `No metafields found for product ${args.product_id}`,
},
],
};
}
return {
content: [
{
type: 'text',
text:
`Metafields for Product ${args.product_id}:\n\n` +
data.metafields
.map(
(field) =>
`• ${field.namespace}.${field.key}\n` +
` Type: ${field.type}\n` +
` Value: ${JSON.stringify(field.value)}\n` +
` Updated: ${new Date(field.updated_at).toLocaleDateString()}`
)
.join('\n\n'),
},
],
};
}
// Fulfillment Tools
async listFulfillmentServices() {
const data = await this.shopifyRequest('fulfillment_services.json');
return {
content: [
{
type: 'text',
text:
`Fulfillment Services (${data.fulfillment_services.length}):\n\n` +
data.fulfillment_services
.map(
(service) =>
`• ${service.name} (ID: ${service.id})\n` +
` Handle: ${service.handle}\n` +
` Tracking Support: ${service.tracking_support ? 'Yes' : 'No'}\n` +
` Inventory Management: ${service.inventory_management ? 'Yes' : 'No'}`
)
.join('\n\n'),
},
],
};
}
async getShippingZones() {
const data = await this.shopifyRequest('shipping_zones.json');
return {
content: [
{
type: 'text',
text:
`Shipping Zones (${data.shipping_zones.length}):\n\n` +
data.shipping_zones
.map(
(zone) =>
`• ${zone.name} (ID: ${zone.id})\n` +
` Countries: ${zone.countries.map((c) => c.name).join(', ')}\n` +
` Weight Based Rates: ${zone.weight_based_shipping_rates.length}\n` +
` Price Based Rates: ${zone.price_based_shipping_rates.length}\n` +
` Carrier Rates: ${zone.carrier_shipping_rate_providers.length}`
)
.join('\n\n'),
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('MCP Shopify server running on stdio');
}
}
// Check if running directly
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new ShopifyMCPServer();
server.run().catch(console.error);
}
export { ShopifyMCPServer };
export default ShopifyMCPServer;