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

# Funciones multi-tool

> Agrupa múltiples herramientas en un solo despliegue con app(): rutas auto-generadas, MCP unificado, cron independiente y config compartida.

<Warning>
  **Preview** — Jelou Functions está en fase de vista previa. La API y el comportamiento pueden cambiar sin previo aviso.
</Warning>

## ¿Cuándo usar `app()` vs `define()`?

| Patrón     | Cuándo usarlo                                                 |
| ---------- | ------------------------------------------------------------- |
| `define()` | Una sola operación con un endpoint HTTP                       |
| `app()`    | Varias herramientas relacionadas que quieres desplegar juntas |

Si tu proyecto tiene un solo handler, usa `define()`. Si necesitas agrupar múltiples operaciones — por ejemplo, enviar y leer emails desde el mismo servicio — usa `app()`.

## Ejemplo básico

```typescript index.ts theme={null}
import { app, define, z } from "@jelou/functions";

export default app({
  config: { cors: { origin: "*" }, timeout: 30_000 },
  tools: {
    enviarEmail: define({
      description: "Envía un correo electrónico",
      input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
      config: { timeout: 5_000 },
      handler: async (input) => ({ sent: true }),
    }),
    leerBandeja: define({
      description: "Lee los mensajes de la bandeja de entrada",
      input: z.object({ limit: z.coerce.number().default(10) }),
      handler: async (input) => ({ messages: [] }),
    }),
  },
});
```

## Prueba local

Inicia el servidor con `jelou functions dev` y prueba cada tool en su ruta:

<CodeGroup>
  ```bash curl /enviar-email theme={null}
  curl -X POST http://localhost:3000/enviar-email \
    -H "Content-Type: application/json" \
    -d '{"to": "maria@example.com", "subject": "Hola", "body": "Bienvenida"}'
  ```

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

<CodeGroup>
  ```bash curl /leer-bandeja theme={null}
  curl http://localhost:3000/leer-bandeja?limit=5
  ```

  ```json Respuesta 200 theme={null}
  {
    "messages": []
  }
  ```
</CodeGroup>

Si envías un campo inválido, recibes un `400` con detalles:

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

  ```json Respuesta 400 theme={null}
  {
    "error": "Validation failed",
    "details": [
      { "path": ["subject"], "message": "Required", "code": "invalid_type" },
      { "path": ["body"], "message": "Required", "code": "invalid_type" }
    ]
  }
  ```
</CodeGroup>

## API

```typescript theme={null}
import { app } from "@jelou/functions";

const edgeApp = app({
  config: { cors: { origin: "*" }, timeout: 30_000 },
  tools: {
    /* ... tus define() aquí ... */
  },
});

export default edgeApp;
```

Firma de tipo:

```typescript theme={null}
function app(options: {
  config?: AppConfig;
  tools: Record<string, AnyEdgeFunction>;
}): EdgeApp;
```

### `AppConfig`

```typescript theme={null}
interface AppConfig {
  cors?: {
    origin?: string | string[];
    methods?: string[];
    headers?: string[];
    credentials?: boolean;
    maxAge?: number;
  };
  timeout?: number;
  methods?: string[];
  mcp?: boolean;
  public?: boolean;
}
```

| Campo     | Tipo       | Predeterminado                          | Descripción                                                 |
| --------- | ---------- | --------------------------------------- | ----------------------------------------------------------- |
| `cors`    | `object`   | `{ origin: "*" }`                       | Configuración CORS global                                   |
| `timeout` | `number`   | `30000`                                 | Timeout en ms para todos los tools                          |
| `methods` | `string[]` | `["GET","POST","PUT","PATCH","DELETE"]` | Métodos HTTP permitidos                                     |
| `mcp`     | `boolean`  | `true`                                  | Registrar tools en el servidor MCP                          |
| `public`  | `boolean`  | `false`                                 | Desactivar autenticación de plataforma para todos los tools |

## Rutas auto-generadas

Tus keys del objeto `tools` se convierten automáticamente a kebab-case para generar las rutas HTTP:

