Skip to main content
Glama
MAghadavoodi

Hermes Google MCP Server

by MAghadavoodi
README.md
# Hermes Google MCP Server

> Schlanker MCP-Server für Google Workspace (Calendar · Gmail · Drive) — entwickelt für den [Hermes Agent](https://github.com/NousResearch/hermes-agent) im Docker-Setup.

## Warum dieses Projekt?

Die in der Hermes-Dokumentation beschriebenen drei Standard-Wege zur Google-Anbindung scheiterten im Docker-Container-Setup:

| Weg | Problem |
|-----|---------|
| **gws CLI MCP** | `gws` v0.22.5 hat keinen `mcp`-Subbefehl |
| **google_workspace_mcp** | asyncio `TaskGroup`-Fehler beim Gateway-Connect |
| **Google Remote MCP** | OAuth-Flow im Container nicht möglich (kein Browser) |

Dieser Server löst alle drei Probleme: Reines Python (stdlib-only), HTTP-Transport (umgeht den asyncio-Bug), nutzt die funktionierende `gws`-CLI-Authentifizierung.

**v2.2.1 (Juli 2026):** Gmail Body-Truncation-Limit (`body_text[:5000]`) entfernt — lange E-Mails (z.B. Medium Daily Digest mit 15+ Artikeln) werden jetzt vollständig gelesen, ohne dass Inhalte abgeschnitten werden. `main()`-Funktion mit `if __name__`-Guard ermöglicht Unit-Tests via `import server`.

## Architektur

```
Hermes Gateway (Docker)         Host (masoud)
─────────────────────────       ─────────────────────────
config.yaml:                    ┌──────────────────────┐
  gws-mcp:                      │ gws-mcp Container    │
    url: http://172.17.0.1 ───► │  http_server.py      │
    :8777/mcp                   │  :8777 → server.py   │
                                │      → gws CLI       │
                                │      → Google APIs   │
                                └──────────────────────┘
                                  ▲ Port 8777 (forwarded)
                                  ▼
                          Google Workspace APIs
                          (Calendar v3 · Gmail v1 · Drive v3)
```

## Container-Resilienz

Der Docker-Container ist gegen Port-Konflikte und unsauberes Herunterfahren abgesichert:

| Maßnahme | Ort | Wirkung |
|----------|-----|---------|
| `init: true` | `docker-compose.yml` | Tini als Init-Prozess — fängt Signale korrekt ab, verhindert Zombie-Prozesse |
| `stop_grace_period: 10s` | `docker-compose.yml` | 10 Sekunden für sauberes Herunterfahren vor SIGKILL |
| `signal.signal(SIGTERM, ...)` | `http_server.py` | Schließt den HTTP-Socket bei Docker-Stop — Port sofort frei |
| `restart: unless-stopped` | `docker-compose.yml` | Container startet nach Crash oder Reboot automatisch neu |

**Ablauf bei `docker stop` / Reboot:**
1. Docker sendet `SIGTERM` an den Container
2. `http_server.py` fängt das Signal und ruft `server.server_close()` auf
3. Port 8777 wird sofort freigegeben — kein Port-Konflikt beim Neustart
4. Container beendet mit Exit-Code `0` (statt 137/SIGKILL)
5. Nach Reboot startet Docker den Container automatisch (`unless-stopped`)

**Manuelle Fehlerbehebung** (falls Container im Status `Exited (137)` hängt):
```bash
docker start gws-mcp
```

## Attachments & Pfadübersetzung

`gws_gmail_send_message` unterstützt Dateianhänge per MIME-Multipart. Da unterschiedliche MCP-Clients
verschiedene Pfadformate übergeben, übersetzt der Server automatisch:

| Quelle | Pfadbeispiel | Container-Pfad |
|--------|-------------|----------------|
| **Hermes TUI** (Docker) | `/opt/data/workspace/report.pdf` | `/workspace/report.pdf` |
| **OpenCode** (Host) | `/home/masoud/.../workspace/report.pdf` | `/workspace/report.pdf` |

**Funktionsweise** (`server.py` Zeile 176–183):

```python
# 1. Hermes Docker-Pfad: /opt/data/workspace/ → /workspace/
p = p.replace("/opt/data/workspace/", "/workspace/")

# 2. Host-Pfad (OpenCode & Co.): Extrahiere alles ab /workspace/
parts = p.split("/workspace/", 1)
if len(parts) == 2:
    p = "/workspace/" + parts[1]
```

Voraussetzung: Der Workspace ist als Volume in den Container eingehängt
(`docker-compose.yml` → `~/Development/HermesProject/hermes-data/workspace:/workspace`).

### Argument-Größenlimit (E2BIG)

Beim Senden großer Anhänge (>100 KB) würde die base64-kodierte MIME-Nachricht
das Linux-Arg-Limit von 128 KB pro Argument überschreiten (`[Errno 7] Argument list too long`).

**Lösung**: Statt `--json {"raw":"<base64>"}` wird die MIME-Nachricht in eine
Temp-Datei geschrieben und via `--upload`-Flag an gws übergeben – die
Gmail API verarbeitet sie als `multipart/upload` (`message/rfc822`).

```python
# MIME-Nachricht in Temp-Datei im cwd (/app) schreiben
tf = NamedTemporaryFile(suffix=".eml", delete=False, dir=".")
tf.write(mime_bytes)
tf.close()

# Statt --json mit inline-base64 → --upload mit Temp-Datei
run_gws(["gws","gmail","users","messages","send",
         "--params", '{"userId":"me"}',
         "--upload", os.path.basename(tf.name),
         "--upload-content-type", "message/rfc822"])
os.unlink(tf.name)
```

## Tools

| Tool | Dienst | CRUD | Beschreibung |
|------|--------|------|-------------|
| `gws_calendar_list_events` | Calendar | READ | Events auflisten (Zeitraum, max. Anzahl) |
| `gws_calendar_create_event` | Calendar | CREATE | Termin erstellen mit Teilnehmern |
| `gws_calendar_update_event` | Calendar | UPDATE | Termin ändern |
| `gws_calendar_delete_event` | Calendar | DELETE | Termin löschen |
| `gws_gmail_list_messages` | Gmail | READ | E-Mail-IDs auflisten (Suchfilter) |
| `gws_gmail_get_message` | Gmail | READ | E-Mail-Inhalt + Body (includeBody=true, keine Truncation) |
| `gws_gmail_send_message` | Gmail | CREATE | E-Mail senden mit Anhängen (MIME) |
| `gws_gmail_delete_message` | Gmail | DELETE | E-Mail in Papierkorb verschieben |
| `gws_drive_list_files` | Drive | READ | Dateien/Ordner auflisten |
| `gws_drive_get_file` | Drive | READ | Datei-Metadaten abrufen |
| `gws_drive_create_file` | Drive | CREATE | Datei erstellen/hochladen |
| `gws_drive_update_file` | Drive | UPDATE | Datei ändern/ersetzen |
| `gws_drive_delete_file` | Drive | DELETE | Datei permanent löschen |
| `gws_drive_download_file` | Drive | READ | Datei herunterladen → workspace/ |

## Voraussetzungen

- **gws CLI** (`npm install -g @googleworkspace/cli`)
- **OAuth 2.0 Desktop-Client** (Google Cloud Console → Credentials)
- **Authentifizierung** via `gws auth login`

## Deployment

Der Server läuft als Docker-Container mit systemd-Integration für automatischen Start nach Reboot.

### Docker Compose

```bash
# 1. Projekt klonen
git clone https://github.com/MAghadavoodi/HermesGoogleMCP.git ~/Development/Google_MCP
cd ~/Development/Google_MCP

# 2. Container bauen & starten
docker compose up -d gws-mcp

# 3. Status prüfen
docker ps --filter name=gws-mcp
```

### systemd-Service (automatischer Start)

```bash
# Service aktivieren (startet Container bei Login)
systemctl --user enable --now gws-mcp.service

# Service verwalten
systemctl --user status gws-mcp
systemctl --user stop gws-mcp
systemctl --user start gws-mcp
```

Die systemd-Unit delegiert an Docker (`docker start`/`docker stop`).  
Die `restart: unless-stopped`-Policy im Container stellt sicher, dass er bei Crash oder Reboot automatisch neu startet.

### Hermes-Konfiguration

In `hermes-data/config.yaml`:

```yaml
mcp_servers:
  gws-mcp:
    url: http://172.17.0.1:8777/mcp
    enabled: true

platform_toolsets:
  cli:
  - gws-mcp
  matrix:
  - gws-mcp
```

## Tests

```bash
# Unit-Tests (ohne Auth, kein gws CLI nötig)
python3 -m pytest tests/test_gmail_body_truncation.py -v

# Integration-Tests (benötigen gws auth)
python3 tests/test_calendar.py   # Calendar CRUD
python3 tests/test_gmail.py      # Gmail CRUD
python3 tests/test_drive.py      # Drive CRUD
python3 tests/test_all_crud.py   # Full CRUD integration
```

## Projektstruktur

```
Google_MCP/
├── src/
│   ├── server.py          # MCP JSON-RPC Engine (CRUD + Attachments)
│   └── http_server.py     # HTTP-Wrapper (Port 8777, SIGTERM-Handler)
├── tests/
│   ├── test_gmail_body_truncation.py  # Unit-Tests: Body-Truncation (kein Limit)
│   ├── test_calendar.py
│   ├── test_gmail.py
│   ├── test_drive.py
│   └── test_all_crud.py
├── docs/
│   └── ARCHITECTURE.md    # Detaillierte Architektur-Doku
├── Dockerfile             # Docker-Image (ubuntu:24.04 + gws CLI)
├── docker-compose.yml     # Docker Compose (init, stop_grace_period)
├── requirements.txt       # stdlib-only, keine pip-Abhängigkeiten
└── .gitignore
```

## Lizenz

MIT — siehe [LICENSE](LICENSE)