# How to sync Outlook ↔ Gmail calendars using AI + MCP (Amazon Quick)

> Source: <https://gist.github.com/aartraju/cedca245d76894ebe44ba2322ab15682>
> Published: 2026-05-23 05:15:07+00:00

# How I Synced My Work Calendar (Outlook) with My Personal Calendar (Gmail) Using AI + MCP

## The Problem

Two calendars. Work life in Outlook, personal life in Gmail. Neither knows the other exists.

**Real scenarios that kept happening:**
- I'd block "Business Travel" on my work calendar (3 days in NYC) but my husband had no idea I'd be gone until I told him
- I'd schedule a doctor appointment for my kiddo on Gmail, then forget to block my work calendar. colleagues would book meetings over it
- Weekend-only family trips didn't need a work block, but a Friday-through-Sunday trip did. I had to think about this manually every time

**What I wanted:**
1. When I add work travel to Outlook → automatically show it on our family Gmail calendar (and notify my husband)
2. When I add a doctor appointment on Gmail → automatically block my work calendar as Out of Office
3. Smart rules: don't sync routine daily blocks, skip weekend-only trips, only sync what matters

## What I Built

A daily sync agent that runs at 9 AM and keeps both calendars in sync, using:
- **Amazon Quick** (AI assistant with calendar access)
- **A lightweight MCP server** (Python script on my Mac that bridges to Google Calendar via OAuth)
- **A scheduled agent** with custom sync rules

## Architecture

```
Outlook (Work Calendar)
    ↕  Amazon Quick reads/writes via built-in Outlook connector
Amazon Quick (AI Agent)
    ↕  Calls MCP tools over stdio
Google Calendar MCP Server (Python on my Mac)
    ↕  Google Calendar REST API (OAuth 2.0)
Gmail Calendar (Personal)
```

## Prerequisites

- Amazon Quick with Outlook calendar connected
- A Google account with Google Calendar
- Python 3.10+ on your machine
- `pip install google-auth google-auth-oauthlib google-api-python-client`

## Step 1: Create a Google Cloud Project + OAuth Credentials

