gmail-mcp-server-typescript

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.

Category
Visit Server

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.readonly
  • https://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 .env beside 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:8080 is 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 as http://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_only as the node user with no-new-privileges, and carries no runtime dependencies — only Node built-ins and global fetch.
  • 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 binds 0.0.0.0, and since the proxy injects a gmail.readonly + gmail.compose bearer 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 a ufw/iptables deny would not close it after the fact.
  • BIND_HOST is 0.0.0.0 inside 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 to 127.0.0.1 so a bare pnpm dev on your laptop is never exposed.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured