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
/**
* REST Handler Tests
* Tests all 5 REST handler modules: catalog, cart, checkout, orders, negotiate.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { RESTRequest } from '../../src/rest/types.js';
import { handleCatalog } from '../../src/rest/catalog.js';
import { handleCart } from '../../src/rest/cart.js';
import { handleCheckout } from '../../src/rest/checkout.js';
import { handleOrders } from '../../src/rest/orders.js';
import { handleNegotiate } from '../../src/rest/negotiate.js';
import { resetDeps } from '../../src/dynamo/factory.js';
// Mock negotiate-terms so we can force errors
import * as negotiateTermsModule from '../../src/tools/negotiate-terms.js';
// ─── Test Request Builder ───
function makeReq(overrides: Partial<RESTRequest> = {}): RESTRequest {
return {
method: 'GET',
path: '/ucp/v1/test',
segments: [],
query: {},
body: null,
headers: {},
...overrides,
};
}
// ─── handleCatalog ───
describe('handleCatalog', () => {
it('GET with query.q returns 200 with products array', async () => {
const res = await handleCatalog(makeReq({ query: { q: 'shirt' } }));
expect(res.statusCode).toBe(200);
const body = res.body as { products: unknown[] };
expect(Array.isArray(body.products)).toBe(true);
});
it('GET without query.q returns 400 bad request', async () => {
const res = await handleCatalog(makeReq());
expect(res.statusCode).toBe(400);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('bad_request');
});
it('GET with segments[0] (productId) returns 404', async () => {
const res = await handleCatalog(makeReq({ segments: ['prod-123'] }));
expect(res.statusCode).toBe(404);
const body = res.body as { error: { code: string; message: string } };
expect(body.error.code).toBe('not_found');
expect(body.error.message).toContain('prod-123');
});
it('POST returns 405 method not allowed', async () => {
const res = await handleCatalog(makeReq({ method: 'POST' }));
expect(res.statusCode).toBe(405);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('method_not_allowed');
});
});
// ─── handleCart ───
describe('handleCart', () => {
it('POST with empty segments creates cart with 201 and cart_id', async () => {
const res = await handleCart(makeReq({ method: 'POST' }));
expect(res.statusCode).toBe(201);
const body = res.body as { cart_id: string };
expect(typeof body.cart_id).toBe('string');
expect(body.cart_id.length).toBeGreaterThan(0);
});
it('GET with segments[0] on nonexistent cart returns 404', async () => {
const res = await handleCart(makeReq({ segments: ['nonexistent-cart-id'] }));
expect(res.statusCode).toBe(404);
});
it('full CRUD flow: create -> add item -> get -> delete item -> verify removed', async () => {
// Step 1: Create cart
const createRes = await handleCart(makeReq({ method: 'POST' }));
expect(createRes.statusCode).toBe(201);
const cartId = (createRes.body as { cart_id: string }).cart_id;
// Step 2: Add item
const addRes = await handleCart(makeReq({
method: 'POST',
segments: [cartId, 'items'],
body: { variant_id: 'v1', quantity: 2 },
}));
expect(addRes.statusCode).toBe(200);
const addBody = addRes.body as { line_items: { variant_id: string }[]; item_count: number };
expect(addBody.line_items).toHaveLength(1);
expect(addBody.line_items[0]!.variant_id).toBe('v1');
expect(addBody.item_count).toBe(2);
// Step 3: Get cart and verify item exists
const getRes = await handleCart(makeReq({ segments: [cartId] }));
expect(getRes.statusCode).toBe(200);
const getBody = getRes.body as { line_items: { variant_id: string }[]; item_count: number };
expect(getBody.line_items).toHaveLength(1);
expect(getBody.item_count).toBe(2);
// Step 4: Remove item
const delRes = await handleCart(makeReq({
method: 'DELETE',
segments: [cartId, 'items', 'v1'],
}));
expect(delRes.statusCode).toBe(200);
// Step 5: Get again and verify removed
const finalRes = await handleCart(makeReq({ segments: [cartId] }));
expect(finalRes.statusCode).toBe(200);
const finalBody = finalRes.body as { line_items: unknown[]; item_count: number };
expect(finalBody.line_items).toHaveLength(0);
expect(finalBody.item_count).toBe(0);
});
it('POST add without variant_id returns 400', async () => {
const createRes = await handleCart(makeReq({ method: 'POST' }));
const cartId = (createRes.body as { cart_id: string }).cart_id;
const res = await handleCart(makeReq({
method: 'POST',
segments: [cartId, 'items'],
body: { quantity: 2 },
}));
expect(res.statusCode).toBe(400);
});
it('PUT returns 405 method not allowed', async () => {
const res = await handleCart(makeReq({ method: 'PUT' }));
expect(res.statusCode).toBe(405);
});
});
// ─── handleCheckout ───
describe('handleCheckout', () => {
beforeEach(() => {
resetDeps();
});
it('POST creates a new checkout session with 201 and status incomplete', async () => {
const res = await handleCheckout(makeReq({ method: 'POST' }));
expect(res.statusCode).toBe(201);
const body = res.body as { id: string; status: string };
expect(typeof body.id).toBe('string');
expect(body.id.length).toBeGreaterThan(0);
expect(body.status).toBe('incomplete');
});
it('GET with valid session ID returns 200 with session', async () => {
// Create a session first
const createRes = await handleCheckout(makeReq({ method: 'POST' }));
const sessionId = (createRes.body as { id: string }).id;
const res = await handleCheckout(makeReq({ segments: [sessionId] }));
expect(res.statusCode).toBe(200);
const body = res.body as { id: string; status: string };
expect(body.id).toBe(sessionId);
});
it('GET with invalid session ID returns 404', async () => {
const res = await handleCheckout(makeReq({ segments: ['nonexistent-id'] }));
expect(res.statusCode).toBe(404);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('not_found');
});
it('PUT updates session and returns 200 with buyer set', async () => {
// Create session
const createRes = await handleCheckout(makeReq({ method: 'POST' }));
const sessionId = (createRes.body as { id: string }).id;
// Update with buyer info
const res = await handleCheckout(makeReq({
method: 'PUT',
segments: [sessionId],
body: { buyer: { email: 'test@example.com' } },
}));
expect(res.statusCode).toBe(200);
const body = res.body as { buyer: { email: string } };
expect(body.buyer.email).toBe('test@example.com');
});
it('POST /checkout/{id}/complete without mandates returns 400', async () => {
// Create session
const createRes = await handleCheckout(makeReq({ method: 'POST' }));
const sessionId = (createRes.body as { id: string }).id;
// Try to complete without mandates
const res = await handleCheckout(makeReq({
method: 'POST',
segments: [sessionId, 'complete'],
body: {},
}));
expect(res.statusCode).toBe(400);
const body = res.body as { error: { code: string; message: string } };
expect(body.error.code).toBe('bad_request');
expect(body.error.message).toContain('mandate');
});
});
// ─── handleOrders ───
describe('handleOrders', () => {
it('GET without orderId returns 400', async () => {
const res = await handleOrders(makeReq());
expect(res.statusCode).toBe(400);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('bad_request');
});
it('GET with orderId returns 200 with found: true (mock data)', async () => {
const res = await handleOrders(makeReq({ segments: ['order-123'] }));
expect(res.statusCode).toBe(200);
const body = res.body as { found: boolean; order: { id: string } };
expect(body.found).toBe(true);
expect(body.order.id).toBe('order-123');
});
it('POST returns 405 method not allowed', async () => {
const res = await handleOrders(makeReq({ method: 'POST' }));
expect(res.statusCode).toBe(405);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('method_not_allowed');
});
});
// ─── handleNegotiate ───
describe('handleNegotiate', () => {
it('POST with cart_id and agent_profile_url returns 200 with negotiation result', async () => {
// First create a cart so cart_id exists (negotiate creates its own session anyway)
const res = await handleNegotiate(makeReq({
method: 'POST',
body: {
cart_id: 'test-cart-1',
agent_profile_url: 'https://example.com/agent/profile.json',
},
}));
expect(res.statusCode).toBe(200);
const body = res.body as { negotiation: { active_capabilities: unknown[] } };
expect(body.negotiation).toBeDefined();
expect(Array.isArray(body.negotiation.active_capabilities)).toBe(true);
});
it('POST without cart_id returns 400', async () => {
const res = await handleNegotiate(makeReq({
method: 'POST',
body: { agent_profile_url: 'https://example.com/agent/profile.json' },
}));
expect(res.statusCode).toBe(400);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('bad_request');
});
it('POST without agent_profile_url returns 400', async () => {
const res = await handleNegotiate(makeReq({
method: 'POST',
body: { cart_id: 'test-cart-2' },
}));
expect(res.statusCode).toBe(400);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('bad_request');
});
it('GET returns 405 method not allowed', async () => {
const res = await handleNegotiate(makeReq());
expect(res.statusCode).toBe(405);
const body = res.body as { error: { code: string } };
expect(body.error.code).toBe('method_not_allowed');
});
it('returns 500 when negotiateTerms throws an error', async () => {
const spy = vi.spyOn(negotiateTermsModule, 'negotiateTerms')
.mockRejectedValueOnce(new Error('Shopify API down'));
const res = await handleNegotiate(makeReq({
method: 'POST',
body: {
cart_id: 'test-cart-err',
agent_profile_url: 'https://example.com/agent/profile.json',
},
}));
expect(res.statusCode).toBe(500);
const body = res.body as { error: { code: string; message: string } };
expect(body.error.code).toBe('internal_error');
expect(body.error.message).toBe('Shopify API down');
spy.mockRestore();
});
it('returns 500 with stringified error for non-Error throws', async () => {
const spy = vi.spyOn(negotiateTermsModule, 'negotiateTerms')
.mockRejectedValueOnce('raw string failure');
const res = await handleNegotiate(makeReq({
method: 'POST',
body: {
cart_id: 'test-cart-err2',
agent_profile_url: 'https://example.com/agent/profile.json',
},
}));
expect(res.statusCode).toBe(500);
const body = res.body as { error: { code: string; message: string } };
expect(body.error.code).toBe('internal_error');
expect(body.error.message).toBe('raw string failure');
spy.mockRestore();
});
});