gmail-mcp-server-typescript
An authenticating proxy that bridges Google's remote Gmail MCP server with OAuth token refresh, letting MCP clients use static credentials to access Gmail.
README
gmail-mcp-server-typescript
An authenticating proxy for Google's remote Gmail MCP server.
Google's Gmail MCP server is a managed service at
https://gmailmcp.googleapis.com/mcp/v1 — there is nothing to self-host. It
authenticates with OAuth 2.0, and access tokens expire hourly. OpenHuman's MCP
client can hold a static credential but cannot refresh an OAuth one, and its
dynamic OAuth path requires RFC 7591 dynamic client registration, which Google
does not offer.
This proxy bridges that gap: it holds a long-lived refresh token, mints access tokens, and injects them upstream. Downstream it speaks plain MCP, so the client authenticates with static credentials that never expire.
OpenHuman --Basic--> Nginx Proxy Manager --> gmail-mcp-proxy:8080 --Bearer--> Google
(both containers on the shared `npm` network)
Design rationale, including the OpenHuman source constraints this is built
around: docs/superpowers/specs/2026-08-08-gmail-mcp-proxy-design.md.
Setup
1. Google Cloud
gcloud services enable gmail.googleapis.com --project=PROJECT_ID
gcloud services enable gmailmcp.googleapis.com --project=PROJECT_ID
Configure the OAuth consent screen with scopes:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.compose
Set the publishing status to "In production". While it is Testing + External, Google expires refresh tokens after 7 days and this proxy will break weekly. Verification is not required — you click through an "unverified app" warning once during consent.
Create an OAuth 2.0 Client ID of type Web application, and note the client ID and secret.
2. Configure (local only)
cp .env.example .env
$EDITOR .env # fill in GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
3. Mint the refresh token
One-time, using the credentials already in .env:
# Register http://localhost:8080/callback on the OAuth client first
pnpm mint
It prints an authorization URL, waits on a throwaway local callback server,
exchanges the code, and prints the refresh token on its own line for easy
copying. Paste it into .env as GOOGLE_REFRESH_TOKEN.
The script only ever reads .env — it never writes to it.
| Flag / var | Effect |
|---|---|
--open |
Launch a browser at the authorization URL |
PORT |
Callback port (default 8080) — must match the registered redirect URI |
ENV_FILE |
Read a different env file |
The script sets access_type=offline and prompt=consent. Both matter:
without the first Google returns no refresh token at all, and without the
second it only issues one on the first authorization for a given
client+account — so a re-run appears to succeed while producing nothing
usable. If you still get no refresh token, revoke at
myaccount.google.com/permissions
and run it again.
Alternatively use the OAuth 2.0 Playground
(gear icon → "Use your own OAuth credentials"), registering
https://developers.google.com/oauthplayground as the redirect URI instead.
4. Build the image
docker build -t gmail-mcp-proxy:latest .
5. Create the shared network
Nginx Proxy Manager runs in its own container, so both need a network they can address each other on. Once, on the host:
docker network create npm
Then attach the NPM container to it as well — add npm to its networks: and
redeploy, or docker network connect npm <npm-container>.
6. Deploy on TrueNAS
docker-compose.yml is written for TrueNAS SCALE → Apps → Custom App.
Paste it in, having replaced the three REPLACE_ME values.
Three things about that file are deliberate:
- Values are literal, not
${...}. TrueNAS has no.envbeside the YAML to interpolate from, so a${GOOGLE_CLIENT_ID}would resolve to an empty string and the container would exit at boot. - There is no
build:key. Custom apps have no build context; the image must already exist on the host, hence step 4. - The published port is loopback-only.
127.0.0.1:40001:8080is a debugging door on the NAS itself, not the path NPM uses — NPM is in its own container and reaches the proxy over the shared network ashttp://gmail-mcp-proxy:8080. See Security notes before widening it.
Verify once it's up, from the NAS:
curl localhost:40001/healthz # => {"ok":true}
.env is only used by local development (pnpm dev) and pnpm mint. It plays
no part in the deployed container.
Environment variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
GOOGLE_CLIENT_ID |
yes | — | OAuth client ID |
GOOGLE_CLIENT_SECRET |
yes | — | OAuth client secret |
GOOGLE_REFRESH_TOKEN |
yes | — | Long-lived token minted in step 3 |
UPSTREAM_URL |
no | https://gmailmcp.googleapis.com/mcp/v1 |
Google's MCP endpoint |
PORT |
no | 8080 |
Port inside the container |
BIND_HOST |
no | 127.0.0.1 |
Interface to bind. Defaults to loopback so a bare pnpm dev is never exposed. Containers must set 0.0.0.0, or the NPM container cannot reach them over the shared network. |
LOG_LEVEL |
no | info |
debug | info | warn | error |
MAX_BODY_BYTES |
no | 41943040 (40 MB) |
Request body cap. Gmail allows 25 MB attachments and base64 inflates ~33%, so a compose call can legitimately reach ~34 MB. Bodies are buffered to allow replay on a 401 retry, so this is also a memory bound. |
TOKEN_ENDPOINT |
no | https://oauth2.googleapis.com/token |
Override for testing |
Missing required variables abort at boot with a clear message rather than failing later as an opaque 502.
Nginx Proxy Manager
Add a Proxy Host with Forward Hostname gmail-mcp-proxy and Forward
Port 8080 — the container name on the shared npm network, not a host IP
and port. Nothing is published on the host, so a <truenas-ip>:<port> target would
not resolve.
Access List (this is NPM's Basic auth): Access Lists → Add → Authorization tab → add a username and password, then select that list on the Proxy Host.
Leave "Satisfy Any" OFF. With it on and a LAN range in the Access tab, nginx treats IP-match or password as sufficient, so your whole LAN reaches Gmail unauthenticated. See NginxProxyManager issue #4984.
Advanced tab — required, or SSE stalls and MCP notifications arrive batched:
proxy_buffering off;
proxy_read_timeout 3600s;
OpenHuman
[[mcp_client.servers]]
name = "gmail"
endpoint = "https://gmail-mcp.your.lan/mcp/v1"
enabled = true
timeout_secs = 60
[mcp_client.servers.auth]
kind = "basic"
username = "svc-openhuman"
password = "<NPM Access List password>"
timeout_secs = 60 because a cold token refresh plus a Gmail round-trip can
exceed the 30s default on the first call.
Development
pnpm install
pnpm typecheck
pnpm test # 30 tests, no network
pnpm dev # runs src/ directly, no build step
Troubleshooting
| Symptom | Cause |
|---|---|
503 + refresh token is dead |
Re-mint GOOGLE_REFRESH_TOKEN. Check publishing status is "In production", not "Testing". |
502 + a message about scopes |
A fresh token was rejected — the OAuth client is missing gmail.readonly / gmail.compose. Not an expiry problem. |
| Notifications arrive in batches | proxy_buffering off; missing from the NPM Advanced tab. |
| 401 from the edge, never reaching the proxy | NPM Access List credentials do not match the OpenHuman config. |
| Works for a week, then stops | Consent screen is still Testing — Google expired the refresh token at 7 days. |
Security notes
- Request and response headers are filtered through an allowlist. The
inbound
Authorization(your NPM Basic credentials, which nginx still forwards upstream) is dropped and replaced, so it can never reach Google. - The container runs
read_onlyas thenodeuser withno-new-privileges, and carries no runtime dependencies — only Node built-ins and globalfetch. - The proxy itself is unauthenticated by design; NPM's Access List is the
boundary. The published port is therefore
127.0.0.1:40001— reachable from the NAS itself for debugging, but with no route from the LAN, so the Access List stays the only way in. - Do not change it to
40001:8080. That binds0.0.0.0, and since the proxy injects agmail.readonly + gmail.composebearer onto every request it receives, any LAN host could then read or send mail without touching NPM. Docker's published-port DNAT is also evaluated ahead of host firewall rules, so aufw/iptablesdeny would not close it after the fact. BIND_HOSTis0.0.0.0inside the container so the NPM container can reach it over the shared network. That is not an exposure: with no published port, "all interfaces" means only the container's own network interfaces. The application defaults to127.0.0.1so a barepnpm devon your laptop is never exposed.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。