index.ts•19.6 kB
#!/usr/bin/env node
import { config } from 'dotenv';
// Detectar si se pasa el argumento 'production' para cargar el archivo .env.production
const envFile = process.argv.includes('production') ? '.env.production' : '.env';
config({ path: envFile });
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { v4 as uuidv4 } from 'uuid';
import { API_BASE_URL, API_EMAIL, authenticate, fetchGetPedidos, fetchGetProductos, fetchGetSizes } from './services/services.js';
import { Pagination, Pedido, Producto, Size } from './interfaces/models.js';
import { fechaToTimestamp, findProductoByDescripcion, findSizeByDescripcion, getEndOfWeek, getStartOfWeek } from './functions/Functions.js';
import { PedidoRequest, PedidosRequest, ProductoPedidoRequest, ProductoRequest } from './interfaces/request.js';
class PedidosAPI {
private baseUrl: string;
private authToken: string | null = null;
private productosCache: Producto[] | null = null;
private sizesCache: Size[] | null = null;
constructor() {
this.baseUrl = API_BASE_URL;
}
/**
* Obtiene todos los productos activos
*/
async getProductos(): Promise<Producto[]> {
if (this.productosCache) {
return this.productosCache;
}
// Asegurar que estamos autenticados
if (!this.authToken) {
this.authToken = await authenticate();
if (!this.authToken) {
throw new Error("No se pudo autenticar para obtener productos");
}
}
this.productosCache = await fetchGetProductos(this.authToken);
return this.productosCache;
}
/**
* Obtiene todos los tamaños disponibles
*/
async getSizes(): Promise<Size[]> {
if (this.sizesCache) {
return this.sizesCache;
}
// Asegurar que estamos autenticados
if (!this.authToken) {
this.authToken = await authenticate();
if (!this.authToken) {
throw new Error("No se pudo autenticar para obtener tamaños");
}
}
this.sizesCache = await fetchGetSizes(this.authToken);
return this.sizesCache;
}
/**
* Obtiene todos los tamaños disponibles
*/
async getPedidos(pedidosRequest:PedidosRequest): Promise<Pedido[]> {
// Asegurar que estamos autenticados
if (!this.authToken) {
this.authToken = await authenticate();
if (!this.authToken) {
throw new Error("No se pudo autenticar para obtener tamaños");
}
}
if (!pedidosRequest) {
return [];
}
let { fechaInicio, fechaFin, estatus } = pedidosRequest;
if (!estatus) {
estatus = 'ALL';
}
if (!fechaInicio && !fechaFin) {
fechaInicio = getStartOfWeek().toISOString().split('T')[0];
fechaFin = getEndOfWeek().toISOString().split('T')[0];
}else if(fechaInicio && !fechaFin) {
fechaFin = fechaInicio
}
const pagination:Pagination = { pageSize: 150,page: 0, totalItems:0 }; // Ajusta el tamaño de página según tus necesidades
const pedidos = await fetchGetPedidos(
this.authToken,
estatus ?? null,
fechaInicio ?? null,
fechaFin ?? null,
pagination
);
return pedidos;
}
/**
* Transforma los parámetros de entrada al formato JSON requerido
*/
async crearPedidoJSON(
fechaEntrega: string,
lugarEntrega: string,
cliente: string,
productosPedido: ProductoPedidoRequest[]
): Promise<PedidoRequest> {
console.log("🔄 Transformando pedido a JSON...");
// Asegurar que tenemos productos y tamaños cargados
const productosForSearch = await this.getProductos();
const sizesForSearch = await this.getSizes();
const productosJSON: ProductoRequest[] = [];
let total = 0;
for (const productoPedido of productosPedido) {
// Buscar el producto
const productoObj = findProductoByDescripcion(productosForSearch,productoPedido.producto);
if (!productoObj) {
throw new Error(`Producto no encontrado: ${productoPedido.producto}`);
}
// Buscar el tamaño
const sizeObj = findSizeByDescripcion(sizesForSearch,productoPedido.tamaño);
if (!sizeObj) {
throw new Error(`Tamaño no encontrado: ${productoPedido.tamaño}`);
}
// Generar ID único para el producto en el pedido
const productoId = uuidv4();
const precio = Number(productoPedido.precio);
const cantidad = Number(productoPedido.cantidad);
const productoJSON: ProductoRequest = {
precio: precio,
producto: {
id: productoObj.id,
imagen: productoObj.imagen || '',
descripcion: productoObj.descripcion
},
id: productoId,
caracteristicas: productoPedido.caracteristicas || [],
size: {
descripcion: sizeObj.descripcion,
id: sizeObj.id
},
cantidad: cantidad
};
productosJSON.push(productoJSON);
total += precio * cantidad;
}
// Crear el JSON final
const pedidoJSON: PedidoRequest = {
total: total,
lugarEntrega: lugarEntrega,
productos: productosJSON,
cliente: cliente,
fechaEntrega: fechaToTimestamp(fechaEntrega),
registradoPor: API_EMAIL
};
console.log("✅ Pedido JSON creado exitosamente");
return pedidoJSON;
}
/**
* Registra el pedido en la API
*/
async registrarPedido(pedidoJSON: PedidoRequest): Promise<any> {
console.log("📝 Registrando pedido...");
if (!this.authToken) {
this.authToken = await authenticate();
if (!this.authToken) {
throw new Error("No se pudo autenticar");
}
}
try {
const response = await fetch(`${this.baseUrl}/api/pedidos`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': `access_token=${this.authToken}`
},
body: JSON.stringify(pedidoJSON)
});
if (response.ok) {
const resultado = await response.json();
console.log("✅ Pedido registrado exitosamente");
return resultado;
} else if (response.status === 401) {
// Token expirado, intentar reautenticar
console.log("🔄 Token expirado, reautenticando...");
this.authToken = null;
this.authToken = await authenticate();
if (this.authToken) {
// Reintentar la petición
return await this.registrarPedido(pedidoJSON);
} else {
throw new Error("No se pudo reautenticar");
}
} else {
const errorText = await response.text();
console.error(`❌ Error registrando pedido: ${response.status} - ${errorText}`);
return { error: `HTTP ${response.status}: ${errorText}` };
}
} catch (error) {
console.error(`❌ Error registrando pedido: ${error}`);
return { error: String(error) };
}
}
/**
* Limpia el caché de productos y tamaños
*/
clearCache(): void {
this.productosCache = null;
this.sizesCache = null;
console.log("🗑️ Caché limpiado");
}
/**
* Invalida el token y limpia caché (útil cuando hay problemas de autenticación)
*/
invalidateAuth(): void {
this.authToken = null;
this.clearCache();
console.log("🔓 Autenticación invalidada y caché limpiado");
}
/**
* Obtiene el estado de autenticación
*/
getAuthStatus(): { authenticated: boolean; tokenAvailable: boolean } {
return {
authenticated: this.authToken !== null,
tokenAvailable: Boolean(this.authToken)
};
}
}
// Inicializar la API
const api = new PedidosAPI();
// Crear el servidor MCP
const server = new Server(
{
name: "pedidos-mcp",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Manejar lista de recursos
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "pedidos://productos",
name: "Productos disponibles",
description: "Lista de todos los productos activos disponibles para pedidos",
mimeType: "application/json",
},
{
uri: "pedidos://sizes",
name: "Tamaños disponibles",
description: "Lista de todos los tamaños disponibles para productos",
mimeType: "application/json",
},
{
uri: "pedidos://auth-status",
name: "Estado de autenticación",
description: "Información sobre el estado actual de autenticación",
mimeType: "application/json",
},
{
uri: "pedidos://pedidos",
name: "Pedidos registrados",
description: "Lista de todos los pedidos registrados (parámetros opcionales: fechaInicio, fechaFin, estatus)",
mimeType: "application/json",
}
],
};
});
// Manejar lectura de recursos
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (!uri.startsWith("pedidos://")) {
throw new Error(`Esquema no soportado: ${uri}`);
}
const path = uri.replace("pedidos://", "");
switch (path) {
case "productos":
const productos = await api.getProductos();
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(productos, null, 2),
},
],
};
case "sizes":
const sizes = await api.getSizes();
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(sizes, null, 2),
},
],
};
case "auth-status":
const status = api.getAuthStatus();
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(status, null, 2),
},
],
};
case "pedidos": {
// Permitir parámetros opcionales por query string: fechaInicio, fechaFin, estatus
let fechaInicio = typeof request.params.fechaInicio === 'string' ? request.params.fechaInicio : undefined;
let fechaFin = typeof request.params.fechaFin === 'string' ? request.params.fechaFin : undefined;
let estatus = typeof request.params.estatus === 'string' ? request.params.estatus : undefined;
const pedidosRequest = { fechaInicio, fechaFin, estatus };
const pedidos = await api.getPedidos(pedidosRequest);
return {
contents: [
{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(pedidos, null, 2),
},
],
};
}
default:
throw new Error(`Recurso no encontrado: ${path}`);
}
});
// Manejar lista de herramientas
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "crear_pedido",
description: "Crea un nuevo pedido transformando los parámetros al formato JSON requerido y registrándolo en la API",
inputSchema: {
type: "object",
properties: {
fechaEntrega: {
type: "string",
description: "Fecha de entrega del pedido (formatos: DD-MM-YYYY)"
},
lugarEntrega: {
type: "string",
description: "Lugar donde se entregará el pedido"
},
cliente: {
type: "string",
description: "Nombre del cliente"
},
productos: {
type: "array",
description: "Lista de productos a incluir en el pedido",
items: {
type: "object",
properties: {
producto: {
type: "string",
description: "Nombre/descripción del producto"
},
cantidad: {
type: "integer",
description: "Cantidad del producto"
},
precio: {
type: "number",
description: "Precio unitario del producto"
},
tamaño: {
type: "string",
description: "Tamaño del producto"
},
caracteristicas: {
type: "array",
items: { type: "string" },
description: "Lista de características especiales del producto"
}
},
required: ["producto", "cantidad", "precio", "tamaño"]
}
}
},
required: ["fechaEntrega", "lugarEntrega", "cliente", "productos"]
}
},
{
name: "consultar_pedidos",
description: "Consulta los pedidos registrados. Parámetros opcionales: fechaInicio, fechaFin, estatus.",
inputSchema: {
type: "object",
properties: {
fechaInicio: {
type: "string",
description: "Fecha de inicio (DD-MM-YYYY)"
},
fechaFin: {
type: "string",
description: "Fecha de fin (DD-MM-YYYY)"
},
estatus: {
type: "string",
description: "Estatus del pedido (ejemplo: ALL, BACKLOG, CANCELED, INCOMPLETE, DELETE)"
}
},
required: []
}
},
{
name: "autenticar",
description: "Autentica con la API para obtener el token de acceso",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false
}
},
{
name: "buscar_producto",
description: "Busca un producto por su descripción",
inputSchema: {
type: "object",
properties: {
descripcion: {
type: "string",
description: "Descripción del producto a buscar"
}
},
required: ["descripcion"]
}
},
{
name: "buscar_todos_productos",
description: "Busca todos los productos disponibles",
inputSchema: {
}
},
{
name: "buscar_tamaño",
description: "Busca un tamaño por su descripción",
inputSchema: {
type: "object",
properties: {
descripcion: {
type: "string",
description: "Descripción del tamaño a buscar"
}
},
required: ["descripcion"]
}
},
{
name: "limpiar_cache",
description: "Limpia el caché de productos y tamaños para forzar una actualización",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false
}
},
{
name: "invalidar_auth",
description: "Invalida la autenticación actual y limpia caché (útil cuando hay problemas de token)",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false
}
}
],
};
});
// Manejar llamadas a herramientas
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "crear_pedido": {
const { fechaEntrega, lugarEntrega, cliente, productos } = args as {
fechaEntrega: string;
lugarEntrega: string;
cliente: string;
productos: ProductoPedidoRequest[];
};
// Crear el JSON del pedido
const pedidoJSON = await api.crearPedidoJSON(
fechaEntrega,
lugarEntrega,
cliente,
productos
);
// Registrar el pedido
const resultado = await api.registrarPedido(pedidoJSON);
const response = {
pedido_creado: pedidoJSON,
resultado_api: resultado
};
return {
content: [
{
type: "text",
text: JSON.stringify(response, null, 2)
}
]
};
}
case "consultar_pedidos": {
// Permitir parámetros opcionales: fechaInicio, fechaFin, estatus
const { fechaInicio, fechaFin, estatus } = args as { fechaInicio?: string; fechaFin?: string; estatus?: string };
const pedidosRequest = { fechaInicio, fechaFin, estatus };
const pedidos = await api.getPedidos(pedidosRequest);
return {
content: [
{
type: "text",
text: JSON.stringify(pedidos, null, 2)
}
]
};
}
case "autenticar": {
const success = await authenticate();
return {
content: [
{
type: "text",
text: `Autenticación: ${success ? 'exitosa' : 'fallida'}`
}
]
};
}
case "buscar_producto": {
const { descripcion: descripcionProducto } = args as { descripcion: string };
const productosForSearch = await api.getProductos();
const producto = findProductoByDescripcion(productosForSearch, descripcionProducto);
return {
content: [
{
type: "text",
text: producto
? JSON.stringify(producto, null, 2)
: `Producto no encontrado: ${descripcionProducto}`
}
]
};
}
case "buscar_todos_productos": {
const productosArray = await api.getProductos();
return {
content: [
{
type: "text",
text: productosArray
? JSON.stringify(productosArray, null, 2)
: `No hay productos disponibles`
}
]
};
}
case "buscar_tamaño": {
const { descripcion: descripcionTamaño } = args as { descripcion: string };
const sizesForSearch = await api.getSizes();
const size = findSizeByDescripcion(sizesForSearch,descripcionTamaño);
return {
content: [
{
type: "text",
text: size
? JSON.stringify(size, null, 2)
: `Tamaño no encontrado: ${descripcionTamaño}`
}
]
};
}
case "limpiar_cache": {
api.clearCache();
return {
content: [
{
type: "text",
text: "Caché limpiado exitosamente"
}
]
};
}
case "invalidar_auth": {
api.invalidateAuth();
return {
content: [
{
type: "text",
text: "Autenticación invalidada y caché limpiado exitosamente"
}
]
};
}
default:
throw new Error(`Herramienta no conocida: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error ejecutando ${name}: ${error}`
}
]
};
}
});
// Función principal
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("🚀 Servidor MCP de Pedidos iniciado");
console.log("user: ", API_EMAIL);
console.log("API Base URL: ", API_BASE_URL);