# Collab Integration — Google Calendar (deployment curls)

Google Calendar tools — same contract as Google Drive / HubSpot collab.

**Postman:** Collab Integration — Google Calendar  
Workspace collection UID: `51852693-5ced4891-695b-4039-918a-c14c852e91c8`

---

## Contract

| | |
|---|---|
| Method / path | `POST /internal/v1/tools/{tool_name}` |
| Local | `http://localhost:6010` |
| Prod | `https://adhoc-mcp.lucos.com` |
| Headers | `Content-Type: application/json`, `X-Lucos-Org-Id` (required). Optional: `X-Lucos-User-Email` (requester). Prod may also need `Authorization: Bearer <COLLAB_S2S_TOKEN>` |
| Body | `{ "environment": "staging" \| "prod", "args": { ... } }` |

Pagination: pass `"after": "<next_after from previous response>"` in `args`.  
Limit default **25** (events) / **100** (calendars), max **250**.

Whose calendar: `args.user` / `as_user` / `for_user` (name or email). Default = requester / admin impersonation.

---

## Access & privacy (important)

| Behavior | Default |
|----------|---------|
| Auth | Service account + Domain-Wide Delegation as the subject user |
| Calendar discovery | `calendarList.list` — only calendars the subject can see |
| Private events | Title/description/location/attendees/attachments **redacted** unless subject is organizer, creator, or attendee (`private_redacted: true`) |
| Free/busy | Busy windows only — no event titles |
| Writes | **Propose → approval_token → confirm** (gated by `GCAL_WRITES_ENABLED`) |

Access boundaries follow Google Calendar ACLs for the impersonated user. Do not expect to read another person’s private event details without being an invitee/organizer.

### Whose calendar is written?

Writes are **not** limited to the ChatGPT / Lucos logged-in user.

| Arg | Meaning |
|-----|---------|
| `user` / `as_user` | **Organizer** — event is created/updated on this person’s calendar (SA impersonates them) |
| `attendees` | Invitees (e.g. schedule for X with Y → `user=X`, `attendees=[{email:Y}]`) |
| Omit `user` / `me` | Requester from `X-Lucos-User-Email`, else admin / “me” fallback |

Confirm reuses `subject_user` stored in the approval token. Allowed domains: `DRIVE_IMPERSONATE_DOMAINS` (e.g. `admedia.com`).

**Confirm needs write scope:** Admin DWD + `GOOGLE_DRIVE_SA_SCOPES` must use `https://www.googleapis.com/auth/calendar` (not only `calendar.readonly`). Propose/cancel-token work with writes enabled even before that; `gcal_confirm_write` fails until the write scope is live.

---

## Server env (do not put secrets in curls)

```bash
GOOGLE_SERVICE_ACCOUNT_FILE=./secrets/drive-sa.json
# Or GOOGLE_SERVICE_ACCOUNT_JSON=...

# Must match Admin Console DWD scopes. Reads:
GOOGLE_DRIVE_SA_SCOPES=https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/calendar.readonly

# For writes, replace calendar.readonly with:
# https://www.googleapis.com/auth/calendar
# (and update Admin DWD to match)

# For room/resource listing (gcal_list_resources), also add:
# https://www.googleapis.com/auth/admin.directory.resource.calendar.readonly

DRIVE_INDEX_ADMIN_EMAIL=admin@admedia.com
DRIVE_IMPERSONATE_DOMAINS=admedia.com

# Approval-gated calendar writes (off by default)
# GCAL_WRITES_ENABLED=false
# GCAL_WRITE_APPROVAL_TTL_SEC=600
# Pending propose→confirm tokens are in-memory (TTL); no SQLite.
```

---

## Tools to enable (MCP / gateway)

**Read**

- `gcal_list_calendars`
- `gcal_list_events`
- `gcal_get_event`
- `gcal_list_event_instances`
- `gcal_get_freebusy` (also `users[]` for multi-person free/busy)
- `gcal_list_resources` (rooms — needs Admin Directory resource scope)

**Write (approval-gated)**

