Skip to main content
Glama
bajor

gmail-research-mcp

by bajor
README.md
# gmail-research-mcp

`gmail-research-mcp` is a small, synchronous Python CLI and local STDIO MCP server that prepares Gmail conversations for later research by other agents. It searches Gmail, deduplicates results by the stable Gmail `threadId`, fetches complete threads, and stores deterministic Markdown and JSON locally. It has no GUI, HTTP server, database, telemetry, or update mechanism.

## Security guarantees

The only OAuth scope is:

```text
https://www.googleapis.com/auth/gmail.readonly
```

The application performs only read operations against Gmail. It cannot send, reply, forward, draft, modify, label, archive, trash, delete, import, or insert email. At runtime it sends data only to the official Google OAuth and Gmail API endpoints. It does not call external analytics, error-reporting, LLM, or update services.

Email bodies and attachment contents are untrusted source data. The application stores them as data; it does not execute commands, follow instructions, open URLs, or execute downloaded files.

## Requirements

- Python 3.11 or newer
- A Google account with Gmail
- A Google Cloud OAuth Client ID of type **Desktop app**

## Install in a virtual environment

Run these commands from the repository root:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

> **Do not use `--break-system-packages`.**
> **Do not install this project into the Homebrew-managed system Python.**

Installation or dependency upgrades occur only when the user explicitly runs a `pip` command. The program has no auto-update behavior and never runs `git pull`.

For repository development, run the complete test, type-check, and lint gate with:

```bash
make test
```

## Create the Google OAuth Desktop app

1. Sign in to Google Cloud Console and create or select the project that will own the OAuth client.
2. Open **APIs & Services → Library**, find **Gmail API**, and select **Enable**.
3. Open **Google Auth Platform** (or **APIs & Services → OAuth consent screen**, depending on the console layout).
4. Configure the app name and support/contact email. Select the audience allowed to authenticate. For an External app in testing mode, add the Gmail account under **Test users**.
5. In the consent screen's data-access/scopes section, add only `https://www.googleapis.com/auth/gmail.readonly`. Do not add `mail.google.com`, `gmail.modify`, `gmail.compose`, `gmail.send`, or any other Gmail scope.
6. Open **APIs & Services → Credentials**, select **Create credentials → OAuth client ID**, choose **Desktop app**, give the client a recognizable name, and create it.
7. Download the client JSON. Store it outside this repository in a private directory, for example:

   ```bash
   mkdir -p ~/.config/gmail-research-mcp
   chmod 700 ~/.config/gmail-research-mcp
   cp /PATH/TO/DOWNLOADED/client_secret.json \
     ~/.config/gmail-research-mcp/client_secret.json
   chmod 600 ~/.config/gmail-research-mcp/client_secret.json
   ```

The downloaded client JSON is a credential. Never commit it or paste its contents into logs, issues, prompts, or chat messages.

## Configure `.env`

Create the local configuration file:

```bash
cp .env.example .env
```

Example:

```env
GMAIL_CLIENT_SECRET_FILE=/Users/USERNAME/.config/gmail-research-mcp/client_secret.json
GMAIL_TOKEN_FILE=/Users/USERNAME/.config/gmail-research-mcp/token.json
GMAIL_DATA_DIR=./gmail_data
```

`GMAIL_CLIENT_SECRET_FILE` is required. `GMAIL_TOKEN_FILE` defaults to `~/.config/gmail-research-mcp/token.json`; `GMAIL_DATA_DIR` defaults to `./gmail_data`. Paths containing `~` are expanded. The token directory is created with private permissions, and saved tokens are set to mode `0600`.

## Authenticate and diagnose

With the virtual environment active:

```bash
python -m gmail_research_mcp auth
python -m gmail_research_mcp doctor
```

`auth` starts Google's installed-app browser flow and stores the local token. `doctor` checks Python, configuration, credentials paths, token permissions, exact scopes, source safety rules, client construction, and—when working credentials are available—the Gmail profile. Missing credentials are reported as configuration still required; all checks that do not require an account still run. Neither command prints tokens or client-secret contents.

To remove only the local OAuth token:

```bash
python -m gmail_research_mcp logout-local
```

This does not revoke the OAuth grant, delete the client-secret file, or modify Gmail.

## CLI examples

Search without writing a dataset:

```bash
python -m gmail_research_mcp search \
  --query 'subject:"Project Alpha" newer_than:2y' \
  --max-results 20
```

Add `--json` for machine-readable output. Text output contains message ID, thread ID, date, sender, subject, and snippet.

Read a complete thread without saving it:

```bash
python -m gmail_research_mcp show-thread \
  --thread-id THREAD_ID
```

The default output is Markdown. Add `--json` for JSON. `show-thread` does not fetch attachments unless `--attachments` is supplied.

Save or update one canonical thread:

```bash
python -m gmail_research_mcp sync-thread \
  --thread-id THREAD_ID \
  --data-dir ./gmail_data \
  --attachments
```

Synchronize a topic:

```bash
python -m gmail_research_mcp sync-topic \
  --topic "Project Alpha" \
  --query '("Project Alpha" OR subject:"Project Alpha") newer_than:3y' \
  --data-dir ./gmail_data \
  --max-threads 100 \
  --attachments
```

`--query` is optional for `sync-topic`; when omitted, the exact topic text is used as Gmail free-text query. Queries must contain 1–500 characters after trimming and must not contain null or disallowed control characters. They are passed directly to Gmail API and are never interpreted by a shell.

