Guía10 min

API existente como tools de agentes: fachada de intención, no 200 endpoints

Resumen

Un OpenAPI con 80 paths no es un toolset. Contrato para envolver una API que ya tienes: 5–12 tools de intención, un adapter que valida y recorta, auth fuera del contexto y el generador OpenAPI solo como borrador. Incluye matriz REST vs tool, checklist y el error de volcar el spec crudo al modelo.

OpenAIAnthropic
Fachada de pocas tools de intención delante de una API REST con decenas de endpoints

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Tu API de facturación, CRM o tickets ya existe. El error de primer deploy es pegarle al modelo los 80 paths del OpenAPI como si cada GET /v1/invoices/{id}/line_items fuera una tool. El modelo no es un cliente HTTP: no lee operationId, no entiende paginación cursor y no debe ver el bearer. La API se envuelve: 5–12 tools de intención, un adapter que valida y recorta, auth fuera del contexto. El generador OpenAPI sirve de borrador, no de contrato.

No es function calling: esa guía cubre el loop (schema → llamada → resultado). Aquí el problema es qué superficie expones cuando el backend ya está escrito. No es MCP: MCP es el cable; esto es el recorte de intención aunque hables HTTP plano. No es versionar schemas: versionar evita breaking; la fachada evita que existan 80 tools que nunca debieron nacer.

Contrato: el modelo elige una intención (“crear factura borrador”, “buscar cliente por NIT”). El adapter traduce a REST, inyecta auth y devuelve un recorte. Cero spec crudo en el prompt.

REST no es un toolset

Un humano con Postman navega 40 endpoints. Un modelo con 40 tools adivina, mezcla list con search y rellena query params que tu API ignora. OpenAI lo dice en las best practices de function calling (verificado 2026-09-06): nombres y descripciones explícitos, no hacer que el modelo rellene argumentos que ya conoces, y menos de 20 functions al inicio de un turno. Anthropic, en Define tools (mismo día): descripciones de 3–4 frases, consolidar operaciones relacionadas y devolver solo señal alta.

Superficie RESTTool de intenciónPor qué
GET /customers, GET /customers/{id}, GET /customers:searchbuscar_cliente({ nit?, email?, nombre? })Un job; el adapter elige path y 404 vs lista vacía
POST /invoices + 12 campos opcionalescrear_factura_borrador({ cliente_id, items[] })Defaults, moneda e impuestos viven en código
GET /invoices?cursor=&limit=listar_facturas({ cliente_id, estado, limite? })Cursor y page size no son decisión del modelo
POST /invoices/{id}/voidanular_factura({ factura_id, motivo })Irreversible: un nombre, no un verbo HTTP genérico
Header Authorizationningún parámetroEl adapter lo inyecta; si el modelo lo ve, se filtra

La columna izquierda es el OpenAPI. La derecha es lo que el modelo puede elegir sin improvisar transporte.

Fachada de 5 a 12, no un espejo

Empieza por los jobs reales del agente, no por el índice de paths. Un agente de cobros en Guatemala casi nunca necesita más que:

  1. buscar_cliente
  2. ver_saldo
  3. crear_factura_borrador
  4. enviar_factura
  5. registrar_pago
  6. anular_factura
  7. listar_facturas

Siete tools. Si mañana aparece “nota de crédito”, agrega una octava; no abras POST /credit-notes más tres GETs de catálogo. OpenAI sugiere evaluar con distinto número de functions y dejar las raras detrás de tool search (gpt-5.4+). Anthropic pide namespacing (billing_crear_factura, no create) cuando hay más de un servicio.

Tope práctico: 12 tools visibles por turno. Por encima, el modelo elige mal y pagas tokens: las definitions cuentan como input. Si tu OpenAPI tiene 80 operations, el 90 % es infra para humanos (webhooks, admin, exports). Eso no entra al loop.

Fachada de intención frente a una API con muchos paths

El adapter hace el trabajo sucio

La tool no es un fetch. Es un handler con tres obligaciones:

  1. Validar el JSON del modelo contra el schema antes de tocar la red. Con OpenAI, strict: true exige additionalProperties: false y todos los campos en required (opcionales como ["string", "null"]). Si el schema no cumple, Responses puede caer a strict: false; no lo dejes implícito.
  2. Traducir a la llamada REST: path, query, body, idempotency key. El modelo no inventa Idempotency-Key ni limit=500.
  3. Recortar la respuesta. Un GET /invoices/123 puede devolver 40 KB de line items, metadata y URLs firmadas. El modelo necesita id, estado, total, moneda y fecha. El resto es truncar resultados: presupuesto por tool, preview + handle truncated, tijera muda prohibida.

