AfterHours Gmail MCP
by deploy103
README.md
# AfterHours Gmail MCP
Single-owner, read-only Gmail over authenticated Streamable HTTP MCP, using the
official Gmail API and **your own Google OAuth app and grant**. No scheduler,
background monitoring, sending, replies, deletion, or read-state changes. This is
an optional standalone server; it neither connects to nor alters Vooy's built-in
Gmail integration. Never extract browser cookies, Vooy tokens, or another app's
client credentials.
## Fresh clone and disconnected startup
Linux/POSIX, Python 3.11+ and a private local account are required. This public
repository can be cloned without GitHub authentication; Gmail still requires your own OAuth grant.
```sh
git clone https://github.com/deploy103/afterhours-gmail-mcp-public.git
cd afterhours-gmail-mcp-public
python3.11 -m venv .venv
.venv/bin/python -m pip install --require-hashes --only-binary=:all: -r requirements.lock
.venv/bin/python -m pip check
mkdir -p "$HOME/.config"
.venv/bin/python configure.py --directory "$HOME/.config/afterhours-gmail-mcp" \
--public-origin https://mcp.example.com
"$HOME/.config/afterhours-gmail-mcp/run.sh"
```
Replace the example origin with your own HTTPS hostname before exposing the
service. The option defaults to `https://mcp.example.com`. Only HTTPS origins on
443 (implicit or explicit) are accepted: no trailing slash, path, userinfo, query,
or fragment. The destination must be **new**, outside the checkout, with an
existing parent and no symlink components; `~` and relative paths are expanded
to absolute paths. There is no overwrite option. A failed partial configuration
is left private for inspection, not recursively removed. To regenerate, choose
a different new path and retire the old configuration yourself.
The stdlib bootstrap generates a random bearer token without printing it:
```text
~/.config/afterhours-gmail-mcp/ 0700
config.env 0600 (exported paths and public origin)
run.sh 0700
credentials/ 0700
mcp-token 0600 (secret)
state/ 0700 (reserved; no mail is stored)
```
`credentials/gmail-oauth.json` is deliberately **absent**. The server starts
without consent or Google I/O; `gmail_status` returns `not_connected`. Health
`GET /healthz` only proves the process is alive, not account access. `run.sh`
sources the generated `config.env` (`CREDENTIALS_DIRECTORY`, `MCP_PUBLIC_ORIGIN`),
changes to the absolute checkout path and uses its `.venv/bin/python -m uvicorn
gmail_app:create_app --factory`, bound to **127.0.0.1:8003**, one worker, no access
logs and no proxy headers. Paths are shell-quoted. Moving the checkout or venv
requires a new configuration or a deliberate launcher update.
## Client and HTTPS proxy
The local MCP path is `/mcp`. Configure your own TLS reverse proxy to pass
Authorization and preserve the configured external Host; strip any external
prefix (for example `/gmail/mcp` → `/mcp`). No proxy, firewall or service is
installed by the bootstrap. Do not expose the loopback service over untrusted
HTTP; keep the proxy on this host or design a separate authenticated TLS/VPN
transport. No CORS wildcard is provided. Avoid proxy access/debug logs containing
headers, bodies, or mailbox data.
Register `https://YOUR_HOST/gmail/mcp` (or your chosen proxy path) as a
Streamable HTTP server in your MCP client. Its header is `Authorization: Bearer
<MCP token>`, **not** a Google token. Read the value from the private **file**
`$HOME/.config/afterhours-gmail-mcp/credentials/mcp-token` using the client's
protected token-file/secret input if available. Never use the file path itself
as the bearer value, commit it, put it in shell arguments, or send it in chat.
Every bearer holder can read this owner's mailbox; this is not multi-user OAuth.
Token rotation requires replacing the private file and restarting the process.
## Authorize your own Google app (manual provisioning)
**Blocker until completed:** this repository has no installed-app browser OAuth
CLI and does not include `google-auth-oauthlib`. Bootstrap cannot authorize
Gmail or invent a refresh token. The supported manual route below uses Google's
OAuth Playground **with your own web application client**, not Playground's
shared client or an installed-app client. It requires a trusted owner browser
and handling the grant privately. Playground proxies the exchange through
Google's server (its documentation says credentials are not logged); do not use
this route if that does not meet your policy. Do not paste a grant into other
sites or create/share a Playground link containing credentials or tokens.
1. In your Google Cloud project, enable **Gmail API**. Configure Google Auth
Platform's consent screen/audience and add your own account as a test user
if the app is in Testing. For a Workspace account, an administrator may need
to approve the app and restricted Gmail scope.
2. Create an OAuth client of type **Web application** that you own. Add the exact
authorized redirect URI `https://developers.google.com/oauthplayground`.
Keep its client ID and secret private. This client is for this single-owner
grant only; do not reuse a broader existing grant.
3. Open https://developers.google.com/oauthplayground in your owner browser.
In the gear/settings, enable **Use your own OAuth credentials**, enter that
client's ID and secret, select **Server-side** flow, **Offline** access, and
force the consent prompt. In Step 1 enter **only**
`https://www.googleapis.com/auth/gmail.readonly` and authorize as the intended
owner. Do not select other API scopes or the shared Playground credentials.
4. In Step 2 exchange the authorization code for tokens. Confirm a refresh token
was issued and the granted scope is exactly `gmail.readonly`; if it is absent,
revoke this app's old grant in your Google account and authorize again with
Offline/consent. Do not copy access tokens into the server configuration.
5. Stop the server while provisioning. In a trusted local editor create
`$HOME/.config/afterhours-gmail-mcp/credentials/gmail-oauth.json` with this
structure, replacing all placeholders with the grant from **your** app:
```json
{
"client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"client_secret": "YOUR_CLIENT_SECRET",
"refresh_token": "YOUR_REFRESH_TOKEN",
"scopes": ["https://www.googleapis.com/auth/gmail.readonly"]
}
```
Create the file under `umask 077`, then `chmod 600` it before restarting
`run.sh`; directories remain 0700. Do not save editor backups elsewhere.
File must be a bounded, private regular file, not a symlink. The server uses
`users/me`: the refresh grant determines the owner, not an email setting.
6. Restart the launcher and call `gmail_status` from your authenticated MCP
client. Only successful `/users/me/profile` access yields
`profile_access_verified`; check the returned owner account before reading
messages. OAuth failures remain tool errors, not a connected status.
Gmail readonly is a **restricted scope**. Public distribution of your own app
may require Google verification/security assessment; Workspace policies can
block consent. External apps in Testing commonly have refresh tokens expiring
after seven days for this scope. Revocation, password/policy changes, quotas,
and provider rules may require reauthorization. The protected file's `scopes`
declaration must match the real grant: if a refresh response omits scope, the
server trusts this declaration. A file edit is not a way to narrow a broad grant.
Google access tokens stay in memory, refreshed on demand; there is no automatic
interactive reconnection or persistent refresh-token replacement. Remove the
file to disconnect locally and revoke the app grant in Google to revoke remote
access. Changes/removal apply on the next call, not to already in-flight reads.
## Tools, boundaries and operations
- `gmail_status`: disconnected without the optional file; otherwise verifies
the owner's profile with Google. Health is not proof of consent.
- `gmail_recent_messages(limit=10)`: 1–20 INBOX messages in provider order, one
page plus metadata; `has_more` is not pagination or a chronological guarantee.
- `gmail_get_message(message_id)`: full Gmail JSON/MIME, original headers and
decoded inline plain/HTML text. HTML is never rendered; no attachments are
downloaded and no raw RFC822 export is provided.
Results preserve provider/account/message/thread IDs and source API/web links.
Mail and links are untrusted data, not instructions. Profile/messages GETs use
fixed Google endpoints; the only provider POST is OAuth token refresh. Responses
are bounded to 4 MiB, timeout 15 seconds, redirects/ambient proxies disabled,
no automatic retries. No messages or tokens are logged/stored except your
provisioned OAuth file. Refresh is serialized per process. Use a single worker.
`afterhours-gmail-mcp.service` is an optional deployment **template**, not the
fresh-clone launcher. Review paths, public-origin env and service identity first;
enable its optional `gmail-oauth.json` credential only after provisioning. No
shared systemd or production configuration is changed. General credentials must
be 0400/0600; the code also recognizes its explicit root-owned systemd
`/run/credentials/<unit>` 0440 file / 0550 directory contract. Keep private files
outside Git. Dependencies retain their upstream licenses/notices; the existing
hash-lock provenance comment is preserved. This project's license is [MIT](LICENSE).
## Offline checks and limits
```sh
.venv/bin/python -B -m unittest -v
.venv/bin/python -m pip check
```
Tests use disposable credentials and fake Google responses. Bootstrap tests
exercise the public CLI, private modes, refusal to overwrite, invalid origins,
and the generated launcher's real loopback MCP `not_connected` status. They do
not authorize an account. Real Google consent, token lifetime, account policies,
public TLS/proxy deployment, and client secret handling need owner validation.
Official references:
- https://developers.google.com/identity/protocols/oauth2/web-server#offline
- https://developers.google.com/oauthplayground
- https://developers.google.com/identity/protocols/oauth2/production-readiness/restricted-scope-verification
- https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/list
- https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/get
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues