---
title: "OAuth or API key: how to connect an AI agent to an MCP server"
description: "What the MCP spec says about OAuth, how Unitpost's hosted server signs a person in, what consent grants, and when a scoped API key fits better."
url: https://www.unitpost.com/blog/agents/mcp-oauth-vs-api-key
section: Blog
updated: 2026-11-02
---
# OAuth or API key: how to connect an AI agent to an MCP server

Use OAuth when a person connects an MCP client such as Claude, Cursor or ChatGPT: the client opens a browser, the person picks a workspace and the permissions, and can revoke that sign-in later. Use an API key where nothing can open a browser: CI jobs, containers, your own agent code, or a client that only takes a header. On Unitpost both reach the same server and pass the same scope checks.

## OAuth or API key: how to connect an AI agent to an MCP server

> What the MCP spec says about OAuth, how Unitpost's hosted server signs a person in, what consent grants, and when a scoped API key fits better.

The MCP spec describes one way to authorize a remote server: OAuth 2.1, run by the client, with a person approving in the browser. Most hosted servers also accept an API key, because a lot of agents run where nobody can click Approve. Which one you want depends on two things: whether a person is present, and who should own the access.

### What does the MCP spec say about authorization?

The current specification (2026-07-28) opens with "Authorization is OPTIONAL for MCP implementations." When a server supports it, an HTTP transport should follow the spec, and a stdio server should not: it should "retrieve credentials from the environment" (MCP authorization (https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization)).

For HTTP, the rules that matter in practice:

- Roles. The MCP server is an OAuth 2.1 resource server. The client is the OAuth client, acting for a person.
- Discovery. Servers must publish OAuth 2.0 Protected Resource Metadata (RFC 9728). A request without a token gets a `401` whose `WWW-Authenticate` header points at that document; clients fall back to well-known URIs when it doesn't.
- PKCE. Clients must use it, with `S256` when they can, and must refuse an authorization server that doesn't advertise support (security considerations (https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations)).
- Audience. Clients send a `resource` parameter naming the server (RFC 8707). Servers must check that a token was issued for them, and must not accept or pass through any other token.
- Where the token goes. In `Authorization: Bearer` on every request, never in the query string.
- Client registration. A client uses pre-registered details when it has them, otherwise a Client ID Metadata Document, with Dynamic Client Registration, now deprecated, as the fallback (client registration (https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration)).
- Scopes. A token without the scope an operation needs gets a `403` with `error="insufficient_scope"` and the scopes required, and the client asks again for those plus what it already holds.

The spec's own example of the first challenge:

```bash
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"
```

The spec says nothing about API keys. A server that takes a static key as a Bearer token has made its own choice, outside the spec's flow.

### How does Unitpost's hosted server sign you in?

Add `https://mcp.unitpost.com/mcp` to your client. The first call without a token answers `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` challenge. A client that follows the spec fetches `/.well-known/oauth-protected-resource`, runs the PKCE code flow against the advertised authorization server and retries with the token. Unitpost prefers Client ID Metadata Documents and keeps Dynamic Client Registration for clients that still use it.

What you see is a browser tab: pick a workspace, approve the permissions, done. There is no key to copy. Access tokens are signed JWTs issued for the MCP URL and last an hour; refresh tokens rotate and last 30 days, and the client refreshes on its own. In Claude Code:

```bash
claude mcp add --transport http unitpost https://mcp.unitpost.com/mcp
```

Then run `/mcp`, select unitpost and choose Authenticate. `unitpost login` in the CLI uses the same flow.

### What does the consent screen grant?

A sign-in acts as you, in one workspace, with the permissions you leave ticked:

- One checkbox per permission, the ones that send to real people listed first, with a Read only shortcut. The list is capped at what your role can do, so a client can't get more than you have.
- Unticking narrows the grant. The server grants what was asked for and ticked, nothing else, so an edited form can only take permissions away.
- A refresh never widens it. Adding a permission is a new sign-in and a new consent. When a tool needs one you left out, the call gets `403 insufficient_scope`, and the challenge carries that tool's scope plus the ones you already granted, so a client that asks again doesn't lose them.
- The grant is yours, not the workspace's. If you leave the workspace or are removed, your sign-ins stop working on the next call.

