import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ShopifyClient } from './clients/shopify.js'; interface ToolModule { name: string; description: string; inputSchema: { type: 'object'; properties: Record; required?: string[]; }; handler: (args: Record, client: ShopifyClient) => Promise; } interface ServerConfig { name: string; version: string; shopifyClient: ShopifyClient; } export class ShopifyMCPServer { private server: Server; private client: ShopifyClient; private toolModules: Map Promise> = new Map(); constructor(config: ServerConfig) { this.client = config.shopifyClient; this.server = new Server( { name: config.name, version: config.version, }, { capabilities: { tools: {}, resources: {}, }, } ); this.setupToolModules(); this.setupHandlers(); } /** * Register lazy-loaded tool modules */ private setupToolModules(): void { // Tool modules will be dynamically imported when needed this.toolModules.set('products', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/products.js'); return module.default; }); this.toolModules.set('orders', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/orders.js'); return module.default; }); this.toolModules.set('customers', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/customers.js'); return module.default; }); this.toolModules.set('inventory', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/inventory.js'); return module.default; }); this.toolModules.set('collections', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/collections.js'); return module.default; }); this.toolModules.set('discounts', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/discounts.js'); return module.default; }); this.toolModules.set('shipping', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/shipping.js'); return module.default; }); this.toolModules.set('fulfillments', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/fulfillments.js'); return module.default; }); this.toolModules.set('themes', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/themes.js'); return module.default; }); this.toolModules.set('pages', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/pages.js'); return module.default; }); this.toolModules.set('blogs', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/blogs.js'); return module.default; }); this.toolModules.set('analytics', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/analytics.js'); return module.default; }); this.toolModules.set('webhooks', async () => { // @ts-ignore - Tool modules not created yet (foundation only) const module = await import('./tools/webhooks.js'); return module.default; }); } /** * Load all tool modules (called on list_tools) */ private async loadAllTools(): Promise { const allTools: ToolModule[] = []; // Only load modules that exist (we haven't created tool files yet) // This prevents errors during foundation setup for (const [moduleName, loader] of this.toolModules.entries()) { try { const tools = await loader(); allTools.push(...tools); } catch (error) { // Tool module doesn't exist yet - this is expected during foundation setup console.log(`[Info] Tool module '${moduleName}' not loaded (not created yet)`); } } return allTools; } /** * Setup MCP protocol handlers */ private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] Listing tools`); const tools = await this.loadAllTools(); return { tools: tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })), }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] Tool call: ${request.params.name}`); try { const tools = await this.loadAllTools(); const tool = tools.find(t => t.name === request.params.name); if (!tool) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'TOOL_NOT_FOUND', message: `Tool '${request.params.name}' not found`, }), }, ], isError: true, }; } const result = await tool.handler( request.params.arguments as Record, this.client ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorCode = (error as Error & { code?: string }).code || 'UNKNOWN_ERROR'; console.error(`[${timestamp}] Tool error:`, errorMessage); return { content: [ { type: 'text', text: JSON.stringify({ error: errorCode, message: errorMessage, }), }, ], isError: true, }; } }); // List resources (for UI apps) this.server.setRequestHandler(ListResourcesRequestSchema, async () => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] Listing resources`); return { resources: [ { uri: 'shopify://products', name: 'Products', description: 'List all products in the store', mimeType: 'application/json', }, { uri: 'shopify://orders', name: 'Orders', description: 'List all orders in the store', mimeType: 'application/json', }, { uri: 'shopify://customers', name: 'Customers', description: 'List all customers in the store', mimeType: 'application/json', }, { uri: 'shopify://inventory', name: 'Inventory', description: 'View inventory levels across locations', mimeType: 'application/json', }, ], }; }); // Read resource (for UI apps) this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] Reading resource: ${request.params.uri}`); const uri = request.params.uri; try { let data: unknown; if (uri === 'shopify://products') { data = await this.client.list('/products.json'); } else if (uri === 'shopify://orders') { data = await this.client.list('/orders.json'); } else if (uri === 'shopify://customers') { data = await this.client.list('/customers.json'); } else if (uri === 'shopify://inventory') { data = await this.client.list('/inventory_levels.json'); } else { throw new Error(`Unknown resource URI: ${uri}`); } return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[${timestamp}] Resource read error:`, errorMessage); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify({ error: 'RESOURCE_READ_ERROR', message: errorMessage, }), }, ], }; } }); } /** * Start the server with stdio transport */ async start(): Promise { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('Shopify MCP server running'); } /** * Get the underlying MCP server instance (for custom transports) */ getServer(): Server { return this.server; } }