Skip to content

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.

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:

VariablePurposeDefault
OPENAGENTEMAIL_API_URLBase URL of the APIhttp://localhost:3100
OPENAGENTEMAIL_API_KEYThe 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.

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.

ItemValue
URLhttps://<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)
AuthAuthorization: Bearer <oa_… / admin API_KEYS / OAuth access token>
SessionNone — no Mcp-Session-Id; each request authenticates on its own
MethodsPOST only (other methods → 405 with Allow: POST)
DiscoveryGET /.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 originEnv 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:

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.

{
"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).

  1. GET /.well-known/oauth-protected-resource (RFC 9728 PRM) — read authorization_servers for the issuer.
  2. GET /.well-known/oauth-authorization-server (RFC 8414) — expect among others:
    • code_challenge_methods_supported: ["S256"]
    • authorization_response_iss_parameter_supported: true
    • client_id_metadata_document_supported: true
    • authorization_endpoint, token_endpoint, revocation_endpoint

Endpoint details: REST API — OAuth Authorization Server.

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).

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.

TokenLifetimeNotes
Access1 hourBearer for POST /mcp
Refresh30 daysRotating — 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 (same resource / client_id binding).
  • Revoke via POST /oauth/revoke (RFC 7009), or revoke the whole grant from Dashboard Authorized clients at /ui/oauth/grants.
ToolWhat 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].

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.

Terminal window
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.ts

Or edit ~/.claude.json / project .mcp.json directly with the JSON block from Generic MCP clients below.

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.

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.

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.

Connect ChatGPT (Pro, Web) via remote OAuth:

  1. Enable Developer mode in Settings.
  2. Create a new app or connector.
  3. Set the MCP URL to https://inbox.openagent.email/mcp.
  4. Choose OAuth for authentication.
  5. In advanced OAuth settings, the client registration method automatically selects Client ID Metadata Document (CIMD). Keep this default — no token needs to be pasted.
  6. 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:

  1. Go to grok.com/connectors and create a new connector.
  2. Select Custom.
  3. Set the connector URL to https://inbox.openagent.email/mcp.
  4. Follow the prompts to complete OAuth authorization. (The connector details page provides an entry to re-authenticate whenever needed.)
  5. In conversations, mention @<connector-name> to call OpenAgentEmail tools.

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"
}
}
}
}

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.

  • Server starts but every tool errors with 401OPENAGENTEMAIL_API_KEY matches neither an admin key (API_KEYS env) 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 startupOPENAGENTEMAIL_API_KEY is 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_for returns nothing — the server clamps waits with MCP_MAX_WAIT_SECONDS (default 60, max 600). Verify inbound mail with ./deploy/doctor.sh before 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.