Migración desde michangarrito/apps/mcp-server - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e34b6828c
commit
d08364c9eb
9
.env
Normal file
9
.env
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# MiChangarrito MCP Server - Environment Variables
|
||||||
|
|
||||||
|
# Backend API
|
||||||
|
BACKEND_URL=http://localhost:3141
|
||||||
|
BACKEND_API_PREFIX=/api/v1
|
||||||
|
|
||||||
|
# Server Name
|
||||||
|
MCP_SERVER_NAME=michangarrito-mcp
|
||||||
|
MCP_SERVER_VERSION=1.0.0
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# michangarrito-mcp-server-v2
|
|
||||||
|
|
||||||
MCP Server de michangarrito - Workspace V2
|
|
||||||
1836
package-lock.json
generated
Normal file
1836
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "michangarrito-mcp-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MiChangarrito MCP Server - Tools for LLM integration",
|
||||||
|
"author": "ISEM",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx watch src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
|
"axios": "^1.6.5",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.8",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/index.ts
Normal file
150
src/index.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
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 { productTools } from './tools/products.js';
|
||||||
|
import { orderTools } from './tools/orders.js';
|
||||||
|
import { fiadoTools } from './tools/fiado.js';
|
||||||
|
import { customerTools } from './tools/customers.js';
|
||||||
|
import { inventoryTools } from './tools/inventory.js';
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3141';
|
||||||
|
|
||||||
|
// Create server instance
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'michangarrito-mcp',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine all tools
|
||||||
|
const allTools = [
|
||||||
|
...productTools,
|
||||||
|
...orderTools,
|
||||||
|
...fiadoTools,
|
||||||
|
...customerTools,
|
||||||
|
...inventoryTools,
|
||||||
|
];
|
||||||
|
|
||||||
|
// List available tools
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: allTools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
const tool = allTools.find((t) => t.name === name);
|
||||||
|
if (!tool) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Tool not found: ${name}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tool.handler(args || {}, BACKEND_URL);
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${message}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List resources
|
||||||
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'michangarrito://config/business',
|
||||||
|
name: 'Business Configuration',
|
||||||
|
description: 'Current business settings and configuration',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'michangarrito://catalog/categories',
|
||||||
|
name: 'Product Categories',
|
||||||
|
description: 'List of product categories',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Read resources
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const { uri } = request.params;
|
||||||
|
|
||||||
|
switch (uri) {
|
||||||
|
case 'michangarrito://config/business':
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify({
|
||||||
|
name: 'MiChangarrito',
|
||||||
|
currency: 'MXN',
|
||||||
|
timezone: 'America/Mexico_City',
|
||||||
|
fiadoEnabled: true,
|
||||||
|
maxFiadoAmount: 500,
|
||||||
|
workingHours: {
|
||||||
|
open: '07:00',
|
||||||
|
close: '22:00',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'michangarrito://catalog/categories':
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify([
|
||||||
|
{ id: 'bebidas', name: 'Bebidas', icon: '🥤' },
|
||||||
|
{ id: 'botanas', name: 'Botanas', icon: '🍿' },
|
||||||
|
{ id: 'abarrotes', name: 'Abarrotes', icon: '🛒' },
|
||||||
|
{ id: 'lacteos', name: 'Lácteos', icon: '🥛' },
|
||||||
|
{ id: 'panaderia', name: 'Panadería', icon: '🍞' },
|
||||||
|
{ id: 'limpieza', name: 'Limpieza', icon: '🧹' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Resource not found: ${uri}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error('MiChangarrito MCP Server running on stdio');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
175
src/tools/customers.ts
Normal file
175
src/tools/customers.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: any, backendUrl: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customerTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'get_customer_info',
|
||||||
|
description: 'Obtiene informacion de un cliente por telefono',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['phone'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/customers/phone/${args.phone}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
customer: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
customer: null,
|
||||||
|
message: 'Cliente no encontrado',
|
||||||
|
isNewCustomer: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'register_customer',
|
||||||
|
description: 'Registra un nuevo cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nombre completo del cliente',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Email del cliente (opcional)',
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Direccion del cliente (opcional)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['phone', 'name'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${backendUrl}/api/v1/customers`, {
|
||||||
|
phone: args.phone,
|
||||||
|
name: args.name,
|
||||||
|
email: args.email,
|
||||||
|
address: args.address,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
customer: data,
|
||||||
|
message: 'Cliente registrado exitosamente',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
customer: {
|
||||||
|
id: 'new-customer-id',
|
||||||
|
phone: args.phone,
|
||||||
|
name: args.name,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
message: 'Cliente registrado exitosamente',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_customer_purchase_history',
|
||||||
|
description: 'Obtiene el historial de compras de un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Numero de compras a mostrar',
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['phone'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`${backendUrl}/api/v1/customers/phone/${args.phone}/purchases?limit=${args.limit || 10}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
purchases: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
purchases: [],
|
||||||
|
summary: {
|
||||||
|
totalPurchases: 0,
|
||||||
|
totalSpent: 0,
|
||||||
|
averageTicket: 0,
|
||||||
|
favoriteProducts: [],
|
||||||
|
},
|
||||||
|
message: 'Sin historial de compras',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_customer_stats',
|
||||||
|
description: 'Obtiene estadisticas del cliente (total compras, productos favoritos, etc)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['phone'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/customers/phone/${args.phone}/stats`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stats: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
totalOrders: 0,
|
||||||
|
totalSpent: 0,
|
||||||
|
averageTicket: 0,
|
||||||
|
lastVisit: null,
|
||||||
|
memberSince: new Date().toISOString(),
|
||||||
|
loyaltyPoints: 0,
|
||||||
|
favoriteProducts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
253
src/tools/fiado.ts
Normal file
253
src/tools/fiado.ts
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: any, backendUrl: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fiadoTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'get_fiado_balance',
|
||||||
|
description: 'Consulta el saldo de fiado (credito) de un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
customer_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del cliente',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const phone = args.customer_phone;
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/customers/phone/${phone}/fiado`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
balance: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
balance: {
|
||||||
|
currentDebt: 0,
|
||||||
|
creditLimit: 500,
|
||||||
|
availableCredit: 500,
|
||||||
|
lastPaymentDate: null,
|
||||||
|
pendingItems: [],
|
||||||
|
},
|
||||||
|
message: 'Cliente sin deuda de fiado',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_fiado',
|
||||||
|
description: 'Registra una compra a fiado (credito) para un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Monto de la compra a fiado',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Descripcion de la compra',
|
||||||
|
},
|
||||||
|
due_date: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Fecha de vencimiento (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customer_phone', 'amount'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${backendUrl}/api/v1/customers/fiado`, {
|
||||||
|
phone: args.customer_phone,
|
||||||
|
amount: args.amount,
|
||||||
|
description: args.description,
|
||||||
|
dueDate: args.due_date,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fiado: data,
|
||||||
|
message: `Fiado de $${args.amount} registrado exitosamente`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fiado: {
|
||||||
|
id: 'mock-fiado-id',
|
||||||
|
amount: args.amount,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
message: `Fiado de $${args.amount} registrado exitosamente`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'register_fiado_payment',
|
||||||
|
description: 'Registra un abono o pago al fiado de un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Monto del abono',
|
||||||
|
},
|
||||||
|
payment_method: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['cash', 'transfer', 'card'],
|
||||||
|
description: 'Metodo de pago',
|
||||||
|
},
|
||||||
|
fiado_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del fiado especifico a abonar (opcional)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customer_phone', 'amount'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${backendUrl}/api/v1/customers/fiado/payment`, {
|
||||||
|
phone: args.customer_phone,
|
||||||
|
amount: args.amount,
|
||||||
|
paymentMethod: args.payment_method || 'cash',
|
||||||
|
fiadoId: args.fiado_id,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payment: data,
|
||||||
|
message: `Abono de $${args.amount} registrado exitosamente`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payment: {
|
||||||
|
id: 'mock-payment-id',
|
||||||
|
amount: args.amount,
|
||||||
|
remainingDebt: 0,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
message: `Abono de $${args.amount} registrado exitosamente`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_fiado_history',
|
||||||
|
description: 'Obtiene el historial de fiados y pagos de un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['pending', 'paid', 'overdue', 'all'],
|
||||||
|
description: 'Filtrar por estado',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Numero maximo de registros',
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customer_phone'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`${backendUrl}/api/v1/customers/phone/${args.customer_phone}/fiado/history`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
history: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
history: {
|
||||||
|
fiados: [],
|
||||||
|
payments: [],
|
||||||
|
summary: {
|
||||||
|
totalDebts: 0,
|
||||||
|
totalPayments: 0,
|
||||||
|
currentBalance: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: 'Sin historial de fiados',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'check_fiado_eligibility',
|
||||||
|
description: 'Verifica si un cliente puede comprar a fiado y cuanto',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Monto que desea fiar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customer_phone'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`${backendUrl}/api/v1/customers/phone/${args.customer_phone}/fiado/eligibility`
|
||||||
|
);
|
||||||
|
const eligible = !args.amount || data.availableCredit >= args.amount;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
eligible,
|
||||||
|
availableCredit: data.availableCredit,
|
||||||
|
currentDebt: data.currentDebt,
|
||||||
|
creditLimit: data.creditLimit,
|
||||||
|
message: eligible
|
||||||
|
? `Puede fiar hasta $${data.availableCredit}`
|
||||||
|
: `Credito insuficiente. Disponible: $${data.availableCredit}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
eligible: true,
|
||||||
|
availableCredit: 500,
|
||||||
|
currentDebt: 0,
|
||||||
|
creditLimit: 500,
|
||||||
|
message: 'Cliente elegible para fiado',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
173
src/tools/inventory.ts
Normal file
173
src/tools/inventory.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: any, backendUrl: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const inventoryTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'check_stock',
|
||||||
|
description: 'Verifica el stock actual de un producto',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
product_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del producto',
|
||||||
|
},
|
||||||
|
product_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nombre del producto (busqueda)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/inventory/${args.product_id}/stock`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stock: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stock: {
|
||||||
|
productId: args.product_id,
|
||||||
|
currentStock: 24,
|
||||||
|
minStock: 5,
|
||||||
|
maxStock: 50,
|
||||||
|
status: 'ok',
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_low_stock_products',
|
||||||
|
description: 'Lista productos con stock bajo que necesitan reabastecimiento',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
threshold: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Umbral de stock bajo (usa el minimo configurado si no se especifica)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/inventory/low-stock`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
lowStockProducts: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
lowStockProducts: [],
|
||||||
|
message: 'No hay productos con stock bajo',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'record_inventory_movement',
|
||||||
|
description: 'Registra un movimiento de inventario (entrada, salida, ajuste)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
product_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del producto',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['purchase', 'sale', 'adjustment', 'waste', 'return'],
|
||||||
|
description: 'Tipo de movimiento',
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Cantidad (positiva para entradas, negativa para salidas)',
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Razon o nota del movimiento',
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Costo unitario (para compras)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['product_id', 'type', 'quantity'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${backendUrl}/api/v1/inventory/movements`, {
|
||||||
|
productId: args.product_id,
|
||||||
|
type: args.type,
|
||||||
|
quantity: args.quantity,
|
||||||
|
reason: args.reason,
|
||||||
|
unitCost: args.cost,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
movement: data,
|
||||||
|
message: `Movimiento de ${args.quantity} unidades registrado`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
movement: {
|
||||||
|
id: 'mock-movement-id',
|
||||||
|
productId: args.product_id,
|
||||||
|
type: args.type,
|
||||||
|
quantity: args.quantity,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
message: `Movimiento de ${args.quantity} unidades registrado`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_inventory_value',
|
||||||
|
description: 'Calcula el valor total del inventario',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Categoria para filtrar (opcional)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/inventory/value`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: {
|
||||||
|
totalCost: 15000,
|
||||||
|
totalSaleValue: 22000,
|
||||||
|
potentialProfit: 7000,
|
||||||
|
productCount: 150,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
189
src/tools/orders.ts
Normal file
189
src/tools/orders.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: any, backendUrl: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'create_order',
|
||||||
|
description: 'Crea un nuevo pedido para un cliente',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de telefono del cliente',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Lista de productos con cantidades',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
product_id: { type: 'string' },
|
||||||
|
quantity: { type: 'number' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payment_method: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['cash', 'card', 'fiado'],
|
||||||
|
description: 'Metodo de pago',
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Notas adicionales del pedido',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customer_phone', 'items'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${backendUrl}/api/v1/orders`, {
|
||||||
|
customerPhone: args.customer_phone,
|
||||||
|
items: args.items,
|
||||||
|
paymentMethod: args.payment_method || 'cash',
|
||||||
|
notes: args.notes,
|
||||||
|
source: 'whatsapp',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
order: data,
|
||||||
|
message: `Pedido ${data.orderNumber} creado exitosamente`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const orderNumber = `MCH-${Date.now().toString(36).toUpperCase()}`;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
order: {
|
||||||
|
id: 'mock-id',
|
||||||
|
orderNumber,
|
||||||
|
status: 'pending',
|
||||||
|
items: args.items,
|
||||||
|
total: args.items.reduce((sum: number, i: any) => sum + (i.quantity * 18), 0),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
message: `Pedido ${orderNumber} creado exitosamente`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_order_status',
|
||||||
|
description: 'Consulta el estado de un pedido',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
order_number: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Numero de pedido (ej: MCH-ABC123)',
|
||||||
|
},
|
||||||
|
customer_phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Telefono del cliente para buscar sus pedidos',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
let url = `${backendUrl}/api/v1/orders`;
|
||||||
|
if (args.order_number) {
|
||||||
|
url = `${backendUrl}/api/v1/orders/number/${args.order_number}`;
|
||||||
|
} else if (args.customer_phone) {
|
||||||
|
url = `${backendUrl}/api/v1/orders?phone=${args.customer_phone}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await axios.get(url);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
orders: Array.isArray(data) ? data : [data],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
orders: [],
|
||||||
|
message: 'No se encontraron pedidos activos',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_order_status',
|
||||||
|
description: 'Actualiza el estado de un pedido',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
order_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del pedido',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['confirmed', 'preparing', 'ready', 'completed', 'cancelled'],
|
||||||
|
description: 'Nuevo estado del pedido',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['order_id', 'status'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.patch(`${backendUrl}/api/v1/orders/${args.order_id}/status`, {
|
||||||
|
status: args.status,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
order: data,
|
||||||
|
message: `Pedido actualizado a estado: ${args.status}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Pedido actualizado a estado: ${args.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancel_order',
|
||||||
|
description: 'Cancela un pedido',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
order_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del pedido a cancelar',
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Razon de la cancelacion',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['order_id'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
await axios.patch(`${backendUrl}/api/v1/orders/${args.order_id}/status`, {
|
||||||
|
status: 'cancelled',
|
||||||
|
cancellationReason: args.reason,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Pedido cancelado exitosamente',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Pedido cancelado exitosamente',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
155
src/tools/products.ts
Normal file
155
src/tools/products.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, any>;
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
handler: (args: any, backendUrl: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const productTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: 'list_products',
|
||||||
|
description: 'Lista productos disponibles, opcionalmente filtrados por categoria',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Categoria para filtrar (bebidas, botanas, abarrotes, lacteos)',
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Termino de busqueda por nombre',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Numero maximo de productos a retornar',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (args.category) params.append('category', args.category);
|
||||||
|
if (args.search) params.append('search', args.search);
|
||||||
|
if (args.limit) params.append('limit', args.limit.toString());
|
||||||
|
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/products?${params}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: data.length,
|
||||||
|
products: data.map((p: any) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
price: p.salePrice,
|
||||||
|
stock: p.stock,
|
||||||
|
category: p.category,
|
||||||
|
available: p.stock > 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Mock response for development
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: 5,
|
||||||
|
products: [
|
||||||
|
{ id: '1', name: 'Coca-Cola 600ml', price: 18.0, stock: 24, category: 'bebidas', available: true },
|
||||||
|
{ id: '2', name: 'Sabritas Original', price: 15.0, stock: 12, category: 'botanas', available: true },
|
||||||
|
{ id: '3', name: 'Leche Lala 1L', price: 28.0, stock: 8, category: 'lacteos', available: true },
|
||||||
|
{ id: '4', name: 'Pan Bimbo Grande', price: 45.0, stock: 5, category: 'panaderia', available: true },
|
||||||
|
{ id: '5', name: 'Fabuloso 1L', price: 32.0, stock: 6, category: 'limpieza', available: true },
|
||||||
|
].filter((p) => !args.category || p.category === args.category),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_product_details',
|
||||||
|
description: 'Obtiene detalles completos de un producto especifico',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
product_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del producto',
|
||||||
|
},
|
||||||
|
product_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nombre del producto (busqueda aproximada)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/products/${args.product_id}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
product: data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
product: {
|
||||||
|
id: args.product_id || '1',
|
||||||
|
name: args.product_name || 'Producto de ejemplo',
|
||||||
|
description: 'Descripcion del producto',
|
||||||
|
price: 18.0,
|
||||||
|
stock: 24,
|
||||||
|
category: 'bebidas',
|
||||||
|
barcode: '7501055300051',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'check_product_availability',
|
||||||
|
description: 'Verifica si un producto esta disponible y su cantidad en stock',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
product_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID del producto',
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Cantidad deseada',
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['product_id'],
|
||||||
|
},
|
||||||
|
handler: async (args, backendUrl) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${backendUrl}/api/v1/products/${args.product_id}`);
|
||||||
|
const requested = args.quantity || 1;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
available: data.stock >= requested,
|
||||||
|
currentStock: data.stock,
|
||||||
|
requestedQuantity: requested,
|
||||||
|
message:
|
||||||
|
data.stock >= requested
|
||||||
|
? `Si hay ${requested} unidades disponibles`
|
||||||
|
: `Solo hay ${data.stock} unidades, necesitas ${requested}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
available: true,
|
||||||
|
currentStock: 24,
|
||||||
|
requestedQuantity: args.quantity || 1,
|
||||||
|
message: 'Producto disponible',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user