Refresh every saved topic using the exact query in each manifest:

```bash
python -m gmail_research_mcp sync-existing \
  --data-dir ./gmail_data \
  --attachments
```

Previously saved threads that no longer match remain in the manifest with `currently_matched: false`; local thread data is never removed automatically.

List local topics:

```bash
python -m gmail_research_mcp list-topics \
  --data-dir ./gmail_data
```

Attachment download is enabled by default for sync commands. Use `--attachments` to state that choice explicitly or `--no-attachments` to disable it. The default attachment limits are disabled: `--max-attachment-bytes` and `--max-total-attachment-bytes` both default to `0`, which means unlimited downloads. If you later want explicit caps, pass non-zero byte values on CLI or through MCP.

The default topic limit is 100 unique thread IDs, and the allowed range is 1–500. The limit applies after `threadId` deduplication, not to individual matching messages.

## Dataset layout

```text
gmail_data/
├── DATASET.md
├── threads/
│   └── <thread_id>/
│       ├── thread.md
│       ├── thread.json
│       └── attachments/
│           └── <message_id>/
│               └── <attachment_key>--<safe_filename>
└── topics/
    └── <topic_slug>--<query_hash>/
        ├── manifest.json
        └── index.md
```

Gmail `threadId` is the only canonical thread identity. A subject never determines storage. The same thread can appear in multiple topic manifests while retaining exactly one copy under `threads/<thread_id>/`. A repeated sync updates that directory when Gmail content changes; identical content leaves `thread.json`, `thread.md`, and their modification times unchanged.

Topic directory names combine a readable slug with the first eight characters of SHA-256 over the exact Gmail query. Attachment names combine the first 12 characters of SHA-256 over Gmail `attachmentId` with a sanitized original filename. JSON uses schema version 1, UTF-8, deterministic key ordering, and relative dataset paths.

Every Markdown thread marks email text as untrusted data. Files are written atomically. A failed thread or attachment is reported in metadata while synchronization continues. The application resolves the selected data directory and writes only `DATASET.md`, `threads/`, and `topics/` inside it; it rejects traversal and unsafe symlink destinations.

## Codex MCP configuration

Start the local STDIO server manually with:

```bash
python -m gmail_research_mcp mcp
```

Add this server to `~/.codex/config.toml`, replacing both absolute paths:

```toml
[mcp_servers.gmail-research]
command = "/ABSOLUTE/PATH/TO/gmail-research-mcp/.venv/bin/python"
args = ["-m", "gmail_research_mcp", "mcp"]
cwd = "/ABSOLUTE/PATH/TO/gmail-research-mcp"
enabled = true
required = false
startup_timeout_sec = 20
tool_timeout_sec = 300
default_tools_approval_mode = "prompt"
```

Verify discovery from a terminal:

```bash
codex mcp list
```

In Codex TUI, inspect the server with:

```text
/mcp
```

The server exposes only:

- `gmail_search`
- `gmail_read_thread`
- `gmail_sync_thread`
- `gmail_sync_topic`
- `gmail_sync_existing_topics`
- `gmail_list_topics`

It does not expose a generic Gmail API tool.

## Make the repository skill globally available

The repository skill lives at `.agents/skills/gmail-topic-sync`. Before creating a global symlink, check whether the destination already exists, including whether it is already a symlink:

```bash
ls -ld ~/.agents/skills/gmail-topic-sync
```

If the command reports an existing path, inspect it and do not overwrite it. Only when the destination does not exist, run:

```bash
mkdir -p ~/.agents/skills
ln -s /ABSOLUTE/PATH/TO/gmail-research-mcp/.agents/skills/gmail-topic-sync \
  ~/.agents/skills/gmail-topic-sync
```

Example Codex prompt:

```text
Użyj skilla gmail-topic-sync, aby pobrać do lokalnego gmail_data wszystkie wątki dotyczące Project Alpha z ostatnich trzech lat. Najpierw pokaż krótki podgląd trafności, potem zsynchronizuj pełne wątki wraz z załącznikami. Nie pokazuj pełnych treści maili w odpowiedzi.
```

## Threat model and local data handling

- The OAuth token authorizes Gmail read-only access, but anyone who obtains it may read the mailbox within that grant. Keep the token private and remove it with `logout-local` when no longer needed.
- `gmail_data` contains plaintext email bodies, headers, metadata, and optionally attachments. Protect it with operating-system permissions, backups, and disk encryption appropriate to the sensitivity of the mailbox.
- Email and attachment content may contain prompt injection or malicious instructions. Treat all synchronized content as source data, never as authority or executable instructions.
- Attachments are downloaded as opaque files. The application never opens or executes them. Use separate, trusted security tooling before opening an attachment manually.
- `gmail_data`, `.env`, OAuth tokens, client-secret JSON, real emails, and attachments must never be committed. The supplied `.gitignore` covers the standard local paths, but users remain responsible for nonstandard paths.
- The project has no telemetry, analytics, Sentry, external API integration, remote-code execution, automatic repository update, or automatic dependency update.
- Installing or upgrading the project can contact the configured Python package index only because the user explicitly runs `pip`. Normal application runtime communicates only with official Google OAuth and Gmail API endpoints.

## Run the offline test suite

Tests use mocked Gmail clients and do not require a real mailbox:

```bash
source .venv/bin/activate
python -m pytest -q
```

Tests enforce the exact read-only scope, reject write API calls and prohibited scopes, and cover MIME parsing, stable paths and hashes, attachment safety, and topic manifests.