index.ts•12.9 kB
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
handleStorefrontApiResult,
requestStorefrontApi,
listAvailableStores,
type TextContentBlock,
} from "../utils/shopify-client.js";
import {
CartLineInput,
CartBuyerIdentityInput,
AttributeInput,
CartLineUpdateInput,
FindProductsInputShape,
GetProductByIdInputShape,
CartCreateInputShape,
CartLinesAddInputShape,
CartLinesUpdateInputShape,
CartLinesRemoveInputShape,
GetCartInputShape,
} from "../utils/input-schemas.js";
export function registerTools(server: McpServer) {
server.tool(
"findProducts",
"Searches or filters products with pagination and sorting.",
FindProductsInputShape,
async ({ storeId, query: searchQuery, after, sortKey, reverse }) => {
const query = `
query FindProducts($after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
products(first: 100, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
edges {
cursor
node {
id
title
handle
vendor
priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } }
}
}
}
}
`;
const graphQLPayload = {
query,
variables: { after, query: searchQuery, sortKey, reverse },
};
console.info(
`[${new Date().toISOString()}] INFO: [tool:findProducts] Executing with search query: ${searchQuery} on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
server.tool(
"getProductById",
"Fetches product by ID. If includeImages=true, also returns the URL of the first image (resized to 75px).",
GetProductByIdInputShape,
async ({ storeId, productId, includeVariants, variantCount, includeImages }) => {
const imageCountForQuery = includeImages ? 1 : undefined; // Only need URL if requested
const query = `
query GetProduct($productId: ID!${includeVariants ? ", $variantCount: Int!" : ""}${includeImages ? ", $imageCount: Int!" : ""}) {
product(id: $productId) {
id
title
descriptionHtml
vendor
${
includeVariants
? `
variants(first: $variantCount) {
edges {
node {
id
title
price {
amount
currencyCode
}
selectedOptions {
name
value
}
}
}
}`
: ""
}
${
includeImages
? `
images(first: $imageCount) {
edges {
node {
id
url
altText
width
height
}
}
}`
: ""
}
}
}`;
const graphQLPayload = {
query,
variables: { productId, variantCount, imageCount: imageCountForQuery },
};
console.info(
`[${new Date().toISOString()}] INFO: [tool:getProductById] Executing for ID: ${productId} on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
// If successful AND image URL requested
if (!proxyResult.errors && includeImages) {
interface ProductData {
// Define expected structure
id?: string;
title?: string;
descriptionHtml?: string;
vendor?: string;
variants?: { edges: unknown[] };
images?: {
edges: {
node: {
url: string;
altText?: string;
width?: number;
height?: number;
};
}[];
};
}
const responseData = proxyResult.data as {
data?: { product?: ProductData };
};
const product = responseData?.data?.product;
const firstImageEdge = product?.images?.edges?.[0];
if (product && firstImageEdge) {
const img = firstImageEdge.node;
const contentBlocks: TextContentBlock[] = [];
// Create productInfo copy for JSON, excluding images array
const productInfoForJson = { ...product };
productInfoForJson.images = undefined;
// Add JSON details block
contentBlocks.push({
type: "text",
text: `Product Details:\n\`\`\`json\n${JSON.stringify(
productInfoForJson,
null,
2,
)}\n\`\`\``,
});
// Construct resized image URL
const separator = img.url.includes("?") ? "&" : "?";
const resizedImageUrl = `${img.url}${separator}width=75`; // Use width=75
// Add the resized image URL as a separate text block
contentBlocks.push({
type: "text",
text: `Image URL (75px): ${resizedImageUrl}`,
});
console.info(
`[${new Date().toISOString()}] INFO: [getProductById] Returning product JSON and resized image URL.`,
);
return { content: contentBlocks };
}
}
// Fallback if no images requested/found or if proxy failed
return handleStorefrontApiResult(proxyResult); // Returns original data wrapped by jsonResult
},
);
// --- Cart Tools ---
server.tool(
"cartCreate",
"Creates a new shopping cart. Uses the Storefront API.",
CartCreateInputShape,
async ({ storeId, lines, buyerIdentity, attributes }) => {
const mutation = `
mutation CartCreate($input: CartInput!) {
cartCreate(input: $input) {
cart { id checkoutUrl cost { totalAmount { amount currencyCode } } lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title } } } } } } }
userErrors { field message }
}
}
`;
const input: {
lines?: unknown;
buyerIdentity?: unknown;
attributes?: unknown;
} = {};
if (lines) input.lines = lines;
if (buyerIdentity) input.buyerIdentity = buyerIdentity;
if (attributes) input.attributes = attributes;
const graphQLPayload = { query: mutation, variables: { input } };
console.info(
`[${new Date().toISOString()}] INFO: [tool:cartCreate] Creating cart on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
server.tool(
"cartLinesAdd",
"Adds line items to an existing shopping cart. Uses the Storefront API.",
CartLinesAddInputShape,
async ({ storeId, cartId, lines }) => {
const mutation = `
mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart { id checkoutUrl cost { totalAmount { amount currencyCode } } lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title } } } } } } }
userErrors { field message }
}
}
`;
const graphQLPayload = { query: mutation, variables: { cartId, lines } };
console.info(
`[${new Date().toISOString()}] INFO: [tool:cartLinesAdd] Adding lines to cart on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
server.tool(
"cartLinesUpdate",
"Updates line items (e.g., quantity) in an existing shopping cart. Uses the Storefront API.",
CartLinesUpdateInputShape,
async ({ storeId, cartId, lines }) => {
const mutation = `
mutation CartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
cartLinesUpdate(cartId: $cartId, lines: $lines) {
cart { id checkoutUrl cost { totalAmount { amount currencyCode } } lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title } } } } } } }
userErrors { field message }
}
}
`;
const graphQLPayload = { query: mutation, variables: { cartId, lines } };
console.info(
`[${new Date().toISOString()}] INFO: [tool:cartLinesUpdate] Updating cart lines on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
server.tool(
"cartLinesRemove",
"Removes line items from an existing shopping cart. Uses the Storefront API.",
CartLinesRemoveInputShape,
async ({ storeId, cartId, lineIds }) => {
const mutation = `
mutation CartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart { id checkoutUrl cost { totalAmount { amount currencyCode } } lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title } } } } } } }
userErrors { field message }
}
}
`;
const graphQLPayload = {
query: mutation,
variables: { cartId, lineIds },
};
console.info(
`[${new Date().toISOString()}] INFO: [tool:cartLinesRemove] Removing lines from cart on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
server.tool(
"getCart",
"Fetches the details of an existing shopping cart by its ID. Uses the Storefront API.",
GetCartInputShape,
async ({ storeId, cartId }) => {
const query = `
query GetCart($cartId: ID!) {
cart(id: $cartId) {
id
createdAt
updatedAt
checkoutUrl
cost { totalAmount { amount currencyCode } subtotalAmount { amount currencyCode } totalTaxAmount { amount currencyCode } totalDutyAmount { amount currencyCode } }
lines(first: 50) {
edges {
node {
id
quantity
cost { totalAmount { amount currencyCode } }
merchandise {
... on ProductVariant {
id
title
price { amount currencyCode }
product { id title handle }
}
}
}
}
}
buyerIdentity { email phone countryCode customer { id } }
attributes { key value }
}
}
`;
const graphQLPayload = { query, variables: { cartId } };
console.info(
`[${new Date().toISOString()}] INFO: [tool:getCart] Getting cart details on store: ${storeId}`,
);
const proxyResult = await requestStorefrontApi({
storeId,
query: graphQLPayload.query,
variables: graphQLPayload.variables,
});
return handleStorefrontApiResult(proxyResult);
},
);
// --- Store Management Tools ---
server.tool(
"listStores",
"Lists all available Shopify stores configured in the system.",
{},
async () => {
const stores = listAvailableStores();
console.info(
`[${new Date().toISOString()}] INFO: [tool:listStores] Returning ${stores.length} configured stores`,
);
return {
content: [
{
type: "text",
text: `Available Stores:\n\`\`\`json\n${JSON.stringify(stores, null, 2)}\n\`\`\``,
},
],
};
},
);
}