{
  "markdown": "# calendar-mcp\n\nAn MCP server that gives an LLM client read and write access to your Google Calendar. It runs as a single process — stdio by default, streamable HTTP optionally — and exposes 23 tools with structured output: listing and searching calendars and events, creating, updating, moving, RSVPing to and deleting events, free/busy queries, busyness analysis, recurring-event projection, and finding a mutual slot and booking it. On top of that it has a scheduling brain that knows your working hours: finding and booking focus time, detecting double-bookings across several accounts at once, proposing better times for a meeting, and auditing where your week actually went. Authentication is Google OAuth 2.0 (Desktop app flow); tokens are cached locally, per account, and refreshed automatically.\n\n## Quick start\n\n**1. Create Google OAuth credentials.** In the [Google Cloud console](https://console.cloud.google.com/), enable the Google Calendar API, then create an OAuth client ID of type **Desktop app**. Copy the client ID and secret. (Details in [Google Cloud setup](#google-cloud-setup).)\n\n**2. Set them in your environment**, or in a `.env` file in the directory you run from (see `example.env`):\n\n```dotenv\nGOOGLE_CLIENT_ID='...'\nGOOGLE_CLIENT_SECRET='...'\n```\n\n**3. Sign in once, then add the server to your client:**\n\n```bash\nuvx calendar-mcp-server auth\n```\n\nThis opens a browser and saves a token in the config directory (see [Accounts](#accounts)); if you already have a `.gcp-saved-tokens.json` or set `TOKEN_FILE_PATH`, that file is used instead. Verify it with `calendar-mcp check`. Add more Google accounts with `calendar-mcp auth --account work`. After that the server runs non-interactively — it never opens a browser on its own unless you set `CALENDAR_MCP_ALLOW_BROWSER_AUTH=1`.\n\n> The PyPI distribution is `calendar-mcp-server`. It installs two identical console scripts, `calendar-mcp` and `calendar-mcp-server`, so `uvx calendar-mcp-server` and a local `calendar-mcp` are the same command.\n\n## Client configuration\n\n**Claude Code**\n\n```bash\nclaude mcp add calendar \\\n  --env GOOGLE_CLIENT_ID=... \\\n  --env GOOGLE_CLIENT_SECRET=... \\\n  -- uvx calendar-mcp-server\n```\n\n**Claude Desktop** — `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"calendar\": {\n      \"command\": \"uvx\",\n      \"args\": [\"calendar-mcp-server\"],\n      \"env\": {\n        \"GOOGLE_CLIENT_ID\": \"...\",\n        \"GOOGLE_CLIENT_SECRET\": \"...\",\n        \"TOKEN_FILE_PATH\": \"/absolute/path/to/.gcp-saved-tokens.json\"\n      }\n    }\n  }\n}\n```\n\n**Cursor** — `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): same `mcpServers` block as above.\n\n**Any other client** that speaks the standard `mcpServers` JSON takes the same entry. `TOKEN_FILE_PATH` is optional now that tokens default to the OS config directory, which does not depend on the working directory the client picks — but if you do set it, make it an absolute path.\n\n## Remote / HTTP mode\n\n```bash\ncalendar-mcp --transport http --host 127.0.0.1 --port 8000\n```\n\nThe MCP endpoint is then `http://127.0.0.1:8000/mcp` (change the path with `--path`).\n\n**There is no authentication layer on the HTTP transport yet.** Anyone who can reach the endpoint gets full access to the calendar the saved token belongs to. Bind it to loopback, or expose it only behind a trusted reverse proxy that authenticates, or on a private network such as a tailnet. Do not put it on a public interface.\n\n## Tools\n\nRead-only tools never change anything. \"Writes\" tools create or modify events;\n`delete_event` is the only one that destroys data.\n\n**Calendars and events**\n\n| Tool | | Description |\n| --- | --- | --- |\n| `list_calendars` | read | List the calendars the user can see, with IDs and timezones. |\n| `find_events` | read | Search a calendar for events, expanding recurring series into instances. |\n| `check_attendee_status` | read | Report who accepted, declined or has not answered an invitation. |\n| `query_free_busy` | read | Busy intervals for one or more calendars, without event details. |\n| `analyze_busyness` | read | Per-day event count and total scheduled minutes over a range. |\n| `project_recurring_events` | read | Compute future occurrences from recurrence rules. |\n| `create_calendar` | writes | Create a new secondary calendar. |\n| `create_event` | writes | Create an event with explicit start/end times and optional attendees. |\n| `quick_add_event` | writes | Create an event from a plain-English phrase, parsed by Google. |\n| `update_event` | writes | Change fields on an event; omitted fields are left untouched. |\n| `move_event` | writes | Reschedule an event, and/or move it to another calendar. |\n| `add_attendee` | writes | Invite one or more people to an existing event. |\n| `respond_to_event` | writes | Set your own RSVP (`accepted`/`declined`/`tentative`/`needsAction`). |\n| `schedule_mutual` | writes | Find the first slot where everyone is free, then book it. |\n| **`delete_event`** | **destroys** | Permanently delete an event. Asks the client to confirm via elicitation when supported. |\n\n**Scheduling brain** — these read your saved preferences (working hours, lunch, buffer, minimum focus block).\n\n| Tool | | Description |\n| --- | --- | --- |\n| `find_focus_time` | read | Uninterrupted blocks in a window that could be used for deep work, longest first. |\n| `detect_conflicts` | read | Double-bookings and too-tight transitions, across every signed-in account at once. |\n| `time_audit` | read | Where the time went: meeting hours, by size, domain, recurrence and person. |\n| `suggest_reschedule` | writes *(opt-in)* | Ranked better times for an existing meeting. Suggests only, unless `apply: true`. |\n| `block_focus_time` | writes | Book the best free blocks as focus time. `dry_run: true` to preview. |\n\n**Local configuration** — no Google call, no `account` argument.\n\n| Tool | | Description |\n| --- | --- | --- |\n| `list_accounts` | read | The accounts you have signed in, and which is the default. |\n| `get_preferences` | read | Working hours, lunch, buffer, minimum focus block, focus calendar. |\n| `set_preferences` | writes | Update and save those preferences. Local file only. |\n\nEvery calendar tool takes an optional trailing `account` argument (`detect_conflicts`\ntakes `accounts`, a list, because checking several at once is the point). All times\nare ISO 8601 strings — a naive timestamp is interpreted in the target calendar's own\ntimezone.\n\n### Scheduling brain\n\nThe five scheduling tools share one idea: your calendar is not the same as your\navailability. They start from your working hours, subtract lunch, subtract what is\nalready booked, and subtract the buffer you want around meetings — then reason about\nwhat is left.\n\nPrompts that exercise them:\n\n- \"Find me six hours of focus time next week and block it out — show me the times first.\"\n- \"Is anything double-booked between my work and personal calendars this week?\"\n- \"My Thursday is back-to-back. Suggest better times for the design review.\"\n- \"Where did my time go last month? Who am I spending it with?\"\n- \"I need a 90-minute deep work block before Friday. Is there one?\"\n\n`find_focus_time` reports what exists; `block_focus_time` defends it by creating\nevents (busy, reminders off, nobody notified), trimming the last block so it books\nexactly the hours you asked for rather than a whole afternoon. `detect_conflicts`\nseparates genuine overlaps from *tight* transitions that merely break your buffer,\nand ignores events you declined, events marked free, and (by default) all-day\nentries. `suggest_reschedule` keeps the meeting's duration, ranks candidate slots by\nfewest attendee conflicts and prefers the event's current day, and moves nothing\nunless you pass `apply: true`.\n\n### Time audit\n\n`time_audit` answers \"how much of my week is meetings?\" in one pass, grouped by\n`day` or `week`:\n\n```\n2026-08-01 .. 2026-09-01, Europe/Berlin, grouped by week\n  Meetings:         41.5 h across 63 meetings\n  Working hours:    152.0 h available (lunch removed)\n  In working hours: 38.0 h -> 25% of the week\n  Busiest week:     2026-W34, 14.0 h\n  Heaviest day:     2026-08-20, 6.5 h\n  By size:          1:1 18.0 h | small 15.5 h | large 8.0 h\n  By recurrence:    recurring 26.0 h | one-off 15.5 h\n  Top people:       a.schmidt@example.com 9.0 h | j.lee@example.com 7.5 h\n```\n\nDeclined meetings, events marked free and all-day entries are left out by default;\n`include_declined` and `include_all_day` bring them back.\n\n### Safety\n\n- **`delete_event` is the only destructive tool.** It asks the client to confirm\n  through MCP elicitation when the client supports it, and proceeds normally when\n  it does not.\n- **`block_focus_time` takes `dry_run`.** Run it with `dry_run: true` to see the\n  exact blocks it would book before anything is written.\n- **`suggest_reschedule` does not move anything by default.** `apply` is `false`\n  and has to be set explicitly, once the user has agreed to a time.\n- **Everything else that writes is additive** — creating or editing an event —\n  and `update_event` leaves fields you omit untouched.\n- **`list_accounts`, `get_preferences` and `set_preferences` never touch Google.**\n  They read and write local files in the config directory.\n\n## Accounts\n\nYou can sign in more than one Google account and pick between them per call.\n\n```bash\ncalendar-mcp auth                    # the default account\ncalendar-mcp auth --account work     # a second, named account\ncalendar-mcp accounts                # list them, with token paths and sign-in state\n```\n\nEvery calendar tool takes an optional `account` argument naming one of these\n(\"what's on my work calendar tomorrow?\"). Omit it and the server uses the\ndefault: `CALENDAR_MCP_DEFAULT_ACCOUNT` if set, otherwise the account named\n`default`, otherwise the only account you have signed in. `list_accounts`\nreturns the same list the CLI prints, so the model can discover the names\nitself.\n\nAccount names must match `[A-Za-z0-9][A-Za-z0-9_-]{0,63}`.\n\n**Where things live.** Tokens and preferences are stored in the OS user config\ndirectory — `%LOCALAPPDATA%\\calendar-mcp` on Windows, `~/.config/calendar-mcp`\non Linux, `~/Library/Application Support/calendar-mcp` on macOS — with one\ntoken file per account under `accounts/`. Override the whole directory with\n`CALENDAR_MCP_CONFIG_DIR`.\n\n**Back-compat.** `TOKEN_FILE_PATH` still works and now means *the default\naccount's token*. If it is set, or if a `.gcp-saved-tokens.json` exists in the\nworking directory, that file is used for the `default` account and nothing\nmoves. Named accounts always live in the config directory.\n\n## Preferences\n\nThe server remembers how you like your week to be shaped, so the scheduling\ntools do not have to guess. Read them with `get_preferences` and change them\nwith `set_preferences` (\"I start at 8 and I want 15 minutes between meetings\").\nPreferences are global — they describe you, not one account — and are stored as\n`preferences.json` in the config directory.\n\n| Field | Default | Meaning |\n| --- | --- | --- |\n| `timezone` | unset | IANA zone the working hours are expressed in, e.g. `Europe/Berlin`. |\n| `working_hours` | Mon–Fri 09:00–17:00 | Per weekday (`mon`…`sun`), a list of `[\"HH:MM\", \"HH:MM\"]` spans. |\n| `buffer_minutes` | `0` | Gap to leave either side of a meeting when proposing times. |\n| `min_focus_block_minutes` | `60` | Shortest free stretch that still counts as usable focus time. |\n| `lunch` | unset | A daily break carved out of the working hours. |\n| `focus_calendar_id` | `primary` | Calendar that focus blocks are booked on. |\n\n```json\n{\n  \"timezone\": \"Europe/Berlin\",\n  \"working_hours\": {\n    \"mon\": [[\"09:00\", \"12:00\"], [\"13:00\", \"18:00\"]],\n    \"tue\": [[\"09:00\", \"17:00\"]],\n    \"wed\": [[\"09:00\", \"17:00\"]],\n    \"thu\": [[\"09:00\", \"17:00\"]],\n    \"fri\": [[\"09:00\", \"15:00\"]],\n    \"sat\": [],\n    \"sun\": []\n  },\n  \"buffer_minutes\": 15,\n  \"min_focus_block_minutes\": 90,\n  \"lunch\": [\"12:30\", \"13:15\"],\n  \"focus_calendar_id\": \"primary\"\n}\n```\n\n`set_preferences` merges: only the arguments you pass change, and the merged\nresult is validated before it is written, so a rejected change leaves the saved\nfile untouched. The one exception is `working_hours`, which is a **whole-schedule\nreplacement** — weekdays you leave out of the dict become non-working days. Pass\n`clear_lunch: true` to remove a lunch break.\n\n## Configuration\n\n| Variable | Default | Purpose |\n| --- | --- | --- |\n| `GOOGLE_CLIENT_ID` | — | OAuth client ID (required). |\n| `GOOGLE_CLIENT_SECRET` | — | OAuth client secret (required). |\n| `TOKEN_FILE_PATH` | `.gcp-saved-tokens.json` | Where the **default** account's OAuth token is cached. |\n| `CALENDAR_MCP_CONFIG_DIR` | OS user config dir | Directory holding per-account tokens (`accounts/`) and `preferences.json`. |\n| `CALENDAR_MCP_DEFAULT_ACCOUNT` | `default` | Account used when a tool's `account` argument is omitted. |\n| `CALENDAR_SCOPES` | `https://www.googleapis.com/auth/calendar` | Scope requested. Use `.../auth/calendar.readonly` for read-only. |\n| `OAUTH_CALLBACK_PORT` | `8080` | Local port for the OAuth callback during `calendar-mcp auth`. |\n| `CALENDAR_MCP_ALLOW_BROWSER_AUTH` | unset | Set to `1` to let the server itself open a browser when no token exists. Off by default so a stdio server never hangs. |\n| `CALENDAR_MCP_LOG_FILE` | unset | Mirror the stderr log to this file. |\n| `CALENDAR_MCP_LOG_LEVEL` | `INFO` | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL`. Overrides `--log-level`. |\n| `HOST` | `127.0.0.1` | Default for `--host` in HTTP mode. |\n| `PORT` | `8000` | Default for `--port` in HTTP mode. |\n\nA `.env` file in the working directory is loaded on startup. Logs never go to stdout — stdout is the MCP protocol channel in stdio mode.\n\n### Commands\n\n```\ncalendar-mcp [--transport {stdio,http}] [--host H] [--port P] [--path /mcp] [--log-level L]\ncalendar-mcp serve ...     # explicit form of the default\ncalendar-mcp auth [--account NAME] [--no-browser]\ncalendar-mcp accounts      # list known accounts; exit 1 if none is signed in\ncalendar-mcp check [--account NAME]   # token status + calendar list; exit 1 if no valid token\ncalendar-mcp --version\n```\n\n`python -m calendar_mcp` accepts the same arguments.\n\n## Google Cloud setup\n\n1. Create or select a project and **enable the Google Calendar API**.\n2. **APIs & Services → Credentials → Create credentials → OAuth client ID → Application type: Desktop app.** Copy the client ID and secret. There is no JSON download to keep.\n3. A Desktop app client has **no \"Authorized redirect URIs\" field** — Google permits `http://localhost` on any port for this client type, which is what the local callback on `OAUTH_CALLBACK_PORT` uses. Nothing to configure there.\n4. On the **OAuth consent screen**: User Type *External*, fill in the app name and contact emails, add the `https://www.googleapis.com/auth/calendar` scope, and **add your own Google account as a test user**. Without that last step the sign-in is rejected.\n\n## Development\n\n```bash\ngit clone https://github.com/deciduus/calendar-mcp\ncd calendar-mcp\nuv venv\nuv pip install -e \".[dev]\"\npytest\n```\n\nLayout: `calendar_mcp/server.py` (the `MCPServer`, shared helpers and the credential provider), `tools/` (one module per tool area — the tool functions themselves), `calendar_actions.py` (Google API calls), `analysis.py`, `timeutil.py` (pure interval maths), `accounts.py` (multi-account token paths), `preferences.py` (the saved schedule), `models.py` (pydantic input/output models), `auth.py` (OAuth), `cli.py` (the `calendar-mcp` command). `scripts/smoke_stdio.py` spawns a real stdio server and checks the handshake and tool list.\n\n## Upgrading from 1.0\n\nNothing breaks. All 15 original tools keep their names and their existing\nparameters; each simply gained an optional trailing `account`. Your existing\n`TOKEN_FILE_PATH` keeps working, and now names the default account's token.\nWhat is new: multiple accounts, saved scheduling preferences, and eight new\ntools (`find_focus_time`, `block_focus_time`, `detect_conflicts`,\n`suggest_reschedule`, `time_audit`, `list_accounts`, `get_preferences`,\n`set_preferences`).\n\n## Upgrading from 0.x\n\n- **Package and command renamed.** The distribution is now `calendar-mcp-server` and installs `calendar-mcp` (and an identical `calendar-mcp-server` alias). Point your client at `uvx calendar-mcp-server` instead of `python /path/to/run_server.py`.\n- **`run_server.py` still works** — it is now a thin shim over the CLI — but it is deprecated and will be removed in a future release.\n- **The FastAPI/uvicorn HTTP API is gone.** There are no REST endpoints, no `/health`, and no separate stdio bridge process; the server is one process on the MCP SDK. If you want HTTP, it is now MCP streamable HTTP at `/mcp`.\n- **Authentication no longer happens implicitly.** Run `calendar-mcp auth` once; the server will not open a browser unless `CALENDAR_MCP_ALLOW_BROWSER_AUTH=1`.\n- **Tool names are unchanged**, so existing prompts keep working. Results are now structured output rather than JSON stuffed into text.\n- **Three new tools:** `move_event`, `respond_to_event`, and `project_recurring_events` (the last previously existed only as internal logic).\n- Removed env vars: `RELOAD`, `MCP_API_HOST`. `HOST`/`PORT` now apply to the MCP HTTP transport.\n\n## License\n\nThis project is dual-licensed to support both open-source collaboration and sustainable development:\n\n1.  **GNU Affero General Public License v3.0 (AGPL-3.0):**\n    *   This software is free to use, modify, and distribute under the terms of the AGPLv3 license. \n    *   Key conditions include that derivative works (including modifications used over a network) must also be licensed under AGPLv3 and their source code made available.\n    *   This license is suitable for open-source projects or internal use where AGPLv3 compliance is feasible.\n    *   See the [LICENSE](LICENSE) file for the full text.\n\n2.  **Commercial License:**\n    *   If the terms of the AGPLv3 are not suitable for your specific use case (e.g., integrating this software into a proprietary, closed-source commercial product or service without complying with AGPLv3's source-sharing requirements), a separate commercial license is available.\n    *   Please contact **deciduusleaf@gmail.com** for inquiries regarding commercial licensing options.\n\nBy using, modifying, or distributing this software, you agree to be bound by the terms of either the AGPLv3 or a separately negotiated commercial license.\n\n<!-- MCP Registry ownership marker; do not remove -->\nmcp-name: io.github.deciduus/calendar-mcp\n",
  "bytes": 18564,
  "sha": "e097dab43ccd85f612af34e3e6df273113da1c232b0430aaa299b21e0c7b64e1",
  "repo_slug": "deciduus/calendar-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_deciduus_calendar_mcp_766b5aac/readme"
}