---
title: "Client setup"
description: "Connect ChatGPT, Claude, Cursor, VS Code, Codex, Gemini CLI and other MCP clients to the hosted MachineMail MCP server, signing in with OAuth or an API key."
canonical: "https://machinemail.dev/docs/agents/clients"
---

# Client setup

> Connect ChatGPT, Claude, Cursor, VS Code, Codex, Gemini CLI and other MCP clients to the hosted MachineMail MCP server, signing in with OAuth or an API key.

Source: https://machinemail.dev/docs/agents/clients

Every client connects to the same endpoint:

```text
URL:  https://mcp.machinemail.dev/mcp
```

There are two ways to authenticate:

- **Sign in with OAuth.** Give the client only the URL. On first use it opens your browser on MachineMail's **Connect an app** page. Choose what it can reach (the whole organization, one pod or one inbox) and approve. The client stores the tokens and renews them; nothing secret goes in a config file. See [OAuth sign-in](https://machinemail.dev/docs/agents/mcp#oauth-sign-in) for how it works.
- **Send an API key.** Set the header `Authorization: Bearer mm_live_...` with a key from the dashboard's [API keys](https://machinemail.dev/app/api-keys) page. Use this where no browser is available (CI, servers, headless agents). For an agent that should only touch one inbox, create a key scoped to that inbox: the MCP tools then see nothing else.

## Supported clients

Every client below is tested against MachineMail by replaying its own sign-in sequence: discovery, client identification, authorization, token exchange, `initialize` and `tools/list`, refresh and sign-out.

