You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
oabrivard 0b9886fc56 Add MCP OAuth auth mode via token introspection 6 days ago
app Add MCP OAuth auth mode via token introspection 6 days ago
docs Add MCP OAuth auth mode via token introspection 6 days ago
tests Add MCP OAuth auth mode via token introspection 6 days ago
typings Fixed PyLance type errors 7 days ago
.DS_Store Debugged gmail access code 1 week ago
.dockerignore Added docker config 7 days ago
.env.example Add MCP OAuth auth mode via token introspection 6 days ago
.gitignore Added unsubscribe email recap 1 week ago
Dockerfile chore(docker): split REST/A2A and MCP into separate services 6 days ago
README.md Add MCP OAuth auth mode via token introspection 6 days ago
docker-compose.yml chore(docker): split REST/A2A and MCP into separate services 6 days ago
pyproject.toml test(protocol): add core and REST/A2A/MCP parity tests 6 days ago
simple-agent-google-credentials.json Updated env variables 1 week ago
uv.lock test(protocol): add core and REST/A2A/MCP parity tests 6 days ago

README.md

Personal Gmail + Calendar Agent

This project runs a small local API service that:

  • scans unread emails in the root Gmail inbox
  • classifies emails with Strands (LINKEDIN, ADVERTISING, VEILLE_TECHNO, OTHER)
  • moves LinkedIn emails to a LinkedIn label/folder
  • moves advertising emails to an Advertising label/folder
  • moves veille techno emails to a VeilleTechno label/folder
  • scans the Advertising label and emails you new unsubscribe links (deduplicated)
  • discovers unsubscribe-ready mailing lists for human review, then auto-unsubscribes selected lists
  • exposes a secure availability endpoint powered by Google Calendar free/busy

1) Prerequisites

  • Python 3.11+
  • uv (installation guide)
  • A Google account
  • An OpenAI-compatible API key for Strands (STRANDS_OPENAI_API_KEY)
  • A Google Cloud project with:
    • Gmail API enabled
    • Google Calendar API enabled
    • OAuth consent configured
    • OAuth Client ID of type Desktop app

2) Google OAuth setup

  1. In Google Cloud Console, create a desktop OAuth client.
  2. Download the client JSON file.
  3. Save it in this project as credentials.json.

The first run opens a browser window for consent and creates token.json. If your existing token was created before gmail.send was added, you may be prompted again.

3) Install and configure

uv sync
cp .env.example .env

Edit .env and set:

  • AGENT_API_KEY to a strong secret for agent-to-agent calls
  • AUTH_MODE (api_key, jwt, or hybrid)
  • optional JWT settings (AUTH_JWT_SECRET, AUTH_JWT_ISSUER, AUTH_JWT_AUDIENCE)
  • STRANDS_OPENAI_API_KEY and optional STRANDS_MODEL_ID / STRANDS_OPENAI_BASE_URL
  • optional unsubscribe digest settings (UNSUBSCRIBE_*)
  • optional scan frequency and additional Gmail filters (GMAIL_QUERY)
  • optional MCP auth override (MCP_AUTH_MODE=inherit|api_key|jwt|hybrid|oauth)
  • for MCP OAuth mode: MCP_OAUTH_INTROSPECTION_URL, optional MCP_OAUTH_CLIENT_ID / MCP_OAUTH_CLIENT_SECRET, and optional MCP_OAUTH_ISSUER / MCP_OAUTH_AUDIENCE

4) Run

Local

uv run uvicorn app.main:app --reload

Docker

docker compose up --build -d personal-agent personal-agent-mcp
docker compose logs -f personal-agent personal-agent-mcp

Docker endpoints:

  • REST + A2A API: http://127.0.0.1:8000
  • MCP streamable HTTP: http://127.0.0.1:8001/mcp

Container mounts:

  • ./${GOOGLE_CLIENT_SECRETS_FILE} -> /app/${GOOGLE_CLIENT_SECRETS_FILE} (read-only)
  • ./${GOOGLE_TOKEN_FILE} -> /app/${GOOGLE_TOKEN_FILE}
  • ./data -> /app/data

At startup, the scheduler runs:

  • Gmail triage (GMAIL_SCAN_INTERVAL_MINUTES)
  • unsubscribe digest (UNSUBSCRIBE_DIGEST_INTERVAL_MINUTES)
  • auto-unsubscribe for approved lists (UNSUBSCRIBE_AUTO_INTERVAL_MINUTES) when UNSUBSCRIBE_AUTO_ENABLED=true

5) API usage

Health check

curl http://127.0.0.1:8000/health

Manual Gmail scan

curl -X POST "http://127.0.0.1:8000/scan?max_results=100" \
  -H "X-API-Key: your-secret"

Availability for other AI agents

