# Email API tools for AI function calling

Source: https://developer.nylas.com/docs/cookbook/agents/email-api-function-calling/

An LLM can't touch a mailbox on its own. It produces text, and your code decides what that text does. To let a model list mail, search a thread, send a reply, or draft a follow-up, you expose those actions as tools: named functions with JSON schemas the model fills in, then your runtime executes against an API. The hard part isn't the model. It's defining a small set of tools that map cleanly to real endpoints across Gmail, Outlook, and four other providers without leaking a token to the model.

This recipe shows the tool definitions for four email actions, the exact endpoint each one calls, and how to surface the same set through the Model Context Protocol so any MCP-aware client can call them.

## Which email APIs support tool use or function calling?

Any HTTP email API can back a function-calling tool, because the model only emits arguments and your code makes the call. The real question is how many endpoints and schemas you wrap. The Nylas Email API exposes one schema across 6 providers, so four tools (list, search, send, reply) cover Gmail, Outlook, Yahoo, iCloud, IMAP, and Exchange.

Tool use works the same across OpenAI, Anthropic, and most agent frameworks: you pass a list of tool schemas, the model returns a tool call with arguments, and you execute it. The friction lives in the API behind each tool. The Gmail API and Microsoft Graph each need their own OAuth app, message schema, and rate budget, so a model that works both inboxes needs eight tools and two parsers. Routing through a single grant collapses that to four tools and one parser. A grant is an authenticated account connected once through OAuth, and the model never sees its token, so revoking the grant revokes every tool at once.

## What email API features matter for AI agent workflows?

Four features decide whether an email API is pleasant to wrap as agent tools: one unified message schema, server-side search that maps to a tool argument, sending through the user's own mailbox, and reply threading by message ID. Miss any one and the model needs extra tools or post-processing, which inflates the tool count and token cost.

A consistent schema matters most. When `GET /v3/grants/{grant_id}/messages` returns the same `id`, `subject`, `from`, and `snippet` fields for every provider, the model reasons over one shape, and passing snippets instead of full bodies keeps the default page of 50 messages (max 200) small enough to fit a tool response without blowing the context window. Server-side filters like `unread`, `subject`, and `search_query_native` let a single tool argument narrow results before they reach the prompt. The send endpoint posts through the connected mailbox, so replies land in the provider's Sent folder, and passing `reply_to_message_id` keeps the conversation threaded. The honest tradeoff: if your agent only ever touches one Gmail inbox and needs Gmail-only features like label colors, the Gmail API directly is fewer moving parts.

## Define a list-messages tool

Start with the read tool, since most agent turns begin by looking at the inbox. The schema below describes a `list_messages` function the model calls when it needs recent mail. It maps to `GET /v3/grants/{grant_id}/messages`, which returns up to 200 messages per page with a `snippet` field holding the first ~100 characters of each body, enough for triage without spending tokens on full HTML.

The `grant_id` is never a tool argument. Your runtime injects it from the authenticated session, so the model can't read another user's mailbox by guessing an ID. The model only fills `unread` and `limit`.

```json [emailTools-Tool schema]
{
  "name": "list_messages",
  "description": "List the user's recent email messages. Returns subject, sender, and a short snippet for each.",
  "parameters": {
    "type": "object",
    "properties": {
      "unread": { "type": "boolean", "description": "Only return unread messages." },
      "limit": { "type": "integer", "description": "How many to return (max 200).", "default": 20 }
    }
  }
}
```

When the model emits a `list_messages` call, your code maps the arguments to query parameters and runs the request. The `unread` and `limit` parameters are both real on the messages endpoint, so the mapping is direct with no translation layer.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?unread=true&limit=20' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

## Define a search-messages tool

Agents constantly need to find one specific thread: "the invoice from billing last week," "anything from the recruiter." Wrap that as a `search_messages` tool backed by the same `GET /v3/grants/{grant_id}/messages` endpoint, using its filter parameters instead of pulling the whole inbox into the prompt. Server-side filtering keeps the response under the 200-message page ceiling and cuts token spend on every search turn.

