243 lines
6.0 KiB
JavaScript
243 lines
6.0 KiB
JavaScript
#!/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',
|
|
},
|
|
})
|
|
}
|
|
}
|
|
})
|