feat(google-calendar): wire freebusy, align tools with API v3, add calendar + sharing tools (#5084)

* feat(google-calendar): wire freebusy, align tools with API v3, add calendar + sharing tools

* fix(google-calendar): address review — trust offset timezones, make list_acl showDeleted usable, harden unshare error parse, clarify update attendees

* fix(google-calendar): wire list q/pageToken into block, harden invite PUT error parse

* fix(google-calendar): make list orderBy user-selectable, clarify update timeZone applies to start/end

* fix(google-calendar): require timeZone for recurring timed events, clarify recurrence replace semantics

* fix(google-calendar): validate grantee before building share ACL body

Throw a clear error when scopeType is user/group/domain but scopeValue is
missing or blank, instead of POSTing a scope-type-only body that the Calendar
ACL API rejects with an opaque error.
This commit is contained in:
Waleed
2026-06-15 20:32:58 -07:00
committed by GitHub
parent 06f1e72d6a
commit b7d30c89f9
18 changed files with 1577 additions and 439 deletions
@@ -48,10 +48,12 @@ Create a new event in Google Calendar. Returns API-aligned fields only.
| `summary` | string | Yes | Event title/summary |
| `description` | string | No | Event description |
| `location` | string | No | Event location |
| `startDateTime` | string | Yes | Start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter |
| `endDateTime` | string | Yes | End date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter |
| `startDateTime` | string | Yes | Start time. Use a datetime with timezone offset \(2025-06-03T10:00:00-08:00\) or a date \(2025-06-03\) for an all-day event |
| `endDateTime` | string | Yes | End time. Use a datetime with timezone offset \(2025-06-03T11:00:00-08:00\) or a date \(2025-06-04\) for an all-day event |
| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided. |
| `attendees` | array | No | Array of attendee email addresses |
| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. |
| `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
#### Output
@@ -60,10 +62,12 @@ Create a new event in Google Calendar. Returns API-aligned fields only.
| --------- | ---- | ----------- |
| `id` | string | Event ID |
| `htmlLink` | string | Event link |
| `hangoutLink` | string | Google Meet link |
| `status` | string | Event status |
| `summary` | string | Event title |
| `description` | string | Event description |
| `location` | string | Event location |
| `recurrence` | json | Recurrence rules |
| `start` | json | Event start |
| `end` | json | Event end |
| `attendees` | json | Event attendees |
@@ -81,7 +85,10 @@ List events from Google Calendar. Returns API-aligned fields only.
| `calendarId` | string | No | Google Calendar ID \(e.g., primary or calendar@group.calendar.google.com\) |
| `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
| `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
| `orderBy` | string | No | Order of events returned \(startTime or updated\) |
| `q` | string | No | Free-text search across event summary, description, location, attendees, and organizer |
| `maxResults` | number | No | Maximum number of events to return \(max 2500\) |
| `pageToken` | string | No | Token for retrieving the next page of results |
| `orderBy` | string | No | Order of events returned \(startTime or updated\). Defaults to startTime. |
| `showDeleted` | boolean | No | Include deleted events |
#### Output
@@ -132,10 +139,12 @@ Update an existing event in Google Calendar. Returns API-aligned fields only.
| `summary` | string | No | New event title/summary |
| `description` | string | No | New event description |
| `location` | string | No | New event location |
| `startDateTime` | string | No | New start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter |
| `endDateTime` | string | No | New end date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter |
| `startDateTime` | string | No | New start time. Use a datetime with timezone offset \(2025-06-03T10:00:00-08:00\) or a date \(2025-06-03\) for an all-day event |
| `endDateTime` | string | No | New end time. Use a datetime with timezone offset \(2025-06-03T11:00:00-08:00\) or a date \(2025-06-04\) for an all-day event |
| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. |
| `attendees` | array | No | Array of attendee email addresses \(replaces existing attendees\) |
| `attendees` | array | No | Array of attendee email addresses \(replaces the existing attendee list\) |
| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. |
| `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
#### Output
@@ -144,10 +153,12 @@ Update an existing event in Google Calendar. Returns API-aligned fields only.
| --------- | ---- | ----------- |
| `id` | string | Event ID |
| `htmlLink` | string | Event link |
| `hangoutLink` | string | Google Meet link |
| `status` | string | Event status |
| `summary` | string | Event title |
| `description` | string | Event description |
| `location` | string | Event location |
| `recurrence` | json | Recurrence rules |
| `start` | json | Event start |
| `end` | json | Event end |
| `attendees` | json | Event attendees |
@@ -298,7 +309,7 @@ Invite attendees to an existing Google Calendar event. Returns API-aligned field
| `calendarId` | string | No | Google Calendar ID \(e.g., primary or calendar@group.calendar.google.com\) |
| `eventId` | string | Yes | Google Calendar event ID to invite attendees to |
| `attendees` | array | Yes | Array of attendee email addresses to invite |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none \(defaults to all\) |
| `replaceExisting` | boolean | No | Whether to replace existing attendees or add to them \(defaults to false\) |
#### Output
@@ -317,6 +328,113 @@ Invite attendees to an existing Google Calendar event. Returns API-aligned field
| `creator` | json | Event creator |
| `organizer` | json | Event organizer |
### `google_calendar_freebusy`
Query free/busy information for one or more Google Calendars. Returns API-aligned fields only.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `calendarIds` | string | Yes | Comma-separated calendar IDs to query \(e.g., "primary,other@example.com"\) |
| `timeMin` | string | Yes | Start of the time range \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
| `timeMax` | string | Yes | End of the time range \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
| `timeZone` | string | No | IANA time zone \(e.g., "UTC", "America/New_York"\). Defaults to UTC. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `timeMin` | string | Start of the queried time range |
| `timeMax` | string | End of the queried time range |
| `calendars` | json | Per-calendar free/busy data with busy periods and any errors |
### `google_calendar_create_calendar`
Create a new secondary calendar. Returns API-aligned fields only.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `summary` | string | Yes | Title of the new calendar |
| `description` | string | No | Description of the new calendar |
| `location` | string | No | Geographic location of the calendar as free-form text |
| `timeZone` | string | No | Time zone of the calendar as an IANA name \(e.g., America/Los_Angeles\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Calendar ID |
| `summary` | string | Calendar title |
| `description` | string | Calendar description |
| `location` | string | Calendar location |
| `timeZone` | string | Calendar time zone |
### `google_calendar_share_calendar`
Grant a user, group, or domain access to a calendar. Returns API-aligned fields only.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `calendarId` | string | No | Calendar ID to share \(e.g., primary or calendar@group.calendar.google.com\) |
| `role` | string | Yes | Access role to grant: freeBusyReader, reader, writer, or owner |
| `scopeType` | string | Yes | Type of grantee: user, group, domain, or default \(public\) |
| `scopeValue` | string | No | Email \(user/group\), domain name \(domain\), or empty for default. Required unless scope type is default. |
| `sendNotifications` | boolean | No | Whether to send a notification email about the change. Defaults to true. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | ACL rule ID |
| `role` | string | Granted access role |
| `scope` | json | Grantee scope \(type and value\) |
### `google_calendar_list_acl`
List the access control rules (sharing) for a calendar. Returns API-aligned fields only.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `calendarId` | string | No | Calendar ID to inspect \(e.g., primary or calendar@group.calendar.google.com\) |
| `maxResults` | number | No | Maximum number of ACL rules to return |
| `pageToken` | string | No | Token for retrieving subsequent pages of results |
| `showDeleted` | boolean | No | Include deleted ACL rules \(with role "none"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `nextPageToken` | string | Next page token |
| `rules` | array | List of ACL rules |
| ↳ `id` | string | ACL rule ID |
| ↳ `role` | string | Access role |
| ↳ `scope` | json | Grantee scope \(type and value\) |
### `google_calendar_unshare_calendar`
Revoke an access control rule (sharing) from a calendar. Returns API-aligned fields only.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `calendarId` | string | No | Calendar ID to modify \(e.g., primary or calendar@group.calendar.google.com\) |
| `ruleId` | string | Yes | ACL rule ID to remove \(e.g., user:person@example.com\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ruleId` | string | Removed ACL rule ID |
| `deleted` | boolean | Whether removal was successful |
## Triggers
+286 -44
View File
@@ -35,6 +35,11 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
{ label: 'List Calendars', id: 'list_calendars' },
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
{ label: 'Invite Attendees', id: 'invite' },
{ label: 'Check Free/Busy', id: 'freebusy' },
{ label: 'Create Calendar', id: 'create_calendar' },
{ label: 'Share Calendar', id: 'share_calendar' },
{ label: 'List Sharing', id: 'list_acl' },
{ label: 'Remove Sharing', id: 'unshare_calendar' },
],
value: () => 'create',
},
@@ -59,7 +64,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
required: true,
},
...SERVICE_ACCOUNT_SUBBLOCKS,
// Calendar selector (basic mode) - not needed for list_calendars
{
id: 'calendarId',
title: 'Calendar',
@@ -71,9 +75,12 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
placeholder: 'Select calendar',
dependsOn: ['credential'],
mode: 'basic',
condition: { field: 'operation', value: 'list_calendars', not: true },
condition: {
field: 'operation',
value: ['list_calendars', 'create_calendar', 'freebusy'],
not: true,
},
},
// Manual calendar ID input (advanced mode) - not needed for list_calendars
{
id: 'manualCalendarId',
title: 'Calendar ID',
@@ -82,10 +89,13 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'list_calendars', not: true },
condition: {
field: 'operation',
value: ['list_calendars', 'create_calendar', 'freebusy'],
not: true,
},
},
// Create Event Fields
{
id: 'summary',
title: 'Event Title',
@@ -178,7 +188,47 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
condition: { field: 'operation', value: 'create' },
},
// List Events Fields
{
id: 'recurrence',
title: 'Recurrence Rule',
type: 'long-input',
placeholder: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR',
condition: { field: 'operation', value: ['create', 'update'] },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an RFC 5545 recurrence rule (RRULE) for a Google Calendar event based on the user's description.
Examples:
- "every weekday" -> RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
- "every Monday" -> RRULE:FREQ=WEEKLY;BYDAY=MO
- "monthly on the 1st" -> RRULE:FREQ=MONTHLY;BYMONTHDAY=1
- "daily for 10 occurrences" -> RRULE:FREQ=DAILY;COUNT=10
Return ONLY the RRULE string - no explanations, no extra text.`,
placeholder: 'Describe the recurrence (e.g., "every weekday", "monthly on the 1st")...',
},
},
{
id: 'addGoogleMeet',
title: 'Add Google Meet',
type: 'dropdown',
condition: { field: 'operation', value: ['create', 'update'] },
mode: 'advanced',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
},
{
id: 'timeZone',
title: 'Time Zone',
type: 'short-input',
placeholder: 'America/Los_Angeles',
condition: { field: 'operation', value: ['create', 'update'] },
mode: 'advanced',
},
{
id: 'timeMin',
title: 'Start Time Filter',
@@ -221,8 +271,37 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
generationType: 'timestamp',
},
},
{
id: 'q',
title: 'Search Query',
type: 'short-input',
placeholder: 'standup',
condition: { field: 'operation', value: 'list' },
},
{
id: 'orderBy',
title: 'Order By',
type: 'dropdown',
condition: { field: 'operation', value: 'list' },
mode: 'advanced',
options: [
{ label: 'Start time', id: 'startTime' },
{ label: 'Last updated', id: 'updated' },
],
value: () => 'startTime',
},
{
id: 'pageToken',
title: 'Page Token',
type: 'short-input',
placeholder: 'Token from a previous response (nextPageToken)',
condition: {
field: 'operation',
value: ['list', 'instances', 'list_calendars', 'list_acl'],
},
mode: 'advanced',
},
// Get Event Fields
{
id: 'eventId',
title: 'Event ID',
@@ -235,7 +314,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
required: true,
},
// Update Event Fields
{
id: 'summary',
title: 'New Event Title',
@@ -325,7 +403,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
condition: { field: 'operation', value: 'update' },
},
// Move Event Fields - Destination calendar selector (basic mode)
{
id: 'destinationCalendar',
title: 'Destination Calendar',
@@ -340,7 +417,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
required: true,
mode: 'basic',
},
// Move Event Fields - Manual destination calendar ID (advanced mode)
{
id: 'manualDestinationCalendarId',
title: 'Destination Calendar ID',
@@ -353,7 +429,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
mode: 'advanced',
},
// Instances Fields
{
id: 'timeMin',
title: 'Start Time Filter',
@@ -401,10 +476,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
title: 'Max Results',
type: 'short-input',
placeholder: '250',
condition: { field: 'operation', value: ['instances', 'list_calendars'] },
condition: { field: 'operation', value: ['list', 'instances', 'list_calendars', 'list_acl'] },
},
// List Calendars Fields
{
id: 'minAccessRole',
title: 'Minimum Access Role',
@@ -419,13 +493,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
},
// Invite Attendees Fields
{
id: 'attendees',
title: 'Attendees (comma-separated emails)',
type: 'short-input',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'invite' },
required: true,
},
{
id: 'replaceExisting',
@@ -438,7 +512,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
],
},
// Quick Add Fields
{
id: 'text',
title: 'Natural Language Event',
@@ -470,10 +543,149 @@ Return ONLY the natural language event text - no explanations.`,
type: 'short-input',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'quick_add' },
},
{
id: 'calendarIds',
title: 'Calendars (comma-separated IDs)',
type: 'short-input',
placeholder: 'primary, teammate@example.com',
condition: { field: 'operation', value: 'freebusy' },
required: true,
},
{
id: 'timeMin',
title: 'Start Time',
type: 'short-input',
placeholder: '2025-06-03T00:00:00Z',
condition: { field: 'operation', value: 'freebusy' },
required: true,
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
Examples:
- "today" -> Calculate today's date at 00:00:00Z
- "next Monday" -> Calculate next Monday at 00:00:00Z
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the start of the range (e.g., "today", "next Monday")...',
generationType: 'timestamp',
},
},
{
id: 'timeMax',
title: 'End Time',
type: 'short-input',
placeholder: '2025-06-04T00:00:00Z',
condition: { field: 'operation', value: 'freebusy' },
required: true,
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
Examples:
- "end of today" -> Calculate today's date at 23:59:59Z
- "next Friday" -> Calculate next Friday at 00:00:00Z
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the end of the range (e.g., "end of today", "next Friday")...',
generationType: 'timestamp',
},
},
{
id: 'summary',
title: 'Calendar Name',
type: 'short-input',
placeholder: 'Team Calendar',
condition: { field: 'operation', value: 'create_calendar' },
required: true,
},
{
id: 'description',
title: 'Calendar Description',
type: 'long-input',
placeholder: 'Shared team events and milestones',
condition: { field: 'operation', value: 'create_calendar' },
},
{
id: 'location',
title: 'Calendar Location',
type: 'short-input',
placeholder: 'San Francisco, CA',
condition: { field: 'operation', value: 'create_calendar' },
},
{
id: 'timeZone',
title: 'Time Zone',
type: 'short-input',
placeholder: 'America/Los_Angeles',
condition: { field: 'operation', value: ['create_calendar', 'freebusy'] },
},
{
id: 'role',
title: 'Access Role',
type: 'dropdown',
condition: { field: 'operation', value: 'share_calendar' },
required: true,
options: [
{ label: 'See free/busy only', id: 'freeBusyReader' },
{ label: 'See all event details (Reader)', id: 'reader' },
{ label: 'Make changes (Writer)', id: 'writer' },
{ label: 'Make changes & manage sharing (Owner)', id: 'owner' },
],
value: () => 'reader',
},
{
id: 'scopeType',
title: 'Grantee Type',
type: 'dropdown',
condition: { field: 'operation', value: 'share_calendar' },
required: true,
options: [
{ label: 'User', id: 'user' },
{ label: 'Group', id: 'group' },
{ label: 'Domain', id: 'domain' },
{ label: 'Public (anyone)', id: 'default' },
],
value: () => 'user',
},
{
id: 'scopeValue',
title: 'Grantee (email or domain)',
type: 'short-input',
placeholder: 'person@example.com',
condition: {
field: 'operation',
value: 'share_calendar',
and: { field: 'scopeType', value: 'default', not: true },
},
required: true,
},
{
id: 'sendNotifications',
title: 'Send Notification Email',
type: 'dropdown',
condition: { field: 'operation', value: 'share_calendar' },
mode: 'advanced',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
},
{
id: 'ruleId',
title: 'ACL Rule ID',
type: 'short-input',
placeholder: 'user:person@example.com',
condition: { field: 'operation', value: 'unshare_calendar' },
required: true,
},
// Notification setting (for create, update, delete, move, quick_add, invite)
{
id: 'sendUpdates',
title: 'Send Email Notifications',
@@ -502,6 +714,11 @@ Return ONLY the natural language event text - no explanations.`,
'google_calendar_list_calendars',
'google_calendar_quick_add',
'google_calendar_invite',
'google_calendar_freebusy',
'google_calendar_create_calendar',
'google_calendar_share_calendar',
'google_calendar_list_acl',
'google_calendar_unshare_calendar',
],
config: {
tool: (params) => {
@@ -526,6 +743,16 @@ Return ONLY the natural language event text - no explanations.`,
return 'google_calendar_quick_add'
case 'invite':
return 'google_calendar_invite'
case 'freebusy':
return 'google_calendar_freebusy'
case 'create_calendar':
return 'google_calendar_create_calendar'
case 'share_calendar':
return 'google_calendar_share_calendar'
case 'list_acl':
return 'google_calendar_list_acl'
case 'unshare_calendar':
return 'google_calendar_unshare_calendar'
default:
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
}
@@ -541,10 +768,8 @@ Return ONLY the natural language event text - no explanations.`,
...rest
} = params
// Use canonical 'calendarId' param directly
const effectiveCalendarId = calendarId ? String(calendarId).trim() : ''
// Use canonical 'destinationCalendarId' param directly
const effectiveDestinationCalendarId = destinationCalendarId
? String(destinationCalendarId).trim()
: ''
@@ -554,30 +779,36 @@ Return ONLY the natural language event text - no explanations.`,
calendarId: effectiveCalendarId || 'primary',
}
// Add destination calendar ID for move operation
if (operation === 'move' && effectiveDestinationCalendarId) {
processedParams.destinationCalendarId = effectiveDestinationCalendarId
}
// Convert comma-separated attendees string to array, only if it has content
if (attendees && typeof attendees === 'string' && attendees.trim().length > 0) {
const attendeeList = attendees
.split(',')
.map((email) => email.trim())
.filter((email) => email.length > 0)
// Only add attendees if we have valid entries
if (attendeeList.length > 0) {
processedParams.attendees = attendeeList
}
}
// Convert replaceExisting string to boolean for invite operation
if (operation === 'invite' && replaceExisting !== undefined) {
processedParams.replaceExisting = replaceExisting === 'true'
}
// Set default sendUpdates to 'all' if not specified for operations that support it
if (processedParams.addGoogleMeet !== undefined) {
processedParams.addGoogleMeet =
processedParams.addGoogleMeet === 'true' || processedParams.addGoogleMeet === true
}
if (operation === 'share_calendar' && processedParams.sendNotifications !== undefined) {
processedParams.sendNotifications =
processedParams.sendNotifications === 'true' ||
processedParams.sendNotifications === true
}
if (
['create', 'update', 'delete', 'move', 'quick_add', 'invite'].includes(operation) &&
!processedParams.sendUpdates
@@ -585,12 +816,10 @@ Return ONLY the natural language event text - no explanations.`,
processedParams.sendUpdates = 'all'
}
// Convert maxResults to number if provided
if (processedParams.maxResults && typeof processedParams.maxResults === 'string') {
processedParams.maxResults = Number.parseInt(processedParams.maxResults, 10)
}
// Remove empty minAccessRole
if (processedParams.minAccessRole === '') {
processedParams.minAccessRole = undefined
}
@@ -607,38 +836,44 @@ Return ONLY the natural language event text - no explanations.`,
oauthCredential: { type: 'string', description: 'Google Calendar access token' },
calendarId: { type: 'string', description: 'Calendar identifier (canonical param)' },
// Create/Update operation inputs
summary: { type: 'string', description: 'Event title' },
description: { type: 'string', description: 'Event description' },
location: { type: 'string', description: 'Event location' },
startDateTime: { type: 'string', description: 'Event start time' },
endDateTime: { type: 'string', description: 'Event end time' },
attendees: { type: 'string', description: 'Attendee email list' },
recurrence: { type: 'string', description: 'Recurrence rule (RRULE)' },
addGoogleMeet: { type: 'boolean', description: 'Attach a Google Meet link' },
timeZone: { type: 'string', description: 'Time zone (IANA name)' },
// List/Instances operation inputs
timeMin: { type: 'string', description: 'Start time filter' },
timeMax: { type: 'string', description: 'End time filter' },
q: { type: 'string', description: 'Free-text search query' },
orderBy: { type: 'string', description: 'Event ordering (startTime or updated)' },
maxResults: { type: 'string', description: 'Maximum number of results' },
pageToken: { type: 'string', description: 'Pagination token from a previous response' },
calendarIds: { type: 'string', description: 'Comma-separated calendar IDs' },
role: { type: 'string', description: 'Access role to grant' },
scopeType: { type: 'string', description: 'Grantee type (user, group, domain, default)' },
scopeValue: { type: 'string', description: 'Grantee email or domain' },
sendNotifications: { type: 'boolean', description: 'Send sharing notification email' },
ruleId: { type: 'string', description: 'ACL rule ID to remove' },
// Get/Update/Delete/Move/Instances/Invite operation inputs
eventId: { type: 'string', description: 'Event identifier' },
// Move operation inputs
destinationCalendarId: {
type: 'string',
description: 'Destination calendar ID (canonical param)',
},
// List Calendars operation inputs
minAccessRole: { type: 'string', description: 'Minimum access role filter' },
// Quick add inputs
text: { type: 'string', description: 'Natural language event' },
// Invite specific inputs
replaceExisting: { type: 'string', description: 'Replace existing attendees' },
// Common inputs
sendUpdates: { type: 'string', description: 'Send email notifications' },
},
outputs: {
@@ -670,6 +905,11 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
'google_calendar_list_calendars_v2',
'google_calendar_quick_add_v2',
'google_calendar_invite_v2',
'google_calendar_freebusy_v2',
'google_calendar_create_calendar_v2',
'google_calendar_share_calendar_v2',
'google_calendar_list_acl_v2',
'google_calendar_unshare_calendar_v2',
],
config: {
...GoogleCalendarBlock.tools?.config,
@@ -682,28 +922,30 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
},
},
outputs: {
// Event outputs (create, get, update, move, quick_add, invite)
id: { type: 'string', description: 'Event ID' },
id: { type: 'string', description: 'Event or calendar ID' },
htmlLink: { type: 'string', description: 'Event link' },
hangoutLink: { type: 'string', description: 'Google Meet link' },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title' },
description: { type: 'string', description: 'Event description' },
location: { type: 'string', description: 'Event location' },
summary: { type: 'string', description: 'Event or calendar title' },
description: { type: 'string', description: 'Event or calendar description' },
location: { type: 'string', description: 'Event or calendar location' },
recurrence: { type: 'json', description: 'Recurrence rules (RRULE)' },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees' },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
// List events outputs
events: { type: 'json', description: 'List of events (list operation)' },
// Delete outputs
eventId: { type: 'string', description: 'Deleted event ID' },
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
// Instances outputs
deleted: { type: 'boolean', description: 'Whether deletion/removal was successful' },
instances: { type: 'json', description: 'List of recurring event instances' },
// List calendars outputs
calendars: { type: 'json', description: 'List of calendars' },
// Common outputs
timeMin: { type: 'string', description: 'Start of the queried free/busy range' },
timeMax: { type: 'string', description: 'End of the queried free/busy range' },
role: { type: 'string', description: 'Granted access role (share operation)' },
scope: { type: 'json', description: 'Grantee scope (share operation)' },
rules: { type: 'json', description: 'List of ACL sharing rules (list sharing operation)' },
ruleId: { type: 'string', description: 'Removed ACL rule ID (remove sharing operation)' },
nextPageToken: { type: 'string', description: 'Next page token' },
timeZone: { type: 'string', description: 'Calendar time zone' },
},
+21 -1
View File
@@ -5901,9 +5901,29 @@
{
"name": "Invite Attendees",
"description": "Invite attendees to an existing Google Calendar event"
},
{
"name": "Check Free/Busy",
"description": "Query free/busy information for one or more Google Calendars"
},
{
"name": "Create Calendar",
"description": "Create a new secondary calendar"
},
{
"name": "Share Calendar",
"description": "Grant a user, group, or domain access to a calendar by creating an ACL rule"
},
{
"name": "List Sharing",
"description": "List the access control rules (sharing) for a calendar"
},
{
"name": "Remove Sharing",
"description": "Revoke an access control rule (sharing) from a calendar"
}
],
"operationCount": 10,
"operationCount": 15,
"triggers": [
{
"id": "google_calendar_poller",
+61 -48
View File
@@ -1,10 +1,18 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarCreateParams,
type GoogleCalendarCreateResponse,
type GoogleCalendarEventRequestBody,
} from '@/tools/google_calendar/types'
import {
assertRecurringTimeZone,
buildEventDateTime,
buildGoogleMeetConferenceData,
normalizeAttendees,
normalizeRecurrence,
} from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/types'
export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCreateResponse> = {
@@ -54,22 +62,21 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
required: true,
visibility: 'user-or-llm',
description:
'Start date and time. MUST include timezone offset (e.g., 2025-06-03T10:00:00-08:00) OR provide timeZone parameter',
'Start time. Use a datetime with timezone offset (2025-06-03T10:00:00-08:00) or a date (2025-06-03) for an all-day event',
},
endDateTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'End date and time. MUST include timezone offset (e.g., 2025-06-03T11:00:00-08:00) OR provide timeZone parameter',
'End time. Use a datetime with timezone offset (2025-06-03T11:00:00-08:00) or a date (2025-06-04) for an all-day event',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time zone (e.g., America/Los_Angeles). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided.',
default: 'America/Los_Angeles',
'IANA time zone (e.g., America/Los_Angeles). Used as-is when provided. For recurring events a time zone is required to expand the recurrence correctly; for one-off events it is only needed when the datetime omits a UTC offset (a naive datetime defaults to America/Los_Angeles).',
},
attendees: {
type: 'array',
@@ -77,6 +84,19 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
visibility: 'user-or-llm',
description: 'Array of attendee email addresses',
},
recurrence: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Recurrence rule(s) in RFC 5545 format (e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR). Separate multiple rules with newlines.',
},
addGoogleMeet: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Attach a Google Meet video conference link to the event',
},
sendUpdates: {
type: 'string',
required: false,
@@ -93,11 +113,12 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
if (params.addGoogleMeet) {
queryParams.append('conferenceDataVersion', '1')
}
const queryString = queryParams.toString()
const finalUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
return finalUrl
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
},
method: 'POST',
headers: (params: GoogleCalendarCreateParams) => ({
@@ -105,21 +126,17 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarCreateParams): GoogleCalendarEventRequestBody => {
// Default timezone if not provided and datetime doesn't include offset
const timeZone = params.timeZone || 'America/Los_Angeles'
const needsTimezone =
!params.startDateTime.includes('+') && !params.startDateTime.includes('-', 10)
const recurrence = normalizeRecurrence(params.recurrence)
const isRecurring = recurrence.length > 0
if (isRecurring) {
assertRecurringTimeZone([params.startDateTime, params.endDateTime], params.timeZone)
}
const eventData: GoogleCalendarEventRequestBody = {
summary: params.summary,
start: {
dateTime: params.startDateTime,
...(needsTimezone ? { timeZone } : {}),
},
end: {
dateTime: params.endDateTime,
...(needsTimezone ? { timeZone } : {}),
},
start: buildEventDateTime(params.startDateTime, params.timeZone),
end: buildEventDateTime(params.endDateTime, params.timeZone),
}
if (params.description) {
@@ -130,29 +147,17 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
eventData.location = params.location
}
// Always set timezone if explicitly provided
if (params.timeZone) {
eventData.start.timeZone = params.timeZone
eventData.end.timeZone = params.timeZone
const attendees = normalizeAttendees(params.attendees)
if (attendees.length > 0) {
eventData.attendees = attendees
}
// Handle both string and array cases for attendees
let attendeeList: string[] = []
if (params.attendees) {
const attendees = params.attendees as string | string[]
if (Array.isArray(attendees)) {
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
// Convert comma-separated string to array
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
if (isRecurring) {
eventData.recurrence = recurrence
}
if (attendeeList.length > 0) {
eventData.attendees = attendeeList.map((email: string) => ({ email }))
if (params.addGoogleMeet) {
eventData.conferenceData = buildGoogleMeetConferenceData()
}
return eventData
@@ -169,10 +174,12 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
metadata: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink,
status: data.status,
summary: data.summary,
description: data.description,
location: data.location,
recurrence: data.recurrence,
start: data.start,
end: data.end,
attendees: data.attendees,
@@ -187,7 +194,7 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
content: { type: 'string', description: 'Event creation confirmation message' },
metadata: {
type: 'json',
description: 'Created event metadata including ID, status, and details',
description: 'Created event metadata including ID, status, Meet link, and details',
},
},
}
@@ -197,15 +204,17 @@ interface GoogleCalendarCreateV2Response {
output: {
id: string
htmlLink: string
hangoutLink: string | null
status: string
summary: string | null
description: string | null
location: string | null
start: any
end: any
attendees: any | null
creator: any
organizer: any
recurrence: string[] | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
@@ -226,29 +235,33 @@ export const createV2Tool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendar
output: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink ?? null,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
recurrence: data.recurrence ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator,
organizer: data.organizer,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
hangoutLink: { type: 'string', description: 'Google Meet link', optional: true },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
recurrence: { type: 'json', description: 'Recurrence rules', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
@@ -0,0 +1,142 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiCalendarResponse,
type GoogleCalendarCreateCalendarParams,
type GoogleCalendarCreateCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export const createCalendarTool: ToolConfig<
GoogleCalendarCreateCalendarParams,
GoogleCalendarCreateCalendarResponse
> = {
id: 'google_calendar_create_calendar',
name: 'Google Calendar Create Calendar',
description: 'Create a new secondary calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
summary: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the new calendar',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the new calendar',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Geographic location of the calendar as free-form text',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Time zone of the calendar as an IANA name (e.g., America/Los_Angeles)',
},
},
request: {
url: () => `${CALENDAR_API_BASE}/calendars`,
method: 'POST',
headers: (params: GoogleCalendarCreateCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarCreateCalendarParams) => {
const body: Record<string, string> = { summary: params.summary }
if (params.description) body.description = params.description
if (params.location) body.location = params.location
if (params.timeZone) body.timeZone = params.timeZone
return body
},
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
content: `Calendar "${data.summary}" created successfully`,
metadata: {
id: data.id,
summary: data.summary,
description: data.description,
location: data.location,
timeZone: data.timeZone,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Calendar creation confirmation message' },
metadata: {
type: 'json',
description: 'Created calendar metadata (id, summary, description, location, timeZone)',
},
},
}
interface GoogleCalendarCreateCalendarV2Response {
success: boolean
output: {
id: string
summary: string
description: string | null
location: string | null
timeZone: string | null
}
}
export const createCalendarV2Tool: ToolConfig<
GoogleCalendarCreateCalendarParams,
GoogleCalendarCreateCalendarV2Response
> = {
id: 'google_calendar_create_calendar_v2',
name: 'Google Calendar Create Calendar',
description: 'Create a new secondary calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: createCalendarTool.oauth,
params: createCalendarTool.params,
request: createCalendarTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
id: data.id,
summary: data.summary,
description: data.description ?? null,
location: data.location ?? null,
timeZone: data.timeZone ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Calendar ID' },
summary: { type: 'string', description: 'Calendar title' },
description: { type: 'string', description: 'Calendar description', optional: true },
location: { type: 'string', description: 'Calendar location', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone', optional: true },
},
}
-1
View File
@@ -70,7 +70,6 @@ export const deleteTool: ToolConfig<GoogleCalendarDeleteParams, GoogleCalendarDe
},
transformResponse: async (response: Response, params) => {
// DELETE returns 204 No Content on success
if (response.status === 204 || response.ok) {
return {
success: true,
+15
View File
@@ -1,35 +1,50 @@
import { createTool, createV2Tool } from '@/tools/google_calendar/create'
import { createCalendarTool, createCalendarV2Tool } from '@/tools/google_calendar/create_calendar'
import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete'
import { freebusyTool, freebusyV2Tool } from '@/tools/google_calendar/freebusy'
import { getTool, getV2Tool } from '@/tools/google_calendar/get'
import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances'
import { inviteTool, inviteV2Tool } from '@/tools/google_calendar/invite'
import { listTool, listV2Tool } from '@/tools/google_calendar/list'
import { listAclTool, listAclV2Tool } from '@/tools/google_calendar/list_acl'
import { listCalendarsTool, listCalendarsV2Tool } from '@/tools/google_calendar/list_calendars'
import { moveTool, moveV2Tool } from '@/tools/google_calendar/move'
import { quickAddTool, quickAddV2Tool } from '@/tools/google_calendar/quick_add'
import { shareCalendarTool, shareCalendarV2Tool } from '@/tools/google_calendar/share_calendar'
import {
unshareCalendarTool,
unshareCalendarV2Tool,
} from '@/tools/google_calendar/unshare_calendar'
import { updateTool, updateV2Tool } from '@/tools/google_calendar/update'
export const googleCalendarCreateTool = createTool
export const googleCalendarCreateCalendarTool = createCalendarTool
export const googleCalendarDeleteTool = deleteTool
export const googleCalendarFreeBusyTool = freebusyTool
export const googleCalendarGetTool = getTool
export const googleCalendarInstancesTool = instancesTool
export const googleCalendarInviteTool = inviteTool
export const googleCalendarListTool = listTool
export const googleCalendarListAclTool = listAclTool
export const googleCalendarListCalendarsTool = listCalendarsTool
export const googleCalendarMoveTool = moveTool
export const googleCalendarQuickAddTool = quickAddTool
export const googleCalendarShareCalendarTool = shareCalendarTool
export const googleCalendarUnshareCalendarTool = unshareCalendarTool
export const googleCalendarUpdateTool = updateTool
export const googleCalendarCreateV2Tool = createV2Tool
export const googleCalendarCreateCalendarV2Tool = createCalendarV2Tool
export const googleCalendarDeleteV2Tool = deleteV2Tool
export const googleCalendarFreeBusyV2Tool = freebusyV2Tool
export const googleCalendarGetV2Tool = getV2Tool
export const googleCalendarInstancesV2Tool = instancesV2Tool
export const googleCalendarInviteV2Tool = inviteV2Tool
export const googleCalendarListV2Tool = listV2Tool
export const googleCalendarListAclV2Tool = listAclV2Tool
export const googleCalendarListCalendarsV2Tool = listCalendarsV2Tool
export const googleCalendarMoveV2Tool = moveV2Tool
export const googleCalendarQuickAddV2Tool = quickAddV2Tool
export const googleCalendarShareCalendarV2Tool = shareCalendarV2Tool
export const googleCalendarUnshareCalendarV2Tool = unshareCalendarV2Tool
export const googleCalendarUpdateV2Tool = updateV2Tool
+120 -259
View File
@@ -1,10 +1,102 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarInviteParams,
type GoogleCalendarInviteResponse,
} from '@/tools/google_calendar/types'
import { normalizeAttendees } from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/types'
interface InviteResult {
data: GoogleCalendarApiEventResponse
totalAttendees: number
newAttendeesAdded: number
shouldReplace: boolean
}
/**
* The Google Calendar update method replaces the entire event resource, so to invite
* attendees we read the existing event, merge the attendee list, then PUT it back.
*/
async function inviteAttendees(
response: Response,
params: GoogleCalendarInviteParams | undefined
): Promise<InviteResult> {
const existingEvent: GoogleCalendarApiEventResponse = await response.json()
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
}
const newAttendeeList = normalizeAttendees(params?.attendees).map((attendee) => attendee.email)
const existingAttendees: CalendarAttendee[] = existingEvent.attendees ?? []
const shouldReplace =
params?.replaceExisting === true || String(params?.replaceExisting) === 'true'
const existingEmails = new Set(
existingAttendees.map((attendee) => attendee.email?.toLowerCase() ?? '')
)
const newAttendeesAdded = shouldReplace
? newAttendeeList.length
: newAttendeeList.filter((email) => !existingEmails.has(email.toLowerCase())).length
let finalAttendees: CalendarAttendee[]
if (shouldReplace) {
finalAttendees = newAttendeeList.map((email) => ({ email, responseStatus: 'needsAction' }))
} else {
finalAttendees = [...existingAttendees]
for (const email of newAttendeeList) {
if (!existingEmails.has(email.toLowerCase())) {
finalAttendees.push({ email, responseStatus: 'needsAction' })
}
}
}
const updatedEvent: Record<string, unknown> = { ...existingEvent, attendees: finalAttendees }
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'creator',
'organizer',
]
for (const field of readOnlyFields) {
delete updatedEvent[field]
}
const calendarId = params?.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
queryParams.append('sendUpdates', params?.sendUpdates ?? 'all')
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId?.trim() ?? '')}?${queryParams.toString()}`
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
if (!putResponse.ok) {
const errorData = await putResponse.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to invite attendees to calendar event')
}
const data: GoogleCalendarApiEventResponse = await putResponse.json()
return {
data,
totalAttendees: data.attendees?.length ?? 0,
newAttendeesAdded,
shouldReplace,
}
}
export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarInviteResponse> = {
id: 'google_calendar_invite',
name: 'Google Calendar Invite Attendees',
@@ -45,7 +137,7 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
type: 'string',
required: false,
visibility: 'user-only',
description: 'How to send updates to attendees: all, externalOnly, or none',
description: 'How to send updates to attendees: all, externalOnly, or none (defaults to all)',
},
replaceExisting: {
type: 'boolean',
@@ -57,8 +149,8 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
request: {
url: (params: GoogleCalendarInviteParams) => {
const calendarId = params.calendarId || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
const calendarId = params.calendarId?.trim() || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId.trim())}`
},
method: 'GET',
headers: (params: GoogleCalendarInviteParams) => ({
@@ -68,163 +160,29 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
},
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
const { data, totalAttendees, newAttendeesAdded, shouldReplace } = await inviteAttendees(
response,
params
)
// Validate required fields exist
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
}
// Process new attendees - handle both string and array formats
let newAttendeeList: string[] = []
if (params?.attendees) {
if (Array.isArray(params.attendees)) {
// Already an array from block processing
newAttendeeList = params.attendees.filter(
(email: string) => email && email.trim().length > 0
)
} else if (
typeof (params.attendees as any) === 'string' &&
(params.attendees as any).trim().length > 0
) {
// Fallback: process comma-separated string if block didn't convert it
newAttendeeList = (params.attendees as any)
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
}
// Calculate final attendees list
const existingAttendees = existingEvent.attendees || []
let finalAttendees: Array<any> = []
// Handle replaceExisting properly - check for both boolean true and string "true"
const shouldReplace =
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
if (shouldReplace) {
// Replace all attendees with just the new ones
finalAttendees = newAttendeeList.map((email: string) => ({
email,
responseStatus: 'needsAction',
}))
} else {
// Add to existing attendees (preserve all existing ones)
// Start with ALL existing attendees - preserve them completely
finalAttendees = [...existingAttendees]
// Get set of existing emails for duplicate checking (case-insensitive)
const existingEmails = new Set(
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
)
// Add only new attendees that don't already exist
for (const newEmail of newAttendeeList) {
const emailLower = newEmail.toLowerCase()
if (!existingEmails.has(emailLower)) {
finalAttendees.push({
email: newEmail,
responseStatus: 'needsAction',
})
}
}
}
// Use the complete existing event object and only modify the attendees field
// This is crucial because the Google Calendar API update method "does not support patch semantics
// and always updates the entire event resource" according to the documentation
const updatedEvent = {
...existingEvent, // Start with the complete existing event to preserve all fields
attendees: finalAttendees, // Only modify the attendees field
}
// Remove read-only fields that shouldn't be included in updates
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'sequence',
'creator',
'organizer',
]
readOnlyFields.forEach((field) => {
delete updatedEvent[field]
})
// Construct PUT URL with query parameters
const calendarId = params?.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params?.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
// Send PUT request to update the event
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
// Handle the PUT response
if (!putResponse.ok) {
const errorData = await putResponse.json()
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
}
const data = await putResponse.json()
const totalAttendees = data.attendees?.length || 0
// Calculate how many new attendees were actually added
let newAttendeesAdded = 0
if (shouldReplace) {
newAttendeesAdded = newAttendeeList.length
} else {
// Count how many of the new emails weren't already in the existing list
const existingEmails = new Set(
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
)
newAttendeesAdded = newAttendeeList.filter(
(email) => !existingEmails.has(email.toLowerCase())
).length
}
// Improved messaging about email delivery
let baseMessage: string
if (shouldReplace) {
baseMessage = `Successfully updated event "${data.summary}" with ${totalAttendees} attendee${totalAttendees !== 1 ? 's' : ''}`
} else if (newAttendeesAdded > 0) {
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
} else {
if (newAttendeesAdded > 0) {
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
} else {
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
}
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
}
const emailNote =
params?.sendUpdates !== 'none'
? ` Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients' Google Calendar settings.`
: ` No email notifications will be sent as requested.`
const content = baseMessage + emailNote
? ' Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients’ Google Calendar settings.'
: ' No email notifications will be sent as requested.'
return {
success: true,
output: {
content,
content: baseMessage + emailNote,
metadata: {
id: data.id,
htmlLink: data.htmlLink,
@@ -258,16 +216,16 @@ interface GoogleCalendarInviteV2Response {
success: boolean
output: {
id: string
htmlLink?: string
status?: string
summary?: string
description?: string
location?: string
start?: any
end?: any
attendees?: any
creator?: any
organizer?: any
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
@@ -282,104 +240,7 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
params: inviteTool.params,
request: inviteTool.request,
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
}
let newAttendeeList: string[] = []
if (params?.attendees) {
if (Array.isArray(params.attendees)) {
newAttendeeList = params.attendees.filter(
(email: string) => email && email.trim().length > 0
)
} else if (
typeof (params.attendees as any) === 'string' &&
(params.attendees as any).trim().length > 0
) {
newAttendeeList = (params.attendees as any)
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
}
const existingAttendees = existingEvent.attendees || []
let finalAttendees: Array<any> = []
const shouldReplace =
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
if (shouldReplace) {
finalAttendees = newAttendeeList.map((email: string) => ({
email,
responseStatus: 'needsAction',
}))
} else {
finalAttendees = [...existingAttendees]
const existingEmails = new Set(
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
)
for (const newEmail of newAttendeeList) {
const emailLower = newEmail.toLowerCase()
if (!existingEmails.has(emailLower)) {
finalAttendees.push({
email: newEmail,
responseStatus: 'needsAction',
})
}
}
}
const updatedEvent = {
...existingEvent,
attendees: finalAttendees,
}
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'sequence',
'creator',
'organizer',
]
readOnlyFields.forEach((field) => {
delete updatedEvent[field]
})
const calendarId = params?.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params?.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
if (!putResponse.ok) {
const errorData = await putResponse.json()
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
}
const data = await putResponse.json()
const { data } = await inviteAttendees(response, params)
return {
success: true,
@@ -393,8 +254,8 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator,
organizer: data.organizer,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
@@ -408,7 +269,7 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
+41 -4
View File
@@ -43,11 +43,31 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
visibility: 'user-or-llm',
description: 'Upper bound for events (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Free-text search across event summary, description, location, attendees, and organizer',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of events to return (max 2500)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving the next page of results',
},
orderBy: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Order of events returned (startTime or updated)',
visibility: 'user-or-llm',
description:
'Order of events: startTime (chronological, the default) or updated (last-modified). startTime is always valid here because singleEvents is set.',
},
showDeleted: {
type: 'boolean',
@@ -64,8 +84,11 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
if (params.timeMin) queryParams.append('timeMin', params.timeMin)
if (params.timeMax) queryParams.append('timeMax', params.timeMax)
if (params.q) queryParams.append('q', params.q)
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
queryParams.append('singleEvents', 'true')
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
queryParams.append('orderBy', params.orderBy || 'startTime')
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
@@ -119,12 +142,26 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
},
}
interface GoogleCalendarListV2Event {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
interface GoogleCalendarListV2Response {
success: boolean
output: {
nextPageToken: string | null
timeZone: string | null
events: Array<Record<string, any>>
events: GoogleCalendarListV2Event[]
}
}
+152
View File
@@ -0,0 +1,152 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiAclListResponse,
type GoogleCalendarListAclParams,
type GoogleCalendarListAclResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export const listAclTool: ToolConfig<GoogleCalendarListAclParams, GoogleCalendarListAclResponse> = {
id: 'google_calendar_list_acl',
name: 'Google Calendar List Sharing',
description: 'List the access control rules (sharing) for a calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to inspect (e.g., primary or calendar@group.calendar.google.com)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of ACL rules to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving subsequent pages of results',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Include deleted ACL rules (with role "none")',
},
},
request: {
url: (params: GoogleCalendarListAclParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params: GoogleCalendarListAclParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclListResponse = await response.json()
const rules = data.items || []
const rulesCount = rules.length
return {
success: true,
output: {
content: `Found ${rulesCount} sharing rule${rulesCount !== 1 ? 's' : ''}`,
metadata: {
nextPageToken: data.nextPageToken,
rules: rules.map((rule) => ({
id: rule.id,
role: rule.role,
scope: rule.scope,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of found sharing rules count' },
metadata: {
type: 'json',
description: 'List of ACL rules with pagination token',
},
},
}
interface GoogleCalendarListAclV2Response {
success: boolean
output: {
nextPageToken: string | null
rules: Array<{ id: string; role: string; scope: { type: string; value?: string } }>
}
}
export const listAclV2Tool: ToolConfig<
GoogleCalendarListAclParams,
GoogleCalendarListAclV2Response
> = {
id: 'google_calendar_list_acl_v2',
name: 'Google Calendar List Sharing',
description:
'List the access control rules (sharing) for a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: listAclTool.oauth,
params: listAclTool.params,
request: listAclTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclListResponse = await response.json()
const rules = data.items || []
return {
success: true,
output: {
nextPageToken: data.nextPageToken ?? null,
rules: rules.map((rule) => ({
id: rule.id,
role: rule.role,
scope: rule.scope,
})),
},
}
},
outputs: {
nextPageToken: { type: 'string', description: 'Next page token', optional: true },
rules: {
type: 'array',
description: 'List of ACL rules',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'ACL rule ID' },
role: { type: 'string', description: 'Access role' },
scope: { type: 'json', description: 'Grantee scope (type and value)' },
},
},
},
},
}
+1 -1
View File
@@ -121,7 +121,7 @@ export const moveTool: ToolConfig<GoogleCalendarMoveParams, GoogleCalendarMoveRe
return {
success: true,
output: {
content: `Event "${data.summary}" moved successfully`,
content: `Event "${data.summary || 'Untitled'}" moved successfully`,
metadata: {
id: data.id,
htmlLink: data.htmlLink,
@@ -79,7 +79,6 @@ export const quickAddTool: ToolConfig<
transformResponse: async (response: Response, params) => {
const data = await response.json()
// Handle attendees if provided
let finalEventData = data
if (params?.attendees) {
let attendeeList: string[] = []
@@ -88,7 +87,6 @@ export const quickAddTool: ToolConfig<
if (Array.isArray(attendees)) {
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
// Convert comma-separated string to array
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
@@ -97,16 +95,13 @@ export const quickAddTool: ToolConfig<
if (attendeeList.length > 0) {
try {
// Update the event with attendees
const calendarId = params.calendarId || 'primary'
const eventId = data.id
// Prepare update data
const updateData = {
attendees: attendeeList.map((email: string) => ({ email })),
}
// Build update URL with sendUpdates if specified
const updateQueryParams = new URLSearchParams()
if (params.sendUpdates !== undefined) {
updateQueryParams.append('sendUpdates', params.sendUpdates)
@@ -114,7 +109,6 @@ export const quickAddTool: ToolConfig<
const updateUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${eventId}${updateQueryParams.toString() ? `?${updateQueryParams.toString()}` : ''}`
// Make the update request
const updateResponse = await fetch(updateUrl, {
method: 'PATCH',
headers: {
@@ -127,13 +121,11 @@ export const quickAddTool: ToolConfig<
if (updateResponse.ok) {
finalEventData = await updateResponse.json()
} else {
// If update fails, we still return the original event but log the error
logger.warn('Failed to add attendees to quick-added event', {
error: await updateResponse.text(),
})
}
} catch (error) {
// If attendee update fails, we still return the original event
logger.warn('Error adding attendees to quick-added event', { error })
}
}
@@ -0,0 +1,160 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiAclRule,
type GoogleCalendarShareCalendarParams,
type GoogleCalendarShareCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
const buildAclBody = (params: GoogleCalendarShareCalendarParams) => {
const scope: { type: string; value?: string } = { type: params.scopeType }
if (params.scopeType !== 'default') {
const value = params.scopeValue?.trim()
if (!value) {
throw new Error(
`A grantee is required when scope type is "${params.scopeType}". Provide an email (user/group) or domain name in scopeValue.`
)
}
scope.value = value
}
return { role: params.role, scope }
}
const buildAclUrl = (params: GoogleCalendarShareCalendarParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
if (params.sendNotifications !== undefined) {
queryParams.append('sendNotifications', String(params.sendNotifications))
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl${queryString ? `?${queryString}` : ''}`
}
export const shareCalendarTool: ToolConfig<
GoogleCalendarShareCalendarParams,
GoogleCalendarShareCalendarResponse
> = {
id: 'google_calendar_share_calendar',
name: 'Google Calendar Share Calendar',
description: 'Grant a user, group, or domain access to a calendar by creating an ACL rule',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to share (e.g., primary or calendar@group.calendar.google.com)',
},
role: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Access role to grant: freeBusyReader, reader, writer, or owner',
},
scopeType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of grantee: user, group, domain, or default (public)',
},
scopeValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Email (user/group), domain name (domain), or empty for default. Required unless scope type is default.',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to send a notification email about the change. Defaults to true.',
},
},
request: {
url: buildAclUrl,
method: 'POST',
headers: (params: GoogleCalendarShareCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: buildAclBody,
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
content: `Granted ${data.role} access to ${data.scope?.value || data.scope?.type}`,
metadata: {
id: data.id,
role: data.role,
scope: data.scope,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Sharing confirmation message' },
metadata: {
type: 'json',
description: 'Created ACL rule (id, role, scope)',
},
},
}
interface GoogleCalendarShareCalendarV2Response {
success: boolean
output: {
id: string
role: string
scope: { type: string; value?: string }
}
}
export const shareCalendarV2Tool: ToolConfig<
GoogleCalendarShareCalendarParams,
GoogleCalendarShareCalendarV2Response
> = {
id: 'google_calendar_share_calendar_v2',
name: 'Google Calendar Share Calendar',
description:
'Grant a user, group, or domain access to a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: shareCalendarTool.oauth,
params: shareCalendarTool.params,
request: shareCalendarTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
id: data.id,
role: data.role,
scope: data.scope,
},
}
},
outputs: {
id: { type: 'string', description: 'ACL rule ID' },
role: { type: 'string', description: 'Granted access role' },
scope: { type: 'json', description: 'Grantee scope (type and value)' },
},
}
+156 -23
View File
@@ -2,8 +2,7 @@ import type { ToolResponse } from '@/tools/types'
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
// Shared attendee interface that matches Google Calendar API specification
interface CalendarAttendee {
export interface CalendarAttendee {
id?: string
email: string
displayName?: string
@@ -18,7 +17,7 @@ interface CalendarAttendee {
interface BaseGoogleCalendarParams {
accessToken: string
calendarId?: string // defaults to 'primary' if not provided
calendarId?: string
}
export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
@@ -28,14 +27,18 @@ export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
startDateTime: string
endDateTime: string
timeZone?: string
attendees?: string[] // Array of email addresses
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
recurrence?: string | string[]
addGoogleMeet?: boolean
}
export interface GoogleCalendarListParams extends BaseGoogleCalendarParams {
timeMin?: string // RFC3339 timestamp
timeMax?: string // RFC3339 timestamp
timeMin?: string
timeMax?: string
q?: string
maxResults?: number
pageToken?: string
singleEvents?: boolean
orderBy?: 'startTime' | 'updated'
showDeleted?: boolean
@@ -55,6 +58,8 @@ export interface GoogleCalendarUpdateParams extends BaseGoogleCalendarParams {
timeZone?: string
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
recurrence?: string | string[]
addGoogleMeet?: boolean
}
export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
@@ -63,16 +68,16 @@ export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
}
export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
text: string // Natural language text like "Meeting with John tomorrow at 3pm"
attendees?: string[] // Array of email addresses (comma-separated string also accepted)
text: string
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams {
eventId: string
attendees: string[] // Array of email addresses to invite
attendees: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
replaceExisting?: boolean // Whether to replace existing attendees or add to them
replaceExisting?: boolean
}
interface GoogleCalendarMoveParams extends BaseGoogleCalendarParams {
@@ -92,10 +97,10 @@ interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams {
export interface GoogleCalendarFreeBusyParams {
accessToken: string
calendarIds: string // Comma-separated calendar IDs (e.g., "primary,other@example.com")
timeMin: string // RFC3339 timestamp (e.g., 2025-06-03T00:00:00Z)
timeMax: string // RFC3339 timestamp (e.g., 2025-06-04T00:00:00Z)
timeZone?: string // IANA time zone (e.g., "UTC", "America/New_York")
calendarIds: string
timeMin: string
timeMax: string
timeZone?: string
}
interface GoogleCalendarListCalendarsParams {
@@ -107,6 +112,40 @@ interface GoogleCalendarListCalendarsParams {
showHidden?: boolean
}
export interface GoogleCalendarCreateCalendarParams {
accessToken: string
summary: string
description?: string
location?: string
timeZone?: string
}
type GoogleCalendarAclRole = 'freeBusyReader' | 'reader' | 'writer' | 'owner'
type GoogleCalendarAclScopeType = 'user' | 'group' | 'domain' | 'default'
export interface GoogleCalendarShareCalendarParams {
accessToken: string
calendarId?: string
role: GoogleCalendarAclRole
scopeType: GoogleCalendarAclScopeType
scopeValue?: string
sendNotifications?: boolean
}
export interface GoogleCalendarListAclParams {
accessToken: string
calendarId?: string
maxResults?: number
pageToken?: string
showDeleted?: boolean
}
export interface GoogleCalendarUnshareCalendarParams {
accessToken: string
calendarId?: string
ruleId: string
}
export type GoogleCalendarToolParams =
| GoogleCalendarCreateParams
| GoogleCalendarListParams
@@ -119,14 +158,20 @@ export type GoogleCalendarToolParams =
| GoogleCalendarInstancesParams
| GoogleCalendarFreeBusyParams
| GoogleCalendarListCalendarsParams
| GoogleCalendarCreateCalendarParams
| GoogleCalendarShareCalendarParams
| GoogleCalendarListAclParams
| GoogleCalendarUnshareCalendarParams
interface EventMetadata {
id: string
htmlLink: string
hangoutLink?: string
status: string
summary: string
description?: string
location?: string
recurrence?: string[]
start: {
dateTime?: string
date?: string
@@ -162,7 +207,6 @@ interface GoogleCalendarToolResponse extends ToolResponse {
}
}
// Specific response types for each operation
export interface GoogleCalendarCreateResponse extends ToolResponse {
output: {
content: string
@@ -242,32 +286,44 @@ interface GoogleCalendarEvent {
}
}
interface GoogleCalendarEventDateTime {
dateTime?: string
date?: string
timeZone?: string
}
interface GoogleCalendarConferenceCreateRequest {
createRequest: {
requestId: string
conferenceSolutionKey: { type: string }
}
}
export interface GoogleCalendarEventRequestBody {
summary: string
description?: string
location?: string
start: {
dateTime: string
timeZone?: string
}
end: {
dateTime: string
timeZone?: string
}
start: GoogleCalendarEventDateTime
end: GoogleCalendarEventDateTime
attendees?: Array<{
email: string
}>
recurrence?: string[]
conferenceData?: GoogleCalendarConferenceCreateRequest
}
export interface GoogleCalendarApiEventResponse {
id: string
status: string
htmlLink: string
hangoutLink?: string
created?: string
updated?: string
summary: string
description?: string
location?: string
recurrence?: string[]
recurringEventId?: string
start: {
dateTime?: string
date?: string
@@ -287,6 +343,7 @@ export interface GoogleCalendarApiEventResponse {
email: string
displayName?: string
}
conferenceData?: Record<string, unknown>
reminders?: {
useDefault: boolean
overrides?: Array<{
@@ -296,6 +353,34 @@ export interface GoogleCalendarApiEventResponse {
}
}
export interface GoogleCalendarApiCalendarResponse {
kind: string
etag: string
id: string
summary: string
description?: string
location?: string
timeZone?: string
}
export interface GoogleCalendarApiAclRule {
kind: string
etag: string
id: string
role: string
scope: {
type: string
value?: string
}
}
export interface GoogleCalendarApiAclListResponse {
kind: string
etag: string
nextPageToken?: string
items: GoogleCalendarApiAclRule[]
}
export interface GoogleCalendarApiListResponse {
kind: string
etag: string
@@ -402,6 +487,50 @@ interface GoogleCalendarListCalendarsResponse extends ToolResponse {
}
}
export interface GoogleCalendarCreateCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
id: string
summary: string
description?: string
location?: string
timeZone?: string
}
}
}
export interface GoogleCalendarShareCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
id: string
role: string
scope: { type: string; value?: string }
}
}
}
export interface GoogleCalendarListAclResponse extends ToolResponse {
output: {
content: string
metadata: {
nextPageToken?: string
rules: Array<{ id: string; role: string; scope: { type: string; value?: string } }>
}
}
}
export interface GoogleCalendarUnshareCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
ruleId: string
deleted: boolean
}
}
}
export type GoogleCalendarResponse =
| GoogleCalendarCreateResponse
| GoogleCalendarListResponse
@@ -414,3 +543,7 @@ export type GoogleCalendarResponse =
| GoogleCalendarInstancesResponse
| GoogleCalendarFreeBusyResponse
| GoogleCalendarListCalendarsResponse
| GoogleCalendarCreateCalendarResponse
| GoogleCalendarShareCalendarResponse
| GoogleCalendarListAclResponse
| GoogleCalendarUnshareCalendarResponse
@@ -0,0 +1,122 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarUnshareCalendarParams,
type GoogleCalendarUnshareCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
const buildUnshareUrl = (params: GoogleCalendarUnshareCalendarParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(params.ruleId.trim())}`
}
export const unshareCalendarTool: ToolConfig<
GoogleCalendarUnshareCalendarParams,
GoogleCalendarUnshareCalendarResponse
> = {
id: 'google_calendar_unshare_calendar',
name: 'Google Calendar Remove Sharing',
description: 'Revoke an access control rule (sharing) from a calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to modify (e.g., primary or calendar@group.calendar.google.com)',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ACL rule ID to remove (e.g., user:person@example.com)',
},
},
request: {
url: buildUnshareUrl,
method: 'DELETE',
headers: (params: GoogleCalendarUnshareCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
content: 'Sharing rule successfully removed',
metadata: {
ruleId: params?.ruleId || '',
deleted: true,
},
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to remove sharing rule')
},
outputs: {
content: { type: 'string', description: 'Removal confirmation message' },
metadata: {
type: 'json',
description: 'Removal details including rule ID',
},
},
}
interface GoogleCalendarUnshareCalendarV2Response {
success: boolean
output: {
ruleId: string
deleted: boolean
}
}
export const unshareCalendarV2Tool: ToolConfig<
GoogleCalendarUnshareCalendarParams,
GoogleCalendarUnshareCalendarV2Response
> = {
id: 'google_calendar_unshare_calendar_v2',
name: 'Google Calendar Remove Sharing',
description:
'Revoke an access control rule (sharing) from a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: unshareCalendarTool.oauth,
params: unshareCalendarTool.params,
request: unshareCalendarTool.request,
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
ruleId: params?.ruleId || '',
deleted: true,
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to remove sharing rule')
},
outputs: {
ruleId: { type: 'string', description: 'Removed ACL rule ID' },
deleted: { type: 'boolean', description: 'Whether removal was successful' },
},
}
+69 -41
View File
@@ -1,11 +1,22 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarEventRequestBody,
type GoogleCalendarUpdateParams,
type GoogleCalendarUpdateResponse,
} from '@/tools/google_calendar/types'
import {
assertRecurringTimeZone,
buildEventDateTime,
buildGoogleMeetConferenceData,
normalizeAttendees,
normalizeRecurrence,
} from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/types'
type EventPatchBody = Partial<GoogleCalendarEventRequestBody>
export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUpdateResponse> = {
id: 'google_calendar_update',
name: 'Google Calendar Update Event',
@@ -59,27 +70,41 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
required: false,
visibility: 'user-or-llm',
description:
'New start date and time. MUST include timezone offset (e.g., 2025-06-03T10:00:00-08:00) OR provide timeZone parameter',
'New start time. Use a datetime with timezone offset (2025-06-03T10:00:00-08:00) or a date (2025-06-03) for an all-day event',
},
endDateTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'New end date and time. MUST include timezone offset (e.g., 2025-06-03T11:00:00-08:00) OR provide timeZone parameter',
'New end time. Use a datetime with timezone offset (2025-06-03T11:00:00-08:00) or a date (2025-06-04) for an all-day event',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time zone (e.g., America/Los_Angeles). Required if datetime does not include offset.',
'IANA time zone (e.g., America/Los_Angeles) applied to the start/end times provided in this update. Provide a new start and/or end time to change the time zone; a time zone on its own is not applied. Required for recurring events to expand the recurrence correctly.',
},
attendees: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Array of attendee email addresses (replaces existing attendees)',
description:
'Array of attendee email addresses. When one or more emails are provided, they replace the existing attendee list. Leaving this empty keeps the current attendees unchanged (it does not clear them).',
},
recurrence: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"Recurrence rule(s) in RFC 5545 format (e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR). Separate multiple rules with newlines. When provided, replaces the event's recurrence; leaving it empty keeps the existing recurrence unchanged. Requires a timeZone for timed events.",
},
addGoogleMeet: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Attach a Google Meet video conference link to the event',
},
sendUpdates: {
type: 'string',
@@ -97,6 +122,9 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
if (params.addGoogleMeet) {
queryParams.append('conferenceDataVersion', '1')
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}`
@@ -106,8 +134,14 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarUpdateParams) => {
const updateData: Record<string, unknown> = {}
body: (params: GoogleCalendarUpdateParams): EventPatchBody => {
const updateData: EventPatchBody = {}
const recurrence = normalizeRecurrence(params.recurrence)
const isRecurring = recurrence.length > 0
if (isRecurring) {
assertRecurringTimeZone([params.startDateTime, params.endDateTime], params.timeZone)
}
if (params.summary !== undefined) {
updateData.summary = params.summary
@@ -122,38 +156,24 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
}
if (params.startDateTime !== undefined) {
const needsTimezone =
!params.startDateTime.includes('+') && !params.startDateTime.includes('-', 10)
updateData.start = {
dateTime: params.startDateTime,
...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}),
}
updateData.start = buildEventDateTime(params.startDateTime, params.timeZone)
}
if (params.endDateTime !== undefined) {
const needsTimezone =
!params.endDateTime.includes('+') && !params.endDateTime.includes('-', 10)
updateData.end = {
dateTime: params.endDateTime,
...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}),
}
updateData.end = buildEventDateTime(params.endDateTime, params.timeZone)
}
// Handle attendees - convert to array format
if (params.attendees !== undefined) {
let attendeeList: string[] = []
const attendees = params.attendees as string | string[]
const attendees = normalizeAttendees(params.attendees)
if (attendees.length > 0) {
updateData.attendees = attendees
}
if (Array.isArray(attendees)) {
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
if (isRecurring) {
updateData.recurrence = recurrence
}
updateData.attendees = attendeeList.map((email: string) => ({ email }))
if (params.addGoogleMeet) {
updateData.conferenceData = buildGoogleMeetConferenceData()
}
return updateData
@@ -170,10 +190,12 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
metadata: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink,
status: data.status,
summary: data.summary,
description: data.description,
location: data.location,
recurrence: data.recurrence,
start: data.start,
end: data.end,
attendees: data.attendees,
@@ -188,7 +210,7 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
content: { type: 'string', description: 'Event update confirmation message' },
metadata: {
type: 'json',
description: 'Updated event metadata including ID, status, and details',
description: 'Updated event metadata including ID, status, Meet link, and details',
},
},
}
@@ -198,15 +220,17 @@ interface GoogleCalendarUpdateV2Response {
output: {
id: string
htmlLink: string
hangoutLink: string | null
status: string
summary: string | null
description: string | null
location: string | null
start: any
end: any
attendees: any | null
creator: any
organizer: any
recurrence: string[] | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
@@ -227,29 +251,33 @@ export const updateV2Tool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendar
output: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink ?? null,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
recurrence: data.recurrence ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator,
organizer: data.organizer,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
hangoutLink: { type: 'string', description: 'Google Meet link', optional: true },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
recurrence: { type: 'json', description: 'Recurrence rules', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
+88
View File
@@ -0,0 +1,88 @@
import { generateId } from '@sim/utils/id'
import type { GoogleCalendarEventRequestBody } from '@/tools/google_calendar/types'
type EventDateTime = GoogleCalendarEventRequestBody['start']
const DEFAULT_TIME_ZONE = 'America/Los_Angeles'
const TZ_OFFSET_PATTERN = /([+-]\d{2}:?\d{2}|Z)$/
/**
* Build a Google Calendar event date/time object from a user-supplied value.
*
* A date-only value (e.g. `2025-06-03`) produces an all-day `{ date }` object.
* A datetime value produces `{ dateTime, timeZone? }`. An explicitly provided
* timezone always wins. Otherwise a default zone is attached only for "naive"
* datetimes that carry no UTC offset — when an offset is present it is authoritative
* and is never overridden with a guessed zone, which would misalign the time.
*
* For recurring events the Calendar API requires a named `timeZone` on start/end;
* callers should pass the user's timezone explicitly (an RFC3339 offset alone is
* insufficient to expand a recurrence across DST).
*/
export function buildEventDateTime(value: string, timeZone: string | undefined): EventDateTime {
const isDateOnly = !value.includes('T')
if (isDateOnly) {
return { date: value }
}
const hasOffset = TZ_OFFSET_PATTERN.test(value)
const result: EventDateTime = { dateTime: value }
if (timeZone) {
result.timeZone = timeZone
} else if (!hasOffset) {
result.timeZone = DEFAULT_TIME_ZONE
}
return result
}
/** Normalize a comma/newline-separated string or array of attendee emails into `[{ email }]`. */
export function normalizeAttendees(
attendees: string | string[] | undefined
): Array<{ email: string }> {
if (!attendees) return []
const list = Array.isArray(attendees)
? attendees
: attendees.split(',').map((email) => email.trim())
return list.filter((email) => email.length > 0).map((email) => ({ email }))
}
/**
* Recurring events require a named `timeZone` on their timed start/end — the Calendar API
* rejects them otherwise, and an RFC3339 offset is not a substitute (an IANA zone cannot be
* derived from a fixed offset). Throws a clear error so we fail fast with guidance instead of
* silently guessing a zone (which would misalign the recurrence) or sending an invalid request.
* All-day recurring events (date-only values) do not need a timezone and are allowed.
*/
export function assertRecurringTimeZone(
dateTimes: Array<string | undefined>,
timeZone: string | undefined
): void {
if (timeZone) return
const hasTimedValue = dateTimes.some((value) => value?.includes('T'))
if (hasTimedValue) {
throw new Error(
'Recurring events require a time zone. Provide the timeZone parameter (an IANA name, e.g. America/New_York).'
)
}
}
/** Normalize recurrence rules (single string, newline-separated string, or array) into an array. */
export function normalizeRecurrence(recurrence: string | string[] | undefined): string[] {
if (!recurrence) return []
const list = Array.isArray(recurrence) ? recurrence : recurrence.split('\n')
return list.map((rule) => rule.trim()).filter((rule) => rule.length > 0)
}
/** Build a `conferenceData.createRequest` payload that asks Google to attach a Meet link. */
export function buildGoogleMeetConferenceData(): GoogleCalendarEventRequestBody['conferenceData'] {
return {
createRequest: {
requestId: generateId(),
conferenceSolutionKey: { type: 'hangoutsMeet' },
},
}
}
+18 -2
View File
@@ -1169,6 +1169,8 @@ import {
} from '@/tools/google_bigquery'
import { googleBooksVolumeDetailsTool, googleBooksVolumeSearchTool } from '@/tools/google_books'
import {
googleCalendarCreateCalendarTool,
googleCalendarCreateCalendarV2Tool,
googleCalendarCreateTool,
googleCalendarCreateV2Tool,
googleCalendarDeleteTool,
@@ -1181,6 +1183,8 @@ import {
googleCalendarInstancesV2Tool,
googleCalendarInviteTool,
googleCalendarInviteV2Tool,
googleCalendarListAclTool,
googleCalendarListAclV2Tool,
googleCalendarListCalendarsTool,
googleCalendarListCalendarsV2Tool,
googleCalendarListTool,
@@ -1189,6 +1193,10 @@ import {
googleCalendarMoveV2Tool,
googleCalendarQuickAddTool,
googleCalendarQuickAddV2Tool,
googleCalendarShareCalendarTool,
googleCalendarShareCalendarV2Tool,
googleCalendarUnshareCalendarTool,
googleCalendarUnshareCalendarV2Tool,
googleCalendarUpdateTool,
googleCalendarUpdateV2Tool,
} from '@/tools/google_calendar'
@@ -6344,8 +6352,12 @@ export const tools: Record<string, ToolConfig> = {
microsoft_planner_update_task_details: microsoftPlannerUpdateTaskDetailsTool,
google_calendar_create: googleCalendarCreateTool,
google_calendar_create_v2: googleCalendarCreateV2Tool,
google_calendar_create_calendar: googleCalendarCreateCalendarTool,
google_calendar_create_calendar_v2: googleCalendarCreateCalendarV2Tool,
google_calendar_delete: googleCalendarDeleteTool,
google_calendar_delete_v2: googleCalendarDeleteV2Tool,
google_calendar_freebusy: googleCalendarFreeBusyTool,
google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool,
google_calendar_get: googleCalendarGetTool,
google_calendar_get_v2: googleCalendarGetV2Tool,
google_calendar_instances: googleCalendarInstancesTool,
@@ -6354,12 +6366,18 @@ export const tools: Record<string, ToolConfig> = {
google_calendar_invite_v2: googleCalendarInviteV2Tool,
google_calendar_list: googleCalendarListTool,
google_calendar_list_v2: googleCalendarListV2Tool,
google_calendar_list_acl: googleCalendarListAclTool,
google_calendar_list_acl_v2: googleCalendarListAclV2Tool,
google_calendar_list_calendars: googleCalendarListCalendarsTool,
google_calendar_list_calendars_v2: googleCalendarListCalendarsV2Tool,
google_calendar_move: googleCalendarMoveTool,
google_calendar_move_v2: googleCalendarMoveV2Tool,
google_calendar_quick_add: googleCalendarQuickAddTool,
google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool,
google_calendar_share_calendar: googleCalendarShareCalendarTool,
google_calendar_share_calendar_v2: googleCalendarShareCalendarV2Tool,
google_calendar_unshare_calendar: googleCalendarUnshareCalendarTool,
google_calendar_unshare_calendar_v2: googleCalendarUnshareCalendarV2Tool,
google_calendar_update: googleCalendarUpdateTool,
google_calendar_update_v2: googleCalendarUpdateV2Tool,
google_contacts_create: googleContactsCreateTool,
@@ -6368,8 +6386,6 @@ export const tools: Record<string, ToolConfig> = {
google_contacts_list: googleContactsListTool,
google_contacts_search: googleContactsSearchTool,
google_contacts_update: googleContactsUpdateTool,
google_calendar_freebusy: googleCalendarFreeBusyTool,
google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool,
google_forms_get_responses: googleFormsGetResponsesTool,
google_forms_get_form: googleFormsGetFormTool,
google_forms_create_form: googleFormsCreateFormTool,