Pular para o conteúdo 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.

Com app() você agrupa múltiplas operações relacionadas em um único deploy. Seus agentes de IA descobrem cada ferramenta individualmente via MCP, e cada uma tem sua própria rota HTTP. Padrão: app() + múltiplos define() + .describe() em cada campo + config compartilhada + ctx.env.get() para secrets.
index.ts
import { app, define, z } from "@jelou/functions";

export default app({
  config: { cors: { origin: "*" }, timeout: 15_000 },
  tools: {
    createContact: define({
      description: "Creates a new contact with name, email, and optional phone",
      input: z.object({
        name: z.string().min(1).describe("Contact's full name"),
        email: z.string().email().describe("Email address"),
        phone: z.string().optional().describe("Phone with country code, e.g.: 593987654321"),
      }),
      output: z.object({
        id: z.string(),
        created: z.boolean(),
      }),
      handler: async (input, ctx) => {
        ctx.log("Creating contact", { 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, created: true };
      },
    }),

    searchContacts: define({
      description: "Searches contacts by name or email. Returns up to 20 results.",
      input: z.object({
        q: z.string().min(1).describe("Search term: contact name or email"),
        limit: z.coerce.number().default(10).describe("Maximum number of results (1-20)"),
      }),
      output: z.object({
        contacts: z.array(z.object({
          id: z.string(),
          name: z.string(),
          email: z.string(),
        })),
        total: z.number(),
      }),
      config: { methods: ["GET"] },
      handler: async (input, ctx) => {
        ctx.log("Searching contacts", { 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 { contacts: data.items, total: data.total };
      },
    }),

    deleteContact: define({
      description: "Deletes a contact by its ID. Returns deletion confirmation.",
      input: z.object({
        contactId: z.string().min(1).describe("Unique ID of the contact to delete"),
      }),
      output: z.object({
        deleted: z.boolean(),
      }),
      handler: async (input, ctx) => {
        ctx.log("Deleting contact", { id: input.contactId });
        const apiKey = ctx.env.get("CRM_API_KEY");

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

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

Testes locais

Inicie o servidor com jelou dev e teste cada ferramenta:
curl -X POST http://localhost:3000/create-contact \
  -H "Content-Type: application/json" \
  -d '{"name": "Maria Garcia", "email": "[email protected]", "phone": "593987654321"}'
curl "http://localhost:3000/search-contacts?q=maria&limit=5"
curl -X POST http://localhost:3000/delete-contact \
  -H "Content-Type: application/json" \
  -d '{"contactId": "ct_a1b2c3"}'
Se você enviar uma entrada inválida, recebe um 400 com os detalhes do erro:
curl -X POST http://localhost:3000/create-contact \
  -H "Content-Type: application/json" \
  -d '{"name": "", "email": "not-an-email"}'

Por que funciona dessa forma

  • Rotas automáticas — as chaves createContact, searchContacts, deleteContact geram /create-contact, /search-contacts, /delete-contact.
  • GET vs POSTsearchContacts usa config: { methods: ["GET"] } para receber parâmetros como query strings. As demais usam POST por padrão.
  • description importa — cada ferramenta tem uma descrição específica que informa ao agente de IA exatamente o que ela faz e o que retorna. “Searches contacts by name or email” é muito melhor do que “Searches contacts”.
  • Config compartilhadacors e timeout são definidos uma vez no app() e se aplicam a todas as ferramentas. Cada ferramenta pode sobrescrevê-los se necessário.
  • Secrets centralizados — as 3 ferramentas usam ctx.env.get("CRM_API_KEY"). Você configura o secret uma vez com jelou secrets set.
  • MCP unificadocurl http://localhost:3000/mcp retorna as 3 ferramentas como ferramentas independentes que o agente pode invocar.