Codex · MCP Server
Codex MCP Server: build messaging and email into your product
One remote MCP server, three lines of config.toml, one header. Codex CLI, the IDE extension and the desktop app read the same entry: your agent reads the Unipile API specification, writes the LinkedIn, WhatsApp, email or calendar integration into your product, and tests it on a Development application. Nothing to install or run locally. Part of the Unipile MCP server.
~/.codex/config.toml
Project .codex/config.toml
Streamable HTTP, http_headers
CLI · IDE extension · desktop app
One config.toml entry, shared by the CLI, the IDE extension and the desktop app. 7-day free trial, no credit card.
Codex · crm-app
Unipile MCP connected
Add LinkedIn people search to my CRM, then let the user open the selected profile and start a conversation.
Read endpointPOST /v2/{account_id}/linkedin/searchschema loaded
Added GET /api/linkedin/search and the results list. Each row keeps the provider ID from the search response, so the profile and the conversation use the same identifier.
Now open the profile and start the conversation from the result row.
Run requestGET /v2/{account_id}/users/{identifier}200 OK
Describe the next feature…
The job
What you are trying to do
Add a LinkedIn, WhatsApp, email or calendar connection to your product. That means reading an API reference, picking the right endpoints, wiring Hosted Auth and its callbacks, then keeping the right IDs from search to message. With the Unipile MCP server in Codex, the agent does that reading for you and writes the code in your stack, from the CLI, the IDE extension or the desktop app.
Connect the Unipile MCP server to CodexUnipile MCP connected
Select the channels you want to connect
developer.unipile.com/mcpConnect all channels9 channels
Instagram/v2/{account_id}/chats↑↓navigate spaceselect ↵connectone URL, one header
Without it
Tabs, guesswork, glue code
Codex guesses endpoint names and payloads from training data, and gets the IDs wrong.
You paste schemas from the reference into the chat, one endpoint at a time.
The first real call happens in production, after the code review.
With the Unipile MCP server
The result in your application
A connect route and a Settings button: each user links their own account through Hosted Auth.
A webhook receiver and an inbox that shows messages and emails as they arrive.
Every request already run once on your Development application before you review the diff.
config.toml, CLI and IDE
Add the Unipile MCP server to Codex
The server is remote: one URL over streamable HTTP and one header. No npx, no local process. One entry in config.toml is read by Codex CLI, the Codex IDE extension and the ChatGPT desktop app, so you configure it once.
Codex CLI installed (npm i -g @openai/codex) or the Codex IDE extension, signed in.
A Development application in the Unipile dashboard, with a Scope and a scoped Account API key.
At least one test account connected to that Scope through Hosted Auth, so the agent can run real requests.
codex mcp add, then the headerregisters the URL in ~/.codex/config.toml
Global configuration~/.codex/config.toml
Project configuration.codex/config.toml (trusted project)
Key from an environment variableenv_http_headers
?
Why two steps on the CLI?codex mcp add takes --url and a bearer token variable, but no custom header flag. The Unipile server authenticates with X-API-KEY, so the command registers the URL and the header goes in config.toml, by hand or with env_http_headers.
# 1. Register the hosted Unipile MCP server (global config)
codex mcp add unipile --url "https://developer.unipile.com/mcp?branch=v2.0"
# Added global MCP server 'unipile'.
# 2. Add the X-API-KEY header to the entry in ~/.codex/config.toml
[mcp_servers.unipile]
url = "https://developer.unipile.com/mcp?branch=v2.0"
http_headers = { "X-API-KEY" = "your-scoped-api-key" }
# 3. Verify
codex mcp get unipile
[mcp_servers.unipile]
url = "https://developer.unipile.com/mcp?branch=v2.0"
http_headers = { "X-API-KEY" = "your-scoped-api-key" }
[mcp_servers.unipile]
url = "https://developer.unipile.com/mcp?branch=v2.0"
http_headers = { "X-API-KEY" = "your-scoped-api-key" }
# Read only inside a trusted project. Keep the key out of git: prefer env_http_headers.
[mcp_servers.unipile]
url = "https://developer.unipile.com/mcp?branch=v2.0"
env_http_headers = { "X-API-KEY" = "UNIPILE_API_KEY" }
# export UNIPILE_API_KEY=your-scoped-api-key before launching codex
Save the file and restart Codex. codex mcp list shows unipile as enabled, and /mcp inside a session lists the server. Verified on codex-cli 0.154.0.
What each line does, verified on codex-cli 0.154.0
codex mcp add unipileCreates the [mcp_servers.unipile] table in the global config.toml. The name is yours; keep it short, it becomes the tool prefix.--url "https://developer.unipile.com/mcp?branch=v2.0"Streamable HTTP transport. Quote the URL: the question mark is a glob character in zsh.http_headers = { "X-API-KEY" = "…" }Static header sent on every request. Use your scoped Account API key, never a Service or global Account key.env_http_headers = { "X-API-KEY" = "UNIPILE_API_KEY" }Same header, value read from the environment at launch. The right form for a project config.toml that lives in git.startup_timeout_sec = 30Optional. Default is 10 s; raise it if the first handshake times out on a slow network.enabled = falseOptional. Disables the server without deleting the entry, useful to switch between Development and Production keys.The Codex-only part
Keep your API key out of config.toml
http_headers writes the key in clear text into a file that ends up in backups and, for a project config, in git. Codex has three ways to send the X-API-KEY header; pick the one that matches where the file lives.
1http_headers, static valueThe form in the Unipile documentation. Fine for a user config on your own machine, never for a file shared in a repository.http_headers = { "X-API-KEY" = "your-scoped-api-key" }
2env_http_headers, read at launchMaps the header name to the name of an environment variable. The file carries no secret, each developer exports their own scoped key. The right form for a project config.toml.env_http_headers = { "X-API-KEY" = "UNIPILE_API_KEY" }export UNIPILE_API_KEY=your-scoped-api-key
3http_headers_helper, from a commandA local command that prints the headers as JSON, for teams that fetch keys from a vault. And remember CODEX_HOME: it moves the whole configuration directory, so a server saved in one terminal can be missing in another.http_headers_helper = "./scripts/unipile-headers.sh"
Verify
Verify the connection
Three checks, in the CLI, inside a session, then with a prompt that only reads the specification. None of them touches a connected account.
1In the Codex CLIlist prints one row per server with its URL and status. get shows the transport, the headers and the removal command.codex mcp listcodex mcp get unipile
2Inside a sessionIn the Codex TUI, the IDE extension (gear menu, MCP servers) and the ChatGPT desktop app (Settings, MCP servers), the same entry appears: one config, three surfaces./mcp# Status enabled, Auth Unsupported is expected: the server uses a header, not OAuth
3In a chat, without touching an accountAsk something that only reads the API specification. If the agent answers with real routes and parameters, the server is wired.Using the Unipile MCP, list the endpoints for LinkedIn people search and their required parameters.
Prompts, not glue code
Give your agent the prompt
Three integration jobs, each with the exact prompt to paste in Codex, the Unipile endpoints the agent reads and runs, and what lands in your project. Paths are complete, on the API base URL
https://api.unipile.com, with your scoped key in the X-API-KEY header.Add LinkedIn people search to my CRM, then let the user open the selected profile and start a conversation from it.
Search endpoints"linkedin search people profile"3 matches
Run requestPOST /v2/{account_id}/linkedin/search10 results
Added
GET /api/linkedin/search (keywords, pagination cursor) and GET /api/linkedin/profiles/:identifier. The results list keeps the provider ID returned by the search, the profile route reuses it, and the "Message" button passes it to the chat creation. Ran both on the Development app.One identifier from the search result to the conversation
The hard part of a LinkedIn feature is not the calls, it is keeping the same identifier from the search row to the profile and then to the message. Codex reads the three contracts through the server, sees which field carries that identifier in each response, and writes the routes so nothing is guessed.
Endpoints the agent uses
POST/v2/{account_id}/linkedin/searchGET/v2/{account_id}/users/{identifier}POST/v2/{account_id}/chats
Common mistake: Searching with one account and messaging with another. The profile and the chat must be opened on the
Build a LinkedIn integration
account_id that ran the search.Generate a typed Node.js and Python client for the Unipile chats and emails routes we use, from the API schemas, with retries on 429.
Read endpointGET /v2/{account_id}/emailsschema loaded
Run requestGET /v2/{account_id}/chats200 OK
Wrote
unipile-client.ts and unipile_client.py from the request and response schemas: typed list and send methods for chats and emails, cursor pagination helper, exponential backoff on 429 with the Retry-After header. Both clients ran the list calls on the Development app.Typed clients from the real schemas, not from memory
Codex does not guess the payloads. It reads the request body and the response schema of each route through the server, generates the types, and runs one call per method on your Development application before you review the diff. The official Node.js and Python SDKs remain the reference; the generated client is yours to keep small.
Endpoints the agent uses
GET/v2/{account_id}/chatsPOST/v2/{account_id}/chats/{chat_id}/messages/sendGET/v2/{account_id}/emailsPOST/v2/{account_id}/emails/send
Common mistake: Retrying a send on a timeout without an idempotency check. A message can leave once; retry the read calls, not the writes.
See the official SDKs
Support multiple connected accounts for each user of my SaaS: they can link several LinkedIn and email accounts and pick which one sends.
Read endpointGET /v2/accountsschema loaded
Run requestGET /v2/accounts3 accounts
Added an
accounts table keyed by user and account_id, a picker in the composer, and POST /api/messages that sends on the selected account. Reconnect states from the accounts route are surfaced as a badge. Verified with three accounts on the Development app.One user, several accounts, one Scope per workspace
Each account your users connect through Hosted Auth gets its own
account_id. The agent designs the mapping between your users and those IDs, reads the account status route to show reconnect and checkpoint states, and routes every send to the account the user picked.Endpoints the agent uses
GET/v2/accountsGET/v2/accounts/{account_id}POST/v2/auth/linkPOST/v2/{account_id}/chats
Common mistake: Storing the account ID on the workspace instead of the user. Accounts belong to the person who connected them; the workspace only groups Scopes and keys.
Implement Hosted Auth with an agent
Development to production
Test on a Development application first
The Unipile dashboard separates a Development application from a Production one. Give Codex a scoped key from the Development application, with one or two test accounts connected through Hosted Auth. The agent runs real requests on those accounts, on behalf of the authenticated user who linked them, within each provider's limits, and nothing touches your users' accounts until you ship. Keep
default_tools_approval_mode on prompt while you build if you want to confirm each write.Validate the connect flow end to end: auth link created server-side, account ID stored on the user.
Validate one read and one write per feature: list chats, send a message on the test account.
Validate a webhook delivery and a reconnect or checkpoint state before switching the key to Production.
crm-app · DevelopmentUsed by Codex
Scopedev-tests · 2 accounts
Key
scoped Account API keyAccountsLinkedIn test account, Gmail test mailbox
Webhooks1 endpoint · message events
crm-app · ProductionUntouched
Scopeone per workspace
Key
scoped keys, in your backend onlyAccountsyour users' own accounts, via Hosted Auth
Troubleshooting
Common errors and what they mean
What you see when a Codex MCP entry is not right, and the fix for each. Most of them come down to the file, the TOML, the trust level or the key.
The server does not show up after editing config.toml
codex mcp list prints nothing, or the entry is missing inside a session.
FixRestart the client: the file is read at startup. Then check CODEX_HOME: it moves the whole configuration directory, so a server saved in one terminal can be invisible in another. Run codex mcp list in the same shell you launch Codex from.
The project configuration is ignored
.codex/config.toml is at the repository root, and Codex still uses the global entry, or none.
FixCodex loads the project layer only for a trusted project. Mark it with trust_level = "trusted" under [projects."/path/to/repo"] in the user config, or move the entry to ~/.codex/config.toml.
Invalid TOML
The file fails to parse and every server disappears at once.
FixA table named [mcp_servers.unipile] exactly, quotes around "X-API-KEY" in the header table, and a table, not a string, for http_headers. A missing closing brace takes the whole file down.
401 Unauthorized on requests
The server is listed and reads the specification, but running a request fails.
FixThe header is missing, the variable named in env_http_headers is not exported in the shell that launched Codex, or the key is a Service or global Account key instead of a scoped Account API key from your Development application.
Settings say the server is unavailable
The IDE extension or the desktop app flags the server, yet actions run.
FixThat check looks for resources, and the Unipile server exposes actions, not resources. Confirm with /mcp inside a session and by running one read call. Nothing to change on your side.
Timed out
Startup or a call exceeds the limit.
FixDefaults are startup_timeout_sec = 10 and tool_timeout_sec = 60. The server is remote, there is no process to start: check the URL (?branch=v2.0 included), the network and any corporate proxy before raising the timeouts.
Codex MCP server FAQ
The questions people actually type: config.toml instead of mcp.json, where it lives, codex mcp add, keeping the key out of the file, the three surfaces, what to check when nothing shows up, timeouts and keys.
No. Codex stores its MCP configuration in
~/.codex/config.toml, in TOML, one table per server named [mcp_servers.<name>]. There is no mcp.json in Codex, and the file is not created at install time: you create it, or codex mcp add creates it for you. A trusted project can also carry a .codex/config.toml at its root.~/.codex/config.toml for the user configuration, .codex/config.toml at the repository root for the project configuration. The CODEX_HOME environment variable moves the whole configuration directory: when a server shows in one terminal and not in another, check it first. Codex CLI, the IDE extension and the desktop app read the same file.Partly.
codex mcp add unipile --url "https://developer.unipile.com/mcp?branch=v2.0" writes the table for a streamable HTTP server, and --bearer-token-env-var covers servers that take a Bearer token. The Unipile server authenticates with an X-API-KEY header, which the command cannot set, so you add http_headers or env_http_headers to the entry it created. Verified on codex-cli 0.154.0.Use
env_http_headers = { "X-API-KEY" = "UNIPILE_API_KEY" }: it maps the header name to the name of an environment variable instead of a value, so the file can be committed without a secret and each developer exports their own scoped Account API key. http_headers is for static values, and http_headers_helper lets a local command produce the headers as JSON.Yes. The three surfaces of one Codex host read the same configuration, so a server added once is available everywhere. In the desktop app and in the extension you can also add it from Settings, MCP servers, Add server, choosing Streamable HTTP. Restart the client after saving the file.
Four causes, in order: the client was not restarted; the file sits under a different
CODEX_HOME than your current shell; the table is in a project .codex/config.toml and the project is not marked trust_level = "trusted", in which case Codex skips the project layer entirely; or the TOML is invalid. Run codex mcp list, then /mcp inside a session.startup_timeout_sec overrides the default 10 second startup timeout and tool_timeout_sec the default 60 second per-tool timeout, both under the server table. The Unipile server is remote over HTTP with no local process to start, so a startup timeout almost always points at the URL, the network or a corporate proxy, not at the server.The server answers without a key when the agent only reads the API specification. To run real requests, create a Scope in your Development application, assign the test accounts, and generate a scoped Account API key for that Scope. Never give an MCP client a Service key or a global Account key.