1. Go to [console.cloud.google.com](https://console.cloud.google.com/)
2. Create a new project (e.g., "Calendar Sync")
3. Enable the **Google Calendar API** (APIs & Services → Library → search "Google Calendar API" → Enable)
4. Configure the OAuth consent screen:
   - Select **External**
   - App name: "Calendar Sync"
   - Add your email as a test user
5. Create OAuth credentials:
   - APIs & Services → Credentials → Create Credentials → OAuth client ID
   - Application type: **Desktop app**
   - Download the JSON file
6. Save it to `~/.config/caldav/client_secret.json`:

```bash
mkdir -p ~/.config/caldav
mv ~/Downloads/client_secret_*.json ~/.config/caldav/client_secret.json
```

**Cost:** Free. Google Calendar API has a massive free tier (1M queries/day). Personal sync uses ~10 calls/day.

## Step 2: Set Up the MCP Server

Save the following Python script to `~/mcp-servers/gcal_mcp_server.py`:

```python
#!/usr/bin/env python3
"""
Google Calendar MCP Server (OAuth 2.0)
Bridges AI assistants to Gmail Calendar via Google Calendar REST API.
"""

import json
import sys
from datetime import datetime, timedelta
from pathlib import Path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

CONFIG_DIR = Path.home() / ".config" / "caldav"
CLIENT_SECRET_PATH = CONFIG_DIR / "client_secret.json"
TOKEN_PATH = CONFIG_DIR / "token.json"
SCOPES = ["https://www.googleapis.com/auth/calendar"]


def get_calendar_service():
    creds = None
    if TOKEN_PATH.exists():
        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(str(CLIENT_SECRET_PATH), SCOPES)
            creds = flow.run_local_server(port=0)
        with open(TOKEN_PATH, "w") as f:
            f.write(creds.to_json())
    return build("calendar", "v3", credentials=creds)


def list_events(start_date, end_date):
    service = get_calendar_service()
    events = service.events().list(
        calendarId="primary",
        timeMin=f"{start_date}T00:00:00Z",
        timeMax=f"{end_date}T23:59:59Z",
        singleEvents=True,
        orderBy="startTime",
    ).execute().get("items", [])
    return [{"summary": e.get("summary", ""), "start": e["start"].get("dateTime", e["start"].get("date")),
             "end": e["end"].get("dateTime", e["end"].get("date")), "id": e.get("id"),
             "all_day": "date" in e["start"]} for e in events]


def create_event(summary, start_date, end_date, description="", all_day=True, attendees=None):
    service = get_calendar_service()
    body = {"summary": summary, "description": description, "status": "confirmed"}
    if all_day:
        body["start"] = {"date": start_date}
        body["end"] = {"date": end_date}
    else:
        body["start"] = {"dateTime": f"{start_date}T09:00:00", "timeZone": "America/Los_Angeles"}
        body["end"] = {"dateTime": f"{end_date}T17:00:00", "timeZone": "America/Los_Angeles"}
    if attendees:
        body["attendees"] = [{"email": email} for email in attendees]
    event = service.events().insert(calendarId="primary", body=body).execute()
    return {"success": True, "id": event.get("id"), "link": event.get("htmlLink")}


def delete_event(event_id):
    service = get_calendar_service()
    service.events().delete(calendarId="primary", eventId=event_id).execute()
    return {"success": True}


# --- MCP JSON-RPC Server (reads from stdin, writes to stdout) ---

TOOLS = {
    "caldav_list_events": {"description": "List Gmail calendar events in a date range.",
        "parameters": {"type": "object", "properties": {
            "start_date": {"type": "string"}, "end_date": {"type": "string"}}, "required": ["start_date", "end_date"]}},
    "caldav_create_event": {"description": "Create event on Gmail calendar. end_date is EXCLUSIVE for all-day events.",
        "parameters": {"type": "object", "properties": {
            "summary": {"type": "string"}, "start_date": {"type": "string"}, "end_date": {"type": "string"},
            "description": {"type": "string", "default": ""}, "all_day": {"type": "boolean", "default": True},
            "attendees": {"type": "array", "items": {"type": "string"}, "default": None}},
            "required": ["summary", "start_date", "end_date"]}},
    "caldav_delete_event": {"description": "Delete event from Gmail calendar by ID.",
        "parameters": {"type": "object", "properties": {"event_id": {"type": "string"}}, "required": ["event_id"]}},
}


def handle_request(request):
    method, params, req_id = request.get("method"), request.get("params", {}), request.get("id")
    if method == "initialize":
        return {"jsonrpc": "2.0", "id": req_id, "result": {"protocolVersion": "2024-11-05",
            "capabilities": {"tools": {}}, "serverInfo": {"name": "gcal-mcp", "version": "2.0.0"}}}
    elif method == "notifications/initialized":
        return None
    elif method == "tools/list":
        return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": [
            {"name": n, "description": s["description"], "inputSchema": s["parameters"]} for n, s in TOOLS.items()]}}
    elif method == "tools/call":
        name, args = params.get("name"), params.get("arguments", {})
        try:
            result = {"caldav_list_events": list_events, "caldav_create_event": create_event,
                      "caldav_delete_event": delete_event}[name](**args)
            return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": json.dumps(result, default=str)}]}}
        except Exception as e:
            return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": json.dumps({"error": str(e)})}], "isError": True}}
    return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown: {method}"}}


if __name__ == "__main__":
    for line in sys.stdin:
        if not line.strip():
            continue
        try:
            resp = handle_request(json.loads(line))
            if resp:
                sys.stdout.write(json.dumps(resp) + "\n")
                sys.stdout.flush()
        except json.JSONDecodeError:
            continue
```

## Step 3: Authorize (One Time)

Run this once to complete the OAuth flow:

```bash
python3 -c "
from google_auth_oauthlib.flow import InstalledAppFlow
from pathlib import Path

flow = InstalledAppFlow.from_client_secrets_file(
    str(Path.home() / '.config/caldav/client_secret.json'),
    ['https://www.googleapis.com/auth/calendar']
)
creds = flow.run_local_server(port=8080)

token_path = Path.home() / '.config/caldav/token.json'
with open(token_path, 'w') as f:
    f.write(creds.to_json())
print('Token saved!')
"
```

A browser window will open. Sign in, click "Advanced" → "Go to Calendar Sync (unsafe)" → "Continue". Token is saved. You won't need to do this again.

## Step 4: Register in Amazon Quick

Settings → Capabilities → MCP Servers → Add:
- **Name:** `calendar_integ` (or any name)
- **Command:** `python3`
- **Args:** `/Users/yourname/mcp-servers/gcal_mcp_server.py`

Toggle on. Should show "Connected."

## Step 5: Create the Scheduled Sync Agent

In Amazon Quick, create a scheduled agent with:
- **Schedule:** Daily at 9:00 AM
- **Tool access:** Outlook (read + create/delete with approval), Gmail MCP (full read/write)

### My Sync Rules

**Outlook → Gmail (work travel → family calendar):**
| Trigger | Action |
|---------|--------|
| Title contains "Aarthi: Business Travel" | Sync + invite husband |
| Title contains "Aarthi: Team event" | Sync + invite husband |
| Category = "Sync to Gmail" | Sync + invite husband |
| Event spans 2+ days | Sync + invite husband |

**Gmail → Outlook (personal → work calendar):**
| Trigger | Action |
|---------|--------|
| Title contains "Kiddo doc" or "Kiddo doc appt" | Block as OOF |
| Event spans 2+ days (with a weekday) | Block as OOF |

**Never sync:**
- Recurring daily blocks (drop-off, pickup, commute). these live natively on both calendars
- Weekend-only events (Sat-Sun trips)
- Other people's OOO notifications
- Workouts, birthdays, routine items

**Deletion sync:** If a source event is deleted, the synced copy is removed too.

## Gotchas & Lessons Learned

1. **Google CalDAV requires OAuth 2.0.** App passwords don't work for CalDAV/Calendar API. Don't waste time trying.
2. **Timezone matters.** Always include `-07:00` (or your offset) when creating timed events on Outlook. Bare datetimes get interpreted as UTC.
3. **Recurring events don't belong in sync.** If an event repeats daily/weekly, create it natively on both calendars. Syncing individual occurrences creates approval fatigue.
4. **Deduplication is essential.** Without it, the agent creates duplicate events on every run.
5. **Outlook's create/delete tools require user approval.** You can't fully automate writes to Outlook. but it's just one tap per event.
6. **end_date is EXCLUSIVE in Google Calendar.** A trip ending June 20 needs `end_date = 2026-06-21`.

## What It Looks Like in Practice

- I add "Aarthi: Business Travel — NYC Summit" to Outlook → my husband gets a Google Calendar invite automatically
- I add "Kiddo doc visit" to Gmail → my Outlook shows OOF for that time
- I delete the doctor appointment from Gmail → the OOF block disappears from Outlook
- Weekend family trips don't clutter my work calendar
- I do nothing except add/remove events where I normally would. the sync just happens.

## License

MIT. do whatever you want with this.