| Key           | Ruta            |
| ------------- | --------------- |
| `enviarEmail` | `/enviar-email` |
| `leerBandeja` | `/leer-bandeja` |
| `MiTool`      | `/mi-tool`      |

Para personalizar una ruta, usa `config.path` en el `define()` individual:

```typescript theme={null}
tools: {
  enviarEmail: define({
    config: { path: "/emails/enviar" },
    handler: async (input) => ({ sent: true }),
  }),
}
```

## Combinación de config

La configuración global de `app()` se aplica a todos los tools. Cada `define()` puede sobreescribir valores específicos:

| Campo     | Comportamiento                 |
| --------- | ------------------------------ |
| `timeout` | El tool sobreescribe el global |
| `methods` | El tool sobreescribe el global |
| `cors`    | El tool sobreescribe el global |
| `mcp`     | El tool sobreescribe el global |
| `public`  | El tool sobreescribe el global |
| `path`    | Siempre por tool               |
| `cron`    | Siempre por tool               |

```typescript theme={null}
export default app({
  config: { timeout: 30_000, cors: { origin: "*" } },
  tools: {
    rapido: define({
      config: { timeout: 5_000 },
      handler: async () => ({ ok: true }),
    }),
    lento: define({
      handler: async () => ({ ok: true }),
    }),
  },
});
```

En este ejemplo, `rapido` tiene un timeout de 5 segundos y `lento` hereda los 30 segundos globales. Ambos comparten la configuración CORS.

## Endpoint de salud

En modo app, `/__health` retorna información de todos los tools:

```json theme={null}
{
  "mode": "app",
  "tools": [
    {
      "key": "enviarEmail",
      "name": "Enviar Email",
      "description": "Envía un correo electrónico",
      "path": "/enviar-email",
      "cron": []
    },
    {
      "key": "leerBandeja",
      "name": "Leer Bandeja",
      "description": "Lee los mensajes de la bandeja de entrada",
      "path": "/leer-bandeja",
      "cron": []
    }
  ]
}
```

## Guardas de tipo

Usa `isEdgeApp()` e `isEdgeFunction()` para identificar el tipo de export en runtime:

```typescript theme={null}
import { isEdgeApp, isEdgeFunction } from "@jelou/functions";

isEdgeApp(module);      // true si fue creado con app()
isEdgeFunction(module);  // true si fue creado con define()
```

## Multi-tool cron

Cada tool dentro de `app()` puede tener sus propios schedules cron independientes. Las peticiones cron se envían a la ruta específica de cada tool.

```typescript theme={null}
export default app({
  tools: {
    limpiezaDiaria: define({
      description: "Limpia registros obsoletos",
      input: z.object({}),
      config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { cleaned: true };
      },
    }),
    sincronizacionHoraria: define({
      description: "Sincroniza datos externos",
      input: z.object({}),
      config: { cron: [{ expression: "0 * * * *" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { synced: true };
      },
    }),
  },
});
```

<Warning>
  El límite de **10** schedules cron es **agregado** entre todos los tools de un `app()`. Si `limpiezaDiaria` tiene 3 y `sincronizacionHoraria` tiene 4, quedan 3 disponibles.
</Warning>

## Multi-tool MCP

Un solo servidor MCP en `/mcp` registra automáticamente todos los tools del `app()`. Para excluir un tool del registro MCP, usa `mcp: false` en su config:

```typescript theme={null}
export default app({
  tools: {
    toolPublico: define({
      description: "Visible para MCP",
      input: z.object({}),
      handler: async () => ({}),
    }),
    toolInterno: define({
      description: "Solo HTTP",
      input: z.object({}),
      config: { mcp: false },
      handler: async () => ({}),
    }),
  },
});
```

En este ejemplo, los agentes IA solo descubren `toolPublico` vía MCP. `toolInterno` solo es accesible por HTTP directo en su ruta `/tool-interno`.

## Rutas expuestas (modo app)

