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