Skip to main content

Crear un MCP compatible con Whaapy

Esta guía es para desarrolladores que quieren exponer un MCP para que Whaapy pueda conectarlo a un agente, tool o subagent. Whaapy usa MCP como una capa estructurada para consultar datos o ejecutar acciones externas. Para que funcione bien, el servidor debe declarar schemas claros, recibir argumentos tipados y devolver resultados verificables.
Si solo quieres conectar un MCP desde el dashboard, revisa Configurar MCPs. Esta página explica cómo construir el servidor MCP que Whaapy va a consumir.

Resumen rápido

Whaapy espera un servidor MCP por HTTP con estas capacidades:
ÁreaRequisito
TransporteStreamable HTTP
ProtocoloJSON-RPC 2.0
Versión MCP2025-06-18
Discoverytools/list
Ejecucióntools/call
SchemainputSchema con raíz type: "object"
Respuestacontent: [{ type: "text", text: "<JSON>" }]
Flujo completo:

Transporte

Expón un endpoint HTTP para MCP:
POST https://tu-dominio.com/mcp
DELETE https://tu-dominio.com/mcp
Whaapy envía estos headers:
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: <session-id si el server lo entregó>
Métodos de autenticación soportados:
TipoCómo se envía
noneSin credenciales
bearerAuthorization: Bearer <token>
api_keyHeader configurado, por ejemplo X-API-Key: <value>
custom_headersUno o varios headers configurados
Timeouts actuales del cliente Whaapy:
OperaciónTimeout
initialize15 segundos
tools/list15 segundos
tools/call30 segundos
notifications/initialized5 segundos
Diseña tools que respondan rápido. Si una operación tarda más de 30 segundos, conviértela en una operación encolada y devuelve un estado verificable.

Handshake

Whaapy inicia la sesión con initialize. Request:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "whaapy-mcp-client",
      "version": "1.0.0"
    }
  }
}
Response:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "mi-mcp",
      "version": "1.0.0"
    }
  }
}
Después Whaapy envía la notificación notifications/initialized:
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}
El servidor puede responder 202 Accepted.

Discovery con tools/list

Whaapy descubre las herramientas con tools/list. Request:
{
  "jsonrpc": "2.0",
  "id": 123,
  "method": "tools/list"
}
Response:
{
  "jsonrpc": "2.0",
  "id": 123,
  "result": {
    "tools": [
      {
        "name": "search_customer",
        "description": "Busca clientes por teléfono, nombre, usuario o ID externo.",
        "inputSchema": {
          "type": "object",
          "additionalProperties": false,
          "required": ["query"],
          "properties": {
            "query": {
              "type": "string",
              "description": "Texto para buscar al cliente"
            },
            "maxResults": {
              "type": "integer",
              "description": "Máximo de resultados a devolver"
            }
          }
        }
      }
    ]
  }
}
Whaapy guarda name, description e inputSchema. Después usa ese schema para validar los argumentos antes de llamar la tool.

Reglas de inputSchema

La raíz del inputSchema debe ser un objeto:
{
  "type": "object",
  "additionalProperties": false,
  "required": ["customerId"],
  "properties": {
    "customerId": {
      "type": "string"
    }
  }
}
Tipos soportados:
TipoUso
stringTexto, IDs, fechas serializadas, teléfonos
numberMontos, porcentajes, scores
integerConteos, límites, cantidades enteras
booleanBanderas explícitas
arrayListas de candidatos, items, errores
objectDatos estructurados anidados
enumValores permitidos
Para arrays, define items:
{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "value": { "type": "string" },
      "source": { "type": "string" }
    },
    "required": ["value"]
  }
}
Para objetos anidados, define properties:
{
  "type": "object",
  "properties": {
    "identityConfirmed": { "type": "boolean" },
    "customerId": { "type": "string" }
  }
}
No declares object o array si esperas recibir un string con JSON adentro. Whaapy valida tipos reales contra el schema.

