/**
* Clind MCP Server - Customer-facing AI Agent
* For Shopify store operations: orders, products, cart, checkout
*
* Supports MCP JSON-RPC 2.0 protocol
*/
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { validateConfig } from './tools/shopify-client.js';
import { trackOrder, getOrderStatus } from './tools/orders.js';
import {
searchProducts,
getProductDetails,
getProductRecommendations,
getBestSellers,
getCollections,
} from './tools/products.js';
import {
addToCart,
createCheckoutLink,
checkShipping,
getShippingZones,
} from './tools/cart.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.set('trust proxy', 1);
// Validate config
try {
validateConfig();
console.log('β
Shopify configuration validated');
} catch (error) {
console.error('β Configuration error:', error);
process.exit(1);
}
function getValidApiKeys(): string[] {
return (process.env.MCP_API_KEYS || '')
.split(',')
.map((k) => k.trim())
.filter((k) => k !== '');
}
function authenticateAPIKey(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void {
const validKeys = getValidApiKeys();
if (validKeys.length === 0) {
next();
return;
}
const authHeader = req.headers['authorization'];
const apiKey = req.headers['x-api-key'] || authHeader?.replace('Bearer ', '');
if (!apiKey || !validKeys.includes(String(apiKey))) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
next();
}
app.use(cors());
app.use(express.json());
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: 'Too many requests' },
});
// Customer-facing MCP Tools
const tools = [
// Order Tracking
{
name: 'track_order',
description: 'Track an order by order number and email. Returns order status, items, and tracking information.',
inputSchema: {
type: 'object',
properties: {
order_number: { type: 'string', description: 'Order number (e.g., "1001" or "#1001")' },
email: { type: 'string', description: 'Email address used for the order' },
},
required: ['order_number', 'email'],
},
},
{
name: 'get_order_status',
description: 'Get a simple status message for an order with tracking info if available.',
inputSchema: {
type: 'object',
properties: {
order_number: { type: 'string', description: 'Order number' },
email: { type: 'string', description: 'Email address' },
},
required: ['order_number', 'email'],
},
},
// Product Discovery
{
name: 'search_products',
description: 'Search for products by name, type, or tags.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query (product name, type, tag)' },
limit: { type: 'number', description: 'Max results (default: 10)' },
},
required: ['query'],
},
},
{
name: 'get_product_details',
description: 'Get detailed information about a specific product.',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'string', description: 'Product ID' },
handle: { type: 'string', description: 'Product handle/slug (alternative to product_id)' },
},
},
},
{
name: 'get_recommendations',
description: 'Get product recommendations based on best sellers.',
inputSchema: {
type: 'object',
properties: {
product_id: { type: 'string', description: 'Current product ID to exclude (optional)' },
limit: { type: 'number', description: 'Number of recommendations (default: 5)' },
},
},
},
{
name: 'get_best_sellers',
description: 'Get the top selling products.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Number of products (default: 10)' },
},
},
},
{
name: 'get_collections',
description: 'Get product collections/categories.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max results (default: 20)' },
},
},
},
// Cart & Checkout
{
name: 'add_to_cart',
description: 'Generate a link to add a product to cart.',
inputSchema: {
type: 'object',
properties: {
variant_id: { type: 'string', description: 'Product variant ID' },
quantity: { type: 'number', description: 'Quantity (default: 1)' },
},
required: ['variant_id'],
},
},
{
name: 'create_checkout_link',
description: 'Create a direct checkout link with products.',
inputSchema: {
type: 'object',
properties: {
items: {
type: 'array',
description: 'Array of items to checkout',
items: {
type: 'object',
properties: {
variant_id: { type: 'string' },
quantity: { type: 'number' },
},
required: ['variant_id', 'quantity'],
},
},
discount_code: { type: 'string', description: 'Discount code (optional)' },
},
required: ['items'],
},
},
{
name: 'check_shipping',
description: 'Check shipping options and estimated delivery for a zipcode.',
inputSchema: {
type: 'object',
properties: {
zipcode: { type: 'string', description: 'Zip/postal code' },
country_code: { type: 'string', description: 'Country code (default: US)' },
},
required: ['zipcode'],
},
},
{
name: 'get_shipping_zones',
description: 'Get available shipping zones and estimated delivery times.',
inputSchema: {
type: 'object',
properties: {},
},
},
];
// MCP Protocol
const MCP_PROTOCOL_VERSION = '2024-11-05';
const SERVER_INFO = { name: 'clind-mcp', version: '1.0.0' };
const SERVER_CAPABILITIES = { tools: {} };
interface JsonRpcRequest {
jsonrpc: '2.0';
id?: string | number | null;
method: string;
params?: Record<string, any>;
}
interface JsonRpcResponse {
jsonrpc: '2.0';
id: string | number | null;
result?: any;
error?: { code: number; message: string };
}
const JSON_RPC_ERRORS = {
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
};
type ToolHandler = (args: Record<string, any>) => Promise<any> | any;
const toolHandlers: Record<string, ToolHandler> = {
track_order: trackOrder,
get_order_status: getOrderStatus,
search_products: searchProducts,
get_product_details: getProductDetails,
get_recommendations: getProductRecommendations,
get_best_sellers: getBestSellers,
get_collections: getCollections,
add_to_cart: addToCart,
create_checkout_link: createCheckoutLink,
check_shipping: checkShipping,
get_shipping_zones: getShippingZones,
};
async function executeToolCall(toolName: string, args: Record<string, any>): Promise<any> {
const handler = toolHandlers[toolName];
if (!handler) {
throw new Error(`Unknown tool: ${toolName}`);
}
return handler(args);
}
function createResponse(id: string | number | null | undefined, result: any): JsonRpcResponse {
return { jsonrpc: '2.0', id: id ?? null, result };
}
function createErrorResponse(
id: string | number | null | undefined,
code: number,
message: string
): JsonRpcResponse {
return { jsonrpc: '2.0', id: id ?? null, error: { code, message } };
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : 'Unknown error';
}
async function handleToolCall(
id: string | number | null | undefined,
params: Record<string, any> | undefined
): Promise<JsonRpcResponse> {
const toolName = params?.name;
const toolArgs = params?.arguments || {};
if (!toolName) {
return createErrorResponse(id, JSON_RPC_ERRORS.INVALID_PARAMS, 'Missing tool name');
}
const toolExists = tools.some((t) => t.name === toolName);
if (!toolExists) {
return createErrorResponse(id, JSON_RPC_ERRORS.METHOD_NOT_FOUND, `Unknown tool: ${toolName}`);
}
try {
const result = await executeToolCall(toolName, toolArgs);
return createResponse(id, {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
isError: false,
});
} catch (error) {
return createResponse(id, {
content: [{ type: 'text', text: JSON.stringify({ error: getErrorMessage(error) }) }],
isError: true,
});
}
}
async function handleMcpRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
const { jsonrpc, id, method, params } = request;
if (jsonrpc !== '2.0') {
return createErrorResponse(id, JSON_RPC_ERRORS.INVALID_REQUEST, 'Invalid JSON-RPC version');
}
try {
switch (method) {
case 'initialize':
return createResponse(id, {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: SERVER_CAPABILITIES,
serverInfo: SERVER_INFO,
});
case 'notifications/initialized':
case 'ping':
return createResponse(id, {});
case 'tools/list':
return createResponse(id, { tools });
case 'tools/call':
return handleToolCall(id, params);
default:
return createErrorResponse(
id,
JSON_RPC_ERRORS.METHOD_NOT_FOUND,
`Method not found: ${method}`
);
}
} catch (error) {
return createErrorResponse(id, JSON_RPC_ERRORS.INTERNAL_ERROR, getErrorMessage(error));
}
}
// Routes
app.get('/', (req, res) => {
res.json({
status: 'ok',
server: 'clind-mcp',
version: '1.0.0',
description: 'Customer-facing AI agent for Shopify store',
capabilities: ['order_tracking', 'product_search', 'recommendations', 'cart', 'checkout', 'shipping'],
});
});
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/tools', (req, res) => {
res.json({ tools });
});
app.get('/mcp', (req, res) => {
res.json({
jsonrpc: '2.0',
result: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: SERVER_CAPABILITIES,
serverInfo: SERVER_INFO,
},
});
});
app.post('/mcp', apiLimiter, authenticateAPIKey, async (req, res) => {
try {
const request = req.body as JsonRpcRequest;
if (Array.isArray(request)) {
const responses = await Promise.all(request.map((r) => handleMcpRequest(r)));
return res.json(responses);
}
const response = await handleMcpRequest(request);
return res.json(response);
} catch (error) {
return res.json({
jsonrpc: '2.0',
id: null,
error: { code: JSON_RPC_ERRORS.PARSE_ERROR, message: 'Parse error' },
});
}
});
app.post('/call', apiLimiter, authenticateAPIKey, async (req, res) => {
const { tool, arguments: args } = req.body;
if (!tool) return res.status(400).json({ error: 'Tool name required' });
const toolExists = tools.some((t) => t.name === tool);
if (!toolExists) return res.status(400).json({ error: `Unknown tool: ${tool}` });
try {
const result = await executeToolCall(tool, args || {});
return res.json({ result });
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return res.status(500).json({ error: message });
}
});
app.listen(PORT, () => {
console.log(`π Clind MCP Server (Customer-facing) running on port ${PORT}`);
console.log(`π Store: ${process.env.SHOPIFY_STORE_URL}`);
console.log('');
console.log('Available tools:');
console.log(' π¦ Order: track_order, get_order_status');
console.log(' ποΈ Products: search_products, get_product_details, get_recommendations, get_best_sellers');
console.log(' π Cart: add_to_cart, create_checkout_link');
console.log(' π Shipping: check_shipping, get_shipping_zones');
console.log('');
console.log(`π‘ MCP: POST http://localhost:${PORT}/mcp`);
console.log(`π REST: POST http://localhost:${PORT}/call`);
});