Skip to main content
Con app() agrupas varias operaciones relacionadas en un solo despliegue. Tus agentes IA descubren cada tool individualmente vía MCP, y cada uno tiene su propia ruta HTTP. Patrón: app() + múltiples define() + .describe() en cada campo + config compartida + ctx.env.get() para secrets.
index.ts
import { app, define, z } from "@jelou/functions";

export default app({
  config: { cors: { origin: "*" }, timeout: 15_000 },
  tools: {
    crearContacto: define({
      description: "Crea un nuevo contacto con nombre, email y teléfono opcional",
      input: z.object({
        nombre: z.string().min(1).describe("Nombre completo del contacto"),
        email: z.string().email().describe("Dirección de correo electrónico"),
        telefono: z.string().optional().describe("Teléfono con código de país, ej: 593987654321"),
      }),
      output: z.object({
        id: z.string(),
        creado: z.boolean(),
      }),
      handler: async (input, ctx) => {
        ctx.log("Creando contacto", { email: input.email });
        const apiKey = ctx.env.get("CRM_API_KEY");

        const res = await fetch("https://crm.example.com/api/contacts", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${apiKey}`,
          },
          body: JSON.stringify(input),
        });

        const data = await res.json();
        return { id: data.id, creado: true };
      },
    }),

    buscarContactos: define({
      description: "Busca contactos por nombre o email. Retorna hasta 20 resultados.",
      input: z.object({
        q: z.string().min(1).describe("Término de búsqueda: nombre o email del contacto"),
        limit: z.coerce.number().default(10).describe("Máximo de resultados (1-20)"),
      }),
      output: z.object({
        contactos: z.array(z.object({
          id: z.string(),
          nombre: z.string(),
          email: z.string(),
        })),
        total: z.number(),
      }),
      config: { methods: ["GET"] },
      handler: async (input, ctx) => {
        ctx.log("Buscando contactos", { q: input.q });
        const apiKey = ctx.env.get("CRM_API_KEY");

        const params = new URLSearchParams({
          q: input.q,
          limit: String(input.limit),
        });

        const res = await fetch(
          `https://crm.example.com/api/contacts?${params}`,
          { headers: { Authorization: `Bearer ${apiKey}` } },
        );

        const data = await res.json();
        return { contactos: data.items, total: data.total };
      },
    }),

    eliminarContacto: define({
      description: "Elimina un contacto por su ID. Retorna confirmación de eliminación.",
      input: z.object({
        contactoId: z.string().min(1).describe("ID único del contacto a eliminar"),
      }),
      output: z.object({
        eliminado: z.boolean(),
      }),
      handler: async (input, ctx) => {
        ctx.log("Eliminando contacto", { id: input.contactoId });
        const apiKey = ctx.env.get("CRM_API_KEY");

        const res = await fetch(
          `https://crm.example.com/api/contacts/${input.contactoId}`,
          {
            method: "DELETE",
            headers: { Authorization: `Bearer ${apiKey}` },
          },
        );

        if (!res.ok) {
          return { eliminado: false };
        }
        return { eliminado: true };
      },
    }),
  },
});

Prueba local

Inicia el servidor con jelou dev y prueba cada tool:
curl -X POST http://localhost:3000/crear-contacto \
  -H "Content-Type: application/json" \
  -d '{"nombre": "María García", "email": "[email protected]", "telefono": "593987654321"}'
curl "http://localhost:3000/buscar-contactos?q=maria&limit=5"
curl -X POST http://localhost:3000/eliminar-contacto \
  -H "Content-Type: application/json" \
  -d '{"contactoId": "ct_a1b2c3"}'
Si envías un input inválido, recibes un 400 con el detalle del error:
curl -X POST http://localhost:3000/crear-contacto \
  -H "Content-Type: application/json" \
  -d '{"nombre": "", "email": "no-es-email"}'

Por qué funciona así

  • Rutas automáticas — las keys crearContacto, buscarContactos, eliminarContacto generan /crear-contacto, /buscar-contactos, /eliminar-contacto.
  • GET vs POSTbuscarContactos usa config: { methods: ["GET"] } para recibir parámetros como query string. Los demás usan POST por defecto.
  • description importa — cada tool tiene una descripción específica que le dice al agente IA exactamente qué hace y qué retorna. “Busca contactos por nombre o email” es mucho mejor que “Busca contactos”.
  • Config compartidacors y timeout se definen una vez en app() y aplican a todos los tools. Cada tool puede sobreescribirlos si necesita.
  • Secrets centralizados — los 3 tools usan ctx.env.get("CRM_API_KEY"). Configuras el secret una vez con jelou secrets set.
  • MCP unificadocurl http://localhost:3000/mcp retorna los 3 tools como herramientas independientes que el agente puede invocar.