Skip to main content
Glama
alcylu

Nightlife Search

by alcylu

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
NameRequiredDescriptionDefault
cityNotokyo
dateNo
genreNo
areaNo
queryNo
limitNo
offsetNo

Implementation Reference

  • 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,
      };
    }
  • 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),
        ),
      );

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alcylu/nightlife-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server