---
title: Giving an OpenAI-based agent an email address
metaTitle: Add email to an OpenAI agent with MCP or REST
description: Connect a GPT-based agent to a real inbox: MCP where the tooling supports it, plain function tools where it does not, and the guardrails to set before it emails anyone.
date: 2026-07-30
author: MCPmailer
tags: Tutorials
---

An agent built on OpenAI tooling can already call functions, so adding email is a question of what you point it at. There are two paths, and the one you take depends on whether your stack speaks MCP. Both end in the same place: an agent with its own address, its own key, and the same limits any other agent has.

![A GPT-based agent connected to an email tool surface](/blog/openai-agent-email-tutorial/hero.webp "Two paths in, one identity at the other end.")

## First, an identity of its own

Create an agent and a key from the [quickstart](/docs/quickstart), or let it [provision its own](/blog/agent-self-signup-email). The key is scoped to one identity, so the agent can only send from the address it owns and read the mail addressed to it.

What you should not do is hand it OAuth credentials to a person's Gmail. The reasoning is in [agent inbox or shared mailbox](/blog/agent-inbox-vs-shared-mailbox), and the short version is that a mailbox-scoped token cannot express per-agent revocation, attribution, or the difference between replying and starting a conversation.

## Path one: MCP, where the tooling supports it

Where your stack can connect to an MCP server, point it at the endpoint and the whole tool surface arrives at once: send, reply, thread, search, wait, contacts, notes, vault. On the OpenAI Agents SDK that is one server object, attached to the agents you want to give an outbox.

```python
import os

from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

async with MCPServerStreamableHttp(
    name="mcpmailer",
    params={
        "url": "https://connect.mcpmailer.com/mcp",
        "headers": {"Authorization": f"Bearer {os.environ['MCPMAILER_KEY']}"},
        "timeout": 320,
    },
    cache_tools_list=True,
) as server:
    agent = Agent(name="Support", mcp_servers=[server])
    result = await Runner.run(agent, "Answer anything unread.")
```

Two details that catch people. `mcp_servers` hangs off the `Agent`, not the run, so an agent you hand off to needs its own reference or it will not have the mail tools. And the `timeout` default is five seconds, which is shorter than `wait_for_reply` can legitimately block for, so raise it if you intend to wait in-band.

Nothing else to define, and nothing to keep in sync when the tool surface changes. The full picture is in [what an MCP email server is](/blog/mcp-email-server), and the [OpenAI Agents SDK page](/with/openai-agents) covers the client-specific detail.

## Path two: function tools over REST

Where MCP is not available, define a small number of function tools and have them call the [REST API](/docs/rest) with the same key. Resist the urge to expose everything: four tools cover most agents, and each extra one is another thing the model can get wrong.

```ts
const tools = [
  {
    type: 'function',
    name: 'get_thread',
    description: 'The whole conversation for a message, oldest first, quoted history removed.',
    parameters: {
      type: 'object',
      properties: { message_id: { type: 'string' } },
      required: ['message_id']
    }
  },
  {
    type: 'function',
    name: 'lookup_contact',
    description: 'What the workspace already knows about this address. Call before writing.',
    parameters: {
      type: 'object',
      properties: { email: { type: 'string' } },
      required: ['email']
    }
  },
  {
    type: 'function',
    name: 'reply_all',
    description: 'Reply in thread, keeping the audience. Use this rather than composing a new send.',
    parameters: {
      type: 'object',
      properties: { message_id: { type: 'string' }, body: { type: 'string' } },
      required: ['message_id', 'body']
    }
  },
  {
    type: 'function',
    name: 'escalate',
    description: 'Hand the thread to a human. Use when a rule fires or a claim cannot be sourced.',
    parameters: {
      type: 'object',
      properties: { message_id: { type: 'string' }, why: { type: 'string' } },
      required: ['message_id', 'why']
    }
  }
];
```

Two design notes in that list. `reply_all` rather than a generic send, because an agent given a compose tool will eventually [break the thread](/blog/email-threading-for-agents). And `escalate` as a real tool, because an escalation path the model can call is followed far more reliably than one described in prose.

## Return refusals to the model, do not swallow them

The most common integration mistake: catching a rejected send and returning "error" to the model, which then tries again. Pass the reason through, and say in the description what it means.

```ts
const res = await fetch('https://mcpmailer.com/v1/messages', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(args)
}).then((r) => r.json());

// The model can act on this. "Something went wrong" it can only retry.
return res.status === 'rejected'
  ? { rejected: true, reason: res.reason, retry_after: res.retryAfter }
  : { sent: true, message_id: res.messageId };
```

`daily_send_quota_exhausted` and `monthly_send_quota_exhausted` carry a reset time, `recipient_suppressed` means that address already bounced or complained, and `monthly_spend_cap_reached` means only the workspace owner can lift it. An agent told what these mean waits or escalates; an agent that sees a generic failure loops.

![Four function tools and one honest refusal path](/blog/openai-agent-email-tutorial/tools.webp "Fewer tools, plus refusals the model can actually act on.")

## Waiting, and the thing that does not fit in one call

Email conversations outlast a single run. Two mechanisms cover it, neither of which is a polling loop.

Inside a conversation the agent is having, `wait_for_reply` blocks server-side and returns when the reply arrives or the timeout expires. For anything arriving cold, take a `message.received` webhook and start a fresh run, rebuilding context with `get_thread` and `lookup_contact`. The patterns are in [long-running email conversations](/blog/wait-for-reply-long-running-agents) and [webhooks or polling](/blog/webhooks-vs-polling-agent-email).

## Guardrails to set before it emails anyone

- **Whitelist mode** on the identity while you build trust, so strangers cannot reach it.
- **A [system prompt](/blog/email-agent-system-prompt)** with escalation triggers that fire without judgement.
- **Untrusted text kept away from privileged tools.** An inbound message is attacker-controlled input, per [prompt injection by email](/blog/prompt-injection-email-agents).
- **Verified [sending domain](/blog/dmarc-record-generator-agents)** before anything goes to a customer.
- **A person reading every thread in week one**, which catches more than any test suite.

## Questions

### Can a GPT-based agent send and receive email?

Yes. Connect an MCP email server where your tooling supports MCP, or define a few function tools that call the REST API with the same scoped key.

### How many email tools should I expose?

Four covers most agents: get the thread, look up the contact, reply in thread, and escalate. Every extra tool is another way for the model to do something you did not intend.

### Why `reply_all` instead of a send tool?

Because a compose tool invites the model to start a new conversation instead of continuing one, which breaks threading for the recipient and for the agent's own later context.

### What should a tool return when a send is refused?

The reason, verbatim, plus any retry-after. A generic error tells the model to retry, and retrying a quota refusal is how a temporary limit becomes a reputation problem.

### Should the agent use my Gmail account?

Only if it genuinely acts as one specific person. Otherwise a dedicated identity gives you per-agent scope, attribution, and revocation that a mailbox token cannot.

### How does it handle replies that arrive tomorrow?

`wait_for_reply` inside an active conversation, and a `message.received` webhook for anything else, rebuilding context from the thread and contact record.

## Related

- [Email for OpenAI Agents SDK agents](/with/openai-agents)
- [Give ChatGPT and GPT-based agents an email inbox](/with/gpt)
- [An email agent in TypeScript](/blog/typescript-email-agent-sdk)
- [What an MCP email server is](/blog/mcp-email-server)
- [REST API](/docs/rest)