The same product rules apply as for a key: a verified `from` domain, the sending gate and the workspace's rate limit.

### Where do you revoke an OAuth connection?

Under Settings, Connected apps, which lists "apps signed in to this workspace with your account: the Unitpost CLI and AI tools connected over MCP", with what each may do and when it was last active. Revoke ends one at once and Revoke all ends every one of yours; the next call answers `401` saying the sign-in was revoked. `unitpost logout` revokes the CLI's sign-in the same way.

People who can manage members also see a member's sign-ins under Workspace, Team, then that member, and can revoke them, except the owner's.

### When is an API key the right choice?

When there is no person and no browser:

- CI jobs, SSH sessions and containers.
- Your own agent code. The Vercel AI SDK, the OpenAI Agents SDK and the Claude Agent SDK connect to the server with a key in a header.
- Clients that can only paste a static header.
- Access that should outlive one person. A key belongs to the workspace, so a teammate leaving doesn't break the job. That is also the risk: someone has to own rotating it.

A `pk_live_…` key is shown once and stored only as a hash. Its scope can't change after it's created, so to change what a job can do you create a new key, deploy it and revoke the old one. Revoking is immediate. No key, not even Full access, can manage keys, the team or billing (API keys (/guides/api-keys)).

With Claude Code, the key goes in a header:

```bash
claude mcp add --transport http --header "Authorization: Bearer pk_live_YOUR_KEY" unitpost https://mcp.unitpost.com/mcp
```

Codex can read it from the environment, so the key never sits in the config file:

```bash
[mcp_servers.unitpost]
url = "https://mcp.unitpost.com/mcp"
bearer_token_env_var = "UNITPOST_API_KEY"
```

### How do scopes work on each path?

Both credentials carry the same capability names, such as `emails:send` or `campaigns:send`. With OAuth you choose them at consent; with a key you choose a preset or a list when you create it. Each MCP tool call becomes one REST request with the same check, so a call outside the grant is `403 insufficient_scope` whichever credential made it.

Three URL options shape a connection. `?readonly=1` is enforced: every write on that connection is refused. `?scoped=1` hides tools the credential can't call. `?toolsets=` only changes which tools are listed; an unlisted tool still runs if the credential allows it, so it saves context, not risk.

For an agent that only sends, that means one key holding `emails:send` and nothing else. For a person in a chat client, it means unticking what this session won't need.

### Which should you pick?

- A person working in Claude, Cursor, ChatGPT or Codex: OAuth, revocable from Connected apps.
- A job with no browser, or your product's own agent: a dedicated key with the fewest scopes that do the job.
- Both at once is fine. They share one workspace, one rate limit and one activity log, which names the key or the person and app behind each change.

## FAQ

### Does the MCP spec require OAuth?

No. The spec says authorization is optional. When a server over HTTP supports it, it should follow the spec's OAuth 2.1 flow; a server over stdio should take credentials from the environment instead. The spec doesn't mention API keys, so a server that accepts one as a Bearer token is making its own choice.

### Can I use OAuth in a CI job?

Not well. The authorization code flow needs a person to approve in a browser. In CI, keep an API key in the job's secrets and send it as Authorization: Bearer. The Unitpost CLI reads the same key from UNITPOST_API_KEY.

### What happens to an OAuth connection when someone leaves the workspace?

On Unitpost it stops at once. An OAuth sign-in belongs to the person, so removing them, or them leaving, revokes their sign-ins and the next call is refused. An API key belongs to the workspace and keeps working until someone revokes it.

## Related

- [MCP server, SDK or REST API: how should an AI agent send email?](https://www.unitpost.com/blog/agents/mcp-vs-api-for-agent-email): The same email send over MCP, an SDK and REST: when each fits, OAuth vs API keys, and why scopes, limits and errors match on all three.
- [Move off SendGrid SMTP after the free plan ended](https://www.unitpost.com/blog/concepts/move-off-sendgrid-smtp): SendGrid retired its free plan. Point the same SMTP client at Unitpost — host, username unitpost, API key as password — without rewriting your app.
- [Send email and SMS from an n8n AI agent with MCP](https://www.unitpost.com/blog/agents/n8n-ai-agent-email-sms): Connect an n8n AI Agent to Unitpost with the MCP Client Tool and a Bearer API key, or call the REST API from an HTTP Request tool.
