/**
* Tests for src/server.ts — createMCPServer()
*
* Verifies that the MCP server is created with the correct name/version,
* all 5 tools are registered, and each tool handler executes correctly.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createMCPServer } from '../src/server.js';
import { scoutInventory } from '../src/tools/scout-inventory.js';
import { negotiateTerms } from '../src/tools/negotiate-terms.js';
import { trackOrder } from '../src/tools/track-order.js';
import { manageCart } from '../src/tools/manage-cart.js';
import { executeCheckout } from '../src/tools/execute-checkout.js';
// ─── Mocks ───
vi.mock('../src/dynamo/factory.js', () => ({
getDeps: vi.fn(() => ({
config: {
shopify: { storeDomain: '', accessToken: '', storefrontToken: '' },
ap2: { signingPrivateKey: '' },
gateway: { baseUrl: '', feeRate: 0.005, feeWalletAddress: '' },
dynamodb: {},
},
sessionManager: {
create: vi.fn(),
get: vi.fn(),
update: vi.fn(),
getStatus: vi.fn(),
transition: vi.fn(),
},
mandateStore: {
store: vi.fn(),
get: vi.fn(),
getByCheckout: vi.fn(),
revoke: vi.fn(),
cleanup: vi.fn(),
},
feeCollector: { calculateFee: vi.fn(), collect: vi.fn() },
verifier: {
verifyIntent: vi.fn(),
verifyCart: vi.fn(),
verifyPayment: vi.fn(),
verifyMandateChain: vi.fn(),
},
guardrail: { validateMandateAmount: vi.fn(), runAllChecks: vi.fn() },
storefrontAPI: null,
adminAPI: null,
})),
}));
vi.mock('../src/tools/scout-inventory.js', () => ({
scoutInventory: vi.fn().mockResolvedValue({ products: [], total: 0 }),
}));
vi.mock('../src/tools/negotiate-terms.js', () => ({
negotiateTerms: vi.fn().mockResolvedValue({ terms: 'accepted' }),
}));
vi.mock('../src/tools/track-order.js', () => ({
trackOrder: vi.fn().mockResolvedValue({ status: 'shipped' }),
}));
vi.mock('../src/tools/manage-cart.js', () => ({
manageCart: vi.fn().mockResolvedValue({ cart_id: 'cart-1' }),
}));
vi.mock('../src/tools/execute-checkout.js', () => ({
executeCheckout: vi.fn().mockResolvedValue({ success: true }),
}));
vi.mock('../src/utils/logger.js', () => ({
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}));
// ─── Helpers ───
type RegisteredToolMap = Record<
string,
{ handler: (...args: unknown[]) => Promise<unknown>; description?: string }
>;
function getTools(server: McpServer): RegisteredToolMap {
return (server as unknown as { _registeredTools: RegisteredToolMap })
._registeredTools;
}
// ─── Tests ───
describe('createMCPServer', () => {
let server: McpServer;
beforeEach(() => {
vi.clearAllMocks();
server = createMCPServer();
});
// ─── Basic creation ───
it('returns an McpServer instance', () => {
expect(server).toBeInstanceOf(McpServer);
});
it('returns a truthy value', () => {
expect(server).toBeTruthy();
});
it('does not throw during creation', () => {
expect(() => createMCPServer()).not.toThrow();
});
it('has the correct server name "shopify-agentic-mcp"', () => {
const serverInfo = (server.server as unknown as { _serverInfo: { name: string } })
._serverInfo;
expect(serverInfo.name).toBe('shopify-agentic-mcp');
});
it('has the correct version "0.1.0"', () => {
const serverInfo = (server.server as unknown as { _serverInfo: { version: string } })
._serverInfo;
expect(serverInfo.version).toBe('0.1.0');
});
// ─── Tool registration ───
it('registers exactly 5 tools', () => {
expect(Object.keys(getTools(server))).toHaveLength(5);
});
it('registers the scout_inventory tool', () => {
expect(getTools(server)).toHaveProperty('scout_inventory');
});
it('registers the negotiate_terms tool', () => {
expect(getTools(server)).toHaveProperty('negotiate_terms');
});
it('registers the execute_checkout tool', () => {
expect(getTools(server)).toHaveProperty('execute_checkout');
});
it('registers the manage_cart tool', () => {
expect(getTools(server)).toHaveProperty('manage_cart');
});
it('registers the track_order tool', () => {
expect(getTools(server)).toHaveProperty('track_order');
});
it('each registered tool has a handler function', () => {
const tools = getTools(server);
for (const toolName of [
'scout_inventory',
'negotiate_terms',
'execute_checkout',
'manage_cart',
'track_order',
]) {
expect(typeof tools[toolName].handler).toBe('function');
}
});
it('each registered tool has a non-empty description', () => {
const tools = getTools(server);
for (const toolName of [
'scout_inventory',
'negotiate_terms',
'execute_checkout',
'manage_cart',
'track_order',
]) {
expect(typeof tools[toolName].description).toBe('string');
expect(tools[toolName].description!.length).toBeGreaterThan(0);
}
});
// ─── Tool handler invocations ───
describe('scout_inventory handler', () => {
it('calls scoutInventory and returns JSON text content', async () => {
const handler = getTools(server).scout_inventory.handler;
const result = (await handler(
{ query: 'shoes', category: 'footwear' },
{} as never,
)) as { content: { type: string; text: string }[] };
expect(scoutInventory).toHaveBeenCalledWith({
query: 'shoes',
category: 'footwear',
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual({
products: [],
total: 0,
});
});
});
describe('negotiate_terms handler', () => {
it('calls negotiateTerms with params and config, returns JSON text content', async () => {
const handler = getTools(server).negotiate_terms.handler;
const result = (await handler(
{
cart_id: 'cart-1',
agent_profile_url: 'https://example.com/profile',
discount_code: 'SAVE10',
},
{} as never,
)) as { content: { type: string; text: string }[] };
expect(negotiateTerms).toHaveBeenCalledWith(
{
cart_id: 'cart-1',
agent_profile_url: 'https://example.com/profile',
discount_code: 'SAVE10',
},
expect.objectContaining({
shopify: expect.any(Object),
}),
);
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual({ terms: 'accepted' });
});
});
describe('execute_checkout handler', () => {
it('calls executeCheckout and returns success result', async () => {
const handler = getTools(server).execute_checkout.handler;
const result = (await handler(
{
checkout_id: 'chk-1',
intent_mandate: 'jws.intent',
cart_mandate: 'jws.cart',
payment_mandate: 'jws.payment',
},
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(executeCheckout).toHaveBeenCalledWith(
{
checkout_id: 'chk-1',
intent_mandate: 'jws.intent',
cart_mandate: 'jws.cart',
payment_mandate: 'jws.payment',
},
expect.objectContaining({
sessionManager: expect.any(Object),
verifier: expect.any(Object),
mandateStore: expect.any(Object),
guardrail: expect.any(Object),
feeCollector: expect.any(Object),
}),
);
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual({ success: true });
expect(result.isError).toBe(false);
});
it('sets isError=true when executeCheckout returns success=false', async () => {
vi.mocked(executeCheckout).mockResolvedValueOnce({
success: false,
errors: ['payment failed'],
} as never);
const handler = getTools(server).execute_checkout.handler;
const result = (await handler(
{
checkout_id: 'chk-2',
intent_mandate: 'jws.i',
cart_mandate: 'jws.c',
payment_mandate: 'jws.p',
},
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(result.isError).toBe(true);
});
it('catches thrown errors and returns isError=true with message', async () => {
vi.mocked(executeCheckout).mockRejectedValueOnce(new Error('boom'));
const handler = getTools(server).execute_checkout.handler;
const result = (await handler(
{
checkout_id: 'chk-3',
intent_mandate: 'jws.i',
cart_mandate: 'jws.c',
payment_mandate: 'jws.p',
},
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.errors[0]).toContain('boom');
});
it('catches non-Error thrown values and returns string representation', async () => {
vi.mocked(executeCheckout).mockRejectedValueOnce('string-error');
const handler = getTools(server).execute_checkout.handler;
const result = (await handler(
{
checkout_id: 'chk-4',
intent_mandate: 'jws.i',
cart_mandate: 'jws.c',
payment_mandate: 'jws.p',
},
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.errors[0]).toContain('string-error');
});
});
describe('manage_cart handler', () => {
it('calls manageCart and returns JSON text content', async () => {
const handler = getTools(server).manage_cart.handler;
const result = (await handler(
{ action: 'create', variant_id: 'var-1', quantity: 2 },
{} as never,
)) as { content: { type: string; text: string }[] };
expect(manageCart).toHaveBeenCalledWith({
action: 'create',
variant_id: 'var-1',
quantity: 2,
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual({ cart_id: 'cart-1' });
});
it('catches thrown errors and returns isError=true', async () => {
vi.mocked(manageCart).mockRejectedValueOnce(new Error('cart error'));
const handler = getTools(server).manage_cart.handler;
const result = (await handler(
{ action: 'get', cart_id: 'nonexistent' },
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error).toContain('cart error');
});
it('catches non-Error thrown values in manage_cart', async () => {
vi.mocked(manageCart).mockRejectedValueOnce(42);
const handler = getTools(server).manage_cart.handler;
const result = (await handler(
{ action: 'get', cart_id: 'test' },
{} as never,
)) as { content: { type: string; text: string }[]; isError: boolean };
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.error).toBe('42');
});
});
describe('track_order handler', () => {
it('calls trackOrder and returns JSON text content', async () => {
const handler = getTools(server).track_order.handler;
const result = (await handler(
{ order_id: 'order-1' },
{} as never,
)) as { content: { type: string; text: string }[] };
expect(trackOrder).toHaveBeenCalledWith({ order_id: 'order-1' });
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text)).toEqual({ status: 'shipped' });
});
});
});