We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/kuro-tomo/shopify-agentic-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* manage-cart Storefront API integration paths
*
* Tests the Shopify Storefront API code paths in handleCreate, handleAdd,
* and handleGet — both success and failure (fallback) scenarios.
*
* The module-scoped `cachedStorefrontAPI` is initialised once on the first
* manageCart call and then reused. Because vitest gives each test file its
* own module scope, the mocks below guarantee that getStorefrontAPI() returns
* a StorefrontAPI instance whose methods we control via mockCreateCart,
* mockAddToCart, and mockGetCart.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { LineItem, CheckoutTotals } from '../../src/types.js';
// ─── Mock fns for StorefrontAPI methods ───
const mockCreateCart = vi.fn();
const mockAddToCart = vi.fn();
const mockGetCart = vi.fn();
// ─── Module mocks (must be hoisted above imports) ───
vi.mock('../../src/shopify/storefront.js', () => ({
StorefrontAPI: vi.fn().mockImplementation(() => ({
createCart: mockCreateCart,
addToCart: mockAddToCart,
getCart: mockGetCart,
})),
}));
vi.mock('../../src/shopify/client.js', () => ({
ShopifyClient: vi.fn().mockImplementation(() => ({})),
}));
vi.mock('../../src/types.js', async () => {
const actual = await vi.importActual('../../src/types.js');
return {
...actual,
loadConfig: vi.fn(() => ({
shopify: {
storeDomain: 'test.myshopify.com',
storefrontToken: 'sf-test-token',
accessToken: 'access-test-token',
apiKey: 'test-key',
apiSecret: 'test-secret',
},
ap2: { signingPrivateKey: 'test-key', verificationPublicKey: undefined },
gateway: { baseUrl: 'http://localhost:3000', feeRate: 0.005, feeWalletAddress: '0xtest' },
dynamodb: { mandatesTable: '', sessionsTable: '', ledgerTable: '' },
logLevel: 'error',
})),
};
});
// Suppress logger output during tests
vi.mock('../../src/utils/logger.js', () => ({
logger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
// ─── Import under test (after mocks are declared) ───
import { manageCart } from '../../src/tools/manage-cart.js';
// ─── Helpers ───
function makeLineItem(overrides: Partial<LineItem> = {}): LineItem {
return {
id: 'li-1',
product_id: 'prod-1',
variant_id: 'gid://shopify/ProductVariant/111',
title: 'Test Widget',
quantity: 2,
unit_amount: 1500,
total_amount: 3000,
type: 'product',
...overrides,
};
}
function makeShopifyTotals(overrides: Partial<CheckoutTotals> = {}): CheckoutTotals {
return {
subtotal: 3000,
tax: 240,
shipping: 0,
discount: 0,
fee: 0,
total: 3240,
currency: 'USD',
...overrides,
};
}
// ─── Tests ───
beforeEach(() => {
vi.clearAllMocks();
});
// ─── handleCreate ───
describe('manageCart — create (Storefront API)', () => {
it('returns a Shopify cart ID when storefront.createCart succeeds', async () => {
const shopifyCartId = 'gid://shopify/Cart/abc123';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
const result = await manageCart({ action: 'create' });
expect(mockCreateCart).toHaveBeenCalledOnce();
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(0);
expect(result.item_count).toBe(0);
expect(result.totals.subtotal).toBe(0);
expect(result.totals.total).toBe(0);
expect(result.totals.currency).toBe('USD');
});
it('falls back to a UUID-based cart when storefront.createCart throws', async () => {
mockCreateCart.mockRejectedValueOnce(new Error('Storefront API unavailable'));
const result = await manageCart({ action: 'create' });
expect(mockCreateCart).toHaveBeenCalledOnce();
// The fallback cart_id is a UUID (not a Shopify GID)
expect(result.cart_id).not.toContain('gid://');
expect(result.cart_id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
expect(result.line_items).toHaveLength(0);
expect(result.item_count).toBe(0);
});
});
// ─── handleAdd ───
describe('manageCart — add (Storefront API)', () => {
it('delegates to storefront addToCart + getCart when both succeed', async () => {
// First create a cart via Shopify
const shopifyCartId = 'gid://shopify/Cart/add-success';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// Set up the add + get mocks
const lineItem = makeLineItem({ variant_id: 'gid://shopify/ProductVariant/222' });
const totals = makeShopifyTotals();
mockAddToCart.mockResolvedValueOnce(undefined);
mockGetCart.mockResolvedValueOnce({
id: shopifyCartId,
lines: [lineItem],
totals,
});
const result = await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'gid://shopify/ProductVariant/222',
quantity: 2,
});
expect(mockAddToCart).toHaveBeenCalledWith(
shopifyCartId,
'gid://shopify/ProductVariant/222',
2,
);
expect(mockGetCart).toHaveBeenCalledWith(shopifyCartId);
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.variant_id).toBe('gid://shopify/ProductVariant/222');
expect(result.line_items[0]!.quantity).toBe(2);
expect(result.totals).toEqual(totals);
expect(result.item_count).toBe(2);
});
it('returns Shopify totals directly (not recalculated) on success', async () => {
// Create a cart
const shopifyCartId = 'gid://shopify/Cart/totals-direct';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// Shopify returns totals with specific tax/fee values that differ from
// the in-memory calculateTotals logic — we expect them passed through.
const customTotals = makeShopifyTotals({
subtotal: 5000,
tax: 500,
shipping: 799,
discount: 100,
fee: 25,
total: 6224,
currency: 'CAD',
});
mockAddToCart.mockResolvedValueOnce(undefined);
mockGetCart.mockResolvedValueOnce({
id: shopifyCartId,
lines: [makeLineItem({ quantity: 5, unit_amount: 1000, total_amount: 5000 })],
totals: customTotals,
});
const result = await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'gid://shopify/ProductVariant/111',
quantity: 5,
});
// Totals should be the exact Shopify values, not locally computed
expect(result.totals.subtotal).toBe(5000);
expect(result.totals.tax).toBe(500);
expect(result.totals.shipping).toBe(799);
expect(result.totals.discount).toBe(100);
expect(result.totals.fee).toBe(25);
expect(result.totals.total).toBe(6224);
expect(result.totals.currency).toBe('CAD');
});
it('falls back to in-memory add when storefront.addToCart throws', async () => {
// Create a cart via Shopify
const shopifyCartId = 'gid://shopify/Cart/add-fail';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// addToCart fails
mockAddToCart.mockRejectedValueOnce(new Error('network timeout'));
const result = await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'variant-fallback',
quantity: 3,
});
expect(mockAddToCart).toHaveBeenCalledOnce();
// getCart should NOT have been called because addToCart threw before it
expect(mockGetCart).not.toHaveBeenCalled();
// Fallback: in-memory line item is created
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.variant_id).toBe('variant-fallback');
expect(result.line_items[0]!.quantity).toBe(3);
// In-memory uses calculateTotals (unit_amount=0 for unknown price)
expect(result.item_count).toBe(3);
});
it('falls back to in-memory add when storefront.getCart throws after addToCart', async () => {
// Create a cart via Shopify
const shopifyCartId = 'gid://shopify/Cart/getfail-after-add';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// addToCart succeeds, but getCart fails
mockAddToCart.mockResolvedValueOnce(undefined);
mockGetCart.mockRejectedValueOnce(new Error('getCart network error'));
const result = await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'variant-get-fail',
quantity: 1,
});
expect(mockAddToCart).toHaveBeenCalledOnce();
expect(mockGetCart).toHaveBeenCalledOnce();
// Falls back to in-memory logic
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.variant_id).toBe('variant-get-fail');
expect(result.item_count).toBe(1);
});
});
// ─── handleGet ───
describe('manageCart — get (Storefront API)', () => {
it('returns Shopify cart data when storefront.getCart succeeds', async () => {
// Create a cart
const shopifyCartId = 'gid://shopify/Cart/get-success';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// Set up the getCart mock with Shopify data
const lineItem = makeLineItem({
id: 'li-shopify-1',
variant_id: 'gid://shopify/ProductVariant/333',
title: 'Shopify Widget',
quantity: 4,
unit_amount: 2500,
total_amount: 10000,
});
const totals = makeShopifyTotals({
subtotal: 10000,
tax: 800,
total: 10800,
});
mockGetCart.mockResolvedValueOnce({
id: shopifyCartId,
lines: [lineItem],
totals,
});
const result = await manageCart({
action: 'get',
cart_id: shopifyCartId,
});
expect(mockGetCart).toHaveBeenCalledWith(shopifyCartId);
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.title).toBe('Shopify Widget');
expect(result.line_items[0]!.quantity).toBe(4);
expect(result.totals).toEqual(totals);
expect(result.item_count).toBe(4);
});
it('updates the local cache entry with fresh Shopify data', async () => {
// Create a cart and add an item in-memory first
const shopifyCartId = 'gid://shopify/Cart/cache-update';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// Add an item via in-memory fallback (addToCart fails)
mockAddToCart.mockRejectedValueOnce(new Error('add fail'));
await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'var-local',
quantity: 1,
});
// Now getCart succeeds with different data from Shopify
const shopifyLine = makeLineItem({
id: 'li-shopify-fresh',
variant_id: 'var-shopify-fresh',
title: 'Fresh from Shopify',
quantity: 7,
unit_amount: 999,
total_amount: 6993,
});
const shopifyTotals = makeShopifyTotals({
subtotal: 6993,
tax: 559,
total: 7552,
currency: 'EUR',
});
mockGetCart.mockResolvedValueOnce({
id: shopifyCartId,
lines: [shopifyLine],
totals: shopifyTotals,
});
const result = await manageCart({
action: 'get',
cart_id: shopifyCartId,
});
// The result should reflect the Shopify data, not the stale local data
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.variant_id).toBe('var-shopify-fresh');
expect(result.totals.currency).toBe('EUR');
expect(result.item_count).toBe(7);
});
it('falls back to in-memory data when storefront.getCart throws', async () => {
// Create a cart
const shopifyCartId = 'gid://shopify/Cart/get-fail';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
// Add an item via in-memory fallback so there is local data
mockAddToCart.mockRejectedValueOnce(new Error('add fail'));
await manageCart({
action: 'add',
cart_id: shopifyCartId,
variant_id: 'var-inmem',
quantity: 2,
});
// getCart throws
mockGetCart.mockRejectedValueOnce(new Error('Shopify unavailable'));
const result = await manageCart({
action: 'get',
cart_id: shopifyCartId,
});
expect(mockGetCart).toHaveBeenCalledWith(shopifyCartId);
// Falls back to in-memory data from the earlier add
expect(result.cart_id).toBe(shopifyCartId);
expect(result.line_items).toHaveLength(1);
expect(result.line_items[0]!.variant_id).toBe('var-inmem');
expect(result.item_count).toBe(2);
// Totals are computed locally via calculateTotals (unit_amount=0 fallback)
expect(result.totals.currency).toBe('USD');
});
it('returns Shopify totals with multiple line items and correct item_count', async () => {
const shopifyCartId = 'gid://shopify/Cart/multi-line';
mockCreateCart.mockResolvedValueOnce(shopifyCartId);
await manageCart({ action: 'create' });
const lines: LineItem[] = [
makeLineItem({ id: 'li-a', variant_id: 'var-a', quantity: 3, total_amount: 4500 }),
makeLineItem({ id: 'li-b', variant_id: 'var-b', quantity: 2, total_amount: 3000 }),
];
const totals = makeShopifyTotals({ subtotal: 7500, total: 8100 });
mockGetCart.mockResolvedValueOnce({
id: shopifyCartId,
lines,
totals,
});
const result = await manageCart({
action: 'get',
cart_id: shopifyCartId,
});
expect(result.line_items).toHaveLength(2);
expect(result.item_count).toBe(5); // 3 + 2
expect(result.totals.subtotal).toBe(7500);
});
});