| Client                                    | How it identifies itself                                                                    | Token endpoint auth                           | Setup                                                        |
| ----------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ |
| ChatGPT (plugins, including dots)         | Client metadata document `chatgpt.com/oauth/client.json`                                    | none (PKCE)                                   | [ChatGPT](#chatgpt)                                          |
| ChatGPT, when the app builder picks DCR   | Dynamic registration                                                                        | `client_secret_post` or `client_secret_basic` | [ChatGPT](#chatgpt)                                          |
| claude.ai, Claude Desktop, mobile, Cowork | Client metadata document `claude.ai/oauth/mcp-oauth-client-metadata`                        | none (PKCE)                                   | [claude.ai and Claude Desktop](#claudeai-and-claude-desktop) |
| Claude Code                               | Client metadata document `claude.ai/oauth/claude-code-client-metadata`                      | none, or a secret with `--client-secret`      | [Claude Code](#claude-code)                                  |
| VS Code (GitHub Copilot)                  | Client metadata document `vscode.dev/oauth/client-metadata.json`, else dynamic registration | none                                          | [VS Code](#vs-code)                                          |
| Cursor                                    | Dynamic registration                                                                        | none, or a static client ID and secret        | [Cursor](#cursor)                                            |
| Codex CLI                                 | Client metadata document `chatgpt.com/oauth/codex/client.json`                              | none (PKCE)                                   | [Codex](#codex)                                              |
| Gemini CLI                                | Dynamic registration                                                                        | none (PKCE)                                   | [Gemini CLI](#gemini-cli)                                    |
| Zed                                       | Client metadata document `zed.dev/oauth/client-metadata.json`                               | none (PKCE)                                   | [Zed](#zed)                                                  |
| Windsurf                                  | Dynamic registration                                                                        | none (PKCE)                                   | [Windsurf](#windsurf)                                        |
| Goose                                     | Client metadata document `goose-docs.ai/oauth/client-metadata.json`                         | none (PKCE)                                   | [Goose](#goose)                                              |
| JetBrains IDEs                            | Dynamic registration (Air), or `mcp-remote`, or an API key                                  | none (PKCE)                                   | [JetBrains IDEs](#jetbrains-ides)                            |
| OpenCode, MCP Inspector, `mcp-remote`     | Dynamic registration                                                                        | none (PKCE)                                   | [OpenCode](#opencode), [Other clients](#other-clients)       |
| OpenAI Responses API, Claude Messages API | No sign-in: you pass a credential                                                           | n/a                                           | [Model APIs](#openai-responses-api-and-claude-messages-api)  |

Each section starts with OAuth. Where it shows an API key, it reads the key from an environment variable, `MACHINEMAIL_API_KEY` in the examples, so it stays out of files you commit:

```bash
export MACHINEMAIL_API_KEY=mm_live_...
```

> **Note:** Apps you connect with OAuth are listed under **Settings → Connected apps** in the dashboard, where you can disconnect them. Don't put auth.md access tokens in client configs: they expire after an
> hour and clients don't renew them.

## Claude Code

Add the server with the CLI, without a header:

```bash
claude mcp add --transport http --scope user machinemail https://mcp.machinemail.dev/mcp
```

Then run `/mcp` in a Claude Code session, select `machinemail` and choose **Authenticate**. Your browser opens on the Connect page. After you approve, Claude Code stores the tokens and refreshes them when they expire. Claude Code identifies itself with a client metadata document, so the Connect page shows it as Claude Code from `claude.ai`.

`--scope user` makes the server available in every project. Leave it out to add it to the current project only, or use `--scope project` to write it to the project's `.mcp.json` for your team. With OAuth, each person signs in with their own account, so the shared file holds no secret:

```json title=".mcp.json"
{
  "mcpServers": {
    "machinemail": {
      "type": "http",
      "url": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

To use an API key instead, for example in CI, add the header:

```bash
claude mcp add --transport http --scope user machinemail https://mcp.machinemail.dev/mcp \
  --header "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

In a shared `.mcp.json`, reference the variable instead of the value (`"headers": { "Authorization": "Bearer ${MACHINEMAIL_API_KEY}" }`), and each person's environment supplies their own key.

Check it with `claude mcp list`, or `/mcp` inside a session.

## ChatGPT

Add MachineMail to ChatGPT as a plugin:

1. Add the plugin with `https://mcp.machinemail.dev/mcp` as the server URL.
2. Sign in with OAuth. Leave any client ID and secret empty: ChatGPT identifies itself with its client metadata document.
3. A MachineMail tab opens; approve there.

ChatGPT calls MachineMail from OpenAI's servers, signs in as a public client with PKCE, and renews its tokens on its own. If you build an app that registers with Dynamic Client Registration instead, MachineMail issues it a client secret when it asks for `client_secret_post` or `client_secret_basic`.

### Dots

A dot is an always-on ChatGPT agent. It uses the same plugins as ChatGPT, so once MachineMail is added as a plugin and signed in, your dot can use it with nothing more to set up. A dot has no email inbox of its own; MachineMail gives it one. Ask the dot to create an inbox, and it can send from that address, wait for replies and read them.

A dot works on its own for long stretches, so a few things are worth setting up before you leave it running:

- **Give it its own inbox.*&#x2A; Ask it to create one with a name and display name, for example &#x2A;"Create an inbox called travel-desk with the display name Travel Desk"*. Mail it sends then comes from an address that is clearly the dot's, and replies land somewhere only it reads.
- **Limit what it can reach.** When you approve the sign-in, you can limit the connection to one pod or one inbox instead of the whole organization. Limited to a pod, the dot can still create inboxes, in that pod only. Limited to one inbox, it can't create any: create the inbox in the dashboard first and choose it when you approve.
- **Watch for new mail with events, not polling.** A dot can subscribe to [MachineMail's events](https://machinemail.dev/docs/agents/mcp#events-chatgpt&#x29; and is woken when mail arrives. Ask it to watch an inbox, for example &#x2A;"Tell me here when a new email arrives in [travel-desk@machinemail.dev](mailto:travel-desk@machinemail.dev)"*, and check that it subscribes to `message.received` for that inbox rather than checking on a schedule. Spam, quarantined mail and mail sent from your own MachineMail inboxes never trigger it.
- **Sign-up codes.** `wait_for_message` waits up to 60 seconds for a new email. It suits a short wait, such as a dot signing up for a service and waiting for its verification code, not ongoing monitoring.
- **Untrusted mail.** Everything the dot reads comes back in [agent-safe form](https://machinemail.dev/docs/agents/mcp#how-tools-present-untrusted-mail), with the trust level first. Quarantined mail is withheld. The server tells the dot to act only on verified senders and never to send because an email asked it to.
- **Sends are immediate.** There is no undo for mail an agent sends. Review what the dot may send on your behalf before you leave it running.
- **Plan limits.** On the Free plan, a busy dot can reach the 100 messages a day limit. Its sends are then refused until 00:00 UTC, and incoming mail is held. See [Limits](https://machinemail.dev/docs/reference/limits).
- **Stopping it.** **Settings → Connected apps** in the dashboard lists the dot's connection. **Disconnect** cuts it off at once. Its inbox and mail stay.

## claude.ai and Claude Desktop

Add MachineMail as a custom connector:

1. Open **Settings → Connectors** and choose **Add custom connector**.
2. Name it `MachineMail` and enter `https://mcp.machinemail.dev/mcp` as the URL. Leave the advanced OAuth settings empty.
3. Choose **Add**, then **Connect**. A MachineMail tab opens; approve there.

The connector is available in claude.ai, Claude Desktop and the mobile apps. On Team and Enterprise plans, an owner adds the connector for the organization, and each member then connects it with their own MachineMail account.

To use an API key in Claude Desktop instead, start the server through the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge, which needs Node.js. Open **Settings → Developer → Edit Config** and add:

```json title="claude_desktop_config.json"
{
  "mcpServers": {
    "machinemail": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.machinemail.dev/mcp", "--header", "Authorization:${MACHINEMAIL_AUTH}"],
      "env": {
        "MACHINEMAIL_AUTH": "Bearer mm_live_..."
      }
    }
  }
}
```

The header value goes through `env` because some platforms split arguments on spaces. Restart Claude Desktop after saving. Without `--header`, `mcp-remote` signs in with OAuth instead and opens your browser on first start.

The file lives at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows.

## Cursor

Add the server to `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` in one project:

```json title="~/.cursor/mcp.json"
{
  "mcpServers": {
    "machinemail": {
      "url": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

Under **Settings → MCP** the server shows that it needs a login. Click it, approve in the browser, and the tools appear.

For an API key, add a `headers` entry. Cursor reads `${env:...}` from the environment it was started in, so export the variable in your shell profile:

```json title="~/.cursor/mcp.json"
{
  "mcpServers": {
    "machinemail": {
      "url": "https://mcp.machinemail.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${env:MACHINEMAIL_API_KEY}"
      }
    }
  }
}
```

## VS Code

Add the server to `.vscode/mcp.json` in a workspace, or run **MCP: Open User Configuration** for every workspace:

```json title=".vscode/mcp.json"
{
  "servers": {
    "machinemail": {
      "type": "http",
      "url": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

Start the server from the **Start** button above it in the file. VS Code asks whether MachineMail may authenticate; allow it and approve in the browser. Then use the tools in Copilot's agent mode.

For an API key, prompt for it once with an input. VS Code stores it securely, so it never sits in the file:

```json title=".vscode/mcp.json"
{
  "inputs": [
    {
      "type": "promptString",
      "id": "machinemail-api-key",
      "description": "MachineMail API key",
      "password": true
    }
  ],
  "servers": {
    "machinemail": {
      "type": "http",
      "url": "https://mcp.machinemail.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${input:machinemail-api-key}"
      }
    }
  }
}
```

## OpenCode

Add a remote server to `opencode.json` (global: `~/.config/opencode/opencode.json`):

```json title="opencode.json"
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "machinemail": {
      "type": "remote",
      "url": "https://mcp.machinemail.dev/mcp",
      "enabled": true
    }
  }
}
```

OpenCode notices the 401 and signs in with OAuth. To start the sign-in yourself, run `opencode mcp auth machinemail`.

For an API key, set `"oauth": false` so OpenCode doesn't try to sign in, and add the header:

```json title="opencode.json"
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "machinemail": {
      "type": "remote",
      "url": "https://mcp.machinemail.dev/mcp",
      "enabled": true,
      "oauth": false,
      "headers": {
        "Authorization": "Bearer {env:MACHINEMAIL_API_KEY}"
      }
    }
  }
}
```

Check it with `opencode mcp list`.

## Codex

Add the server to `~/.codex/config.toml` (or `.codex/config.toml` in a trusted project). `bearer_token_env_var` names the environment variable that holds an API key; Codex sends it as `Authorization: Bearer ...`:

```toml title="~/.codex/config.toml"
[mcp_servers.machinemail]
url = "https://mcp.machinemail.dev/mcp"
bearer_token_env_var = "MACHINEMAIL_API_KEY"
```

Codex versions with OAuth support for MCP servers can sign in instead. Leave out `bearer_token_env_var` and run `codex mcp login machinemail`. Codex identifies itself with its client metadata document and listens on a loopback port for the response.

Check it with `codex mcp list`, or `/mcp` inside a session.

## Gemini CLI

Add the server to `~/.gemini/settings.json` (or `.gemini/settings.json` in a project). `httpUrl` selects Streamable HTTP:

```json title="~/.gemini/settings.json"
{
  "mcpServers": {
    "machinemail": {
      "httpUrl": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

Gemini CLI notices the 401 and signs in with OAuth on first use; to start it yourself, run `/mcp auth machinemail` in a session. For an API key, add `"headers": { "Authorization": "Bearer $MACHINEMAIL_API_KEY" }` instead.

## Zed

Add a remote context server in Zed's `settings.json`:

```json title="settings.json"
{
  "context_servers": {
    "machinemail": {
      "url": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

Zed signs in with OAuth when the server answers 401, identifying itself with its client metadata document. For an API key, add `"headers": { "Authorization": "Bearer mm_live_..." }`.

## Windsurf

Add the server to `~/.codeium/windsurf/mcp_config.json`:

```json title="mcp_config.json"
{
  "mcpServers": {
    "machinemail": {
      "serverUrl": "https://mcp.machinemail.dev/mcp"
    }
  }
}
```

Windsurf starts the OAuth sign-in when you add the server. For an API key, add `"headers": { "Authorization": "Bearer ${env:MACHINEMAIL_API_KEY}" }`.

## Goose

Run `goose configure`, choose &#x2A;*Add Extension → Remote Extension (Streamable HTTP)**, name it `machinemail` and enter `https://mcp.machinemail.dev/mcp`. Goose signs in with OAuth on first use, identifying itself with its client metadata document.

## JetBrains IDEs

JetBrains AI Assistant connects to remote MCP servers with static headers only, so use an API key there, or run the server through `mcp-remote` (stdio), which signs in with OAuth:

```json title="AI Assistant → Model Context Protocol"
{
  "mcpServers": {
    "machinemail": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.machinemail.dev/mcp"]
    }
  }
}
```

JetBrains Air and the GitHub Copilot plugin sign in with OAuth directly: give them the URL only.

## OpenAI Responses API and Claude Messages API

The model APIs call MCP servers for you but don't run a sign-in: pass a credential with each request. Use an API key (an inbox-scoped key keeps the model to one inbox). An OAuth access token from your own app works too, but it lasts one hour and the API won't refresh it.

```bash title="OpenAI Responses API"
curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5",
    "input": "List my MachineMail inboxes.",
    "tools": [{
      "type": "mcp",
      "server_label": "machinemail",
      "server_url": "https://mcp.machinemail.dev/mcp",
      "authorization": "'"$MACHINEMAIL_API_KEY"'",
      "require_approval": "never"
    }]
  }'
```

```bash title="Claude Messages API (MCP connector)"
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: mcp-client-2025-11-20" -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 4096,
    "messages": [{"role": "user", "content": "List my MachineMail inboxes."}],
    "mcp_servers": [{
      "type": "url",
      "url": "https://mcp.machinemail.dev/mcp",
      "name": "machinemail",
      "authorization_token": "'"$MACHINEMAIL_API_KEY"'"
    }],
    "tools": [{"type": "mcp_toolset", "mcp_server_name": "machinemail"}]
  }'
```

## Other clients

Any client that supports remote MCP servers over Streamable HTTP works. Give it:

| Setting   | Value                                                                                 |
| --------- | ------------------------------------------------------------------------------------- |
| Transport | Streamable HTTP (sometimes labelled `http` or `streamable-http`)                      |
| URL       | `https://mcp.machinemail.dev/mcp`                                                     |
| Auth      | OAuth (automatic in clients that support MCP authorization), or the header below      |
| Header    | `Authorization: Bearer mm_live_...`, when the client cannot sign in or has no browser |

Notes for clients that differ:

- **Only stdio supported?** Run the server through the `mcp-remote` bridge: `npx -y mcp-remote https://mcp.machinemail.dev/mcp`, which signs in with OAuth. To use an API key, add `--header "Authorization:${MACHINEMAIL_AUTH}"` as in the Claude Desktop example.
- **Configured with `https://api.machinemail.dev/mcp` or `https://machinemail.dev/mcp`?** Those legacy URLs are the same server and keep working (`https://machinemail.dev/mcp` tokens included). New setups should use `https://mcp.machinemail.dev/mcp`. If OAuth sign-in fails on `https://api.machinemail.dev/mcp`, switch to the new URL: see [Legacy endpoint](https://machinemail.dev/docs/agents/mcp).
- **Only the older HTTP+SSE transport supported?** The server does not offer it. Use `mcp-remote`, which speaks Streamable HTTP to MachineMail and stdio to your client.
- **MCP Inspector:** enter the URL and use **Quick OAuth Flow**; it registers itself and signs in from the browser.
- **Writing your own client?** [OAuth sign-in](https://machinemail.dev/docs/agents/mcp#oauth-sign-in) lists the discovery documents, endpoints and rules. Use PKCE with `S256` and send `resource=https://mcp.machinemail.dev/mcp`. Use a client metadata document URL as `client_id`, or register at `https://machinemail.dev/oauth2/register` (ask for `client_secret_basic` or `client_secret_post` if your app keeps a secret on a server).

## Check the connection

Ask the model to call `list_inboxes`. With a fresh organization it answers `No inboxes yet. Use create_inbox.` Then try a round trip:

> Create an inbox with a one-hour TTL, tell me its address, and wait for a message to arrive.

Send mail to the address from anywhere and the model gets it back from `wait_for_message`, with its trust level.

If the client reports a 401:

- **With OAuth:** the app may have been disconnected under **Settings → Connected apps**, or its refresh token expired after 30 days unused. Sign in again from the client.
- **With an API key:** the key is missing, mistyped or revoked. Test it with curl:

  ```bash
  curl -s -o /dev/null -w "%{http_code}\n" https://api.machinemail.dev/v0/inboxes -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
  ```

  `200` means the key is good and the problem is in the client config.
