#!/usr/bin/env node const apiBase = (process.env.OPTICZ_API_BASE || 'http://127.0.0.1:3000').replace(/\/$/, '') const apiKey = process.env.HERMES_API_KEY || 'hermes-demo-key-change-me' const tools = [ { name: 'opticz_status', description: 'Check OptiqueStock Hermes API health.', inputSchema: { type: 'object', properties: {}, }, }, { name: 'opticz_summary', description: 'Get store counts, low stock products, pending workshop count, and recent sales.', inputSchema: { type: 'object', properties: {}, }, }, { name: 'opticz_search_clients', description: 'Search OptiqueStock clients by name, phone, or email.', inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query.' }, limit: { type: 'number', description: 'Maximum results, default 20, max 50.' }, }, }, }, { name: 'opticz_search_products', description: 'Search active products by reference, designation, brand, or category.', inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query.' }, category: { type: 'string', description: 'Optional category filter, e.g. MONTURE or VERRE.' }, inStockOnly: { type: 'boolean', description: 'Only include products with stock. Defaults true.' }, limit: { type: 'number', description: 'Maximum results, default 25, max 75.' }, }, }, }, { name: 'opticz_create_client', description: 'Create a client, or return the existing client when the phone number already exists.', inputSchema: { type: 'object', required: ['nom', 'prenom', 'telephone'], properties: { nom: { type: 'string' }, prenom: { type: 'string' }, telephone: { type: 'string' }, email: { type: 'string' }, adresse: { type: 'string' }, ville: { type: 'string' }, codePostal: { type: 'string' }, notes: { type: 'string' }, }, }, }, { name: 'opticz_create_repair_sale', description: 'Create a paid repair sale for an existing client.', inputSchema: { type: 'object', required: ['clientId', 'repairType', 'repairDescription', 'repairPrice'], properties: { clientId: { type: 'string' }, repairType: { type: 'string' }, repairDescription: { type: 'string' }, repairPrice: { type: 'number' }, additionalCharges: { type: 'number' }, paymentMode: { type: 'string', enum: ['ESPECES', 'CARTE', 'CHEQUE', 'VIREMENT', 'BON_CAISSE'], }, }, }, }, ] async function api(path, options = {}) { const response = await fetch(`${apiBase}${path}`, { ...options, headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...(options.headers || {}), }, }) const text = await response.text() const data = text ? JSON.parse(text) : null if (!response.ok) { throw new Error(data?.error || `HTTP ${response.status}`) } return data } function query(params) { const search = new URLSearchParams() for (const [key, value] of Object.entries(params || {})) { if (value !== undefined && value !== null && value !== '') { search.set(key, String(value)) } } const value = search.toString() return value ? `?${value}` : '' } async function callTool(name, args) { switch (name) { case 'opticz_status': return api('/api/hermes/status') case 'opticz_summary': return api('/api/hermes/summary') case 'opticz_search_clients': return api(`/api/hermes/clients${query(args)}`) case 'opticz_search_products': return api(`/api/hermes/products${query(args)}`) case 'opticz_create_client': return api('/api/hermes/clients', { method: 'POST', body: JSON.stringify(args || {}), }) case 'opticz_create_repair_sale': return api('/api/hermes/sales/repair', { method: 'POST', body: JSON.stringify(args || {}), }) default: throw new Error(`Unknown tool: ${name}`) } } function send(message) { process.stdout.write(`${JSON.stringify(message)}\n`) } async function handle(message) { const { id, method, params } = message if (method === 'initialize') { send({ jsonrpc: '2.0', id, result: { protocolVersion: params?.protocolVersion || '2024-11-05', capabilities: { tools: {}, }, serverInfo: { name: 'opticz-hermes-mcp', version: '0.1.0', }, }, }) return } if (method === 'notifications/initialized') return if (method === 'tools/list') { send({ jsonrpc: '2.0', id, result: { tools }, }) return } if (method === 'tools/call') { try { const result = await callTool(params?.name, params?.arguments || {}) send({ jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }, }) } catch (error) { send({ jsonrpc: '2.0', id, result: { isError: true, content: [ { type: 'text', text: error instanceof Error ? error.message : 'Unknown error', }, ], }, }) } return } send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unsupported method: ${method}`, }, }) } let buffer = '' process.stdin.setEncoding('utf8') process.stdin.on('data', (chunk) => { buffer += chunk const lines = buffer.split(/\r?\n/) buffer = lines.pop() || '' for (const line of lines) { if (!line.trim()) continue try { handle(JSON.parse(line)) } catch (error) { send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: error instanceof Error ? error.message : 'Parse error', }, }) } } })