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