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

> Group multiple tools in a single deployment with app(): contact management with create, search, and delete.

With `app()` you group multiple related operations in a single deployment. Your AI agents discover each tool individually via MCP, and each has its own HTTP route.

**Pattern:** `app()` + multiple `define()` + `.describe()` on each field + shared config + `ctx.env.get()` for secrets.

```typescript index.ts theme={null}
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 };
      },
    }),
  },
});
```

## Local testing

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

<CodeGroup>
  ```bash curl /create-contact theme={null}
  curl -X POST http://localhost:3000/create-contact \
    -H "Content-Type: application/json" \
    -d '{"name": "Maria Garcia", "email": "maria@example.com", "phone": "593987654321"}'
  ```

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

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

  ```json Response 200 theme={null}
  {
    "contacts": [
      { "id": "ct_a1b2c3", "name": "Maria Garcia", "email": "maria@example.com" }
    ],
    "total": 1
  }
  ```
</CodeGroup>

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

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

If you send an invalid input, you receive a `400` with the error details:

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

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

## Why it works this way

* **Automatic routes** — the keys `createContact`, `searchContacts`, `deleteContact` generate `/create-contact`, `/search-contacts`, `/delete-contact`.
* **GET vs POST** — `searchContacts` uses `config: { methods: ["GET"] }` to receive parameters as query strings. The others use POST by default.
* **`description` matters** — each tool has a specific description that tells the AI agent exactly what it does and what it returns. "Searches contacts by name or email" is much better than "Searches contacts".
* **Shared config** — `cors` and `timeout` are defined once in `app()` and apply to all tools. Each tool can override them if needed.
* **Centralized secrets** — all 3 tools use `ctx.env.get("CRM_API_KEY")`. You configure the secret once with `jelou secrets set`.
* **Unified MCP** — `curl http://localhost:3000/mcp` returns the 3 tools as independent tools the agent can invoke.
