search_events
Find nightlife events by city, date, genre, or area. Filter concerts, festivals, and clubs with date options like tonight or this weekend.
Instructions
Search nightlife events. Supports city, date filters (tonight, this_weekend, ISO date, ISO range), genre, and area.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| city | No | tokyo | |
| date | No | ||
| genre | No | ||
| area | No | ||
| query | No | ||
| limit | No | ||
| offset | No |
Implementation Reference
- src/services/events.ts:726-891 (handler)The core implementation of the searchEvents logic, handling city context resolution, date filtering, database querying, and final formatting of event summaries.
export async function searchEvents( supabase: SupabaseClient, config: AppConfig, input: SearchEventsInput, ): Promise<SearchEventsOutput> { const citySlug = normalizeCity(input.city, config.defaultCity); const city = await getCityContext( supabase, citySlug, config.defaultCountryCode, ); if (!city) { return { city: citySlug, date_filter: input.date || null, events: [], unavailable_city: await unavailableCityPayload( supabase, citySlug, config.nightlifeBaseUrl, config.topLevelCities, ), }; } const now = new Date(); let parsedDate: ReturnType<typeof parseDateFilter>; try { parsedDate = parseDateFilter( input.date, now, city.timezone, city.serviceDayCutoffTime, ); } catch (error) { const message = error instanceof Error ? error.message : "Invalid date filter."; throw new NightlifeError("INVALID_DATE_FILTER", message); } const genreEventIds = input.genre ? await resolveGenreEventIds(supabase, input.genre) : null; if (genreEventIds && genreEventIds.size === 0) { return { city: city.slug, date_filter: parsedDate?.label || null, events: [], unavailable_city: null, }; } const queryText = input.query ? sanitizeIlike(input.query) : ""; // DB ILIKE (unchanged) const queryNeedle = input.query ? normalizeQuery(input.query) : ""; // client-side filter (NEW) const limit = coerceLimit(input.limit); const offset = coerceOffset(input.offset); const needsClientFiltering = Boolean(input.area) || Boolean(queryNeedle) || Boolean(genreEventIds && genreEventIds.size > 0); const baseRange = needsClientFiltering ? { from: 0, to: Math.min(199, offset + limit + 50) } : { from: offset, to: offset + limit - 1 }; let occurrences: EventOccurrenceRow[]; let clientPagingApplied = false; if (genreEventIds && genreEventIds.size > 0) { occurrences = await fetchOccurrencesByIds( supabase, city.id, Array.from(genreEventIds), parsedDate, city.timezone, city.serviceDayCutoffTime, queryText, ); if (!input.area && !queryNeedle) { occurrences = occurrences.slice(offset, offset + limit); clientPagingApplied = true; } } else { let query = supabase .from("event_occurrences") .select(OCCURRENCE_SELECT) .eq("published", true) .eq("city_id", city.id); if (!input.area && !queryNeedle) { query = query.order("featured", { ascending: false }); } query = query .order("start_at", { ascending: true }) .range(baseRange.from, baseRange.to); if (parsedDate) { const window = serviceDateWindowToUtc( parsedDate.startServiceDate, parsedDate.endServiceDateExclusive, city.timezone, city.serviceDayCutoffTime, ); query = query.gte("start_at", window.startIso).lt("start_at", window.endIso); } if (queryText) { query = query.or( `name_en.ilike.%${queryText}%,description_en.ilike.%${queryText}%`, ); } const { data: rows, error } = await query; if (error) { throw new NightlifeError("DB_QUERY_FAILED", "Failed to fetch events.", { cause: error.message, }); } occurrences = (rows || []) as unknown as EventOccurrenceRow[]; } const occurrenceIds = occurrences.map((row) => row.id); const metadata = await fetchOccurrenceMetadata(supabase, occurrenceIds); const fallbackCurrency = defaultCurrencyForCountry(city.countryCode); let summaries = occurrences.map((row) => toEventSummary( row, city.slug, config.nightlifeBaseUrl, fallbackCurrency, metadata, ), ); if (input.area) { summaries = summaries.filter((summary) => matchArea( occurrences.find((row) => row.id === summary.event_id)!, input.area || "", ), ); } if (queryNeedle) { summaries = summaries.filter((summary) => { const row = occurrences.find((occurrence) => occurrence.id === summary.event_id); if (!row) { return false; } return matchQuery(row, queryNeedle, summary.performers, summary.genres); }); } if (needsClientFiltering && !clientPagingApplied) { summaries = summaries.slice(offset, offset + limit); } return { city: city.slug, date_filter: parsedDate?.label || null, events: summaries, unavailable_city: null, }; } - src/tools/events.ts:165-187 (registration)The tool registration for "search_events" in the MCP server, defining the input schema and connecting it to the service function.
export function registerEventTools(server: McpServer, deps: ToolDeps): void { server.registerTool( "search_events", { description: "Search nightlife events. Supports city, date filters (tonight, this_weekend, ISO date, ISO range), genre, and area.", inputSchema: { city: z.string().default(deps.config.defaultCity), date: z.string().optional(), genre: z.string().optional(), area: z.string().optional(), query: z.string().optional(), limit: z.number().int().min(1).max(20).default(10), offset: z.number().int().min(0).default(0), }, outputSchema: searchEventsOutputSchema, }, async (args) => runTool( "search_events", searchEventsOutputSchema, async () => searchEvents(deps.supabase, deps.config, args), ), );