> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whaapy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crear un MCP compatible con Whaapy

> Especificación técnica para exponer herramientas MCP que el agente de Whaapy pueda descubrir, validar y ejecutar correctamente

# 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.

<Info>
  Si solo quieres conectar un MCP desde el dashboard, revisa [Configurar MCPs](/guides/agent-mcps). Esta página explica cómo construir el servidor MCP que Whaapy va a consumir.
</Info>

***

## Resumen rápido

Whaapy espera un servidor MCP por HTTP con estas capacidades:

| Área        | Requisito                                     |
| ----------- | --------------------------------------------- |
| Transporte  | Streamable HTTP                               |
| Protocolo   | JSON-RPC 2.0                                  |
| Versión MCP | `2025-06-18`                                  |
| Discovery   | `tools/list`                                  |
| Ejecución   | `tools/call`                                  |
| Schema      | `inputSchema` con raíz `type: "object"`       |
| Respuesta   | `content: [{ type: "text", text: "<JSON>" }]` |

Flujo completo:

```mermaid theme={null}
flowchart LR
    whaapy["Whaapy"] -->|"initialize"| mcpServer["MCP Server"]
    whaapy -->|"tools/list"| toolCatalog["Tool Catalog"]
    whaapy -->|"tools/call"| toolHandler["Tool Handler"]
    toolHandler --> externalSystem["External System"]
    toolHandler -->|"content text JSON"| whaapy
```

***

## Transporte

Expón un endpoint HTTP para MCP:

```text theme={null}
POST https://tu-dominio.com/mcp
DELETE https://tu-dominio.com/mcp
```

Whaapy envía estos headers:

```http theme={null}
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:

| Tipo             | Cómo se envía                                        |
| ---------------- | ---------------------------------------------------- |
| `none`           | Sin credenciales                                     |
| `bearer`         | `Authorization: Bearer <token>`                      |
| `api_key`        | Header configurado, por ejemplo `X-API-Key: <value>` |
| `custom_headers` | Uno o varios headers configurados                    |

Timeouts actuales del cliente Whaapy:

| Operación                   | Timeout     |
| --------------------------- | ----------- |
| `initialize`                | 15 segundos |
| `tools/list`                | 15 segundos |
| `tools/call`                | 30 segundos |
| `notifications/initialized` | 5 segundos  |

<Warning>
  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.
</Warning>

***

## Handshake

Whaapy inicia la sesión con `initialize`.

Request:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "whaapy-mcp-client",
      "version": "1.0.0"
    }
  }
}
```

Response:

```json theme={null}
{
  "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`:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}
```

El servidor puede responder `202 Accepted`.

***

## Discovery con tools/list

Whaapy descubre las herramientas con `tools/list`.

Request:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 123,
  "method": "tools/list"
}
```

Response:

```json theme={null}
{
  "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:

```json theme={null}
{
  "type": "object",
  "additionalProperties": false,
  "required": ["customerId"],
  "properties": {
    "customerId": {
      "type": "string"
    }
  }
}
```

Tipos soportados:

| Tipo      | Uso                                        |
| --------- | ------------------------------------------ |
| `string`  | Texto, IDs, fechas serializadas, teléfonos |
| `number`  | Montos, porcentajes, scores                |
| `integer` | Conteos, límites, cantidades enteras       |
| `boolean` | Banderas explícitas                        |
| `array`   | Listas de candidatos, items, errores       |
| `object`  | Datos estructurados anidados               |
| `enum`    | Valores permitidos                         |

Para arrays, define `items`:

```json theme={null}
{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "value": { "type": "string" },
      "source": { "type": "string" }
    },
    "required": ["value"]
  }
}
```

Para objetos anidados, define `properties`:

```json theme={null}
{
  "type": "object",
  "properties": {
    "identityConfirmed": { "type": "boolean" },
    "customerId": { "type": "string" }
  }
}
```

<Warning>
  No declares `object` o `array` si esperas recibir un string con JSON adentro. Whaapy valida tipos reales contra el schema.
</Warning>

***

## Objetos y arrays reales

Este es el error más común al integrar MCPs con Whaapy: pasar estructuras como strings JSON.

Bueno:

```json theme={null}
{
  "rawExtracted": {
    "monto": "$350.00",
    "banco_destino": "BBVA"
  },
  "trackingKeyCandidates": [
    {
      "value": "260604017724349474I",
      "source": "clave_rastreo",
      "confidence": "high"
    }
  ]
}
```

Malo:

```json theme={null}
{
  "rawExtracted": "{\"monto\":\"$350.00\",\"banco_destino\":\"BBVA\"}",
  "trackingKeyCandidates": "[{\"value\":\"260604017724349474I\"}]"
}
```

Si el schema dice:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "ok": true,
  "status": "success",
  "data": {
    "paymentId": "pay_123"
  },
  "toolName": "submit_payment_evidence",
  "safety": "external_send",
  "durationMs": 842
}
```

