Skip to main content
Glama
ideadesignmedia

Google Calendar MCP Server

README.md
# @ideadesignmedia/gcal-mcp

Private MCP server (stdio) and CLI for linking and operating multiple Google Calendar accounts. It stores tokens in a local SQLite database and can encrypt refresh tokens at rest with a user password.

The server speaks MCP over stdio and keeps stdout clean (JSON‑RPC only); all human‑readable logs go to stderr.

## Requirements

- Node.js >= 18.17
- A Google Cloud OAuth client (Web application) with Calendar API enabled
  - You will need `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`

## One‑liner (npx)

Use `npx` to run without installing globally. The package name is `@ideadesignmedia/gcal-mcp` and the binary is `gcal-mcp`.

```bash
# Help
npx -y @ideadesignmedia/gcal-mcp --help

# Link an account (prints an authorization URL)
npx -y @ideadesignmedia/gcal-mcp add \
  --client-id "$GOOGLE_CLIENT_ID" \
  --client-secret "$GOOGLE_CLIENT_SECRET"

# Lock the database (encrypts existing refresh tokens)
npx -y @ideadesignmedia/gcal-mcp passwd --pass 'your-strong-pass'

# Start the MCP server over stdio (stdout is JSON only)
GMAIL_MCP_DB_PASS='your-strong-pass' \
  npx -y @ideadesignmedia/gcal-mcp start
```

## Database

- Default location: `~/.idm/gcal-mcp/db.sqlite`
- Override with `--db <path>` on any command
- When locked, refresh tokens are encrypted with AES‑GCM using a DEK derived from your password via scrypt

## Global Flags

- `--db <path>`: SQLite database path (defaults above)
- `--pass <pass>`: Database password (where applicable). For `start`, consider `GMAIL_MCP_DB_PASS` instead.
- `--read-only`: Start server with write tools disabled (create/update/delete)

Environment variables:

