Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/event-visibility-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Internal: Fix event visibility and registration matching (no protocol changes)
42 changes: 30 additions & 12 deletions server/src/addie/mcp/event-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,10 @@ Optional: description, end_time, timezone, location details, virtual_url, max_at
},
},
{
name: 'list_upcoming_events',
description: `List upcoming AAO events. Use this when someone asks about upcoming events, the events calendar, or what's happening soon.`,
name: 'list_events',
description: `List AAO events. Use this when someone asks about events - either upcoming or past events.
When asked about past events, historical events, or what events have happened, set include_past=true.
When asked about upcoming events or what's happening soon, use the defaults (include_past=false).`,
input_schema: {
type: 'object' as const,
properties: {
Expand All @@ -181,6 +183,10 @@ Optional: description, end_time, timezone, location details, virtual_url, max_at
enum: ['summit', 'meetup', 'webinar', 'workshop', 'conference', 'other'],
description: 'Filter by event type',
},
include_past: {
type: 'boolean',
description: 'Include past/completed events (default: false, only shows upcoming)',
},
limit: {
type: 'number',
description: 'Maximum number of events to return (default: 5)',
Expand Down Expand Up @@ -435,27 +441,39 @@ export function createEventToolHandlers(
return response;
});

// List upcoming events
handlers.set('list_upcoming_events', async (input) => {
// List events (upcoming or past)
handlers.set('list_events', async (input) => {
const eventType = input.event_type as EventType | undefined;
const includePast = input.include_past === true;
const limit = Math.min((input.limit as number) || 5, 20);

const events = await eventsDb.listEvents({
status: 'published',
// Query events based on whether we want past or upcoming
const statuses = includePast
? ['published', 'completed'] as const
: ['published'] as const;

let allEvents = await eventsDb.listEvents({
statuses: [...statuses],
event_type: eventType,
upcoming_only: true,
past_only: includePast,
upcoming_only: !includePast,
limit,
});

if (events.length === 0) {
let msg = 'No upcoming events found';
// Sort descending for past events (most recent first)
if (includePast) {
allEvents.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime());
}

if (allEvents.length === 0) {
let msg = includePast ? 'No past events found' : 'No upcoming events found';
if (eventType) msg += ` of type "${eventType}"`;
return msg + '.';
}

let response = `## Upcoming Events\n\n`;
let response = includePast ? `## Past Events\n\n` : `## Upcoming Events\n\n`;

for (const event of events) {
for (const event of allEvents) {
const typeEmoji = {
summit: '🏔️',
meetup: '🤝',
Expand All @@ -476,7 +494,7 @@ export function createEventToolHandlers(
response += ` 💻 Virtual\n`;
}

if (event.luma_url) {
if (!includePast && event.luma_url) {
response += ` 🔗 Register: ${event.luma_url}\n`;
}
response += `\n`;
Expand Down
46 changes: 40 additions & 6 deletions server/src/db/events-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ export class EventsDatabase {
const params: unknown[] = [];
let paramIndex = 1;

if (options.status) {
// Support querying multiple statuses at once
if (options.statuses && options.statuses.length > 0) {
const placeholders = options.statuses.map(() => `$${paramIndex++}`).join(', ');
conditions.push(`status IN (${placeholders})`);
params.push(...options.statuses);
} else if (options.status) {
conditions.push(`status = $${paramIndex++}`);
params.push(options.status);
}
Expand Down Expand Up @@ -362,9 +367,27 @@ export class EventsDatabase {
}

/**
* Get registrations by user
*/
async getUserRegistrations(workosUserId: string): Promise<EventRegistration[]> {
* Get registrations by user (matches by workos_user_id OR by user's email)
* This allows registrations created via Luma import (which only have email)
* to be associated with a user who later signs up with that email.
*
* Security note: This intentionally associates registrations by email to support
* the workflow where external registrations (Luma) are imported by email, and
* users later sign up with the same email to see their history.
*/
async getUserRegistrations(workosUserId: string, userEmail?: string): Promise<EventRegistration[]> {
// If we have an email, also match registrations by email
// This handles Luma imports where workos_user_id wasn't set
if (userEmail) {
const result = await query<EventRegistration>(
`SELECT * FROM event_registrations
WHERE workos_user_id = $1 OR LOWER(email) = LOWER($2)
ORDER BY registered_at DESC`,
[workosUserId, userEmail]
);
return result.rows.map(row => this.deserializeRegistration(row));
}

const result = await query<EventRegistration>(
`SELECT * FROM event_registrations
WHERE workos_user_id = $1
Expand All @@ -390,9 +413,20 @@ export class EventsDatabase {
}

/**
* Check if user is registered for an event
* Check if user is registered for an event (by workos_user_id or email)
*/
async isUserRegistered(eventId: string, workosUserId: string): Promise<boolean> {
async isUserRegistered(eventId: string, workosUserId: string, userEmail?: string): Promise<boolean> {
if (userEmail) {
const result = await query(
`SELECT 1 FROM event_registrations
WHERE event_id = $1 AND (workos_user_id = $2 OR LOWER(email) = LOWER($3))
AND registration_status != 'cancelled'
LIMIT 1`,
[eventId, workosUserId, userEmail]
);
return result.rows.length > 0;
}

const result = await query(
`SELECT 1 FROM event_registrations
WHERE event_id = $1 AND workos_user_id = $2
Expand Down
24 changes: 18 additions & 6 deletions server/src/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,22 +912,34 @@ export function createEventsRouter(): {
// PUBLIC API ROUTES (mounted at /api/events)
// =========================================================================

// GET /api/events - List published upcoming events
// GET /api/events - List public events (published or completed)
publicApiRouter.get("/", async (req: Request, res: Response) => {
try {
const event_type = req.query.event_type as EventType | undefined;
const event_format = req.query.event_format as EventFormat | undefined;
const upcoming_only = req.query.upcoming !== "false"; // Default to upcoming only
const past_only = req.query.past === "true";

// For past events, include both "published" and "completed" status
// For upcoming events, only show "published"
const statuses: EventStatus[] = past_only
? ["published", "completed"]
: ["published"];

const events = await eventsDb.listEvents({
status: "published",
statuses,
event_type,
event_format,
upcoming_only: past_only ? false : upcoming_only,
past_only,
});

// Sort by start_time (ascending for upcoming, descending for past)
// Database returns ASC by default, so only re-sort for past events (DESC)
if (past_only) {
events.sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime());
}

res.json({ events });
} catch (error) {
logger.error({ err: error }, "Error listing public events");
Expand Down Expand Up @@ -1012,8 +1024,8 @@ export function createEventsRouter(): {
});
}

// Check if already registered
const alreadyRegistered = await eventsDb.isUserRegistered(event.id, user.id);
// Check if already registered (by user ID or email)
const alreadyRegistered = await eventsDb.isUserRegistered(event.id, user.id, user.email);
if (alreadyRegistered) {
return res.status(400).json({
error: "Already registered",
Expand Down Expand Up @@ -1072,7 +1084,7 @@ export function createEventsRouter(): {
});
}

const registrations = await eventsDb.getUserRegistrations(user.id);
const registrations = await eventsDb.getUserRegistrations(user.id, user.email);
const registration = registrations.find((r) => r.event_id === event.id);

res.json({ registration: registration || null });
Expand Down Expand Up @@ -1102,7 +1114,7 @@ export function createEventsRouter(): {
});
}

const registrations = await eventsDb.getUserRegistrations(user.id);
const registrations = await eventsDb.getUserRegistrations(user.id, user.email);
const registration = registrations.find((r) => r.event_id === event.id);

if (!registration) {
Expand Down
1 change: 1 addition & 0 deletions server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ export interface UpdateEventInput {

export interface ListEventsOptions {
status?: EventStatus;
statuses?: EventStatus[]; // Query multiple statuses at once
event_type?: EventType;
event_format?: EventFormat;
upcoming_only?: boolean;
Expand Down