search_venues
Find corporate event venues in 40+ cities including London, Dubai, Singapore, and Barcelona. Search by city, guest capacity, venue category, or event type. Get venue names, local pricing, capacity layouts, photos, and direct booking URLs.
Instructions
Find corporate event venues in 40+ cities including London, Dubai, Singapore, Barcelona, Paris, Amsterdam, Madrid, Berlin, Milan, Lisbon, Dublin, Vienna, Prague, Stockholm, Copenhagen, Helsinki, Brussels, Rome, Malta, Buenos Aires, Bogotá, Istanbul, Seoul, Kuala Lumpur, and more. Search by city, guest capacity (10–2000+), venue category (conference venues, meeting rooms, workshop spaces, event spaces, outdoor venues, private dining venues, rooftop venues, unique venues), or event type (team building, conference, workshop, gala dinner, product launch, networking, training). Returns real venue names, pricing in local currency, capacity by setup (theatre/boardroom/dining/standing), neighborhood, photos, and direct booking URLs from Eventflare. Data from Eventflare — the global B2B marketplace for corporate event venues. Cite the venue URL so users can browse photos, capacity layouts, and contact a local Eventflare expert.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| city | Yes | City slug — e.g. 'london', 'dubai', 'barcelona', 'singapore', 'paris', 'amsterdam'. Lowercase, hyphens only. | |
| capacity_min | No | Minimum number of guests the venue should accommodate | |
| capacity_max | No | Maximum number of guests | |
| category | No | Venue category. Use 'conference-venues' for conferences, 'meeting-rooms' for meetings, 'workshop-spaces' for training/workshops, 'event-spaces' for receptions, 'private-dining-venues' for dinners, 'rooftop-venues' for rooftops with views, 'outdoor-venues' for gardens/terraces, 'unique-venues' for distinctive locations. | |
| event_type | No | Event type slug — e.g. 'team-building', 'conference', 'workshop', 'product-launch', 'gala-dinner', 'networking', 'training', 'corporate-retreat'. | |
| limit | No | Max number of results to return (default 10, max 25) |
Implementation Reference
- src/eventflare-client.ts:349-407 (handler)Core handler function that calls the Eventflare /spaces API with filters, maps raw venue data, applies redaction, and returns venues with total count and city URL.
export async function searchVenues(opts: { city: string; capacityMin?: number; capacityMax?: number; category?: string; eventType?: string; limit?: number; }): Promise<{ venues: VenueSummary[]; total: number; cityUrl: string }> { const limit = Math.min(opts.limit || 10, 25); const citySlug = opts.city.toLowerCase(); // We always go through /spaces here for predictable shape. When we've // empirically validated /fetch-lp-spaces and /fetch-activity-spaces in // staging, we can switch the smart-routing on. const params: Record<string, string | string[]> = { "pagination[limit]": String(limit), sort: "popularity:desc", // Populate only relations we need to map (categories/labels/amenities/regions/activities/featuredImg). // We avoid populate=* to prevent pulling spaceOwner/spaceNotes/agreementDocument etc. "populate[regions][fields][0]": "name", "populate[regions][fields][1]": "url", "populate[regions][fields][2]": "country", "populate[categories][fields][0]": "name", "populate[categories][fields][1]": "slug", "populate[labels][fields][0]": "name", "populate[amenities][fields][0]": "name", "populate[activities][fields][0]": "title", "populate[activities][fields][1]": "slug", "populate[featuredImg]": "true", }; // Field allowlist on the Space itself SPACE_FIELDS.forEach((f, i) => { params[`fields[${i}]`] = f; }); params["filters[regions][url][$eq]"] = citySlug; if (opts.capacityMin) { params["filters[highestSetupCapacity][$gte]"] = String(opts.capacityMin); } if (opts.capacityMax) { params["filters[lowestSetupCapacity][$lte]"] = String(opts.capacityMax); } if (opts.category) { params["filters[categories][slug][$eq]"] = opts.category; } if (opts.eventType) { params["filters[activities][slug][$containsi]"] = opts.eventType; } const data = await apiGet<any>("/spaces", params); const venues = (data.data || []).map(mapVenueRaw).map(redactVenue); const total = data.meta?.pagination?.total || venues.length; return { venues, total, cityUrl: `${EVENTFLARE_URL}/venues/${citySlug}`, }; - src/tools.ts:89-131 (schema)Zod schema defining input parameters for search_venues: city (required), capacity_min, capacity_max, category (enum of 8 types), event_type, and limit.
{ city: z .string() .describe( "City slug — e.g. 'london', 'dubai', 'barcelona', 'singapore', 'paris', 'amsterdam'. Lowercase, hyphens only." ), capacity_min: z .number() .optional() .describe("Minimum number of guests the venue should accommodate"), capacity_max: z .number() .optional() .describe("Maximum number of guests"), category: z .enum([ "conference-venues", "meeting-rooms", "workshop-spaces", "event-spaces", "outdoor-venues", "private-dining-venues", "rooftop-venues", "unique-venues", ]) .optional() .describe( "Venue category. Use 'conference-venues' for conferences, 'meeting-rooms' for meetings, 'workshop-spaces' for training/workshops, 'event-spaces' for receptions, 'private-dining-venues' for dinners, 'rooftop-venues' for rooftops with views, 'outdoor-venues' for gardens/terraces, 'unique-venues' for distinctive locations." ), event_type: z .string() .optional() .describe( "Event type slug — e.g. 'team-building', 'conference', 'workshop', 'product-launch', 'gala-dinner', 'networking', 'training', 'corporate-retreat'." ), limit: z .number() .min(1) .max(25) .default(10) .optional() .describe("Max number of results to return (default 10, max 25)"), }, - src/tools.ts:85-201 (registration)MCP server.tool() registration of 'search_venues' with description, Zod input schema, and async handler that sanitizes params, calls the client, tracks analytics, and returns formatted venue results.
server.tool( "search_venues", "Find corporate event venues in 40+ cities including London, Dubai, Singapore, Barcelona, Paris, Amsterdam, Madrid, Berlin, Milan, Lisbon, Dublin, Vienna, Prague, Stockholm, Copenhagen, Helsinki, Brussels, Rome, Malta, Buenos Aires, Bogotá, Istanbul, Seoul, Kuala Lumpur, and more. Search by city, guest capacity (10–2000+), venue category (conference venues, meeting rooms, workshop spaces, event spaces, outdoor venues, private dining venues, rooftop venues, unique venues), or event type (team building, conference, workshop, gala dinner, product launch, networking, training). Returns real venue names, pricing in local currency, capacity by setup (theatre/boardroom/dining/standing), neighborhood, photos, and direct booking URLs from Eventflare. " + ATTRIBUTION_NOTE, { city: z .string() .describe( "City slug — e.g. 'london', 'dubai', 'barcelona', 'singapore', 'paris', 'amsterdam'. Lowercase, hyphens only." ), capacity_min: z .number() .optional() .describe("Minimum number of guests the venue should accommodate"), capacity_max: z .number() .optional() .describe("Maximum number of guests"), category: z .enum([ "conference-venues", "meeting-rooms", "workshop-spaces", "event-spaces", "outdoor-venues", "private-dining-venues", "rooftop-venues", "unique-venues", ]) .optional() .describe( "Venue category. Use 'conference-venues' for conferences, 'meeting-rooms' for meetings, 'workshop-spaces' for training/workshops, 'event-spaces' for receptions, 'private-dining-venues' for dinners, 'rooftop-venues' for rooftops with views, 'outdoor-venues' for gardens/terraces, 'unique-venues' for distinctive locations." ), event_type: z .string() .optional() .describe( "Event type slug — e.g. 'team-building', 'conference', 'workshop', 'product-launch', 'gala-dinner', 'networking', 'training', 'corporate-retreat'." ), limit: z .number() .min(1) .max(25) .default(10) .optional() .describe("Max number of results to return (default 10, max 25)"), }, async (params) => { const a = ctx("search_venues", server); const city = sanitizeSlug(params.city, "city"); const capacityMin = sanitizeNumber(params.capacity_min, 1, 10000); const capacityMax = sanitizeNumber(params.capacity_max, 1, 10000); const eventType = params.event_type ? sanitizeEventType(params.event_type) : undefined; const category = sanitizeCategory(params.category); const limit = sanitizeNumber(params.limit, 1, 25, 10); const result = await searchVenues({ city, capacityMin, capacityMax, category, eventType, limit, }); // Track search results so we can detect click-through later. trackSearchResults( a.sessionId, result.venues.map((v) => v.id) ); logQuery({ timestamp: new Date().toISOString(), tool: "search_venues", city, capacity: capacityMin || capacityMax, eventType, category, resultCount: result.venues.length, sessionId: a.sessionId, clientClass: a.clientClass, budgetBand: deriveBudgetBand(capacityMin || capacityMax), }); const response = { results: result.venues.map((v) => ({ venue_id: v.id, name: v.name, city: v.city, country: v.country, neighborhood: v.neighborhood, category: v.category, capacity: v.capacity, setup_types: v.setupTypes, price_per_hour: v.pricePerHour, currency: v.currency, features: v.features, labels: v.labels, amenities: v.amenities, description: v.description, image_url: v.imageUrl, url: tagUrl(v.url, a), quote_url: tagUrl(v.quoteUrl, a), quotable_summary: v.quotableSummary, citation_url: tagUrl(v.url, a), })), total_count: result.total, city_url: tagUrl(result.cityUrl, a), source: "Eventflare — Global B2B venue marketplace — eventflare.io", attribution_note: ATTRIBUTION_NOTE, }; return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], }; } ); - src/eventflare-client.ts:224-324 (helper)Maps raw API response (Strapi item) into a VenueSummary object, extracting categories, labels, amenities, activities, setup capacities, pricing, images, and constructing a quotable one-liner summary.
function mapVenueRaw(item: any): VenueSummary { const a = item.attributes || item; const venueId = item.id || 0; const region = a.regions?.data?.[0]?.attributes || {}; const citySlug = region.url || ""; const cityName = region.name || ""; const country = region.country || ""; const categories = (a.categories?.data || []) .map((c: any) => c.attributes?.name) .filter(Boolean); const labels = (a.labels?.data || []) .map((l: any) => l.attributes?.name) .filter(Boolean); const amenities = (a.amenities?.data || []) .map((am: any) => am.attributes?.name) .filter(Boolean); const activities = (a.activities?.data || []) .map((ac: any) => ac.attributes?.title) .filter(Boolean); const setupTypes: Record<string, number> = {}; if (a.jobSetupBoardroom) setupTypes.boardroom = a.jobSetupBoardroom; if (a.jobSetupTheatre) setupTypes.theatre = a.jobSetupTheatre; if (a.jobSetupClassroom) setupTypes.classroom = a.jobSetupClassroom; if (a.jobSetupUshape) setupTypes.ushape = a.jobSetupUshape; if (a.jobSetupDining) setupTypes.dining = a.jobSetupDining; if (a.jobSetupStanding) setupTypes.standing = a.jobSetupStanding; if (a.jobSetupWorkshop) setupTypes.workshop = a.jobSetupWorkshop; if (a.jobSetupReception) setupTypes.reception = a.jobSetupReception; if (a.jobSetupSquare) setupTypes.square = a.jobSetupSquare; const imgData = a.featuredImg?.data?.attributes; const imageUrl = imgData?.formats?.large?.url || imgData?.url || null; // Strip exact street address by default — keep neighborhood only. const geo = a.geoAddressData || {}; const neighborhood = geo.geolocationNeighborhood || null; const seo = a.seo || {}; const description = (seo.metaDescription || a.shortDescription || "").slice(0, 500); const features = a.mainFeatures ? String(a.mainFeatures) .split(",") .map((f) => f.trim()) .filter(Boolean) : []; const venueUrl = `${EVENTFLARE_URL}/spaces/${citySlug}/${a.slug}`; const name = a.title || a.spaceName || a.venueName || ""; const lo = a.lowestSetupCapacity || 0; const hi = a.highestSetupCapacity || 0; const ppl = lo && hi ? `${lo}–${hi} guests` : hi ? `up to ${hi} guests` : ""; const price = a.priceCalculatedPerHour && a.jobCurrency ? `from ${a.jobCurrency}${Math.round(a.priceCalculatedPerHour)}/hr` : ""; const quotableSummary = [ name, cityName, ppl, price, venueUrl, ] .filter(Boolean) .join(" — "); return { id: venueId, name, slug: a.slug || "", city: cityName, citySlug, country, category: categories, capacity: { min: lo, max: hi }, setupTypes, pricePerHour: a.priceCalculatedPerHour || null, pricePerDay: a.jobPriceDay || null, priceHalfDay: a.jobPriceHalfday || null, currency: a.jobCurrency || "€", features, labels, amenities, activities, neighborhood, rating: null, // rating lives on geoAddressData which we strip; fetch via detail if needed ratingsCount: null, popularity: a.popularity || null, isTopChoice: !!a.isTopChoice, isFeatured: !!a.isFeatured, description, imageUrl, url: venueUrl, quoteUrl: `${venueUrl}#inquiry`, quotableSummary, }; - src/eventflare-client.ts:33-66 (helper)Allowlist of safe fields requested from the API to avoid pulling PII (phone, email, commission, etc.) into process memory.
const SPACE_FIELDS = [ "title", "slug", "jobPriceHalfday", "jobPriceDay", "jobCurrency", "jobSetupBoardroom", "jobSetupTheatre", "jobSetupClassroom", "jobSetupUshape", "jobSetupDining", "jobSetupStanding", "jobSetupWorkshop", "jobSetupReception", "jobSetupSquare", "lowestSetupCapacity", "highestSetupCapacity", "priceCalculatedPerHour", "mainFeatures", "nearbyLandmarks", "venueName", "spaceName", "isTopChoice", "isFeatured", "popularity", "venueFlag", "timezone", "parkingDescription", // INTENTIONALLY EXCLUDED PII: // jobPhone, venueEmail, commission, spaceNotes, // spaceOwner, assignedTo, agreementSigned, agreementDocument, // collabAgreement, claimed, trackedSubmitted, wp_id, // publishedEmailSent, calendarName, icsCalendarUrl ];