Add Hermes API and MCP access
This commit is contained in:
242
mini-services/hermes-mcp/server.mjs
Normal file
242
mini-services/hermes-mcp/server.mjs
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/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',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user