Gemini CLI · MCP Server
Gemini CLI MCP Server: build messaging, email and calendar features
One remote MCP server, one command, one header. Add it to Gemini CLI and your agent reads the Unipile API specification, writes the LinkedIn, WhatsApp, email or calendar integration into your project, and tests it on a Development application. Nothing to install or run locally. Part of the Unipile MCP server.
~/.gemini/settings.json
Project .gemini/settings.json
Streamable HTTP, X-API-KEY header
gemini mcp add, --scope user
One command writes settings.json, user or project. 7-day free trial, no credit card.
Gemini CLI · booking-app
Unipile MCP connected
Add calendar scheduling to my booking app: read each user's availability and create the event on their Google or Outlook calendar.
Read endpointGET /v2/{account_id}/calendarsschema loaded
Added GET /api/availability (reads events over the requested window) and POST /api/bookings, which creates the event on the calendar the user connected. Time zones come from the calendar, not from the browser.
Now send the confirmation email from the same user's mailbox.
Run requestPOST /v2/{account_id}/calendars/{calendar_id}/events200 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 Gemini CLI, the agent does that reading for you and writes the code in your stack, from the terminal.
Connect the Unipile MCP server to Gemini CLIUnipile 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
Gemini CLI 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.
settings.json, user or project
Add the Unipile MCP server to Gemini CLI
The server is remote: one URL over streamable HTTP and one header. No npx, no local process. One command writes the entry in settings.json, user or project, and Gemini CLI connects at the next launch.
Gemini CLI installed (npm i -g @google/gemini-cli) and signed in, in a folder you have trusted.
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.
gemini mcp add, one linewrites settings.json for you
User settings~/.gemini/settings.json
Project settings.gemini/settings.json (default scope)
Key from an environment variable$UNIPILE_API_KEY in headers
?
User or project?The command defaults to project scope, which writes .gemini/settings.json in the current folder. Pass --scope user for every project on the machine. Project settings override user settings, and both need a trusted folder to load.
# Register the hosted Unipile MCP server for every project (user scope)
gemini mcp add --transport http --scope user \
--header "X-API-KEY: your-scoped-api-key" \
unipile "https://developer.unipile.com/mcp?branch=v2.0"
# MCP server "unipile" added to user settings. (http)
# Verify, or type /mcp inside a session
gemini mcp list
# ✓ unipile: https://developer.unipile.com/mcp?branch=v2.0 (http) - Connected
{
"mcpServers": {
"unipile": {
"url": "https://developer.unipile.com/mcp?branch=v2.0",
"type": "http",
"headers": { "X-API-KEY": "your-scoped-api-key" }
}
}
}
{
"mcpServers": {
"unipile": {
"url": "https://developer.unipile.com/mcp?branch=v2.0",
"type": "http",
"headers": { "X-API-KEY": "$UNIPILE_API_KEY" }
}
}
}
// Committed with the repository: keep the key in the environment, not in the file.
{
"mcpServers": {
"unipile": {
"url": "https://developer.unipile.com/mcp?branch=v2.0",
"type": "http",
"headers": { "X-API-KEY": "${{UNIPILE_API_KEY}}" }
}
}
}
// export UNIPILE_API_KEY=your-scoped-api-key before launching gemini
Save the file and launch gemini in a trusted folder. gemini mcp list shows unipile as Connected, and /mcp inside a session lists the server. Verified on gemini-cli 0.60.0.
What each flag does, verified on gemini-cli 0.60.0
--transport httpRequired: the default is stdio, a local process. The Unipile server is remote over streamable HTTP. The CLI writes it as "type": "http".--scope userWrites ~/.gemini/settings.json. Without it the entry goes to .gemini/settings.json in the current folder, the project scope.--header "X-API-KEY: …"Repeatable. Your scoped Account API key, never a Service or global Account key. Any position in the command works.unipile "https://developer.unipile.com/mcp?branch=v2.0"The name is yours. Quote the URL: the question mark is a glob character in zsh."$UNIPILE_API_KEY"In settings.json, $VAR or ${VAR} is read from the environment at launch, so a project file can be committed without a secret.--timeout 30000Optional, in milliseconds. Raise it only if the first handshake fails on a slow network or behind a proxy.The Gemini CLI-only part
httpUrl, url, trust and your API key
Three things in settings.json that decide whether the server connects, and that no other client has in this form.
1httpUrl or urlThe documentation maps httpUrl to streamable HTTP and url to SSE. The gemini mcp add command writes url with "type": "http", and both connect. If you write the file by hand, use httpUrl: a bare url without a type is read as SSE, the most cited cause of a Disconnected status."httpUrl": "https://developer.unipile.com/mcp?branch=v2.0"
2$VAR inside headersGemini CLI expands $NAME and ${NAME} in settings.json, headers included. The project file can be committed without a secret, and each developer exports their own scoped Account API key."headers": { "X-API-KEY": "$UNIPILE_API_KEY" }export UNIPILE_API_KEY=your-scoped-api-key
3A trusted folder, and trust left unsetIn an untrusted folder every server is listed as Disabled, user level included. Trust the folder at the first prompt or with the trust command. Leave the trust option of the server unset: it would skip the confirmation before each action.gemini trust
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.
1From the terminallist prints one row per server with its transport and status. A checkmark and Connected means the handshake succeeded; a circle and Disabled means the folder is not trusted.gemini mcp list# ✓ unipile: … (http) - Connected
2Inside a sessionType /mcp to see every configured server with its state, Connected, Disconnected or Disabled. The Unipile server appears with its actions ready to be called./mcp# or /mcp desc for the description of each action
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 Gemini CLI, 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 calendar scheduling to my booking app: read each user's availability over a week and create the meeting on the calendar they connected, Google or Outlook.
Read endpointGET /v2/{account_id}/calendars/{calendar_id}/eventsschema loaded
Run requestPOST /v2/{account_id}/calendars/{calendar_id}/events201 Created
Added
GET /api/availability (events over the requested window, busy slots computed server-side) and POST /api/bookings, which creates the event with attendees on the user's calendar and stores the returned event ID. Time zones are taken from the calendar. Created a test event on the Development app.Availability and event creation on the calendar your user connected
Google Calendar and Outlook Calendar share the same calendar routes. Gemini CLI reads the calendar, event and attendee schemas through the server, writes the availability computation and the booking route, and creates one event on a test calendar before you review the diff.
Endpoints the agent uses
GET/v2/{account_id}/calendarsGET/v2/{account_id}/calendars/{calendar_id}/eventsPOST/v2/{account_id}/calendars/{calendar_id}/events
Common mistake: Computing availability in the browser's time zone. Use the calendar's time zone from the API response, or the slot is off by hours for a remote attendee.
See every channel on the Unipile MCP server
Our inbox already syncs LinkedIn and WhatsApp with Unipile. Add Instagram and Telegram with the same thread model and the same reply endpoint.
Search endpoints"chats messages attendees"3 matches
Run requestGET /v2/{account_id}/chats200 OK
No new model: Instagram and Telegram chats come through the same chat and message routes, so the sync job now iterates over every connected account regardless of provider, and the reply endpoint sends on the chat's own account. Added the two provider badges and ran the sync on the Development app with one Instagram and one Telegram test account.
Same routes, two more channels, no new model
Because every messaging provider is exposed through the same chat routes, extending an inbox is a matter of connecting accounts through Hosted Auth and letting the sync cover them. The agent checks the provider-specific fields in the schemas, such as attendee identifiers, and keeps the thread model unchanged.
Endpoints the agent uses
GET/v2/accountsGET/v2/{account_id}/chatsGET/v2/{account_id}/chats/{chat_id}/messagesPOST/v2/{account_id}/chats/{chat_id}/messages/send
Common mistake: Hardcoding the provider in the sync. Iterate over the accounts route and let the provider field of each account drive the badge, not the code path.
Build a unified inbox with an agent
Review my current Unipile integration for incorrect IDs, unsafe retries and calls that should be webhooks, and propose the fixes.
Read endpointPOST /v2/{account_id}/chats/{chat_id}/messages/sendschema loaded
Read endpointPOST /v2/webhooks/endpoints/schema loaded
Found three issues: the send call used the workspace's first account instead of the chat's account; the retry wrapper re-sent messages on a timeout; the inbox polled the chats route every 30 seconds. Proposed patches: account ID taken from the chat, retries limited to reads, a webhook endpoint for message events. Applied and verified on the Development app.
An audit against the real contract, not against memory
The agent compares your code with the request and response schemas it reads through the server: which field carries the account, which calls are idempotent, which polling loops the webhook events replace. The review lands as a diff you can apply, tested on your Development application.
Endpoints the agent uses
GET/v2/accounts/{account_id}POST/v2/{account_id}/chats/{chat_id}/messages/sendPOST/v2/webhooks/endpoints/GET/v2/webhooks/conversations/
Common mistake: Accepting a fix that retries a write. A message or an email leaves once; the safe retry is on the read side, with the webhook as the source of truth.
Wire webhooks with an agent
Development to production
Test on a Development application first
The Unipile dashboard separates a Development application from a Production one. Give Gemini CLI 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. Leave
trust unset on the server entry while you build, so the CLI asks before each action that writes.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 Gemini CLI
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 gemini mcp list and /mcp show when an entry is not right, and the fix for each. Most of them come down to the transport key, the scope, the folder trust or the key.
No MCP servers configured.
gemini mcp list finds no entry from the current folder.
FixThe file must be ~/.gemini/settings.json or .gemini/settings.json at the root of the folder you launched gemini from, with mcpServers at the top level of the JSON. A trailing comma makes the whole file ignored.
Disconnected
The server is listed with a cross and Disconnected.
FixCheck the transport key first: httpUrl for streamable HTTP, or url with "type": "http" as the command writes it. A bare url is read as SSE. Then the URL itself, with ?branch=v2.0, and any corporate proxy.
Disabled, folder untrusted
Every server, user level included, shows a circle and Disabled with a warning about the folder.
FixGemini CLI loads MCP servers only in a trusted folder. Accept the prompt at first launch or run gemini trust in the project, then list again.
Added in the wrong place
The entry works in one project and is missing in another.
Fixgemini mcp add defaults to project scope and writes .gemini/settings.json in the current folder. Add --scope user for every project, and remember that a project file overrides the user file for the same server name.
401 Unauthorized on requests
The server shows Connected, the agent reads the specification, but running an action fails.
FixConnected does not check the key. The header is missing, the variable named in headers is not exported in the shell that launched gemini, there is a stray space around the value, or the key is a Service or global Account key instead of a scoped Account API key.
Timed out
The handshake or an action exceeds the limit.
FixThe default timeout is 600000 ms. The server is remote, there is no process to start: a timeout at startup points at the network, a proxy or the URL. Set timeout on the entry only after those three are ruled out.
Gemini CLI MCP server FAQ
The questions people actually type: httpUrl or url, where settings.json lives, one command instead of JSON, keeping the key out of the file, Disconnected, No MCP servers configured, the VS Code extension and keys.
The documentation defines
httpUrl as the streamable HTTP endpoint and url as an SSE endpoint. The gemini mcp add --transport http command writes url together with "type": "http", and both forms connect on current versions. If you write the file by hand, use httpUrl: a bare url without a type is read as SSE, the most cited cause of a Disconnected server.User settings in
~/.gemini/settings.json, project settings in .gemini/settings.json at the root of your project, both under the mcpServers key, and project settings override user settings. Note the default of the command: gemini mcp add writes to the project scope unless you pass --scope user.Yes, and it is the recommended path:
gemini mcp add --transport http --scope user --header "X-API-KEY: your-scoped-api-key" unipile "https://developer.unipile.com/mcp?branch=v2.0". The --header flag is repeatable and can sit anywhere in the command. Verified on gemini-cli 0.60.0.Write the header value as
"$UNIPILE_API_KEY" or "${UNIPILE_API_KEY}". Gemini CLI expands environment variables in settings.json, headers included, so the project file can be committed without a secret and each developer exports their own scoped Account API key before launching gemini.In order: the transport key (
httpUrl for streamable HTTP, or url with "type": "http"), a project settings.json that overrides yours or the reverse, the folder trust (an untrusted folder disables every server), and finally a status shown as Disconnected while actions work, which comes from an optional ping in the specification. Run gemini mcp list, then /mcp inside a session.Gemini CLI found no
mcpServers entry in the configuration it reads from the current folder. Check that the file is in ~/.gemini/ or in .gemini/ at the project root, that mcpServers sits at the top level of the JSON, and that the JSON is valid: a trailing comma is enough to have the file ignored.This page covers Gemini CLI in the terminal, and the configuration described here is the settings.json read by the CLI. The extension has its own MCP settings in VS Code; check its documentation before assuming the file is shared.
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.