Merge pull request #18 from josedario87/feature/refactor-resources-add-delete-chat
All checks were successful
Deploy conversation layer / deploy (push) Successful in 2m14s

Feature/refactor resources add delete chat
This commit is contained in:
josedario87
2025-06-07 00:22:21 -06:00
committed by GitHub
13 changed files with 714 additions and 132 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: sync-from-github sync-to-github chat router agent
.PHONY: sync-from-github sync-to-github chat router agent mcp
# Pull latest changes from the GitHub mirror and push them to Gitea
sync-from-github:
@@ -18,4 +18,5 @@ router:
agent:
cd conversation-layer-agent && npm install && npm run dev
mcp:
cd mcp && npm install && npm run dev

View File

@@ -55,7 +55,7 @@ let mcpTransport: StreamableHTTPClientTransport | undefined;
async function getMcpClient(): Promise<Client> {
if (!mcpClient) {
mcpClient = new Client({ name: 'planilla-client', version: '1.0.0' });
mcpClient = new Client({ name: 'conversation-layer-mcp-client', version: '1.0.0' });
mcpTransport = new StreamableHTTPClientTransport(new URL(MCP_URL));
await mcpClient.connect(mcpTransport);
}
@@ -73,7 +73,7 @@ const systemPromt = `
- cuando sos activado, tenes que entender los mensajes del objeto 'conversation' desde el ultimo mensaje pues es el mas nuevo.
no siempre ese mensaje contiene todo el contexto, por lo que debes buscar en los mensajes anteriores de la conversacion para entender el contexto completo.
- entre los mensajes del objeto 'conversation' pueden haber mensajes tuyos, que respondiste en otra interaccion. recorda que podes aprovechar esta capacidad para hablar con vos mismo en el futuro
- tenes la capacidad de llamar de manera secuencial a las herramientas del mcp planilla, eso significa que podes llamar una herramienta, esperar su respuesta y usar esa respuesta para llamar a la siguiente
- tenes la capacidad de llamar de manera secuencial a las herramientas del mcp que maneja la conversation-layer, eso significa que podes llamar una herramienta, esperar su respuesta y usar esa respuesta para llamar a la siguiente
- la respuesta final siempre la tenes que dar hasta terminar de llamar a todas tus herramientas y obtener un resultado de cada una de ellas para luego usar eso para guiar tu respuesta final
## tu proposito
@@ -95,6 +95,11 @@ const systemPromt = `
###aclaratorias
- los mensajes e2e son mensajes que utiliza whatsapp para notificar cosas de su sistema, no son mensajes de los usuarios y no debes responderlos.
- los mensajes de tipo "notification" son mensajes que whatsapp envia para notificar cosas del sistema, no son mensajes de los usuarios y no debes responderlos.
### mcp
aqui se encuentrar registradas todas las herramientas que podes utilizar, hay dos grandes grupos de herramientas: conversation y whatsapp.
- las herramientas de conversation son para manipular los conversation objects que al final es todo lo que todos los agents ven
- las herramientas de whatsapp son para manipular las acciones que nucleo toma en su propiia cuenta de whatsapp, por ejemplo enviar mensajes, crear grupos, etc.
`;
const app = express();
@@ -110,14 +115,14 @@ app.post('/', async (req, res) => {
try {
const contents = `systemPrompt: ${systemPromt}\nConversation:\n${JSON.stringify(conversation)}\n`;
console.log(' contents', contents);
// console.log(' contents', contents);
const config: any = {};
// if (message.toLowerCase().includes('/planilla')) {
if (true) {
console.log('Using Model Context Protocol tools ', MCP_URL);
// const client = await getMcpClient();
// config.tools = [mcpToTool(client)];
const client = await getMcpClient();
config.tools = [mcpToTool(client)];
}
const result = await genAI.models.generateContent({
model: 'gemini-2.0-flash',

View File

@@ -45,12 +45,24 @@ services:
environment:
- PORT=8001
- GEMINI_API_KEY= AIzaSyA9fI1mron-NVgghygu7B4sco7t6raXB8M
- MCP_URL= http:planilla-mcp:5000/mcp
- MCP_URL=http://conversation-layer-mcp:5000/mcp
ports:
- "8001:8001"
networks:
- principal
conversation-layer-mcp:
build: ./mcp
image: gitea.interno.com/nucleo000/conversation-layer-mcp:latest
container_name: conversation-layer-mcp
environment:
- PORT=5000
- WHATSAPP_ROUTER_URL=http://whatsapp-router:3001
ports:
- "5000:5000"
networks:
- principal
volumes:
nucleo_whatsapp_sessions:

View File

@@ -1,11 +1,12 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import registerTareas from "./modules/tareas.js";
import registerWhatsappIntegration from "./modules/whatsappIntegration.js";
import registerConversationIntegration from "./modules/conversationIntegration.js";
export function createServer() {
const server = new McpServer({ name: "planilla-mcp", version: "0.1.0" });
const server = new McpServer({ name: "conversation-layer-mcp", version: "0.1.0" });
registerTareas(server);
registerWhatsappIntegration(server);
registerConversationIntegration(server);
return server;
}

View File

@@ -3,7 +3,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
import express from "express";
import { createServer } from "./createServer.js";
console.log('este no tiene variables de entorno, es un servidor MCP Planilla');
console.log('este no tiene variables de entorno, es un servidor MCP conversation-layer');
async function main() {
@@ -13,14 +13,14 @@ async function main() {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("MCP Planilla server listo (stdio)");
console.log("MCP conversation-layer server listo (stdio)");
} else {
// bootLog("Modo transporte: HTTP streamable");
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
console.log(`[HTTP] ${req.method} ${req.originalUrl} ${req.statusCode} ${req.res.body}`);
console.log(`[HTTP] ${req.method} ${req.originalUrl} ${req.statusCode}`);
next();
});
@@ -59,7 +59,7 @@ async function main() {
);
app.listen(port, () => {
console.log(`MCP Planilla HTTP server listening on port ${port}`);
console.log(`MCP conversation-layer HTTP server listening on port ${port}`);
});
}
}

View File

@@ -1,4 +1,12 @@
export const API_BASE_URL = process.env.PLANILLA_API_URL || "http://localhost:4000";
import dotenv from "dotenv";
dotenv.config();
export const API_BASE_URL = process.env.WHATSAPP_ROUTER_URL;
if (!API_BASE_URL) {
console.error("FATAL: WHATSAPP_ROUTER_URL environment variable is not set.");
process.exit(1);
}
export async function fetchJSON(path, options = {}) {
const method = options.method || "GET";

View File

@@ -0,0 +1,69 @@
// mcp/modules/conversationIntegration.js
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { fetchJSON } from "../lib/api.js";
const log = (...args) => console.log("[MCP ConversationIntegration]", ...args); // Changed log prefix
export default function registerConversationIntegration(server) {
// --- Conversation Actions ---
// Tool: conversation.list-conversations
server.tool(
"conversation.list-conversations",
"Retrieves a list of all conversations.",
{}, // No input parameters
async () => {
log("Tool invoked", "conversation.list-conversations");
const result = await fetchJSON("/conversations");
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: conversation.get-conversation-details
server.tool(
"conversation.get-conversation-details",
"Retrieves details for a specific conversation by its ID.",
{
id: z.string().describe("ID of the conversation"),
},
async (params) => {
log("Tool invoked", "conversation.get-conversation-details", params);
const result = await fetchJSON(`/conversations/${params.id}`);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: conversation.update
server.tool(
"conversation.update",
"Updates/rebuilds a conversation by its ID.",
{
id: z.string().describe("ID of the conversation to update"),
},
async (params) => {
log("Tool invoked", "conversation.update", params);
const result = await fetchJSON(`/conversations/${params.id}/update`, {
method: "POST",
// No body needed for this specific route as per analysis
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: conversation.delete
server.tool(
"conversation.delete",
"Deletes a conversation by its ID.",
{
id: z.string().describe("ID of the conversation to delete"),
},
async (params) => {
log("Tool invoked", "conversation.delete", params);
const result = await fetchJSON(`/conversations/${params.id}`, {
method: "DELETE",
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
}

View File

@@ -1,111 +0,0 @@
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { fetchJSON } from "../lib/api.js";
const log = (...args) => console.log("[MCP]", ...args);
export default function registerTareas(server) {
server.resource("tarea-list", "tarea://list", async (uri) => {
log("Recurso solicitado", "tarea-list");
const tareas = await fetchJSON("/api/tareas");
return { contents: [{ uri: uri.href, text: JSON.stringify(tareas) }] };
});
server.resource(
"tarea",
new ResourceTemplate("tarea://{id}", { list: undefined }),
async (uri, { id }) => {
log("Recurso solicitado", `tarea ${id}`);
const tarea = await fetchJSON(`/api/tareas/${id}`);
return { contents: [{ uri: uri.href, text: JSON.stringify(tarea) }] };
}
);
server.tool(
"create-tarea",
"Crea una tarea",
{
empleado_id: z.number(),
planilla_id: z.number().optional(),
titulo: z.string(),
precio: z.number().optional(),
estado: z.string().optional(),
observacion: z.string().optional(),
fecha: z.string(),
tipo: z.string().optional(),
fecha_anulado: z.string().optional(),
creador_id: z.number().optional(),
anulador_id: z.number().optional(),
},
async (params) => {
log("Tool invocada", "create-tarea", params);
const tarea = await fetchJSON("/api/tareas", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(tarea) }] };
}
);
server.tool(
"update-tarea",
"Actualiza una tarea",
{
id: z.number(),
empleado_id: z.number().optional(),
planilla_id: z.number().optional(),
titulo: z.string().optional(),
precio: z.number().optional(),
estado: z.string().optional(),
observacion: z.string().optional(),
fecha: z.string().optional(),
tipo: z.string().optional(),
fecha_anulado: z.string().optional(),
anulador_id: z.number().optional(),
},
async ({ id, ...updates }) => {
log("Tool invocada", "update-tarea", { id, ...updates });
const tarea = await fetchJSON(`/api/tareas/${id}`, {
method: "PUT",
body: JSON.stringify(updates),
});
return { content: [{ type: "text", text: JSON.stringify(tarea) }] };
}
);
server.tool(
"delete-tarea",
"Elimina una tarea",
{ id: z.number() },
async ({ id }) => {
log("Tool invocada", "delete-tarea", { id });
await fetchJSON(`/api/tareas/${id}`, { method: "DELETE" });
return { content: [{ type: "text", text: `Tarea ${id} eliminada` }] };
}
);
server.tool(
"search-tareas",
"Busca tareas. `q` matchea id, empleado_id, planilla_id o título. Si no mandas filtros devuelve los primeros 100 registros.",
{
q: z.string().optional(),
empleado_id: z.number().optional(),
planilla_id: z.number().optional(),
estado: z.string().optional(),
titulo: z.string().optional(),
fecha_desde: z.string().optional(),
fecha_hasta: z.string().optional(),
},
async (params) => {
log("Tool invocada", "search-tareas", params);
const qs = new URLSearchParams(
Object.entries(params)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
);
if (qs.toString() === "") qs.append("limit", "100");
const tareas = await fetchJSON(`/api/tareas/search?${qs.toString()}`);
return { content: [{ type: "text", text: JSON.stringify(tareas) }] };
}
);
}

View File

@@ -0,0 +1,211 @@
// mcp/modules/whatsappIntegration.js
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { fetchJSON } from "../lib/api.js";
const log = (...args) => console.log("[MCP WhatsAppIntegration]", ...args); // Changed log prefix
export default function registerWhatsappIntegration(server) {
// --- WhatsApp Actions ---
// Tool: whatsapp.send-text
server.tool(
"whatsapp.send-text",
"Sends a text message via WhatsApp.",
{
to: z.string().describe("Recipient ID (e.g., 1234567890@c.us)"),
content: z.string().describe("Text message content"),
},
async (params) => {
log("Tool invoked", "whatsapp.send-text", params);
const result = await fetchJSON("/send-text", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.send-image
server.tool(
"whatsapp.send-image",
"Sends an image message via WhatsApp.",
{
to: z.string().describe("Recipient ID"),
path: z.string().describe("Path or URL to the image"),
caption: z.string().optional().describe("Image caption"),
},
async (params) => {
log("Tool invoked", "whatsapp.send-image", params);
const result = await fetchJSON("/send-image", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.send-file
server.tool(
"whatsapp.send-file",
"Sends a file message via WhatsApp.",
{
to: z.string().describe("Recipient ID"),
path: z.string().describe("Path or URL to the file"),
filename: z.string().optional().describe("Name of the file"),
caption: z.string().optional().describe("File caption"),
},
async (params) => {
log("Tool invoked", "whatsapp.send-file", params);
const result = await fetchJSON("/send-file", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.list-chats
server.tool(
"whatsapp.list-chats",
"Retrieves a list of all chats.",
{}, // No input parameters
async () => {
log("Tool invoked", "whatsapp.list-chats");
const result = await fetchJSON("/chats");
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.get-chat-details
server.tool(
"whatsapp.get-chat-details",
"Retrieves details for a specific chat by its ID.",
{
chatId: z.string().describe("ID of the chat (e.g., 1234567890@c.us)"),
},
async (params) => {
log("Tool invoked", "whatsapp.get-chat-details", params);
const result = await fetchJSON(`/chats/${params.chatId}`);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.get-chat-messages
server.tool(
"whatsapp.get-chat-messages",
"Retrieves messages for a specific chat.",
{
chatId: z.string().describe("ID of the chat"),
limit: z.number().optional().describe("Number of messages to retrieve"),
includeMe: z.boolean().optional().describe("Include messages sent by me"),
includeNotifications: z.boolean().optional().describe("Include notification messages"),
},
async ({ chatId, ...queryParams}) => {
log("Tool invoked", "whatsapp.get-chat-messages", { chatId, queryParams });
const qs = new URLSearchParams(
Object.entries(queryParams)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
);
const result = await fetchJSON(`/chats/${chatId}/messages?${qs.toString()}`);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
server.tool(
"whatsapp.delete-chat",
"Deletes a chat by its ID. (Note: The underlying OpenWA endpoint for this is assumed and might need verification)",
{
chatId: z.string().describe("ID of the chat to delete (e.g., 1234567890@c.us)"),
},
async (params) => {
log("Tool invoked", "whatsapp.delete-chat", params);
const result = await fetchJSON(`/chats/${params.chatId}`, {
method: "DELETE",
});
// Consider what a successful deletion should return.
// OpenWA might return a success message or just a 200/204.
// For now, we'll return the full result.
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.create-group
server.tool(
"whatsapp.create-group",
"Creates a new WhatsApp group.",
{
groupName: z.string().describe("Name of the group"),
contactIds: z.array(z.string()).describe("Array of contact IDs to add to the group"),
},
async (params) => {
log("Tool invoked", "whatsapp.create-group", params);
const result = await fetchJSON("/groups", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.get-contact-details
server.tool(
"whatsapp.get-contact-details",
"Retrieves details for a specific contact by ID.",
{
contactId: z.string().describe("ID of the contact (e.g., 1234567890@c.us)"),
},
async (params) => {
log("Tool invoked", "whatsapp.get-contact-details", params);
const result = await fetchJSON(`/contacts/${params.contactId}`);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.get-blocklist
server.tool(
"whatsapp.get-blocklist",
"Retrieves the blocklist.",
{}, // No input parameters
async () => {
log("Tool invoked", "whatsapp.get-blocklist");
const result = await fetchJSON("/blocklist");
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.block-contact
server.tool(
"whatsapp.block-contact",
"Blocks a contact on WhatsApp.",
{
contactId: z.string().describe("ID of the contact to block"),
},
async (params) => {
log("Tool invoked", "whatsapp.block-contact", params);
const result = await fetchJSON("/blocklist/block", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Tool: whatsapp.unblock-contact
server.tool(
"whatsapp.unblock-contact",
"Unblocks a contact on WhatsApp.",
{
contactId: z.string().describe("ID of the contact to unblock"),
},
async (params) => {
log("Tool invoked", "whatsapp.unblock-contact", params);
const result = await fetchJSON("/blocklist/unblock", {
method: "POST",
body: JSON.stringify(params),
});
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
}

337
mcp/package-lock.json generated
View File

@@ -1,19 +1,21 @@
{
"name": "planilla-mcp-server",
"name": "conversation-layer-mcp-server",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planilla-mcp-server",
"name": "conversation-layer-mcp-server",
"version": "0.1.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"zod": "^3.24.3"
},
"devDependencies": {
"node-cron": "^4.0.5",
"nodemon": "^3.1.10",
"prisma": "^6.8.2"
}
},
@@ -104,6 +106,37 @@
"node": ">= 0.6"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -124,6 +157,28 @@
"node": ">=18"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -161,6 +216,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@@ -248,6 +333,17 @@
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -393,6 +489,18 @@
"express": "^4.11 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
@@ -427,6 +535,20 @@
"node": ">= 0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -473,6 +595,18 @@
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -485,6 +619,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -536,6 +679,12 @@
"node": ">=0.10.0"
}
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -549,6 +698,48 @@
"node": ">= 0.10"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@@ -619,6 +810,18 @@
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -643,6 +846,43 @@
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -698,6 +938,18 @@
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkce-challenge": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
@@ -743,6 +995,12 @@
"node": ">= 0.10"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -780,6 +1038,18 @@
"node": ">= 0.8"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -828,6 +1098,18 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
@@ -961,6 +1243,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -969,6 +1263,30 @@
"node": ">= 0.8"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -977,6 +1295,15 @@
"node": ">=0.6"
}
},
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -991,6 +1318,12 @@
"node": ">= 0.6"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@@ -1,5 +1,5 @@
{
"name": "planilla-mcp-server",
"name": "conversation-layer-mcp-server",
"version": "0.1.0",
"type": "module",
"scripts": {
@@ -8,11 +8,13 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"zod": "^3.24.3"
},
"devDependencies": {
"node-cron": "^4.0.5",
"nodemon": "^3.1.10",
"prisma": "^6.8.2"
}
}

View File

@@ -48,6 +48,29 @@ router.post('/send-text', async (req: Request, res: Response) => {
}
});
// DELETE /chats/:chatId
router.delete('/chats/:chatId', async (req: Request, res: Response) => {
try {
const { chatId } = req.params;
if (!chatId) {
return res.status(400).json({ error: 'Missing "chatId" in request params' });
}
// openWaUrl is checked by middleware and available in module scope
const result = await whatsappClient.deleteChat(openWaUrl!, chatId);
res.json(result);
} catch (error: any) {
console.error(`[routes/whatsappActions] Error in DELETE /chats/${req.params.chatId}:`, error.message);
try {
// Attempt to parse error message if it's from whatsappClient's structured error
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
return res.status(parsedError.status || 500).json({ error: parsedError });
} catch (e) {
// If not, send the plain error message
res.status(500).json({ error: error.message || 'Failed to delete chat' });
}
}
});
// POST /send-image
router.post('/send-image', async (req: Request, res: Response) => {
try {

View File

@@ -42,6 +42,34 @@ export async function sendTextMessage(
}
}
/**
* Deletes a chat by its ID via OpenWA.
* (Assumes OpenWA supports a /deleteChat endpoint or similar via POST)
* @param openWaUrl The base URL of the OpenWA instance.
* @param chatId The ID of the chat to delete.
* @returns A promise that resolves to the API response.
*/
export async function deleteChat(
openWaUrl: string,
chatId: string
): Promise<OpenWAResponse> { // Using OpenWAResponse<any> for now
try {
// Assuming OpenWA uses a POST request for actions like deleteChat
// The actual endpoint name ('/deleteChat') is a guess and might need adjustment.
const response = await axios.post(`${openWaUrl}/deleteChat`, {
args: { chatId },
});
return response.data?.response || response.data;
} catch (error: any) {
console.error(`[whatsappClient] Error deleting chat ${chatId}:`, error.message);
if (axios.isAxiosError(error) && error.response) {
console.error('[whatsappClient] Axios error details:', error.response.data);
throw new Error(`whatsappClient API error (${openWaUrl}/deleteChat): ${error.response.status} - ${JSON.stringify(error.response.data)}`);
}
throw new Error(`whatsappClient error (${openWaUrl}/deleteChat): ${error.message}`);
}
}
/**
* Sends an image message via OpenWA.
* @param openWaUrl The base URL of the OpenWA instance.