- `GMAIL_MCP_DB_PASS`: Password used by `start` (and as a fallback elsewhere)
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`: OAuth credentials (used by `add` if flags not supplied)

## Commands

- `start`
  - Starts the MCP server over stdio
  - Respects `--read-only`
  - Stdout: JSON‑RPC messages only; logs to stderr
  - Examples:
    - `GMAIL_MCP_DB_PASS='...' npx -y @ideadesignmedia/gcal-mcp start`

- `add`
  - Links a Google account and stores its refresh token
  - Options:
    - `--client-id <id>` / `--client-secret <secret>` (or use env vars)
    - `--device` for Device Code flow (headless), otherwise uses loopback redirect and prints a URL to visit (default port 43112; change with `--listen-port <port>`)
    - `--scopes <list>` to provide custom scopes (comma or space separated)
    - `--choose-scopes` to interactively select calendar access (read-only vs read/write) and optionally add extra scopes
  - Example:
    - `npx -y @ideadesignmedia/gcal-mcp add --client-id $GOOGLE_CLIENT_ID --client-secret $GOOGLE_CLIENT_SECRET`
    - `npx -y @ideadesignmedia/gcal-mcp add --choose-scopes --client-id $GOOGLE_CLIENT_ID --client-secret $GOOGLE_CLIENT_SECRET`

- `list`
  - Lists linked accounts (id, email, display name, created)

- `remove <key>`
  - Removes an account and its tokens by id or email

- `passwd`
  - Locks the database or rotates an existing password
  - Options:
    - `--pass <pass>`: Password to set when locking, or new password when rotating
    - `--rotate`: Rotate the existing password (requires `--old-pass`)
    - `--old-pass <old>`: Old password for rotation
    - `--hint <text>`: Optional password hint stored alongside the KDF parameters

## MCP Server Integration

Any MCP‑capable client can launch this server as a stdio process. A generic config entry looks like:

```json
{
  "mcpServers": {
    "gcal-mcp": {
      "command": "npx",
      "args": ["-y", "@ideadesignmedia/gcal-mcp", "start"],
      "env": {
        "GMAIL_MCP_DB_PASS": "${secret:GCAL_DB_PASS}",
        "GOOGLE_CLIENT_ID": "${env:GOOGLE_CLIENT_ID}",
        "GOOGLE_CLIENT_SECRET": "${env:GOOGLE_CLIENT_SECRET}"
      }
    }
  }
}
```

Notes:

- The `start` command writes only JSON‑RPC to stdout as required by MCP stdio.
- Use your client’s secret storage for `GMAIL_MCP_DB_PASS` where available.

## Provided Tools

The server exposes the following tools. Names and parameter schemas follow the Chat Completions function tool shape.

Primary calendar only:

- All operations target the account’s primary calendar automatically; there is no `calendarId` parameter.
  - This removes confusion between account vs calendar. The server passes `primary` internally.

Account resolution rules (applies wherever `account` is accepted):

- Accepts exact `email` or `id`.
- Also accepts a partial, case-insensitive match on email or display name.
- If `account` is omitted and exactly one account is linked, that account is used.
- If multiple accounts would match or are linked with no `account` provided, the tool throws a helpful error. Use `gcal-list_accounts` to choose deterministically.

- `gcal-list_accounts`
  - Parameters: none
  - Returns: `{ accounts: Array<{ id: string, email: string, displayName: string | null }> }`

- `gcal-resolve_account`
  - Parameters:
    - `query` (string; optional; email/id or partial). If omitted or blank, returns all accounts.
  - Returns: `{ query: string, matches: Array<{ id: string, email: string, displayName: string | null }>, exact: boolean, ambiguous: boolean, count: number }`

- `gcal-search_events`
  - Parameters:
    - `account` (string; optional)
    - `q` (string, optional)
    - `timeMin` (ISO string, optional)
    - `timeMax` (ISO string, optional)
    - `maxResults` (integer 1..250, optional)
  - Returns: `{ events: GoogleCalendarEvent[] }` (verbatim event objects)

- `gcal-get_event`
  - Parameters:
    - `account` (string; optional)
    - `eventId` (string)
  - Returns: `GoogleCalendarEvent`

- `gcal-create_event`
  - Parameters:
    - `account` (string; optional)
    - `summary` (string)
    - `description` (string, optional)
    - `location` (string, optional)
    - `start` (object: `{ dateTime?: string, date?: string, timeZone?: string }`)
    - `end` (object: `{ dateTime?: string, date?: string, timeZone?: string }`)
    - `attendees` (array of `{ email: string }`, optional)
  - Returns: `GoogleCalendarEvent`

- `gcal-update_event`
  - Parameters:
    - `account` (string; optional)
    - `eventId` (string)
    - `patch` (object; partial event resource)
  - Returns: `GoogleCalendarEvent`

- `gcal-delete_event`
  - Parameters:
    - `account` (string; optional)
    - `eventId` (string)
  - Returns: `{ ok: true }` on success

Read‑only mode:

- Start with `--read-only` to disable `gcal-create_event`, `gcal-update_event`, and `gcal-delete_event`.

## OAuth Flows

- Default: Loopback flow. The CLI opens your browser to grant access and listens on `127.0.0.1:43112` for the redirect (customizable via `--listen-port`).
- Headless: Device Code flow with `--device`. The CLI prints a code and verification URL to the terminal.
- Scopes used: `https://www.googleapis.com/auth/calendar`, plus `openid email profile` to capture account identity.

## Security Notes

- When locked, refresh tokens are encrypted at rest (AES‑GCM). The key is derived with scrypt using per‑DB salt and stored KDF parameters.
- The MCP `start` command emits no human‑readable output on stdout.
- Prefer setting `GMAIL_MCP_DB_PASS` via your MCP client’s secret system; avoid committing secrets in configs.

## Troubleshooting

- Database is locked
  - Set `GMAIL_MCP_DB_PASS` or pass `--pass` when needed. For `start`, use the env var.

- OAuth did not return a refresh_token
  - Ensure you used the provided `add` command (it requests offline access with consent). If you previously granted access without offline permission, remove prior consent in your Google Account or create a new OAuth client.

- Loopback callback failed
  - Check firewall or try `--device` for Device Code flow.

- MCP client shows parse errors
  - Verify you are launching the server with `start` and not another command. STDOUT must remain clean JSON‑RPC.

---

MIT © Idea Design Media