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

# Multi-tool functions

> Group multiple tools in a single deployment with app(): auto-generated routes, unified MCP, independent cron, and shared config.

<Warning>
  **Preview** — Jelou Functions is in preview. The API and behavior may change without prior notice.
</Warning>

## When to use `app()` vs `define()`?

| Pattern    | When to use it                                     |
| ---------- | -------------------------------------------------- |
| `define()` | A single operation with one HTTP endpoint          |
| `app()`    | Multiple related tools you want to deploy together |

If your project has a single handler, use `define()`. If you need to group multiple operations — for example, sending and reading emails from the same service — use `app()`.

## Basic example

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

export default app({
  config: { cors: { origin: "*" }, timeout: 30_000 },
  tools: {
    sendEmail: define({
      description: "Sends an email",
      input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
      config: { timeout: 5_000 },
      handler: async (input) => ({ sent: true }),
    }),
    readInbox: define({
      description: "Reads inbox messages",
      input: z.object({ limit: z.coerce.number().default(10) }),
      handler: async (input) => ({ messages: [] }),
    }),
  },
});
```

## Local testing

Start the server with `jelou functions dev` and test each tool at its route:

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

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

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

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

If you send an invalid field, you receive a `400` with details:

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

  ```json Response 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: {
    /* ... your define() here ... */
  },
});

export default edgeApp;
```

Type signature:

```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;
}
```

| Field     | Type       | Default                                 | Description                      |
| --------- | ---------- | --------------------------------------- | -------------------------------- |
| `cors`    | `object`   | `{ origin: "*" }`                       | Global CORS configuration        |
| `timeout` | `number`   | `30000`                                 | Timeout in ms for all tools      |
| `methods` | `string[]` | `["GET","POST","PUT","PATCH","DELETE"]` | Allowed HTTP methods             |
| `mcp`     | `boolean`  | `true`                                  | Register tools in the MCP server |

## Auto-generated routes

Your `tools` object keys are automatically converted to kebab-case to generate HTTP routes:

| Key         | Route         |
| ----------- | ------------- |
| `sendEmail` | `/send-email` |
| `readInbox` | `/read-inbox` |
| `MyTool`    | `/my-tool`    |

To customize a route, use `config.path` in the individual `define()`:

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

## Config combination

The global config of `app()` applies to all tools. Each `define()` can override specific values:

| Field     | Behavior                  |
| --------- | ------------------------- |
| `timeout` | Tool overrides the global |
| `methods` | Tool overrides the global |
| `cors`    | Tool overrides the global |
| `mcp`     | Tool overrides the global |
| `path`    | Always per tool           |
| `cron`    | Always per tool           |

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

In this example, `fast` has a 5-second timeout and `slow` inherits the global 30 seconds. Both share the CORS configuration.

## Health endpoint

In app mode, `/__health` returns information about all tools:

```json theme={null}
{
  "mode": "app",
  "tools": [
    {
      "key": "sendEmail",
      "name": "Send Email",
      "description": "Sends an email",
      "path": "/send-email",
      "cron": []
    },
    {
      "key": "readInbox",
      "name": "Read Inbox",
      "description": "Reads inbox messages",
      "path": "/read-inbox",
      "cron": []
    }
  ]
}
```

## Type guards

Use `isEdgeApp()` and `isEdgeFunction()` to identify the export type at runtime:

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

isEdgeApp(module);      // true if created with app()
isEdgeFunction(module);  // true if created with define()
```

## Multi-tool cron

Each tool inside `app()` can have its own independent cron schedules. Cron requests are sent to the specific route of each tool.

```typescript theme={null}
export default app({
  tools: {
    dailyCleanup: define({
      description: "Cleans up stale records",
      input: z.object({}),
      config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { cleaned: true };
      },
    }),
    hourlySync: define({
      description: "Syncs external data",
      input: z.object({}),
      config: { cron: [{ expression: "0 * * * *" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { synced: true };
      },
    }),
  },
});
```

<Warning>
  The **10** cron schedule limit is **aggregated** across all tools in an `app()`. If `dailyCleanup` has 3 and `hourlySync` has 4, only 3 remain available.
</Warning>

## Multi-tool MCP

A single MCP server at `/mcp` automatically registers all tools from the `app()`. To exclude a tool from the MCP registry, use `mcp: false` in its config:

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

In this example, AI agents only discover `publicTool` via MCP. `internalTool` is only accessible via direct HTTP at its `/internal-tool` route.

## Exposed routes (app mode)

| Route         | Description                                              |
| ------------- | -------------------------------------------------------- |
| `/__health`   | Health check with tool list                              |
| `/mcp`        | Unified MCP server (unless `config.mcp: false` globally) |
| `/<tool-key>` | Each tool's route (kebab-case or custom `config.path`)   |

## Common issues

<Tabs>
  <Tab title="404 on tool route">
    `tools` keys are converted to **kebab-case**. If your key is `sendEmail`, the route is `/send-email`, not `/sendEmail`.

    Verify the actual routes with the health check:

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

      ```json Response theme={null}
      {
        "mode": "app",
        "tools": [
          { "key": "sendEmail", "path": "/send-email" },
          { "key": "readInbox", "path": "/read-inbox" }
        ]
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Validation fails with 400">
    Schema validation runs before the handler. If you receive a `400`, check the `details` array:

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

      ```json Response 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>

    Each entry in `details` indicates the field (`path`), what was expected (`message`), and the Zod code (`code`).
  </Tab>

  <Tab title="Tool does not appear in MCP">
    If a tool has `config: { mcp: false }`, it is not registered in the MCP server. Verify which tools are exposed:

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

      ```json Response theme={null}
      {
        "tools": [
          { "name": "send-email", "description": "Sends an email", "inputSchema": { "..." } }
        ]
      }
      ```
    </CodeGroup>

    If a tool is missing, check that it does not have `mcp: false` in its `config`.
  </Tab>
</Tabs>

## Next steps

<CardGroup cols={2}>
  <Card title="Testing" icon="flask-vial" href="/guides/functions/testing">
    Learn to test multi-tool functions with `createMockApp()`.
  </Card>

  <Card title="MCP" icon="plug" href="/guides/functions/mcp">
    Configure the unified MCP server for your AI agents.
  </Card>

  <Card title="Cron" icon="clock" href="/guides/functions/cron">
    Independent schedules per tool inside `app()`.
  </Card>
</CardGroup>