Objetos y arrays reales

Este es el error más común al integrar MCPs con Whaapy: pasar estructuras como strings JSON. Bueno:
{
  "rawExtracted": {
    "monto": "$350.00",
    "banco_destino": "BBVA"
  },
  "trackingKeyCandidates": [
    {
      "value": "260604017724349474I",
      "source": "clave_rastreo",
      "confidence": "high"
    }
  ]
}
Malo:
{
  "rawExtracted": "{\"monto\":\"$350.00\",\"banco_destino\":\"BBVA\"}",
  "trackingKeyCandidates": "[{\"value\":\"260604017724349474I\"}]"
}
Si el schema dice:
{
  "rawExtracted": { "type": "object" },
  "trackingKeyCandidates": { "type": "array" }
}
entonces Whaapy espera:
  • rawExtracted como objeto real
  • trackingKeyCandidates como array real
No como texto serializado.

Ejecución con tools/call

Cuando el agente decide usar una capacidad, Whaapy llama tools/call. Request:
{
  "jsonrpc": "2.0",
  "id": 456,
  "method": "tools/call",
  "params": {
    "name": "submit_payment_evidence",
    "arguments": {
      "amount": 350,
      "rawExtracted": {
        "monto": "$350.00",
        "banco_destino": "BBVA"
      },
      "trackingKeyCandidates": [
        {
          "value": "260604017724349474I",
          "source": "clave_rastreo",
          "confidence": "high"
        }
      ]
    }
  }
}
Whaapy valida antes de ejecutar:
  • campos requeridos
  • tipos primitivos
  • enum
  • arrays
  • objetos anidados
  • propiedades extra si additionalProperties: false
Si los argumentos no pasan validación, Whaapy no llama al MCP y registra un error de validación.

Respuesta de tools

MCP exige que la respuesta tenga content. Whaapy funciona mejor si devuelves un solo bloque text con JSON válido. Respuesta recomendada:
{
  "content": [
    {
      "type": "text",
      "text": "{\"ok\":true,\"status\":\"success\",\"data\":{\"paymentId\":\"pay_123\",\"matched\":true},\"toolName\":\"submit_payment_evidence\",\"safety\":\"external_send\",\"durationMs\":842}"
    }
  ],
  "isError": false
}
El JSON dentro de text debería seguir este envelope:
{
  "ok": true,
  "status": "success",
  "data": {
    "paymentId": "pay_123"
  },
  "toolName": "submit_payment_evidence",
  "safety": "external_send",
  "durationMs": 842
}
Campos recomendados:
CampoTipoDescripción
okbooleanSi la operación tuvo éxito
statusstringEstado de negocio
dataobjectResultado estructurado
errorsarrayErrores estructurados si falló
toolNamestringNombre de la tool ejecutada
safetystringRiesgo o clase de operación
durationMsnumberDuración de la operación
Estados recomendados:
StatusCuándo usarlo
successLa operación terminó correctamente
failedNo se pudo completar
needs_more_infoFaltan datos del usuario
pending_confirmationHay candidatos o acciones que requieren confirmar
queuedLa operación quedó en proceso

Errores

Para errores técnicos del protocolo, usa error JSON-RPC:
{
  "jsonrpc": "2.0",
  "id": 456,
  "error": {
    "code": -32602,
    "message": "Invalid arguments",
    "data": {
      "field": "customerId"
    }
  }
}
Para errores de negocio dentro de una tool, devuelve isError: true y un payload estructurado:
{
  "content": [
    {
      "type": "text",
      "text": "{\"ok\":false,\"status\":\"needs_more_info\",\"errors\":[{\"code\":\"missing_customer_identity\",\"message_es\":\"Falta confirmar la identidad del cliente\",\"field\":\"customerId\",\"retryable\":true}],\"nextQuestion\":\"¿Me compartes tu número de servicio o usuario?\",\"toolName\":\"get_customer_balance\"}"
    }
  ],
  "isError": true
}
Formato recomendado de errores:
{
  "errors": [
    {
      "code": "missing_customer_identity",
      "message": "Customer identity is required",
      "message_es": "Falta confirmar la identidad del cliente",
      "field": "customerId",
      "retryable": true
    }
  ]
}
Usa message_es cuando el agente atienda usuarios en español. Whaapy puede usar ese mensaje para explicar el siguiente paso con menos ambigüedad.

