Sección 02
Contrato de herramientas
Referencia de lo que el Gateway envía a cada herramienta y de lo que espera de vuelta.
Métodos del protocolo
| Método | Exigencia | Uso desde el Gateway |
|---|---|---|
server/discover | DEBE | Primera llamada. Anuncia versiones soportadas, capacidades e identidad. Con el SDK oficial, este método solo lo instala la entrada HTTP moderna: si monta el transporte a mano, su servidor responde -32601 y no pasa la certificación. El servidor de referencia usa el camino correcto y lo documenta en el código. |
tools/list | DEBE | Catálogo con esquemas. Al registrar, al notificar cambio y al expirar la caché (15 min). |
tools/call | DEBE | Ejecución de una herramienta. |
subscriptions/listen | PUEDE | Aviso toolsListChanged cuando cambia el catálogo. Sin esto, el refresco es por caducidad. |
El Gateway solo acepta resultados con resultType: "complete", que es lo que el SDK emite por
defecto. El patrón de varias vueltas (input_required) no se consume: toda herramienta se
resuelve en una sola llamada.
Transporte
- Un único endpoint HTTPS POST, certificado válido.
- Sin estado: la revisión 2026-07-28 eliminó las sesiones de protocolo y el saludo de inicialización. Cada petición es autónoma.
- Cabeceras estándar
Mcp-MethodyMcp-Nameen cada POST. - Sin reanudación de flujo: si la conexión se corta antes de recibir la respuesta, el Gateway reintenta como petición nueva, con un identificador de petición nuevo y la misma clave de idempotencia. De ahí la exigencia de la garantía 1. Esto es distinto de un tiempo de espera agotado: ahí el Gateway no reintenta, informa al agente y la conversación sigue sin la herramienta.
tools/listDEBERÍA devolver las herramientas en orden estable. El propio estándar lo recomienda: un orden cambiante invalida la caché del modelo y degrada la latencia.
Definición de una herramienta
{
"name": "abrir_caso_soporte",
"title": "Abrir caso de soporte",
"description": "Registra un caso de soporte para un cliente identificado y devuelve su número de radicado. Úsala solo cuando el cliente ya describió el problema. No la uses para consultar el estado de un caso existente.",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["documento", "tipo", "descripcion"],
"properties": {
"documento": { "type": "string", "maxLength": 20 },
"tipo": { "enum": ["falla", "instalacion", "cobro"] },
"descripcion": { "type": "string", "maxLength": 400 },
"prioridad": { "type": ["string", "null"], "enum": ["alta", "normal", null] }
}
},
"outputSchema": {
"type": "object",
"required": ["radicado", "estado"],
"properties": {
"radicado": { "type": "string" },
"estado": { "enum": ["abierto", "en_cola"] },
"sla_horas": { "type": "integer" }
}
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": true
}
}Reglas del esquema
| Regla | Exigencia | Motivo |
|---|---|---|
Todo parámetro opcional acepta null | DEBE | Sirven las dos formas: "type": ["string","null"] o anyOf con una rama {"type":"null"} (es lo que genera zod con .nullable()). Ver el aviso de abajo: es el requisito que más veces se pasa por alto y el que más rompe. |
required con solo lo imprescindible | DEBE | Un required largo hace que el modelo invente valores para poder llamar a la herramienta. Deje fuera todo lo que su sistema pueda resolver por defecto. |
additionalProperties: false | DEBE | Un esquema abierto invita al modelo a inventar campos. Es seguro: los metadatos del Gateway viajan en _meta, no en arguments. |
Valores cerrados como enum | DEBE | Lo que no esté enumerado, el modelo lo adivina en lenguaje natural. |
maxLength en todo texto libre | DEBE | Protege su backend y acota el coste del turno. |
| Sin identificadores internos como entrada obligatoria | DEBE | El modelo no puede construir un identificador suyo. Si hace falta, exponga primero una herramienta que lo resuelva. |
| Una operación por herramienta | DEBE | Un parámetro accion que multiplexa varias operaciones degrada la elección del modelo y no pasa la certificación. |
Sin anidamiento profundo ni oneOf/anyOf en la raíz | DEBERÍA | Objetos planos de una capa se aciertan mucho más. |
Por qué los opcionales DEBEN aceptar null. El proveedor del modelo valida el llamado contra
su esquema antes de entregárnoslo. Si un parámetro opcional declara un tipo estricto y el
modelo envía null (cosa que hace a menudo en vez de omitir el campo), la API responde 400 y
se cae la generación completa: no es que falle esa herramienta, es que el agente se queda sin
respuesta en medio de la conversación. Nos ocurrió en producción con nuestras propias
herramientas; desde entonces todos los opcionales de nuestro catálogo admiten null. Los suyos
también deben admitirlo.
Clasificación de la herramienta: _meta en su definición
Cada herramienta DEBE declarar en el _meta de su propia definición a qué escenario del agente
pertenece y con qué verbo se le describe al modelo. No se manda en un archivo aparte ni se configura
en un panel: viaja con el catálogo, así que cuando usted añade una herramienta, ya viene clasificada.
Clave en el _meta de la herramienta | Exigencia | Valor |
|---|---|---|
com.asixto/capability | DEBE | Una de las claves canónicas de escenario de la página 04. Determina en qué conversaciones se expone. |
com.asixto/verb | DEBE | El verbo de negocio en infinitivo con el que el agente la nombra ante el cliente: "agendar una cita", "consultar el estado de un caso". |
com.asixto/capabilities | PUEDE | Lista, si la misma herramienta sirve a varios escenarios (["pqr","soporte"]). Sustituye a la clave singular. |
{
"name": "abrir_caso_soporte",
"description": "…",
"inputSchema": { "…": "…" },
"annotations": { "readOnlyHint": false, "destructiveHint": false, "idempotentHint": true },
"_meta": {
"com.asixto/capabilities": ["pqr", "soporte", "garantia"],
"com.asixto/verb": "abrir un caso de soporte"
}
}Por qué es obligatorio y no un detalle. El agente no expone todo su catálogo en cada
conversación: filtra por escenario, y el texto que le dice al modelo qué puede hacer se construye
con esos verbos. Una herramienta sin capability no se expone nunca, y una sin verb queda
disponible pero el modelo no sabe que resuelve ese caso, así que casi no la usa. Los dos campos son
tres palabras de trabajo y son la diferencia entre una herramienta que se usa y una que no.
Una clave de escenario que no exista se rechaza en la certificación. No las invente: están todas en la página 04.
Nombres y longitudes
| Elemento | Límite | Motivo |
|---|---|---|
| Nombre de herramienta | ≤ 40 caracteres, ^[a-z0-9_]+$, único en su catálogo | El proveedor del modelo acepta ^[a-zA-Z0-9_-]{1,64}$. El Gateway prefija con ext__{empresa}__ para no colisionar con las herramientas nativas, y ese prefijo reserva 24 caracteres del presupuesto. Un nombre más largo no se puede exponer. |
description de la herramienta | ≤ 700 caracteres | El proveedor rechaza descripciones de más de 1.024. El margen es para el prefijo de contexto que añade el agente. |
description de cada parámetro | ≤ 200 caracteres | Cada carácter se paga en cada turno de la conversación. |
snake_case, verbo + entidad:consultar_cliente,crear_cita,mover_etapa.- Estables. Renombrar equivale a publicar otra herramienta y obliga a recertificar.
- El prefijo lo pone el Gateway; no aparece en su servidor.
Descripciones
La descripción es el texto que decide si el agente acierta al elegir la herramienta. No es documentación interna.
- Una o dos frases: qué hace y cuándo NO usarla.
- Sin tono, sin marca, sin instrucciones dirigidas al modelo (
"responde amablemente…"). Esa capa la pone el agente; si usted la duplica, entra en conflicto. - Sin datos volátiles (precios, horarios, inventario): eso va en la respuesta, no en la descripción, que se cachea.
Anotaciones y su efecto
| Anotación | Efecto en el Gateway |
|---|---|
readOnlyHint: true | Se clasifica como nivel read. Se ejecuta sin confirmación. |
destructiveHint: true | Se clasifica como nivel admin: queda fuera de los agentes con nivel read o write. |
idempotentHint: true | Declara que repetir la llamada es seguro. Hoy el runtime no reintenta por su cuenta: ante timeout degrada y sigue la conversación. La anotación describe su herramienta; no active un comportamiento nuestro que aún no existe. |
| Ausentes | Se asume el caso más restrictivo: escritura no idempotente, nivel admin. |
Las anotaciones son pistas, no autoridad: el estándar las define como no verificables por el consumidor. El Gateway las usa para clasificar el nivel; la decisión de exponer o no es del control de acceso de Asixto y de la configuración del tenant. Declarar de solo lectura una herramienta que escribe es un incumplimiento de contrato y bloquea la certificación.
No delegue la confirmación en el agente. Que el agente pida confirmación antes de una acción sensible es una instrucción al modelo, y las instrucciones al modelo no son deterministas: en nuestras mediciones se cumplen alrededor del 92% de las veces.
Para cualquier operación irreversible (cobrar, cancelar, borrar) la guarda que cuenta es la de su sistema: exija un campo de confirmación explícito en el esquema, valide estado y propiedad, y rechace lo que no cuadre. El porcentaje restante, cuando hay dinero o una cita de por medio, se convierte en un incidente.
Metadatos que envía el Gateway
Van en el campo _meta de los params de tools/call, no en arguments. Esto es deliberado:
_meta es el punto de extensión que define el propio estándar, así que su inputSchema puede
seguir cerrado sin rechazar nada. El SDK se los entrega sin que usted declare nada.
Clave en _meta | Tipo | Uso |
|---|---|---|
com.asixto/idempotencyKey | string | Clave determinista por operación lógica, derivada de la conversación, la herramienta y la firma de los argumentos. En toda escritura: persistirla y no repetir el efecto. |
com.asixto/conversationId | string | Ámbito de la conversación en curso. Dos conversaciones distintas nunca comparten clave de idempotencia. |
com.asixto/requestId | string | Correlación de traza. Regístrelo: es la referencia para cualquier incidente. |
com.asixto/channel | "chat" o "voice" | Canal. Úselo: en voice la respuesta se lee en voz alta y aplican las restricciones de la página 07. |
com.asixto/companyId | string | Empresa del tenant. Relevante si opera varias marcas o unidades de negocio detrás del mismo servidor. |
com.asixto/priceListId | string o null | Lista de precios vigente para esa conversación. Si su catálogo tiene varias, el precio debe salir de esta, no del precio por defecto. |
com.asixto/currency | string | Moneda del tenant en ISO 4217 (COP, USD…). Los importes que devuelva deben ser de esta moneda. |
com.asixto/timezone | string | Zona horaria del tenant (America/Bogota). Toda fecha que devuelva debe llevar offset coherente con ella. |
{
"method": "tools/call",
"params": {
"name": "abrir_caso_soporte",
"arguments": { "documento": "CC1032…", "tipo": "falla", "descripcion": "…" },
"_meta": {
"com.asixto/idempotencyKey": "9f2a7c…",
"com.asixto/conversationId": "cnv_77b…",
"com.asixto/requestId": "req_c41…",
"com.asixto/channel": "voice",
"com.asixto/companyId": "cmp_31a…",
"com.asixto/priceListId": "pl_mayorista",
"com.asixto/currency": "COP",
"com.asixto/timezone": "America/Bogota"
}
}
}Lo que el Gateway NO envía: una identidad autenticada del usuario final. La conversación ocurre en WhatsApp, en un chat web o por teléfono, y ahí no hay sesión. Quien dice ser el cliente lo afirma en la conversación. Consecuencia directa para usted: la validación de propiedad (garantía 2 de la página 05) se hace contra el identificador que su propia herramienta de consulta ya resolvió en esa conversación, no contra un token de usuario.
Forma de la respuesta
content: texto breve para el modelo. Es lo que puede terminar leído en voz alta.structuredContent: los datos, conformes a suoutputSchema.
Devuelva datos ya resueltos: el nombre en vez del identificador, el precio final en vez de sus componentes, la fecha completa en vez de un código interno. Una respuesta pensada para una tabla en pantalla no sirve en una llamada telefónica.
Tamaño: 3.000 caracteres. No es un consejo: el runtime sanea y recorta a 3.000 caracteres todo resultado antes de inyectarlo al modelo, dejando una marca de truncado. No es un error, así que su servidor no se entera: simplemente el agente responde con datos incompletos. Si su respuesta natural es más grande, pagine (página 07).
Una búsqueda sin resultados es un éxito, no un error. Devuelva la lista vacía y el total en 0. Si la reporta como error, el agente entra en la rama de fallo técnico y reintenta o cambia de paso en vez de decirle al cliente, con naturalidad, que no encontró nada.
Prohibido en la respuesta: identificadores internos, datos de terceros, campos administrativos, trazas, tokens. El resultado entra al contexto del modelo y queda en la traza de la conversación.
Un fallo de negocio usa la misma respuesta, marcada como error. No es un error de transporte: se
devuelve con isError: true y el objeto de error en structuredContent. La forma exacta y los seis
valores admitidos están en la página 06.
{
"isError": true,
"content": [{ "type": "text", "text": "{\"error\":{…}}" }],
"structuredContent": { "error": { "error_type": "NOT_FOUND", "user_message": "No encontré una cita con esos datos." } }
}El campo resultType que exige el estándar lo pone el SDK, no usted.
Media (imágenes, PDF, audio): no en la v1. El envío de archivos por el canal exige un identificador de media resuelto contra el backend de Asixto; una URL o un adjunto que devuelva su herramienta no se le entrega al cliente final, y en llamada se elimina del texto. Si su caso de uso lo necesita, escálelo antes de construirlo.