curl -X POST "http://127.0.0.1:8000/availability" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret" \
  -d '{
    "start": "2026-03-09T09:00:00+01:00",
    "end": "2026-03-09T10:00:00+01:00",
    "calendar_ids": ["primary"]
  }'

If available is true, there are no busy slots in that range.

A2A discovery

curl http://127.0.0.1:8000/.well-known/agent-card.json

A2A SendMessage availability

curl -X POST "http://127.0.0.1:8000/a2a/rpc" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret" \
  -d '{
    "jsonrpc": "2.0",
    "id": "req-1",
    "method": "SendMessage",
    "params": {
      "start": "2026-03-09T09:00:00+01:00",
      "end": "2026-03-09T10:00:00+01:00",
      "calendar_ids": ["primary"]
    }
  }'

MCP server (availability tool)

Run MCP on a dedicated port:

uv run uvicorn app.mcp_main:app --host 0.0.0.0 --port 8001

With Docker Compose, MCP is provided by the personal-agent-mcp service.

MCP streamable HTTP endpoint:

http://127.0.0.1:8001/mcp

By default, MCP exposes only check_availability. To expose internal mutation tools (scan_mailbox, list_unsubscribe_candidates, execute_unsubscribe), set:

MCP_ENABLE_MUTATION_TOOLS=true

MCP auth mode defaults to inherit (same mode as AUTH_MODE). To force OAuth only on MCP:

MCP_AUTH_MODE=oauth
MCP_OAUTH_INTROSPECTION_URL=https://issuer.example/oauth2/introspect
MCP_OAUTH_CLIENT_ID=your-resource-server-client-id
MCP_OAUTH_CLIENT_SECRET=your-resource-server-client-secret
MCP_OAUTH_ISSUER=https://issuer.example
MCP_OAUTH_AUDIENCE=personal-agent-mcp

Scopes required per MCP tool:

  • check_availability: availability:read
  • scan_mailbox: mail:scan
  • list_unsubscribe_candidates: unsubscribe:read
  • execute_unsubscribe: unsubscribe:execute

Manual unsubscribe digest

curl -X POST "http://127.0.0.1:8000/unsubscribe-digest?max_results=500" \
  -H "X-API-Key: your-secret"

Human-in-the-loop unsubscribe candidates

curl -X POST "http://127.0.0.1:8000/unsubscribe/candidates?max_results=500" \
  -H "X-API-Key: your-secret"

Execute unsubscribe for selected mailing lists

curl -X POST "http://127.0.0.1:8000/unsubscribe/execute" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret" \
  -d '{
    "selected_candidate_ids": ["abc123def456", "987zyx654wvu"],
    "remember_selection": true
  }'

Trigger auto-unsubscribe run (approved lists only)

curl -X POST "http://127.0.0.1:8000/unsubscribe/auto-run?max_results=500" \
  -H "X-API-Key: your-secret"

Classification behavior

  • Scan scope is always forced to in:inbox is:unread (root inbox + unread).
  • GMAIL_QUERY is treated as additional filters (for example -label:AgentProcessed).
  • Strands classification is used for each email (LINKEDIN, ADVERTISING, VEILLE_TECHNO, OTHER).
  • LinkedIn has priority over advertising inside the classifier prompt.
  • Set LLM_FALLBACK_TO_RULES=true only if you want rules-based backup when LLM calls fail.
  • Every scanned message gets an AgentProcessed label to avoid reprocessing loops.

Unsubscribe digest behavior

  • Reads emails from UNSUBSCRIBE_QUERY (default label:Advertising).
  • Extracts unsubscribe URLs from List-Unsubscribe headers and message content.
  • Removes duplicates within the run and across runs.
  • Persists already sent links in UNSUBSCRIBE_STATE_FILE.
  • Sends only new links by email, unless UNSUBSCRIBE_SEND_EMPTY_DIGEST=true.

Human-In-The-Loop Unsubscribe

  • Candidate discovery groups advertising messages into mailing lists using headers (List-Id, From) and unsubscribe methods.
  • You pick the candidate_id values you want to unsubscribe from.
  • Only selected lists are executed.
  • Executed unsubscribe methods are persisted and never executed twice.
  • Selected lists can be remembered (remember_selection=true) for scheduled auto-unsubscribe.
  • State is saved in UNSUBSCRIBE_HIL_STATE_FILE.

Runbooks

  • A2A protocol runbook: docs/a2a.md
  • MCP protocol runbook: docs/mcp.md
  • Security, rotation, and rollout checklist: docs/security.md

Notes

  • Gmail "folders" are labels. This agent creates:
    • LinkedIn
    • VeilleTechno
    • Advertising
    • AgentProcessed
  • Messages classified as LinkedIn/Advertising/Veille_Techno are removed from INBOX (moved out of inbox).