From 50a8eb3b3bcc21bce89ddeaf3b11b1e93afe0b92 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 15 Jan 2026 12:13:07 -0500 Subject: [PATCH 1/2] fix: Accept both UUID and Zoom meeting IDs in meeting tools - Show meeting ID in list_upcoming_meetings output so Claude can see it - Update get_meeting_details to try UUID first, then Zoom meeting ID - Update rsvp_to_meeting to try UUID first, then Zoom meeting ID - Update cancel_meeting to try UUID first, then Zoom meeting ID - Update add_meeting_attendee to try UUID first, then Zoom meeting ID This fixes the issue where Claude would try to use Zoom meeting IDs from meeting links but the tools only accepted our internal UUIDs. Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-meeting-id-lookup.md | 12 ++++++ server/src/addie/mcp/meeting-tools.ts | 60 +++++++++++++++++++-------- 2 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 .changeset/fix-meeting-id-lookup.md diff --git a/.changeset/fix-meeting-id-lookup.md b/.changeset/fix-meeting-id-lookup.md new file mode 100644 index 0000000000..40bbb03b93 --- /dev/null +++ b/.changeset/fix-meeting-id-lookup.md @@ -0,0 +1,12 @@ +--- +--- + +fix: Accept both UUID and Zoom meeting IDs in meeting tools + +Updated meeting tools to accept both internal UUIDs and Zoom meeting IDs: +- Show meeting ID in list_upcoming_meetings output +- get_meeting_details, cancel_meeting, rsvp_to_meeting, add_meeting_attendee + now try UUID first, then fall back to Zoom meeting ID lookup + +This fixes the issue where Claude would see Zoom meeting IDs in meeting links +but the tools only accepted internal UUIDs. diff --git a/server/src/addie/mcp/meeting-tools.ts b/server/src/addie/mcp/meeting-tools.ts index caae338d51..7df8b9a81b 100644 --- a/server/src/addie/mcp/meeting-tools.ts +++ b/server/src/addie/mcp/meeting-tools.ts @@ -649,6 +649,7 @@ export function createMeetingToolHandlers( for (const meeting of meetings) { response += `📅 **${meeting.title}**\n`; + response += ` ID: ${meeting.id}\n`; response += ` ${formatDate(meeting.start_time)} at ${formatTime(meeting.start_time, meeting.timezone)}\n`; if (!groupName) { response += ` Group: ${meeting.working_group_name}\n`; @@ -707,14 +708,22 @@ export function createMeetingToolHandlers( // Get meeting details handlers.set('get_meeting_details', async (input) => { - const meetingId = input.meeting_id as string; + const inputId = input.meeting_id as string; - const meeting = await meetingsDb.getMeetingWithGroup(meetingId); + // Try to find by our UUID first, then by Zoom meeting ID + let meeting = await meetingsDb.getMeetingWithGroup(inputId); + if (!meeting) { + // Maybe it's a Zoom meeting ID - look that up first + const meetingByZoom = await meetingsDb.getMeetingByZoomId(inputId); + if (meetingByZoom) { + meeting = await meetingsDb.getMeetingWithGroup(meetingByZoom.id); + } + } if (!meeting) { - return `❌ Meeting not found: "${meetingId}"`; + return `❌ Meeting not found: "${inputId}". Use list_upcoming_meetings to find the correct meeting ID.`; } - const attendees = await meetingsDb.getAttendeesForMeeting(meetingId); + const attendees = await meetingsDb.getAttendeesForMeeting(meeting.id); const accepted = attendees.filter(a => a.rsvp_status === 'accepted'); const declined = attendees.filter(a => a.rsvp_status === 'declined'); const pending = attendees.filter(a => a.rsvp_status === 'pending'); @@ -759,21 +768,25 @@ export function createMeetingToolHandlers( return '❌ Unable to identify you. Please make sure you\'re logged in.'; } - const meetingId = input.meeting_id as string; + const inputId = input.meeting_id as string; const response = input.response as 'accepted' | 'declined' | 'tentative'; const note = input.note as string | undefined; - const meeting = await meetingsDb.getMeetingById(meetingId); + // Try to find by our UUID first, then by Zoom meeting ID + let meeting = await meetingsDb.getMeetingById(inputId); + if (!meeting) { + meeting = await meetingsDb.getMeetingByZoomId(inputId); + } if (!meeting) { - return `❌ Meeting not found: "${meetingId}"`; + return `❌ Meeting not found: "${inputId}". Use list_upcoming_meetings to find the correct meeting ID.`; } // Check if user is already an attendee - let attendee = await meetingsDb.getAttendee(meetingId, userId); + let attendee = await meetingsDb.getAttendee(meeting.id, userId); if (attendee) { // Update existing RSVP - attendee = await meetingsDb.updateAttendee(meetingId, userId, { + attendee = await meetingsDb.updateAttendee(meeting.id, userId, { rsvp_status: response, rsvp_note: note, }); @@ -785,7 +798,7 @@ export function createMeetingToolHandlers( : userEmail; attendee = await meetingsDb.addAttendee({ - meeting_id: meetingId, + meeting_id: meeting.id, workos_user_id: userId, email: userEmail, name: userName, @@ -810,9 +823,14 @@ export function createMeetingToolHandlers( const meetingId = input.meeting_id as string; - const meeting = await meetingsDb.getMeetingById(meetingId); + // Try to find by our UUID first, then by Zoom meeting ID + let meeting = await meetingsDb.getMeetingById(meetingId); + if (!meeting) { + // Maybe it's a Zoom meeting ID instead of our UUID + meeting = await meetingsDb.getMeetingByZoomId(meetingId); + } if (!meeting) { - return `❌ Meeting not found: "${meetingId}"`; + return `❌ Meeting not found: "${meetingId}". Use list_upcoming_meetings to find the correct meeting ID.`; } if (meeting.status === 'cancelled') { @@ -820,7 +838,8 @@ export function createMeetingToolHandlers( } try { - const result = await meetingService.cancelMeeting(meetingId); + // Use meeting.id (our UUID) not the input meetingId (might be Zoom ID) + const result = await meetingService.cancelMeeting(meeting.id); let response = `✅ Cancelled: **${meeting.title}**\n`; response += `Cancellation notices have been sent to attendees.`; @@ -830,7 +849,7 @@ export function createMeetingToolHandlers( response += result.errors.map(e => `• ${e}`).join('\n'); } - logger.info({ meetingId, cancelledBy: getUserId() }, 'Meeting cancelled via Addie'); + logger.info({ meetingId: meeting.id, cancelledBy: getUserId() }, 'Meeting cancelled via Addie'); return response; } catch (error) { @@ -844,17 +863,22 @@ export function createMeetingToolHandlers( const permCheck = await checkSchedulePermission(); if (permCheck) return permCheck; - const meetingId = input.meeting_id as string; + const inputId = input.meeting_id as string; const email = input.email as string; const name = input.name as string | undefined; - const meeting = await meetingsDb.getMeetingById(meetingId); + // Try to find by our UUID first, then by Zoom meeting ID + let meeting = await meetingsDb.getMeetingById(inputId); + if (!meeting) { + meeting = await meetingsDb.getMeetingByZoomId(inputId); + } if (!meeting) { - return `❌ Meeting not found: "${meetingId}"`; + return `❌ Meeting not found: "${inputId}". Use list_upcoming_meetings to find the correct meeting ID.`; } try { - const result = await meetingService.addAttendeesToMeeting(meetingId, [ + // Use meeting.id (our UUID) not the inputId (might be Zoom ID) + const result = await meetingService.addAttendeesToMeeting(meeting.id, [ { email, name }, ]); From 808cf33058b84a42c541067e75c121c6c94671bf Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 15 Jan 2026 12:25:19 -0500 Subject: [PATCH 2/2] fix: Strip Z suffix from Google Calendar dateTime to fix timezone Applied the same timezone fix to Google Calendar events that was applied to Zoom meetings in PR #779. The Z suffix was causing Google Calendar to interpret times as UTC, resulting in meetings showing 5 hours earlier than intended for ET. Renamed formatDateForZoom to formatDateWithoutZ since it's now used for both Zoom and Google Calendar. Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-calendar-timezone.md | 11 +++++++++++ server/src/services/meeting-service.ts | 25 +++++++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 .changeset/fix-calendar-timezone.md diff --git a/.changeset/fix-calendar-timezone.md b/.changeset/fix-calendar-timezone.md new file mode 100644 index 0000000000..32b4b61c6b --- /dev/null +++ b/.changeset/fix-calendar-timezone.md @@ -0,0 +1,11 @@ +--- +--- + +fix: Strip Z suffix from Google Calendar dateTime to fix timezone + +Applied the same timezone fix to Google Calendar events that was applied to +Zoom meetings. The Z suffix was causing Google Calendar to interpret times +as UTC, resulting in meetings showing 5 hours earlier than intended for ET. + +For example, "1 PM ET" was showing as "8 AM ET" because the Z suffix told +Google Calendar the time was 13:00 UTC (which is 8 AM ET). diff --git a/server/src/services/meeting-service.ts b/server/src/services/meeting-service.ts index a6a26c906f..05b2faf068 100644 --- a/server/src/services/meeting-service.ts +++ b/server/src/services/meeting-service.ts @@ -33,17 +33,17 @@ const ZOOM_HOST_EMAIL = process.env.ZOOM_HOST_EMAIL || 'addie@agenticadvertising /** * Format a Date as ISO string WITHOUT the Z suffix. - * This is needed for Zoom API which interprets times with Z as UTC, - * but times without Z in the specified timezone parameter. + * This is needed for both Zoom and Google Calendar APIs which interpret + * times with Z as UTC, but times without Z in the specified timezone parameter. * - * When Claude sends "2026-01-15T11:00:00" for 11 AM ET, JavaScript parses it - * as 11:00 UTC (server local time). By stripping the Z suffix, we tell Zoom - * to interpret "2026-01-15T11:00:00" in the timezone we specify (America/New_York), - * resulting in the correct 11:00 AM ET meeting time. + * When Claude sends "2026-01-15T13:00:00" for 1 PM, JavaScript parses it + * as 13:00 server local time (UTC on Fly.io). By stripping the Z suffix, + * we tell the APIs to interpret "2026-01-15T13:00:00" in the timezone we + * specify (America/New_York), resulting in the correct 1 PM ET meeting time. */ -function formatDateForZoom(date: Date): string { - // toISOString() returns "2026-01-15T11:00:00.000Z" - // We strip the milliseconds and Z to get "2026-01-15T11:00:00" +function formatDateWithoutZ(date: Date): string { + // toISOString() returns "2026-01-15T13:00:00.000Z" + // We strip the milliseconds and Z to get "2026-01-15T13:00:00" return date.toISOString().replace(/\.\d{3}Z$/, ''); } @@ -103,7 +103,7 @@ export async function scheduleMeeting(options: ScheduleMeetingOptions): Promise< zoomMeeting = await zoom.createMeeting(ZOOM_HOST_EMAIL, { topic: `${workingGroup.name}: ${options.title}`, // Format without Z suffix so Zoom interprets in the specified timezone - start_time: formatDateForZoom(options.startTime), + start_time: formatDateWithoutZ(options.startTime), duration: durationMinutes, timezone, agenda: options.agenda || options.description, @@ -172,15 +172,16 @@ export async function scheduleMeeting(options: ScheduleMeetingOptions): Promise< })); // Build calendar event + // Use formatDateWithoutZ so Google interprets in the specified timezone const eventInput: calendar.CreateCalendarEventInput = { summary: `${workingGroup.name}: ${options.title}`, description: buildCalendarDescription(options.description, options.agenda, zoomMeeting), start: { - dateTime: options.startTime.toISOString(), + dateTime: formatDateWithoutZ(options.startTime), timeZone: timezone, }, end: { - dateTime: endTime.toISOString(), + dateTime: formatDateWithoutZ(endTime), timeZone: timezone, }, attendees: attendeeEmails,