> ## 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.

# App multi-tool

> Agrupa múltiples herramientas en un solo despliegue con app(): gestión de contactos con crear, buscar y eliminar.

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.

```typescript index.ts theme={null}
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 functions dev` y prueba cada tool:

<CodeGroup>
  ```bash curl /crear-contacto theme={null}
  curl -X POST http://localhost:3000/crear-contacto \
    -H "Content-Type: application/json" \
    -d '{"nombre": "María García", "email": "maria@example.com", "telefono": "593987654321"}'
  ```

  ```json Respuesta 200 theme={null}
  {
    "id": "ct_a1b2c3",
    "creado": true
  }
  ```
</CodeGroup>

<CodeGroup>
  ```bash curl /buscar-contactos theme={null}
  curl "http://localhost:3000/buscar-contactos?q=maria&limit=5"
  ```

  ```json Respuesta 200 theme={null}
  {
    "contactos": [
      { "id": "ct_a1b2c3", "nombre": "María García", "email": "maria@example.com" }
    ],
    "total": 1
  }
  ```
</CodeGroup>

<CodeGroup>
  ```bash curl /eliminar-contacto theme={null}
  curl -X POST http://localhost:3000/eliminar-contacto \
    -H "Content-Type: application/json" \
    -d '{"contactoId": "ct_a1b2c3"}'
  ```

  ```json Respuesta 200 theme={null}
  {
    "eliminado": true
  }
  ```
</CodeGroup>

Si envías un input inválido, recibes un `400` con el detalle del error:

<CodeGroup>
  ```bash curl con input inválido theme={null}
  curl -X POST http://localhost:3000/crear-contacto \
    -H "Content-Type: application/json" \
    -d '{"nombre": "", "email": "no-es-email"}'
  ```

  ```json Respuesta 400 theme={null}
  {
    "error": "Validation failed",
    "details": [
      { "path": ["nombre"], "message": "String must contain at least 1 character(s)", "code": "too_small" },
      { "path": ["email"], "message": "Invalid email", "code": "invalid_string" }
    ]
  }
  ```
</CodeGroup>

## Por qué funciona así

* **Rutas automáticas** — las keys `crearContacto`, `buscarContactos`, `eliminarContacto` generan `/crear-contacto`, `/buscar-contactos`, `/eliminar-contacto`.
* **GET vs POST** — `buscarContactos` 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 compartida** — `cors` 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 unificado** — `curl http://localhost:3000/mcp` retorna los 3 tools como herramientas independientes que el agente puede invocar.
