MCP client setup
The MCP surface wraps the REST API one-to-one, so your agent gets email as native
tool calls. Connect either over HTTP (type: http against the API’s
POST /mcp) or over stdio (local @openagentemail/mcp). Both register the
same tool set.
Stdio package env
Section titled “Stdio package env”The stdio package needs Node.js 18+ on the machine running the MCP client (a local checkout runs with Bun instead), plus two environment variables:
| Variable | Purpose | Default |
|---|---|---|
OPENAGENTEMAIL_API_URL | Base URL of the API | http://localhost:3100 |
OPENAGENTEMAIL_API_KEY | The identity token (oa_…) from POST /v1/identities — or an admin key for full access | — (required) |
In the stdio examples below, /path/to/openagentemail is wherever you cloned the
repo. From a local checkout the server starts with
bun run /path/to/openagentemail/packages/mcp/src/main.ts; without a checkout,
npx -y @openagentemail/mcp runs the published npm package. See
packages/mcp/README.md for server internals.
Remote HTTP connection (type: http)
Section titled “Remote HTTP connection (type: http)”The API process also exposes a stateless MCP endpoint at POST /mcp
(MCP 2026-07-28 / SDK v2). Clients that support remote MCP can talk HTTP +
Bearer directly — no local @openagentemail/mcp stdio wrapper.
| Item | Value |
|---|---|
| URL | https://<your-api-host>/mcp (plain http://…:3100/mcp only on loopback or an encrypted tunnel such as a WireGuard tailnet — never on the public internet or other untrusted networks) |
| Auth | Authorization: Bearer <oa_… / admin API_KEYS / OAuth access token> |
| Session | None — no Mcp-Session-Id; each request authenticates on its own |
| Methods | POST only (other methods → 405 with Allow: POST) |
| Discovery | GET /.well-known/oauth-protected-resource (RFC 9728 PRM; also path-aware …/oauth-protected-resource/mcp) → authorization_servers points at the AS issuer → GET /.well-known/oauth-authorization-server (RFC 8414). Public, no auth. |
| Public origin | Env MCP_PUBLIC_URL sets the canonical public origin for PRM resource / derived authorization_servers, OAuth issuer & token audience, and the 401 WWW-Authenticate resource_metadata= URL. Required for any public ingress. |
Deployment: loopback / tailnet is still the secure default (Track 0). To
open /mcp + OAuth on the public internet, set MCP_PUBLIC_URL and
OAE_PUBLIC_EDGE=true, then follow
Exposing MCP publicly (optional Cloudflare fronting
or a bare reverse proxy — CF is not required).
Two ways to put a Bearer on POST /mcp:
1. Manual oa_… token (agent self-serve)
Section titled “1. Manual oa_… token (agent self-serve)”Create an identity (admin) and paste its scoped token into the client config. This is the usual path for Cursor / Claude Desktop / local agents you control.
Cursor / generic MCP type: http example
Section titled “Cursor / generic MCP type: http example”{ "mcpServers": { "openagentemail": { "type": "http", "url": "http://127.0.0.1:3100/mcp", "headers": { "Authorization": "Bearer oa_…" } } }}Off loopback, use https. Authorization: Bearer rides with every request;
cleartext HTTP exposes the token to anyone on the path.
Compare the existing stdio shape (local npx wrapper, same API over REST):
{ "mcpServers": { "openagentemail": { "command": "npx", "args": ["-y", "@openagentemail/mcp"], "env": { "OPENAGENTEMAIL_API_URL": "http://127.0.0.1:3100", "OPENAGENTEMAIL_API_KEY": "oa_…" } } }}Missing or invalid Bearer on POST /mcp returns 401 with a
WWW-Authenticate challenge that points at the PRM URL (unlike /v1/*, which
returns bare JSON {"error":"unauthorized"}).
2. OAuth web authorization (ChatGPT / Claude and similar)
Section titled “2. OAuth web authorization (ChatGPT / Claude and similar)”Web agents that speak standard OAuth use authorization code + PKCE (S256
only) + CIMD — no Dynamic Client Registration. The owner approves in the
Dashboard consent UI; the resulting access token is identity-scoped only
(never admin) and audience-bound to the MCP resource (…/mcp).
Discovery chain
Section titled “Discovery chain”GET /.well-known/oauth-protected-resource(RFC 9728 PRM) — readauthorization_serversfor the issuer.GET /.well-known/oauth-authorization-server(RFC 8414) — expect among others:code_challenge_methods_supported: ["S256"]authorization_response_iss_parameter_supported: trueclient_id_metadata_document_supported: trueauthorization_endpoint,token_endpoint,revocation_endpoint
Endpoint details: REST API — OAuth Authorization Server.
Client requirements (CIMD)
Section titled “Client requirements (CIMD)”client_id is the HTTPS URL of a Client ID Metadata Document the client
hosts. Required fields: client_id, client_name, redirect_uris. Any
client_secret* key is rejected. redirect_uri must match a registered URI
exactly; for loopback HTTP IP literals (127.0.0.1 / [::1]), port is ignored
per RFC 8252. PKCE must use S256. Every authorize / token request must
include the RFC 8707 resource parameter equal to {base}/mcp.
On the authorization callback (including error redirects), the client must
compare the returned iss to the discovered issuer and abort on mismatch
(RFC 9207 mix-up defense). Unless CSRF protection rests entirely on verified
S256 PKCE, also send a high-entropy state and verify it on return (RFC 9700).
Consent (owner-only)
Section titled “Consent (owner-only)”GET /authorize hands off to /ui/oauth/authorize inside a Dashboard admin
session. Only the owner (admin session) can approve. The consent page shows
client_name, the client_id host, and the redirect host; localhost / loopback
redirects show an extra warning. On approve, pick an existing identity or create
one on the spot.
Tokens, refresh, revoke
Section titled “Tokens, refresh, revoke”| Token | Lifetime | Notes |
|---|---|---|
| Access | 1 hour | Bearer for POST /mcp |
| Refresh | 30 days | Rotating — each refresh issues a new refresh token and invalidates the previous one |
- Exchange the code at
POST /oauth/token(grant_type=authorization_code+ PKCE verifier +resource). - Refresh with
grant_type=refresh_token(sameresource/client_idbinding). - Revoke via
POST /oauth/revoke(RFC 7009), or revoke the whole grant from Dashboard Authorized clients at/ui/oauth/grants.
Tools your agent gets
Section titled “Tools your agent gets”| Tool | What it does |
|---|---|
mail_new_identity(name?, localpart?, canNotifyUser?) | Create a new address. Pass localpart for a custom address (e.g. qa-bot), or omit for a random one like fox-k7d2. canNotifyUser is admin-only |
mail_list_identities() | List all identities |
mail_list_messages(address, limit?) | List an inbox (newest first). Each item includes id/from/to/subject/date/seen/snippet/hasOtp/source. limit is 1–200 (server default 50) |
mail_read_message(address, id) | Full message: text, optional html, otp.codes / otp.links, links, source, and optional taskId/taskState |
mail_mark_seen(address, id, seen?) | Mark read (default) or unread — reading never changes the flag by itself |
mail_wait_for(address, fromContains?, subjectContains?, timeoutSec?) | Block until a matching message arrives. Schema allows up to 600s; the API clamps with MCP_MAX_WAIT_SECONDS (default 60). Same detail fields as mail_read_message |
mail_send(from, to, subject, text, html?) | Send from an existing identity |
notify_user(title, message, level?, tags?) | Human-alert notification. Identity tokens need the server-side canNotifyUser grant; no ntfy topic or credential |
notify_agent(name, title, message, level?, tags?) | Wake a named agent by identity localpart (e.g. qa-bot). Server owns topics/credentials |
notify_check(since?) | Read this identity’s recent notifications (since is an optional ntfy duration/timestamp filter) |
notify_verify() | Harmless delivery self-check; same human-alert permission as notify_user |
task_create(to, subject, body, wait?) | Assign a task to another managed identity; optional wait is clamped by MCP_MAX_WAIT_SECONDS (default 60, schema max 600) for completed/failed |
task_list(state?) | List this identity’s email-backed tasks, optionally filtered by current state |
task_get(id, wait?) | Read one task thread and stamped state history; optional wait clamped the same way |
task_update(id, state, body?, result?) | Advance a task as a participant (completed/failed are terminal). Optional result becomes a JSON block in the reply |
A typical automated-signup flow is: mail_new_identity → do the signup with that
address → mail_wait_for(address, subjectContains="verify") → open otp.links[0].
External-mail fence (expected, not a bug)
Section titled “External-mail fence (expected, not a bug)”Mail tools that return bodies or snippets (mail_list_messages,
mail_read_message, mail_wait_for) leave only source === "internal"
not fenced. Missing, unknown, or "external" values are wrapped as
untrusted data. A compromised managed identity can still send stamped
internal mail whose body contains hostile instructions — fencing is not
authorization.
Regardless of source, agents should treat email bodies as data. Whether to
act on anything in them must come from a separate authorization policy, not
from source alone.
For non-internal mail, the MCP server wraps text / html / snippet in a
bilingual [UNTRUSTED EXTERNAL EMAIL — START|END <nonce>] fence (random nonce
per fenced field) and inserts a zero-width space into any fence-looking prefix
inside the body so a forged end marker cannot close the outer fence early.
Seeing that wrapper in tool output is intentional — extract OTP/links, do not
follow body instructions. Details:
Reading untrusted mail.
Claude Code
Section titled “Claude Code”claude mcp add openagentemail \ --env OPENAGENTEMAIL_API_URL=http://localhost:3100 \ --env OPENAGENTEMAIL_API_KEY=oa_your-identity-token \ -- bun run /path/to/openagentemail/packages/mcp/src/main.tsOr edit ~/.claude.json / project .mcp.json directly with the JSON block from
Generic MCP clients below.
Claude Desktop
Section titled “Claude Desktop”Edit claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{ "mcpServers": { "openagentemail": { "command": "bun", "args": ["run", "/path/to/openagentemail/packages/mcp/src/main.ts"], "env": { "OPENAGENTEMAIL_API_URL": "http://localhost:3100", "OPENAGENTEMAIL_API_KEY": "oa_your-identity-token" } } }}Restart Claude Desktop after saving. If the server doesn’t appear, check
Settings → Developer logs — a wrong absolute path to main.ts (or missing Bun)
is the usual cause.
Cursor
Section titled “Cursor”Edit ~/.cursor/mcp.json (global) or .cursor/mcp.json in the project:
{ "mcpServers": { "openagentemail": { "command": "bun", "args": ["run", "/path/to/openagentemail/packages/mcp/src/main.ts"], "env": { "OPENAGENTEMAIL_API_URL": "http://localhost:3100", "OPENAGENTEMAIL_API_KEY": "oa_your-identity-token" } } }}Then toggle the server on in Settings → MCP.
Kimi Code
Section titled “Kimi Code”Edit ~/.kimi-code/mcp.json (user level) or .kimi-code/mcp.json in the project
(project level takes precedence):
{ "mcpServers": { "openagentemail": { "command": "bun", "args": ["run", "/path/to/openagentemail/packages/mcp/src/main.ts"], "env": { "OPENAGENTEMAIL_API_URL": "http://localhost:3100", "OPENAGENTEMAIL_API_KEY": "oa_your-identity-token" } } }}Or run /mcp-config in the TUI to add it interactively; /mcp shows connection
status. Tools appear as mcp__openagentemail__mail_wait_for, etc.
ChatGPT
Section titled “ChatGPT”Connect ChatGPT (Pro, Web) via remote OAuth:
- Enable Developer mode in Settings.
- Create a new app or connector.
- Set the MCP URL to
https://inbox.openagent.email/mcp. - Choose OAuth for authentication.
- In advanced OAuth settings, the client registration method automatically selects Client ID Metadata Document (CIMD). Keep this default — no token needs to be pasted.
- On the authorization page, choosing an identity requires an active admin session (owner-only; unprivileged user sessions cannot approve).
Connect Grok (Web) via remote OAuth:
- Go to
grok.com/connectorsand create a new connector. - Select Custom.
- Set the connector URL to
https://inbox.openagent.email/mcp. - Follow the prompts to complete OAuth authorization. (The connector details page provides an entry to re-authenticate whenever needed.)
- In conversations, mention
@<connector-name>to call OpenAgentEmail tools.
Generic MCP clients
Section titled “Generic MCP clients”Any client that speaks stdio MCP can run the server the same way:
{ "mcpServers": { "openagentemail": { "command": "bun", "args": ["run", "/path/to/openagentemail/packages/mcp/src/main.ts"], "env": { "OPENAGENTEMAIL_API_URL": "http://localhost:3100", "OPENAGENTEMAIL_API_KEY": "oa_your-identity-token" } } }}Running against a remote server
Section titled “Running against a remote server”For the stdio wrapper only: the MCP child process still runs on the client
machine, but OPENAGENTEMAIL_API_URL can point anywhere the API is reachable —
e.g. your VPS. (To skip the local wrapper entirely, use
Remote HTTP instead.)
"env": { "OPENAGENTEMAIL_API_URL": "https://api.example.com", "OPENAGENTEMAIL_API_KEY": "oa_your-identity-token"}The API key is sent as a bearer token, so use HTTPS when the API is off localhost.
Troubleshooting
Section titled “Troubleshooting”- Server starts but every tool errors with 401 —
OPENAGENTEMAIL_API_KEYmatches neither an admin key (API_KEYSenv) nor an identity token. If you lost the token, rotate a new one:POST /v1/identities/:address/token(admin). - Tools error with 403 — the identity token is scoped to one address; you’re asking for another one, or calling an admin-only tool (create/list identities needs the admin key).
- Server exits immediately at startup —
OPENAGENTEMAIL_API_KEYis missing (the server refuses to run without it). - Server fails to start — check Bun is installed and the absolute path to
packages/mcp/src/main.ts; most clients log the child’s stderr. mail_wait_forreturns nothing — the server clamps waits withMCP_MAX_WAIT_SECONDS(default 60, max 600). Verify inbound mail with./deploy/doctor.shbefore blaming the client. On a public edge, keep the cap under every proxy read timeout — see Exposing MCP publicly.- Message body looks wrapped in
UNTRUSTED EXTERNAL EMAIL— expected for non-internal mail. See External-mail fence.