- `gcal_propose_create_event` (`add_meet`, `room` / `room_email`)
- `gcal_propose_update_event` (reschedule + optional `add_meet`)
- `gcal_propose_cancel_event`
- `gcal_propose_rsvp` (`response`: accepted|declined|tentative)
- `gcal_confirm_write`
- `gcal_cancel_write`

---

## Setup (shell)

```bash
BASE=http://localhost:6010   # or https://adhoc-mcp.lucos.com
ORG=00000000-0000-0000-0000-000000000001
# Optional requester identity:
# USER_EMAIL=you@admedia.com
# Optional on prod:
# export COLLAB_S2S_TOKEN=...
# AUTH_HDR=(-H "Authorization: Bearer $COLLAB_S2S_TOKEN")
```

Add `-H "X-Lucos-User-Email: $USER_EMAIL"` when testing as a specific requester.  
Add `"${AUTH_HDR[@]}"` on prod if Bearer is required.

---

## 1. Health

```bash
curl -sS "$BASE/healthz"
```

---

## 2. gcal_list_calendars

Discover calendars the subject can access (with `access_role`).

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_calendars" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","limit":50}}'
```

Optional: `"min_access_role":"reader"` | `freeBusyReader` | `writer` | `owner`.  
Page with `"after":"<next_after>"`.

---

## 3. gcal_list_events

Default window: now → +7 days. Expands recurring instances (`single_events: true`).

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_events" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","limit":25}}'
```

### With time range + title query

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_events" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"alice@admedia.com","calendar_id":"primary","time_min":"2026-08-26T00:00:00+05:30","time_max":"2026-09-02T00:00:00+05:30","query":"standup","limit":50}}'
```

### Meetings involving two people

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_events" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"alice@admedia.com","also_user":"bob@admedia.com","limit":25}}'
```

### Recurring masters (do not expand)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_events" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","single_events":false,"order_by":"updated","limit":25}}'
```

### Include cancelled + paginate

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_events" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","include_cancelled":true,"after":"PAGE_TOKEN","limit":25}}'
```

---

## 4. gcal_get_event

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_get_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","calendar_id":"primary","event_id":"EVENT_ID"}}'
```

Response includes organizer, attendees, attachments, recurrence, and `is_private` / `private_redacted`.

---

## 5. gcal_list_event_instances

Instances of a recurring series.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_event_instances" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","event_id":"RECURRING_EVENT_ID","time_min":"2026-08-01T00:00:00Z","time_max":"2026-10-01T00:00:00Z","limit":50}}'
```

---

## 6. gcal_get_freebusy

Busy windows only (no titles). Respects freeBusy access.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_get_freebusy" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","calendar_id":"primary","time_min":"2026-08-26T00:00:00+05:30","time_max":"2026-08-27T00:00:00+05:30"}}'
```

### Multiple calendars

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_get_freebusy" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","calendar_ids":["primary","team@admedia.com"],"time_min":"2026-08-26T00:00:00+05:30","time_max":"2026-08-27T00:00:00+05:30"}}'
```

### Multi-person (org-style)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_get_freebusy" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","users":["alice@admedia.com","bob@admedia.com"],"time_min":"2026-08-26T09:00:00+05:30","time_max":"2026-08-26T18:00:00+05:30"}}'
```

---

## 6b. gcal_list_resources (rooms)

Requires DWD scope `admin.directory.resource.calendar.readonly`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_list_resources" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"query":"conf","limit":50}}'
```

---

## 7. Approval-gated writes

Server env (required):

```bash
GCAL_WRITES_ENABLED=true
# GCAL_WRITE_APPROVAL_TTL_SEC=600
```

SA scopes + Admin DWD must include `https://www.googleapis.com/auth/calendar` (not just `.readonly`).  
No Slack — propose → token → confirm.

### 7a. Propose create (does not create yet)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_create_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","title":"Collab API sync","start":"2026-08-27T15:00:00+05:30","end":"2026-08-27T15:30:00+05:30","description":"Test from collab API","attendees":[{"email":"bob@admedia.com"}],"visibility":"default"}}'
```

### With Google Meet + room

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_create_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","title":"Sync + Meet","start":"2026-08-28T11:00:00+05:30","end":"2026-08-28T11:30:00+05:30","add_meet":true,"room":"RESOURCE_EMAIL_FROM_LIST_RESOURCES","attendees":[{"email":"bob@admedia.com"}]}}'
```