Auth fuera del contexto. El token vive en el proceso, en un secret store o en /run/secrets. Si el schema tiene api_key o authorization, el modelo lo va a emitir o a pedir. Eso es fuga, no “flexibilidad”. Ver secretos de agentes.

async function crearFacturaBorrador(args: unknown, ctx: TenantCtx) {
  const input = CrearFactura.parse(args); // Zod / JSON Schema
  const res = await billing.fetch("/v1/invoices", {
    method: "POST",
    headers: {
      authorization: `Bearer ${ctx.token}`, // nunca sale del adapter
      "idempotency-key": ctx.idempotencyKey,
    },
    body: JSON.stringify(toBillingPayload(input, ctx.defaults)),
  });
  const full = await res.json();
  return pick(full, ["id", "status", "total", "currency", "due_date"]);
}

El intern test de OpenAI aplica: un humano con solo el schema ¿sabría usarlo? Si pregunta “¿NIT o tax_id?”, el schema está mal.

OpenAPI es borrador, no contrato

Un generador (o un script que recorre paths) produce un espejo 1:1: un tool por operationId, parámetros de query crudos, securitySchemes colados y descripciones copiadas del YAML. Sirve para inventar la primera lista y tachar. No se commitea como toolset.

Flujo que sí escala:

  1. Exporta o copia el OpenAPI (la spec vive en swagger.io/specification, verificada 2026-09-06).
  2. Marca las 5–12 operations que cubren jobs del agente.
  3. Reescribe nombres y descripciones en idioma de intención. Anthropic pide ≥3–4 oraciones: qué hace, cuándo sí, cuándo no, qué no devuelve.
  4. Junta operaciones que siempre van en secuencia (get + mark_seen → un ver_saldo que ya marca).
  5. Escribe el adapter a mano. Tests sin LLM: JSON válido, JSON extra, 401, 404, payload enorme. Eso es tests de tools, no un eval del modelo.

Si el generador te deja 47 tools “por si acaso”, no las cargues. Tool search existe precisamente para no inyectar el catálogo entero en el system message.

Adapter que valida, traduce a REST y recorta la respuesta

Matriz de decisión

SeñalFachada (sí)Espejo OpenAPI (no)
Jobs del agente ≤ 12Un tool por jobUn tool por path
El humano ya tiene cliente_id en sesiónCero parámetro; el código lo ponecustomer_id en el schema
Respuesta > 2–4 KBRecorte + truncatedJSON completo al LLM
Acción irreversibleNombre explícito + confirmación en el handlerPOST genérico con action libre
Token / cookie / HMACHeader en el procesoCampo del tool
Cambio breaking del backendVersión de toolset, no 80 diffsRegenerar todo el spec encima

Claude acepta nombres ^[a-zA-Z0-9_-]{1,64}$. No uses puntos ni espacios copiados de operationId tipo Invoices_GetById. Prefijo de servicio + verbo de negocio.

Checklist de release

  • ≤ 12 tools visibles por turno; el resto no existe o va a tool search.
  • Cada tool tiene descripción de intención (cuándo sí / cuándo no) y parámetros que un intern usaría sin Slack.
  • strict: true (o equivalente) con additionalProperties: false; opcionales como unión con null.
  • Cero api_key, cookie o URL firmada en schema, logs o tool result.
  • Adapter con tests sin LLM: schema inválido, 4xx, recorte de payload.
  • OpenAPI guardado como referencia; el toolset se reviewa como código, no como dump del generador.
  • Acciones irreversibles con nombre propio, no un CRUD genérico.

FAQ

¿Y si el agente “necesita” explorar la API? No. Exploración es trabajo de un humano con docs. El agente ejecuta jobs. Si aparece un job nuevo, abres una tool nueva con PR, no le das request({ method, path, body }).

¿MCP no resuelve esto? MCP transporta tools. Si publicas 80 tools MCP clonadas del OpenAPI, el modelo sigue ahogado. La fachada es anterior al cable.

¿Puedo dejar GET libres y wrappear solo los POST? Los GET ruidosos también saturan contexto (listas sin recorte, PII, URLs). Envuelve lectura y escritura. La diferencia es el permiso del handler, no el verbo HTTP.

¿Qué hago con webhooks y admin? No son tools del agente de producto. Viven en el backend y en CI, no en el loop.

Siguiente paso: envuelve un job hoy (buscar + una escritura) con adapter y tests sin LLM. Luego la tercera tool. El curso gratis cubre el loop; esta guía evita que ese agente hable REST crudo.