Saltar al contenido principal

Documentation Index

Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt

Use this file to discover all available pages before exploring further.

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.