All-day:

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_create_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","title":"OOO","all_day_start":"2026-09-01","all_day_end":"2026-09-02"}}'
```

Recurring (RRULE):

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_create_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","title":"Weekly standup","start":"2026-08-27T10:00:00+05:30","end":"2026-08-27T10:15:00+05:30","recurrence":["RRULE:FREQ=WEEKLY;BYDAY=WE"]}}'
```

Response: `approval_token`, `expires_at`, `preview`.

### 7b. Propose update

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_update_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","event_id":"EVENT_ID","title":"Updated title","location":"Meet","send_updates":"all"}}'
```

### 7c. Propose cancel (delete)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_cancel_event" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","event_id":"EVENT_ID","send_updates":"all"}}'
```

### 7c2. Propose RSVP

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_propose_rsvp" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"user":"me","event_id":"EVENT_ID","response":"accepted","send_updates":"all"}}'
```

`response`: `accepted` | `declined` | `tentative` | `needsAction`.

### 7d. Confirm

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_confirm_write" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"approval_token":"gcal_write_..."}}'
```

### 7e. Cancel pending token

```bash
curl -sS -X POST "$BASE/internal/v1/tools/gcal_cancel_write" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"approval_token":"gcal_write_..."}}'
```

---

## Response shape (list events)

```json
{
  "ok": true,
  "tool": "gcal_list_events",
  "environment": "staging",
  "subject_user": "alice@admedia.com",
  "impersonated_user": "alice@admedia.com",
  "calendar_id": "primary",
  "time_min": "...",
  "time_max": "...",
  "result_count": 2,
  "has_more": false,
  "next_after": null,
  "rows": [
    {
      "event_id": "...",
      "title": "Standup",
      "status": "confirmed",
      "visibility": "default",
      "start": "2026-08-26T10:00:00+05:30",
      "end": "2026-08-26T10:15:00+05:30",
      "hangout_link": "https://meet.google.com/abc-defg-hij",
      "conference_uri": "https://meet.google.com/abc-defg-hij",
      "meet_code": "abc-defg-hij",
      "organizer": { "email": "alice@admedia.com", "self": true },
      "attendees": [{ "email": "bob@admedia.com", "response": "accepted" }],
      "attachments": [],
      "recurrence": [],
      "recurring_event_id": null,
      "is_private": false,
      "private_redacted": false,
      "html_link": "https://www.google.com/calendar/event?eid=..."
    }
  ]
}
```

Private events the subject cannot detail-view look like:

```json
{
  "title": "(private event)",
  "is_private": true,
  "private_redacted": true,
  "attendees": [],
  "attachments": [],
  "description": null,
  "location": null
}
```

---

## Notes for ChatGPT / callers

- Fireflies correlation (`fireflies_correlate_meeting`) matches Calendar by **event id**, then **Meet code**, then full Meet URL, then fuzzy title+time, then Drive by title.
- Event rows now include `meet_code` / `conference_uri` for that correlation.
- Always resolve `user` (name or email) when asking about someone else’s calendar.
- Default list window is **now → +7 days**; pass `time_min` / `time_max` for other ranges.
- Use `gcal_list_calendars` first when calendar_id is unknown.
- Use `gcal_get_freebusy` for availability — not event titles.
- Use `gcal_list_event_instances` for a known recurring master `event_id`.
- Private details are redacted when access does not allow them — do not invent titles.
- Empty list + access OK ≠ missing scopes. Prefer “no events matched” over inventing a permissions failure.
- Schedule X with Y: `user=X` (organizer) + `attendees=[{email:Y}]` → propose → confirm.
- Writes are **propose → confirm** only; nothing hits Google Calendar until `gcal_confirm_write`.
- Writes require `GCAL_WRITES_ENABLED=true` + Calendar **write** scope on SA/DWD for confirm.