The endpoint accepts more than 10 filter parameters, including `subject`, `from`, `any_email`, `received_after`, and `search_query_native` for provider-native operators like Gmail's `is:unread` syntax. Map the model's arguments onto those parameters and let the API do the matching.

```json [emailTools-Search schema]
{
  "name": "search_messages",
  "description": "Search the user's mailbox by sender, subject, or keyword.",
  "parameters": {
    "type": "object",
    "properties": {
      "from": { "type": "string", "description": "Sender email address to match." },
      "subject": { "type": "string", "description": "Text to match in the subject line." }
    },
    "required": []
  }
}
```

A call with `from` set to a billing address becomes a filtered request. The `from` and `subject` filters run on the provider, so the model gets back only matching messages rather than scanning a full page itself.

```bash [emailTools-Search request]
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?from=billing@vendor.com&subject=invoice' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

## Define send and reply tools

Sending is where guardrails matter, so keep send and reply as two distinct tools with tight schemas. The `send_message` tool maps to `POST /v3/grants/{grant_id}/messages/send`, which delivers through the user's own mailbox across all 6 providers with no SMTP setup. The reply tool is the same endpoint plus a `reply_to_message_id` field, which threads the new message under the original conversation in Gmail and Outlook alike.

Keep the schemas minimal. The model supplies `to`, `subject`, and `body`; your runtime adds the grant and any policy checks. For autonomous flows, never let the model set the recipient without validation, since one wrong address is unrecoverable once sent. The reply tool's schema is below; the `send_message` tool is the same shape without `reply_to_message_id`.

```json [emailTools-Reply schema]
{
  "name": "reply_to_message",
  "description": "Reply to an existing message, keeping it in the same thread.",
  "parameters": {
    "type": "object",
    "properties": {
      "reply_to_message_id": { "type": "string", "description": "ID of the message being replied to." },
      "to": { "type": "array", "items": { "type": "object" }, "description": "Recipients." },
      "body": { "type": "string", "description": "Reply text." }
    },
    "required": ["reply_to_message_id", "to", "body"]
  }
}
```

The executed request posts the reply. Passing `reply_to_message_id` is what derives the send type as a reply and keeps threading intact, so the message shows up as part of the original conversation rather than a new one.

```bash [emailTools-Reply request]
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "reply_to_message_id": "<MESSAGE_ID>",
    "to": [{ "name": "Leyah Miller", "email": "leyah@example.com" }],
    "body": "Thanks, confirming the invoice is paid."
  }'
```

## How do I expose these tools through MCP?

The Model Context Protocol is an open standard from Anthropic that lets any compatible client discover and call tools from a server over a defined wire format. Wrapping the four email tools as an MCP server means Claude Code, Claude Desktop, and other MCP clients can list mail or send a reply without you writing client-specific glue for each one.

An MCP server advertises the same four tool definitions you'd pass to a model directly, then handles each call by running the matching request against `api.us.nylas.com`. The protocol, published in November 2024, separates tool discovery from execution, so the client lists available tools at connect time and the server holds the grant and API key. That keeps credentials out of the model context entirely, the same trust boundary as direct function calling. For a working setup that connects an MCP client to a connected mailbox, see [use Nylas with Claude Code through MCP](/docs/cookbook/ai/mcp/claude-code/). For the raw fetch-and-route plumbing without a framework, see [connect an LLM to a user's inbox](/docs/cookbook/ai/connect-llm-to-inbox/).


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


## What's next

- [Build an autonomous email agent](/docs/cookbook/agents/autonomous-email-agent/) to wrap these send tools in rate caps, allowlists, and a kill switch
- [Connect an LLM to a user's inbox](/docs/cookbook/ai/connect-llm-to-inbox/) for the fetch-and-route plumbing behind each tool
- [Use Nylas with Claude Code through MCP](/docs/cookbook/ai/mcp/claude-code/) to call these tools from an MCP client
- [Getting started with Nylas](/docs/v3/getting-started/) to create a project, connector, and your first grant