Campos recomendados:

| Campo        | Tipo    | Descripción                    |
| ------------ | ------- | ------------------------------ |
| `ok`         | boolean | Si la operación tuvo éxito     |
| `status`     | string  | Estado de negocio              |
| `data`       | object  | Resultado estructurado         |
| `errors`     | array   | Errores estructurados si falló |
| `toolName`   | string  | Nombre de la tool ejecutada    |
| `safety`     | string  | Riesgo o clase de operación    |
| `durationMs` | number  | Duración de la operación       |

Estados recomendados:

| Status                 | Cuándo usarlo                                     |
| ---------------------- | ------------------------------------------------- |
| `success`              | La operación terminó correctamente                |
| `failed`               | No se pudo completar                              |
| `needs_more_info`      | Faltan datos del usuario                          |
| `pending_confirmation` | Hay candidatos o acciones que requieren confirmar |
| `queued`               | La operación quedó en proceso                     |

***

## Errores

Para errores técnicos del protocolo, usa error JSON-RPC:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "errors": [
    {
      "code": "missing_customer_identity",
      "message": "Customer identity is required",
      "message_es": "Falta confirmar la identidad del cliente",
      "field": "customerId",
      "retryable": true
    }
  ]
}
```

<Tip>
  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.
</Tip>

***

## 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`:

```text theme={null}
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ón          | Evidencia recomendada                         |
| --------------- | --------------------------------------------- |
| Crear ticket    | `ticketId`, `ticketUrl`, `status`             |
| Registrar pago  | `paymentId`, `receiptId`, `matchedInvoiceIds` |
| Crear cita      | `eventId`, `startAt`, `calendarId`            |
| Actualizar CRM  | `recordId`, `updatedFields`                   |
| Encolar proceso | `jobId`, `queuedAt`                           |

Ejemplo:

```json theme={null}
{
  "ok": true,
  "status": "success",
  "data": {
    "ticketId": "4482",
    "ticketUrl": "/tickets/ver/4482/",
    "status": "created"
  },
  "toolName": "create_support_ticket",
  "safety": "mutation"
}
```

<Warning>
  No devuelvas éxito parcial como si fuera éxito final. Si la operación quedó pendiente, usa `status: "queued"` o `status: "pending_confirmation"`.
</Warning>

***

## Checklist antes de entregar un MCP

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

***

## Ejemplo TypeScript

Ejemplo mínimo usando el SDK oficial:

```typescript theme={null}
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

<CardGroup cols={2}>
  <Card title="Configurar MCPs" icon="plug" href="/guides/agent-mcps">
    Conecta el MCP al agente desde Whaapy.
  </Card>

  <Card title="Configurar Tools" icon="wrench" href="/guides/agent-tools">
    Define cuándo el agente debe ejecutar cada capacidad.
  </Card>
</CardGroup>