| Ruta          | Descripción                                                     |
| ------------- | --------------------------------------------------------------- |
| `/__health`   | Health check con lista de tools                                 |
| `/mcp`        | Servidor MCP unificado (a menos que `config.mcp: false` global) |
| `/<tool-key>` | Ruta de cada tool (kebab-case o `config.path` personalizado)    |

## Problemas comunes

<Tabs>
  <Tab title="404 en la ruta del tool">
    Las keys de `tools` se convierten a **kebab-case**. Si tu key es `enviarEmail`, la ruta es `/enviar-email`, no `/enviarEmail`.

    Verifica las rutas reales con el health check:

    <CodeGroup>
      ```bash curl theme={null}
      curl http://localhost:3000/__health
      ```

      ```json Respuesta theme={null}
      {
        "mode": "app",
        "tools": [
          { "key": "enviarEmail", "path": "/enviar-email" },
          { "key": "leerBandeja", "path": "/leer-bandeja" }
        ]
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Validación falla con 400">
    La validación del schema se ejecuta antes que el handler. Si recibes un `400`, revisa el array `details`:

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST http://localhost:3000/enviar-email \
        -H "Content-Type: application/json" \
        -d '{"to": 12345}'
      ```

      ```json Respuesta 400 theme={null}
      {
        "error": "Validation failed",
        "details": [
          { "path": ["to"], "message": "Expected string, received number", "code": "invalid_type" },
          { "path": ["subject"], "message": "Required", "code": "invalid_type" },
          { "path": ["body"], "message": "Required", "code": "invalid_type" }
        ]
      }
      ```
    </CodeGroup>

    Cada entrada en `details` indica el campo (`path`), qué esperaba (`message`) y el código Zod (`code`).
  </Tab>

  <Tab title="Tool no aparece en MCP">
    Si un tool tiene `config: { mcp: false }`, no se registra en el servidor MCP. Verifica qué tools están expuestos:

    <CodeGroup>
      ```bash curl theme={null}
      curl http://localhost:3000/mcp
      ```

      ```json Respuesta theme={null}
      {
        "tools": [
          { "name": "enviar-email", "description": "Envía un correo electrónico", "inputSchema": { "..." } }
        ]
      }
      ```
    </CodeGroup>

    Si falta un tool, revisa que no tenga `mcp: false` en su `config`.
  </Tab>
</Tabs>

## Public per-tool

Puedes mezclar tools públicos y protegidos en el mismo `app()`:

```typescript theme={null}
export default app({
  tools: {
    webhookPagos: define({
      description: "Recibe callbacks de Stripe",
      input: z.object({ event: z.string() }),
      config: { public: true, mcp: false },
      handler: async (input, ctx) => {
        // Cualquier cliente puede llamar — valida la firma
        return { acknowledged: true };
      },
    }),
    consultarSaldo: define({
      description: "Consulta saldo de un cliente",
      input: z.object({ telefono: z.string() }),
      handler: async (input) => {
        // Requiere X-Jelou-Token
        return { saldo: 150.00 };
      },
    }),
  },
});
```

En `/__health`, cada tool reporta su estado `public`:

```json theme={null}
{
  "tools": [
    { "key": "webhookPagos", "path": "/webhook-pagos", "public": true },
    { "key": "consultarSaldo", "path": "/consultar-saldo", "public": false }
  ]
}
```

<Tip>
  Consulta la [guía de funciones públicas](/guides/functions/public) para más detalles sobre `public` y cómo validar webhooks.
</Tip>

## Siguientes pasos

<CardGroup cols={2}>
  <Card title="Pruebas" icon="flask-vial" href="/guides/functions/testing">
    Aprende a probar funciones multi-tool con `createMockApp()`.
  </Card>

  <Card title="MCP" icon="plug" href="/guides/functions/mcp">
    Configura el servidor MCP unificado para tus agentes IA.
  </Card>

  <Card title="Cron" icon="clock" href="/guides/functions/cron">
    Schedules independientes por tool dentro de `app()`.
  </Card>
</CardGroup>
