First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
# Google Calendar MCP Server (gcal)
## Overview
A Python MCP server providing **read + write** access to Google Calendar via the Google Calendar API v3.
**Server name:** `gcal`
**Transport:** `stdio` (spawns `python3 scripts/gcal_mcp_server.py`)
**Location:** `scripts/gcal_mcp_server.py`
The server also emits push notifications (`event/new_calendar_event`) to the agent when new events are created on the primary calendar (polled every 5 min).
---
## Tools
All tools are callable as `mcp__gcal__<tool>`; the table lists the bare `<tool>` names.
| Tool | Required params | Optional params | Description |
|------|-----------------|-----------------|-------------|
| `status` | *(none)* | *(none)* | Self-check: verifies credentials load, the token refreshes, and the Calendar API responds. Call first when another gcal tool fails. |
| `list_calendars` | *(none)* | *(none)* | List calendars accessible to the user (surfaces `calendar_id` values). |
| `list_events` | *(none)* | `calendar_id`, `time_min`, `time_max`, `max_results`, `full_text`, `time_zone` | Chronological event listing. `time_min` defaults to NOW. |
| `get_event` | `event_id` | `calendar_id` | Read a single event by ID, including attendees + RSVP status. |
| `create_event` | `summary`, `start`, `end` | `description`, `location`, `attendees`, `recurrence`, `time_zone`, `calendar_id`, `reminders` | Create an event; returns ID + HTML link. |
| `update_event` | `event_id` | `summary`, `start`, `end`, `description`, `location`, `attendees`, `time_zone`, `calendar_id`, `reminders` | Patch fields of an existing event; omitted fields are preserved. |
| `delete_event` | `event_id` | `calendar_id` | Permanently delete an event. Irreversible. |
| `respond_to_event` | `event_id`, `response` | `calendar_id` | Set RSVP (`accepted`, `declined`, `tentative`, `needsAction`). |
### `reminders` format
`reminders` (on `create_event` / `update_event`) accepts a JSON array whose items are **either** integers (minutes before the event, popup reminder) **or** objects `{"method": "popup"|"email", "minutes": <int>}`. Overrides calendar defaults. Examples: `[10, 30, 60]` or `[{"method": "email", "minutes": 60}, {"method": "popup", "minutes": 10}]`.
---
## Authentication
### Credentials File
OAuth 2.0 user credentials are stored in:
- **Default path:** `./secrets/google_creds.json` (relative to project root)
- **Override:** `GOOGLE_CREDS_PATH` env var
Required OAuth scope (granted by the setup script):
```
https://www.googleapis.com/auth/calendar
```
### Token Refresh
The access token expires after ~1 hour. The server **refreshes it automatically** in two places:
1. At startup, if the cached token is expired.
2. **Mid-session**, on the first 401 / `RefreshError` from any tool — `_call()` refreshes once, persists the new token to `secrets/google_creds.json`, and retries the call.
If the refresh token itself has been revoked or expired, `status` and every tool return an actionable `Error:` instructing to re-run `scripts/gcal_oauth_setup.py`.
### Git Safety
`./secrets/` is in `.gitignore` — credentials are never committed.
---
## Setup
### 1. Create OAuth credentials
1. Go to [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials).
2. Create an **OAuth client ID** of type *Desktop app*.
3. Put `client_id` and `client_secret` in `secrets/google_oauth_client.json`:
```json
{ "client_id": "....apps.googleusercontent.com", "client_secret": "GOCSPX-..." }
```
4. Enable the **Google Calendar API** under APIs & Services.
### 2. Run the OAuth flow
```bash
.venv/bin/python scripts/gcal_oauth_setup.py
```
Opens a browser, asks for the Calendar scope, and saves the token to `secrets/google_creds.json`.
### 3. Install dependencies
`google-auth`, `google-auth-oauthlib`, `google-api-python-client` are already listed in `requirements.txt`. Re-running `./run.sh` installs them automatically; or:
```bash
uv pip install -r requirements.txt
```
### 4. Register the server with the agent
```
register_mcp(
name="gcal",
transport="stdio",
command="python3",
args=["scripts/gcal_mcp_server.py"]
)
```
---
## Usage Examples
### Self-check: is Calendar working?
```
mcp__gcal__status()
```
Returns `Status: READY ✅ (ok)` if credentials load, the token refreshes, and the Calendar API responds; otherwise a `Status: … ❌/⚠️ (…)` report with a `What to do:` list. Call this first whenever another gcal tool fails.
### List events in the next 7 days
```
mcp__gcal__list_events(
calendar_id="primary",
time_min="2026-06-22T00:00:00+02:00",
time_max="2026-06-29T00:00:00+02:00",
max_results=50,
time_zone="Europe/Rome"
)
```
`time_min` / `time_max` are also accepted as `start_time` / `end_time` for backward compatibility. If `time_min` is omitted it defaults to NOW.
### Search events
```
mcp__gcal__list_events(
full_text="dentist",
time_min="2026-01-01T00:00:00+01:00",
time_max="2026-12-31T00:00:00+01:00"
)
```
### Get / create / RSVP
```
mcp__gcal__get_event(event_id="<id>")
mcp__gcal__create_event(
summary="Dentist",
start="2026-07-01T09:00:00",
end="2026-07-01T10:00:00",
reminders=[30, {"method": "email", "minutes": 120}]
)
mcp__gcal__respond_to_event(event_id="<id>", response="accepted")
```
---
## Enable / Disable
```
toggle_item(kind="mcp", id="gcal", enabled=false) # disable
toggle_item(kind="mcp", id="gcal", enabled=true) # re-enable
restart # required for changes to take effect
```
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `google-api-python-client` | 2.197.0 | Google API Python client (Calendar v3) |
| `google-auth` | 2.55.0 | Auth / token refresh |
| `google-auth-oauthlib` | 1.4.0 | Local OAuth flow (setup script) |
---
## Error Handling
Every Google API exception is mapped to an actionable `Error:` string (flagged with `isError: true`):
| Condition | Response |
|-----------|----------|
| Credentials file missing | `"Error: Credentials file not found at …. Run scripts/gcal_oauth_setup.py…"` |
| Token refresh failed / revoked | `"Error: Calendar API token refresh failed …. Re-run scripts/gcal_oauth_setup.py…"` |
| HTTP 401 | `"Error: Calendar API rejected the access token (401)…. Re-run scripts/gcal_oauth_setup.py…"` |
| HTTP 403 | `"Error: Calendar API returned 403 Forbidden. The OAuth scopes … are insufficient, or the Calendar API is disabled in the Google Cloud Console…"` |
| HTTP 404 | `"Error: Calendar API returned 404 Not Found. Check the event/calendar ID…"` |
| HTTP 429 | `"Error: Calendar API rate limit exceeded (429). Wait a moment and retry."` |
| HTTP 400 | `"Error: Calendar API rejected the request as invalid (400). Check the parameters. Detail: …"` |
| HTTP 5xx | `"Error: Calendar API returned a server error (HTTP …). Retry in a moment."` |
| Missing required param | `"Error: Missing required parameter '<name>'"` |
| Unknown tool | `"Error: Unknown tool: <name>"` |
All errors are logged to stderr with the `[gcal_mcp]` prefix.
---
## Protocol
Implements JSON-RPC 2.0 over stdio (same as gmail / gmaps / whatsapp servers):
- **Requests:** read from stdin, one JSON object per line
- **Responses:** written to stdout
- **Notifications:** server-initiated (`event/new_calendar_event`), no `id`
- **Logs:** stderr only, prefixed `[gcal_mcp]`
Supported methods: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`.
---
## When to Update This File
- A tool is added, removed, or renamed
- Auth mechanism changes
- Credential path or scope changes
- New error cases are mapped
- Protocol version changes
+283
View File
@@ -0,0 +1,283 @@
# Gmail MCP Server (gmail)
## Overview
A Python MCP server providing **read, modify, and send** access to Gmail via the Gmail API v1.
**Server name:** `gmail`
**Transport:** `stdio` (spawns `python3 scripts/gmail_mcp_server.py`)
**Location:** `scripts/gmail_mcp_server.py`
The server also emits push notifications (`event/new_email`) to the agent when new mail lands in the INBOX (polled every 60 s via the Gmail History API).
### Permissions (safe by design)
| Capability | Yes/No |
|------------|--------|
| Read messages & threads | ✅ |
| Search messages | ✅ |
| List labels | ✅ |
| Modify labels (mark read, star, archive) | ✅ |
| Send email | ✅ |
| Create labels | ✅ |
| Download attachments | ✅ |
| Trash message (reversible) | ❌ *removed from tools* |
| Untrash message | ❌ *removed from tools* |
| Permanently delete | ❌ *scope not granted* |
The server uses the `gmail.modify` + `gmail.labels` scopes, which allow all operations **except permanent deletion**.
---
## Tools
All tools are callable as `mcp__gmail__<tool>`; the table lists the bare `<tool>` names.
| Tool | Required params | Optional params | Description |
|------|-----------------|-----------------|-------------|
| `status` | *(none)* | *(none)* | Self-check: verifies credentials load, the token refreshes, and the Gmail API responds. Call first when another gmail tool fails. |
| `list_messages` | *(none)* | `query`, `max_results`, `label_ids`, `page_token` | List messages with optional Gmail search query and label filter. Returns subject, sender, date, message/thread IDs. Pass `page_token` (from a previous response) to fetch the next page. |
| `get_message` | `message_id` | `include_body` | Read a single message by ID (body included by default, truncated at 10000 chars). HTML-only emails are converted to readable text; attachment filenames are listed. |
| `get_thread` | `thread_id` | *(none)* | Read all messages in a thread, newest last. |
| `list_labels` | *(none)* | *(none)* | List labels with total + unread counts (resolves label IDs). |
| `modify_message` | `message_id` | `add_labels`, `remove_labels` | Add/remove labels (mark read, archive, star). Each label arg accepts a string or array. |
| `send_message` | `to`, `subject`, `body` | `cc`, `bcc`, `in_reply_to`, `thread_id`, `attachments` | Send an email; supports in-thread replies and file attachments. |
| `get_profile` | *(none)* | *(none)* | Account email, total message/thread count, history ID. |
| `create_label` | `name` | `label_list_visibility`, `message_list_visibility` | Create a label; returns the new label ID. |
| `download_attachments` | `message_id` | `folder` | Save all attachments from a message. Defaults to `data/gmail_attachments/`. |
> **Removed:** `search_messages` (a literal alias of `list_messages` — use `list_messages` with the `query` parameter instead, which supports full Gmail search syntax).
### Gmail Search Syntax
The `query` parameter on `list_messages` supports Gmail's native syntax:
| Example | Meaning |
|---------|---------|
| `from:john@example.com` | Messages from a sender |
| `to:maria@example.com` | Messages to a recipient |
| `subject:meeting` | Messages with "meeting" in subject |
| `is:unread` / `is:starred` | Unread / starred messages |
| `in:inbox` / `in:sent` | Messages in Inbox / Sent |
| `after:2024/01/01` / `before:2024/06/01` | Date range |
| `has:attachment` | Messages with attachments |
| `label:MY_LABEL` | Messages with a custom label |
Combine with spaces: `from:john is:unread after:2024/06/01`.
---
## Authentication
### Credentials File
OAuth 2.0 user credentials are stored in:
- **Default path:** `./secrets/gmail_creds.json` (relative to project root)
- **Override:** `GMAIL_CREDS_PATH` env var
Required OAuth scopes (granted by the setup script):
```
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.labels
```
### Token Refresh
The access token expires after ~1 hour. The server **refreshes it automatically** in two places:
1. At startup, if the cached token is expired.
2. **Mid-session**, on the first 401 / `RefreshError` from any tool — `_call()` refreshes once, persists the new token to `secrets/gmail_creds.json`, and retries the call.
If the refresh token itself has been revoked or expired, `status` and every tool return an actionable `Error:` instructing to re-run `scripts/gmail_oauth_setup.py`.
### Git Safety
`./secrets/` is in `.gitignore` — credentials are never committed.
---
## Setup
### 1. Create OAuth credentials
1. Go to [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials).
2. Create an **OAuth client ID** of type *Desktop app*.
3. Put `client_id` and `client_secret` in `secrets/google_oauth_client.json`:
```json
{ "client_id": "....apps.googleusercontent.com", "client_secret": "GOCSPX-..." }
```
4. Enable the **Gmail API** under APIs & Services.
(The OAuth client file is shared with gcal; a single Desktop client works for both.)
### 2. Run the OAuth flow
```bash
.venv/bin/python scripts/gmail_oauth_setup.py
```
Opens a browser, asks for the Gmail scopes, and saves the token to `secrets/gmail_creds.json`.
### 3. Install dependencies
`google-auth`, `google-auth-oauthlib`, `google-api-python-client` are already listed in `requirements.txt`. Re-running `./run.sh` installs them automatically; or:
```bash
uv pip install -r requirements.txt
```
### 4. Register the server with the agent
```
register_mcp(
name="gmail",
transport="stdio",
command="python3",
args=["scripts/gmail_mcp_server.py"]
)
```
---
## Usage Examples
### Self-check: is Gmail working?
```
mcp__gmail__status()
```
Returns `Status: READY ✅ (ok)` with the account email if credentials load, the token refreshes, and the Gmail API responds; otherwise a `Status: … ❌/⚠️ (…)` report with a `What to do:` list. Call this first whenever another gmail tool fails.
### List unread messages
```
mcp__gmail__list_messages(query="is:unread", max_results=10)
```
When more results exist, the response ends with a `More results available. Use page_token='...'` line. Pass that token back to page forward:
```
mcp__gmail__list_messages(query="is:unread", max_results=10, page_token="09876...")
```
### Read a message
```
mcp__gmail__get_message(message_id="190abc123...", include_body=true)
```
The body is the `text/plain` part when present; for HTML-only emails it is converted to readable text and labelled `--- Body (converted from HTML) ---`. If the message has attachments, their filenames are listed on an `Attachments:` line — download them with `download_attachments` (which only needs the `message_id`).
### Mark as read / archive
```
mcp__gmail__modify_message(message_id="190abc123...", remove_labels=["UNREAD"])
mcp__gmail__modify_message(message_id="190abc123...", remove_labels="INBOX") # archive
```
### Send / reply in-thread
```
mcp__gmail__send_message(
to="friend@example.com",
subject="Hello!",
body="How are you?"
)
mcp__gmail__send_message(
to="sender@example.com",
subject="Re: Original subject",
body="This is my reply.",
in_reply_to="190abc123...",
thread_id="190thread456..."
)
```
`in_reply_to` adds RFC 2822 threading headers; `thread_id` attaches the message to the correct Gmail thread. For a proper reply visible in all clients, pass both.
### Send with attachments
```
mcp__gmail__send_message(
to="friend@example.com",
subject="Documents attached",
body="See the two files attached.",
attachments=[
"data/gmail_attachments/PREVENTIVO DI SPESA.pdf",
"uploads/gmail_attachments/00035.pdf"
]
)
```
`attachments` is an array of local file paths. Each path is either absolute or relative to the **project root**; the server reads every file from disk, guesses its MIME type, and adds it to a `multipart/mixed` message. **If any path does not exist the email is NOT sent** — the tool returns `Error: attachment not found: <path>` so the agent can correct it. Total attachment size is limited to ~25 MB (the Gmail `messages.send` request embeds the message as base64).
### Download attachments
```
mcp__gmail__download_attachments(message_id="190abc123...")
```
Saves into `data/gmail_attachments/` (served via `/data/gmail_attachments/...` in the frontend). Override with `folder`.
---
## Enable / Disable
```
toggle_item(kind="mcp", id="gmail", enabled=false) # disable
toggle_item(kind="mcp", id="gmail", enabled=true) # re-enable
restart # required for changes to take effect
```
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `google-api-python-client` | 2.197.0 | Google API Python client (Gmail v1) |
| `google-auth` | 2.55.0 | Auth / token refresh |
| `google-auth-oauthlib` | 1.4.0 | Local OAuth flow (setup script) |
---
## Error Handling
Every Google API exception is mapped to an actionable `Error:` string (flagged with `isError: true`):
| Condition | Response |
|-----------|----------|
| Credentials file missing | `"Error: Credentials file not found at …. Run scripts/gmail_oauth_setup.py…"` |
| Credentials invalid & no refresh token | `"Error: Credentials invalid and cannot be refreshed. Re-run scripts/gmail_oauth_setup.py."` |
| Token refresh failed / revoked | `"Error: Gmail API token refresh failed …. Re-run scripts/gmail_oauth_setup.py…"` |
| HTTP 401 | `"Error: Gmail API rejected the access token (401)…. Re-run scripts/gmail_oauth_setup.py…"` |
| HTTP 403 | `"Error: Gmail API returned 403 Forbidden. The OAuth scopes … are insufficient, or the Gmail API is disabled in the Google Cloud Console…"` |
| HTTP 404 | `"Error: Gmail API returned 404 Not Found. Check the message/thread/attachment ID."` |
| HTTP 429 | `"Error: Gmail API rate limit exceeded (429). Wait a moment and retry."` |
| HTTP 400 | `"Error: Gmail API rejected the request as invalid (400). Check the parameters. Detail: …"` |
| HTTP 5xx | `"Error: Gmail API returned a server error (HTTP …). Retry in a moment."` |
| Missing required param | `"Error: Missing required parameter '<name>'"` |
| Unknown tool | `"Error: Unknown tool: <name>"` |
All errors are logged to stderr with the `[gmail_mcp]` prefix.
---
## Protocol
Implements JSON-RPC 2.0 over stdio (same as gcal / gmaps / whatsapp servers):
- **Requests:** read from stdin, one JSON object per line
- **Responses:** written to stdout
- **Notifications:** server-initiated (`event/new_email`), no `id`
- **Logs:** stderr only, prefixed `[gmail_mcp]`
Supported methods: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`.
---
## When to Update This File
- A tool is added, removed, or renamed
- Auth mechanism or scopes change
- Credential path changes
- New error cases are mapped
- Protocol version changes
+224
View File
@@ -0,0 +1,224 @@
# Google Maps MCP Server (gmaps)
## Overview
A Python MCP server providing **public-transit & mapping** capabilities via the Google Maps Platform APIs.
**Server name:** `gmaps`
**Transport:** `stdio` (spawns `python3 scripts/gmaps_mcp_server.py`)
**Location:** `scripts/gmaps_mcp_server.py`
---
## Tools
All tools are callable as `mcp__gmaps__<tool>`; the table lists the bare `<tool>` names.
| Tool | Required params | Optional params | Description |
|------|-----------------|-----------------|-------------|
| `status` | *(none)* | *(none)* | Self-check: verifies the API key is valid and the Geocoding API responds. Call first when another Maps tool fails. |
| `directions` | `origin`, `destination` | `mode`, `departure_time`, `transit_mode`, `transit_routing_preference`, `alternatives`, `language` | Step-by-step directions (transit, driving, walking, bicycling) |
| `geocode` | `address` | `language`, `region` | Address / place name → coordinates + place_id |
| `reverse_geocode` | `lat`, `lng` | `language` | Coordinates → formatted address |
| `search_places` | *(at least one of `query`/`location`)* | `radius`, `type`, `language` | Find nearby stations, stops, POIs |
| `distance_matrix` | `origins`, `destinations` | `mode`, `language` | Travel time & distance between multiple points |
---
## Authentication
### API Key
Unlike Gmail/Calendar (OAuth), Google Maps uses a **plain API key**.
**Priority order:**
1. Environment variable `GOOGLE_MAPS_API_KEY`
2. File `secrets/gmaps_api_key.txt` (first non-empty line)
The `secrets/` directory is in `.gitignore` — the key will not be committed.
### Required Google Cloud APIs
Enable all four in the [Google Cloud Console](https://console.cloud.google.com/apis/library):
| API | Used by |
|-----|---------|
| **Directions API** | `directions` |
| **Geocoding API** | `geocode`, `reverse_geocode` |
| **Places API** | `search_places` |
| **Distance Matrix API** | `distance_matrix` |
---
## Setup
### 1. Create API Key
1. Go to [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials)
2. Click **Create credentials → API key**
3. (Recommended) Restrict the key to the four APIs above
### 2. Save the key
```bash
echo "YOUR_API_KEY_HERE" > secrets/gmaps_api_key.txt
```
Or set the environment variable in your shell/`run.sh`:
```bash
export GOOGLE_MAPS_API_KEY=AIza...
```
### 3. Install the Python dependency
```bash
.venv/bin/pip install googlemaps
# or, if you re-run run.sh, it installs requirements.txt automatically
```
`googlemaps` is already listed in `requirements.txt`.
### 4. Register the server with the agent
Ask the agent:
```
register_mcp(
name="gmaps",
transport="stdio",
command="python3",
args=["scripts/gmaps_mcp_server.py"]
)
```
---
## Usage Examples
### Self-check: is Maps working?
```
mcp__gmaps__status()
```
Returns `OK: …` if the API key is present, valid, and the Geocoding API responds; otherwise an `Error:` string explaining what to fix. Call this first whenever another Maps tool fails.
### Transit directions home (now)
```
mcp__gmaps__directions(
origin="Piazza del Duomo, Milano",
destination="casa mia", ← or the real address saved in agent memory
mode="transit"
)
```
### Prefer train, fewer transfers
```
mcp__gmaps__directions(
origin="current location",
destination="Via Roma 1, Torino",
mode="transit",
transit_mode="train",
transit_routing_preference="fewer_transfers"
)
```
### Find the nearest metro station
```
mcp__gmaps__search_places(
query="metro",
location="Piazza Garibaldi, Napoli",
radius=500,
type="subway_station"
)
```
### How long does it take from A to B?
```
mcp__gmaps__distance_matrix(
origins="Stazione Centrale, Milano",
destinations="Aeroporto Malpensa",
mode="transit"
)
```
---
## Enable / Disable
```
toggle_item(kind="mcp", id="gmaps", enabled=false) # disable
toggle_item(kind="mcp", id="gmaps", enabled=true) # re-enable
restart # required for changes to take effect
```
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `googlemaps` | latest | Google Maps Platform Python client |
---
## Parameter notes
### Coordinates format
Whenever a parameter accepts coordinates, pass them as a **`"latitude,longitude"` decimal string with no spaces**, e.g. `"45.4654,9.1866"`. Never pass an array or separate fields.
### `departure_time`
Must be the **literal string `"now"`** or an **ISO 8601 datetime string with timezone offset**, e.g. `"2025-06-15T08:30:00+02:00"`. Never pass a Unix timestamp integer — the tool now rejects non-string values with an explicit error.
### `transit_mode`
Restricts results to a specific vehicle: `"train"` = intercity/regional rail, `"subway"` = metro, `"tram"` = trams, `"bus"` = buses, `"rail"` = any rail. Omit to allow any vehicle.
---
## Error Handling
| Error | Response |
|-------|----------|
| API key not found | `"Error: Google Maps API key not found. Set GOOGLE_MAPS_API_KEY…"` |
| `googlemaps` not installed | `"Error: Missing dependency: No module named 'googlemaps'. Run: pip install googlemaps"` |
| `OVER_QUERY_LIMIT` | `"Error: <API> API quota exceeded (OVER_QUERY_LIMIT). Check usage and billing in the Google Cloud Console."` |
| `REQUEST_DENIED` | `"Error: <API> API request denied (REQUEST_DENIED). Verify that the API key … is valid and that the <API> API is enabled …"` |
| `INVALID_REQUEST` | `"Error: <API> API rejected the request as invalid (INVALID_REQUEST). Check that the addresses, coordinates, and parameters are well-formed."` |
| `MAX_ELEMENTS_EXCEEDED` | `"Error: <API> API returned MAX_ELEMENTS_EXCEEDED — too many origins×destinations at once. …"` |
| `NOT_FOUND` | `"Error: <API> API could not geocode one of the supplied places. …"` |
| Timeout | `"Error: <API> API request timed out. Retry in a moment."` |
| No route found | `"No routes found from '…' to '…'."` |
| Missing required param | `"Error: Missing required parameter '…'"` |
| Invalid `departure_time` | `"Error: 'departure_time' must be 'now' or an ISO 8601 string …"` |
All error responses are flagged with `isError: true`. The error label `<API>` reflects which Google Maps Platform API was being called (Directions, Geocoding, Places, Distance Matrix).
All errors are logged to stderr with `[gmaps_mcp]` prefix.
---
## Protocol
Implements JSON-RPC 2.0 over stdio (same as gcal and gmail servers):
- **Requests:** read from stdin, one JSON object per line
- **Responses:** written to stdout
- **Logs:** stderr only, prefixed `[gmaps_mcp]`
Supported methods: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`.
---
## When to Update This File
- New tools added
- Auth mechanism changes (e.g. OAuth migration)
- New transport option added
- Error cases change
+177
View File
@@ -0,0 +1,177 @@
# SerpAPI Google Flights MCP Server (serpapi_flights)
## Overview
A Python MCP server providing **flight search** capabilities via the SerpAPI Google Flights engine.
**Server name:** `serpapi_flights`
**Transport:** `stdio` (spawns `python3 scripts/mcp/serpapi_flights/server.py`)
**Location:** `scripts/mcp/serpapi_flights/server.py`
---
## Tools
| Tool | Required params | Optional params | Description |
|------|-----------------|-----------------|-------------|
| `serpapi_search_flights` | `departure_id`, `arrival_id`, `outbound_date` | `return_date`, `adults`, `children`, `infants_in_seat`, `infants_on_lap`, `stops`, `currency`, `preferred_cabins`, `hl`, `max_results` | One-way or round-trip flight search on Google Flights |
The previous `serpapi_lookup_airport` tool (hardcoded dictionary of ~100 airports) was removed: the LLM already knows the common IATA codes, and SerpAPI itself accepts both airport codes (`JFK`, `FCO`) and city codes covering all airports of a city (`NYC`, `ROM`, `MIL`, `LON`).
---
## Authentication
### API key
SerpAPI uses a single API key (no OAuth).
**Priority order:**
1. Environment variable `SERPAPI_API_KEY`
2. File `secrets/serpapi_api_key.txt` (first non-empty line)
The `secrets/` directory is in `.gitignore` — the key will not be committed.
### Get a key
1. Sign up at [serpapi.com](https://serpapi.com).
2. Copy your API key from the dashboard.
### Save the key
```bash
echo "YOUR_SERPAPI_KEY_HERE" > secrets/serpapi_api_key.txt
```
Or set the environment variable:
```bash
export SERPAPI_API_KEY=your_key_here
```
---
## Setup
### 1. Install the Python dependency
`httpx` is listed in the project root `requirements.txt` and is installed automatically by `run.sh` into `.venv/`. To install manually:
```bash
uv pip install httpx
# or: .venv/bin/pip install httpx
```
The local `scripts/mcp/serpapi_flights/requirements.txt` is kept for standalone use and contains only `httpx>=0.27.0`.
### 2. Register the server with the agent
Ask the agent:
```
register_mcp(
name="serpapi_flights",
transport="stdio",
command="python3",
args=["scripts/mcp/serpapi_flights/server.py"]
)
```
---
## Usage Examples
### One-way, cheapest Milan → Rome next week
```
mcp__serpapi_flights__serpapi_search_flights(
departure_id="MIL",
arrival_id="ROM",
outbound_date="2026-07-01"
)
```
### Round-trip London → New York, 2 adults, business class, non-stop only
```
mcp__serpapi_flights__serpapi_search_flights(
departure_id="LON",
arrival_id="NYC",
outbound_date="2026-08-01",
return_date="2026-08-15",
adults=2,
preferred_cabins="business",
stops=0,
currency="USD"
)
```
---
## Parameter notes
### Airport vs city codes
`departure_id` and `arrival_id` must be **exactly 3 ASCII letters**, uppercased automatically. Both forms are accepted:
- **Airport code** — a specific airport: `JFK`, `FCO` (Rome Fiumicino), `LGW` (London Gatwick).
- **City code** — covers all airports of a city: `NYC`, `ROM`, `MIL`, `LON`. **Prefer city codes** when the user does not name a specific airport.
### `stops`
Integer enum: `0` = non-stop only, `1` = max one stop, `2` = max two stops. Omit to allow any.
### `type` (handled automatically)
The server sends SerpAPI `type=2` for one-way (no `return_date`) and `type=1` for round-trip. The caller never sets `type` directly.
### `currency`
ISO 4217 code (3 uppercase letters). Default `EUR`.
### `hl`
Language code for SerpAPI result text (e.g. `"en"`, `"it"`). Default `"en"`.
---
## Error Handling
| Error | Response |
|-------|----------|
| API key missing | `"Error: SerpAPI API key not found. Set SERPAPI_API_KEY env var or create secrets/serpapi_api_key.txt …"` |
| HTTP 401 | `"Error: Invalid SerpAPI API key. Check secrets/serpapi_api_key.txt or the SERPAPI_API_KEY env var."` |
| HTTP 429 | `"Error: SerpAPI rate limit exceeded. Wait a moment and retry."` |
| HTTP 400 | `"Error: Bad request — <body>. Verify airport codes (3-letter IATA) and dates."` |
| Timeout (30s) | `"Error: Request to SerpAPI timed out (30s). …"` |
| Missing/invalid param | `"Error: 'outbound_date' must be in YYYY-MM-DD format (got '…')."` |
| Unknown tool | `"Error: Unknown tool: <name>"` |
| SerpAPI error in body | `"Error: SerpAPI returned an error: <message>"` |
Every error is returned as a tool result with `isError: true` (detected by the `Error:` prefix).
All errors are logged to stderr with `[serpapi_flights_mcp]` prefix.
---
## Protocol
Implements JSON-RPC 2.0 over stdio (same shape as the `gmaps`, `gcal`, `gmail`, and `whatsapp` servers):
- **Requests:** read from stdin, one JSON object per line
- **Responses:** written to stdout
- **Logs:** stderr only, prefixed `[serpapi_flights_mcp]`
Supported methods: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`.
The server is fully synchronous: stdin is read line-by-line, each `tools/call` performs a blocking `httpx.Client.get` to SerpAPI and returns the formatted result. No background threads, no FastMCP framework.
---
## When to Update This File
- New tools added
- Auth mechanism changes
- SerpAPI parameters added or removed
- Error cases change
+349
View File
@@ -0,0 +1,349 @@
# WhatsApp MCP Server (whatsapp)
## Overview
A Node.js MCP server that exposes WhatsApp as a set of tools for the LLM, using **whatsapp-web.js** + Puppeteer (headless Chromium).
**Server name:** `whatsapp`
**Transport:** `stdio` (spawns `node scripts/whatsapp_mcp/index.js`)
**Location:** `scripts/whatsapp_mcp/index.js`
### Capabilities
| Capability | Enabled |
|------------|---------|
| List chats and groups | ✅ |
| Read messages from a chat | ✅ |
| Search messages by keyword | ✅ |
| Search contacts by name | ✅ |
| Send messages | ✅ |
| Send media (image/video/audio/document, from file or URL) | ✅ |
| Download received media (photos, videos, documents) | ✅ |
| Logout / reset session without restart | ✅ |
| Address a contact by phone number (no lookup needed) | ✅ |
| Edit/delete messages | ❌ not implemented |
---
## ⚠️ Important notes on WhatsApp
- **whatsapp-web.js is unofficial**: it drives WhatsApp Web through Chromium. WhatsApp may ban the number in case of heavy or anomalous use.
- **Safe use**: reading your own groups and sending individual messages is in the tolerated grey area. Do not use it for spam or bulk automation.
- **Recommended number**: using a secondary number or a parallel WhatsApp Business account reduces the risk.
---
## Tools
All tools are callable as `mcp__whatsapp__<tool>`; the table lists the bare `<tool>` names.
| Tool | Parameters | Description |
|------|------------|-------------|
| `status` | *(none)* | Connection status as a plain-language report: the state, what it means, and step-by-step fix instructions when not operational. Cross-checks the live socket (`getState()`) to catch silently dropped sessions |
| `get_qr` | *(none)* | QR code to scan with the phone — path/URL to a PNG or HTML page, with ASCII fallback (only when status = QR_READY) |
| `logout` | *(none)* | Ends the session, **clears the cached credentials on disk** and re-initializes the client → new QR without restart. Use it when the session has expired/got stuck or to link a different phone |
| `list_chats` | `max_chats` (int, default 20, max 50) | List recent chats with name, ID and unread count |
| `get_messages` | `chat_id` **or** `number`, `limit` (int, default 20, max 100), `offset` (int, default 0) | Messages from a chat/group with pagination support. Media messages are tagged with their type and a `download id` |
| `send_message` | `chat_id` **or** `number`, `message` (required) | Send a text message |
| `send_media` | `chat_id` **or** `number`, `source` (required: file path or http(s) URL), `caption`, `as_document` (bool) | Send an image/video/audio/document |
| `download_media` | `message_id` (required) | Download the media attached to a message; saves it under `data/whatsapp_media/` and returns the path + `/data/` URL |
| `search_messages` | `query` (required), `max_results` (int, default 20, max 50) | Search by keyword across all chats |
| `search_contacts` | `query` (required), `max_results` (int, default 20, max 50) | Search saved contacts by name (partial, case-insensitive). Use it to find the ID of a contact not present in recent chats |
### chat_id format
- **Contact:** `39xxxxxxxxxx@c.us` (international prefix without `+`, followed by `@c.us`)
- **Group:** `xxxxxxxxxx-xxxxxxxxxx@g.us`
Correct chat_ids are obtained via `list_chats` (recent chats) or `search_contacts` (saved contacts not in recent chats).
**Shortcut for individual contacts:** `get_messages`, `send_message` and `send_media` also accept a plain `number` (phone number with country code, e.g. `393331234567` or `+39 333 123 4567`) instead of a `chat_id`. The server resolves it via `getNumberId` (which also verifies the number is on WhatsApp), so there is no need to look up the chat_id first. Groups must still be addressed by `chat_id`.
---
## Authentication
### First time (QR scan)
On the first launch there is no saved session. The client generates a QR code:
1. The LLM calls `status` → response `QR_READY`
2. The LLM calls `get_qr` → returns the QR
3. The user scans the QR with WhatsApp → **Settings → Linked Devices → Link a Device**
4. The state moves to `AUTHENTICATED`, then `READY`
The QR is also saved to a file (PNG at `data/whatsapp_qr.png`, HTML at `secrets/whatsapp_qr.html`, ASCII fallback at `secrets/whatsapp_qr.txt`).
### Subsequent sessions
The session is persisted in `secrets/whatsapp_session/` (managed by whatsapp-web.js's `LocalAuth`). On server restart the session is restored automatically, with no need to scan the QR again.
### Browser lifecycle & restart cleanup (self-healing)
The Puppeteer profile lives at `secrets/whatsapp_session/session` and Chromium takes an **exclusive lock** on it (`SingletonLock`): two browsers cannot open the same profile at once.
The host (skald) kills MCP servers with **SIGKILL** on shutdown/restart (`kill_on_drop` in the Rust MCP client — see `crates/mcp-client/src/server.rs`). SIGKILL is untrappable, so the node process dies instantly and the headless Chromium it spawned is **orphaned** (reparented to init), keeping the profile locked. The next launch would then fail with *"The browser is already running for &lt;profile&gt;. Use a different `userDataDir` or stop the running browser first."* — and every WhatsApp tool stays dead until the orphan is killed by hand.
To make this self-healing, on **every startup** (in `initClient()``cleanupStaleBrowser()`) the server:
1. Runs `pgrep -f <profile-dir>` and `SIGKILL`s every process still bound to the profile. Because skald runs a single WhatsApp MCP at a time and SIGKILLs the old node before spawning the new one, any such process is guaranteed to be a leftover orphan.
2. Removes the stale `SingletonLock` / `SingletonCookie` / `SingletonSocket` files so a fresh launch is unblocked.
For the shutdown paths that *are* trappable (clean stdin EOF, `SIGTERM`/`SIGINT` from a container/OS stop), the server closes the browser cleanly (`client.destroy()`, awaited with a 5 s cap) so it releases the lock on the way out. The startup cleanup is the backstop that covers the SIGKILL path. The saved login (under `session/Default/`) is untouched, so no QR re-scan is needed after a restart.
### Logout / expired session (without restart)
When the session expires or gets stuck (state `DISCONNECTED`), on restart `LocalAuth` would reload the invalid session from `secrets/whatsapp_session/`, immediately returning to the disconnected state. Previously the only fix was to delete that folder by hand and restart.
Now `logout` is enough:
1. Attempts a clean logout (`client.logout()`), tolerating failure if the browser page is already dead;
2. As a fallback, closes the browser (`destroy()`) to release the locks on the profile;
3. **Force-deletes `secrets/whatsapp_session/`** (the cached token);
4. Removes any stale QR files;
5. Re-initializes the client → generates a new QR within a few seconds.
```
mcp__whatsapp__logout()
# → wait a few seconds
mcp__whatsapp__get_qr()
# → scan the new QR
```
No server restart required.
### Token storage
| File/Directory | Contents |
|---|---|
| `secrets/whatsapp_session/` | Persistent WhatsApp session (LocalAuth) — deleted by `logout` |
| `secrets/whatsapp_qr.html` / `data/whatsapp_qr.png` | Temporary QR code (removed after authentication or a logout) |
| `secrets/whatsapp_qr.txt` | ASCII fallback of the QR (when `qrcode` is unavailable) |
| `data/whatsapp_media/` | Media downloaded via `download_media`, served at `/data/whatsapp_media/` |
Everything under `secrets/` is in `.gitignore` via the `secrets/` rule.
---
## Setup (one-time)
### 1. Install the Node.js dependencies
```bash
cd scripts/whatsapp_mcp
npm install
```
This installs `whatsapp-web.js`, `puppeteer` (includes Chromium, ~300MB), `qrcode` and `qrcode-terminal`.
### 2. Register the server (have the agent do it)
```
register_mcp(
name="whatsapp",
transport="stdio",
command="node",
args=["scripts/whatsapp_mcp/index.js"]
)
```
### 3. First authentication
```
mcp__whatsapp__status()
# → QR_READY
mcp__whatsapp__get_qr()
# → shows the QR, scan it with the phone
```
---
## Usage examples
### See recent chats
```
mcp__whatsapp__list_chats(max_chats=10)
```
### Read the latest messages from a group
```
mcp__whatsapp__get_messages(
chat_id="1234567890-9876543210@g.us",
limit=50
)
```
### Page through history (older messages)
`offset` skips the most recent messages, exposing the preceding window:
```
# Last 20 messages
get_messages(chat_id="...", limit=20, offset=0)
# Messages 2140 (previous)
get_messages(chat_id="...", limit=20, offset=20)
# Messages 4160 (even older)
get_messages(chat_id="...", limit=20, offset=40)
```
Limit: `limit + offset` cannot exceed 200 in a single call (a `fetchMessages` constraint).
### Find the contact of someone not in recent chats
```
mcp__whatsapp__search_contacts(query="Luca")
# → Luca Rossi [contact] | ID: 393331234567@c.us
```
### Search for what was said about a topic
```
mcp__whatsapp__search_messages(query="Monday meeting")
```
### Send a message
```
# By chat_id (groups, or chats already open)
mcp__whatsapp__send_message(
chat_id="393331234567@c.us",
message="Hi! Are you there?"
)
# Or directly by number — no list_chats/search_contacts needed first
mcp__whatsapp__send_message(
number="+39 333 123 4567",
message="Hi! Are you there?"
)
```
### Send media (image, video, document)
```
# From a local file (path relative to the project root, or absolute)
mcp__whatsapp__send_media(
number="393331234567",
source="data/report.pdf",
caption="Here is the report",
as_document=true
)
# From a URL
mcp__whatsapp__send_media(
chat_id="1234567890-9876543210@g.us",
source="https://example.com/photo.jpg",
caption="Look at this"
)
```
### Download received media
`get_messages` tags media messages with a `download id`:
```
mcp__whatsapp__get_messages(number="393331234567", limit=10)
# → [2026-06-22 10:01:00] Luca [image, download id="true_39...@c.us_3EB0..."]: invoice photo
mcp__whatsapp__download_media(message_id="true_39...@c.us_3EB0...")
# → saved to data/whatsapp_media/... (also served at /data/whatsapp_media/...)
```
---
## Connection states
`status` returns a self-describing report — it states the lifecycle state, explains it in plain language, and lists concrete next steps whenever it is not `READY`. The agent should not need this table; it is here for reference.
| State | Meaning | What to do |
|-------|---------|------------|
| `INITIALIZING` | Browser starting up, session loading | Wait a few seconds |
| `QR_READY` | QR scan needed | Call `get_qr` and scan |
| `AUTHENTICATED` | QR scanned, session being established | Wait (→ READY automatically) |
| `READY` | Operational | All tools available |
| `DISCONNECTED` | Connection/session lost | Call `logout` to reset and log in again (no restart) |
### Live socket cross-check
The lifecycle `state` above is driven by whatsapp-web.js **events**, so it can lag behind a session that drops silently. When `state` is `READY`, `status` also queries the live socket (`client.getState()`, a `WAState`) and reports a mismatch with tailored instructions:
| Live `WAState` while READY | Reported as | Fix suggested |
| --- | --- | --- |
| `CONNECTED` | `READY ✅ (ok)` | — |
| `UNPAIRED` / `UNPAIRED_IDLE` | `action needed` | Device unlinked from phone → `logout` + re-scan |
| `CONFLICT` | `action needed` | WhatsApp Web open elsewhere → close it, or `logout` + re-scan |
| `TIMEOUT` | `transient` | May auto-reconnect → wait and re-check; if stuck, `logout` |
| `DEPRECATED_VERSION` | `needs maintenance` | Update the `whatsapp-web.js` dependency (developer task) |
| `getState()` fails / other | `uncertain` | Browser may have crashed → wait and re-check; if stuck, `logout` |
---
## Enable / Disable
### Disable (when not needed)
```
toggle_item(kind="mcp", id="whatsapp", enabled=false)
restart
```
### Re-enable
```
toggle_item(kind="mcp", id="whatsapp", enabled=true)
restart
```
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `whatsapp-web.js` | ^1.34.7 | WhatsApp Web client |
| `puppeteer` | ^25.1.0 | Headless Chromium (bundled) |
| `qrcode` | ^1.5.4 | Generates the QR as PNG / data-URL (HTML) |
| `qrcode-terminal` | ^0.12.0 | ASCII QR fallback |
**System requirements:**
- Node.js ≥ 18
- ~500MB of space for Puppeteer/Chromium
- A background Chromium process while the server is running
---
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `whatsapp-web.js not found` | `npm install` not run | `cd scripts/whatsapp_mcp && npm install` |
| `WhatsApp not ready (status: INITIALIZING)` | Server just started | Wait 15-30 seconds |
| `WhatsApp not ready (status: QR_READY)` | Session expired/missing | Call `get_qr` and scan |
| `WhatsApp not ready (status: DISCONNECTED)` | Connection/session lost | Call `logout`, wait, then `get_qr` and scan |
| Stays `DISCONNECTED` even after restart | Expired cached token in `secrets/whatsapp_session/` | Call `logout` (clears the cache and regenerates the QR) |
| `The browser is already running for …/session` | An orphaned Chromium from a hard-killed (SIGKILL) prior run still holds the profile lock | **Auto-healed on next startup** by `cleanupStaleBrowser()` (kills the orphan + clears the lock). See *Browser lifecycle & restart cleanup* |
| Chat ID not found | Wrong ID | Use `list_chats` to get the correct IDs |
---
## Protocol
Implements JSON-RPC 2.0 over stdio (same pattern as gmail and gcal):
- **Requests:** JSON on stdin (one per line)
- **Responses:** JSON on stdout
- **Logs:** stderr with the `[whatsapp_mcp]` prefix
Supported methods: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`
---
## When to update this file
- New tools added to the server
- Changed session/QR paths under `secrets/`
- New connection states
- Changed dependency versions