Contrato semántico de una tool

Además del schema, la descripción debe explicar cuándo usar la tool y qué significa un resultado correcto. Incluye estas secciones en description:
When to use:
Cuando el cliente envía un comprobante de transferencia y hay datos suficientes para validarlo.

Never use for:
Pagos en efectivo, comprobantes ilegibles o casos donde el cliente no corresponde.

Identity:
Requiere identityConfirmed=true o un candidato explícito pendiente de confirmación.

Side effects:
Valida el SPEI y registra evidencia de pago.

Success criteria:
ok=true, status=success y data.paymentId presente.

Fallback:
Si faltan datos, devolver status=needs_more_info con nextQuestion.
Buenas descripciones reducen llamadas incorrectas y evitan que el agente prometa acciones no ejecutadas.

Acciones con side effects

Si la tool modifica algo externo, no basta con devolver texto. Devuelve evidencia verificable:
AcciónEvidencia recomendada
Crear ticketticketId, ticketUrl, status
Registrar pagopaymentId, receiptId, matchedInvoiceIds
Crear citaeventId, startAt, calendarId
Actualizar CRMrecordId, updatedFields
Encolar procesojobId, queuedAt
Ejemplo:
{
  "ok": true,
  "status": "success",
  "data": {
    "ticketId": "4482",
    "ticketUrl": "/tickets/ver/4482/",
    "status": "created"
  },
  "toolName": "create_support_ticket",
  "safety": "mutation"
}
No devuelvas éxito parcial como si fuera éxito final. Si la operación quedó pendiente, usa status: "queued" o status: "pending_confirmation".

Checklist antes de entregar un MCP

El servidor responde initialize correctamente.
tools/list devuelve todas las tools con name, description e inputSchema.
Cada inputSchema tiene raíz type: "object".
Los campos object y array se reciben como estructuras reales, no strings JSON.
Las tools devuelven content con JSON parseable.
Los errores tienen code, mensaje claro y, si aplica, field.
Las acciones con side effects devuelven IDs verificables.
Las tools con datos faltantes devuelven needs_more_info y nextQuestion.
Las credenciales usan permisos mínimos.
Los casos de timeout o sistema externo caído tienen fallback claro.

Ejemplo TypeScript

Ejemplo mínimo usando el SDK oficial:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "mi-mcp",
  version: "1.0.0",
});

server.tool(
  "search_customer",
  "Busca clientes por teléfono, nombre, usuario o ID externo. When to use: cuando Whaapy necesita identificar al cliente antes de revelar datos. Never use for: confirmar identidad por sí sola.",
  {
    query: z.string().describe("Texto de búsqueda"),
    maxResults: z.number().int().optional().describe("Máximo de resultados"),
  },
  async ({ query, maxResults = 5 }) => {
    const matches = await searchCustomers(query, maxResults);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            ok: true,
            status: matches.length > 0 ? "success" : "needs_more_info",
            data: { matches },
            nextQuestion: matches.length === 0 ? "¿Me compartes otro dato para ubicarte?" : undefined,
            toolName: "search_customer",
            safety: "read_only",
          }),
        },
      ],
      isError: false,
    };
  }
);

async function searchCustomers(query: string, maxResults: number) {
  return [];
}

Siguiente paso

Configurar MCPs

Conecta el MCP al agente desde Whaapy.

Configurar Tools

Define cuándo el agente debe ejecutar cada capacidad.