First Version
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <profile>. 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 21–40 (previous)
|
||||
get_messages(chat_id="...", limit=20, offset=20)
|
||||
|
||||
# Messages 41–60 (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
|
||||
Reference in New Issue
Block a user