Eventbrite MCP Server¶
This document provides comprehensive documentation for the Eventbrite Model Context Protocol (MCP) server integration in Inttrest, covering setup, configuration, and usage.
๐ฏ Overview¶
The Eventbrite MCP server provides seamless integration with Eventbrite's event platform, allowing Inttrest to discover, fetch, and synchronize event data from one of the world's largest event platforms.
graph TB
subgraph "Eventbrite MCP Architecture"
A[MCP Client] --> B[Eventbrite MCP Server]
B --> C[Eventbrite API]
B --> D[Data Processing]
B --> E[Event Normalization]
D --> F[Event Filtering]
D --> G[Geocoding]
D --> H[Category Mapping]
E --> I[Standardized Event Data]
C --> J[Event Search API]
C --> K[Event Details API]
C --> L[Organization API]
C --> M[Venue API]
end
subgraph "Inttrest Integration"
N[AI Chat Interface] --> A
O[Event Discovery] --> A
P[Map Component] --> A
I --> Q[MongoDB Storage]
I --> R[Real-time Updates]
I --> S[User Interface]
end
style B fill:#e3f2fd
style C fill:#e8f5e8
style I fill:#fff3e0
๐ฆ Installation & Setup¶
Prerequisites¶
- Node.js 18+ with npm/pnpm
- Eventbrite API Key (OAuth token)
- MCP SDK (@modelcontextprotocol/sdk)
Installation¶
# Navigate to MCP servers directory
cd mcp_servers/eventbrite-mcp
# Install dependencies
npm install
# or
pnpm install
# Build the server
npm run build
Configuration¶
Create a .env file in the eventbrite-mcp directory:
# Eventbrite API Configuration
EVENTBRITE_API_KEY=your_eventbrite_oauth_token_here
EVENTBRITE_API_BASE_URL=https://www.eventbriteapi.com/v3
# MCP Server Configuration
MCP_SERVER_PORT=3001
MCP_SERVER_HOST=localhost
# Cache Configuration
CACHE_TTL=300000 # 5 minutes in milliseconds
CACHE_MAX_SIZE=1000
# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=60000 # 1 minute
# Logging
LOG_LEVEL=info
LOG_FILE=eventbrite-mcp.log
Package Dependencies¶
{
"name": "eventbrite-mcp-server",
"version": "1.0.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"axios": "^1.6.0",
"node-cache": "^5.1.2",
"winston": "^3.11.0",
"zod": "^4.1.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"tsx": "^4.0.0"
}
}
๐๏ธ Server Implementation¶
Core Server Structure¶
// src/eventbrite-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { EventbriteAPI } from './api/eventbrite-api.js'
import { EventProcessor } from './processors/event-processor.js'
import { Logger } from './utils/logger.js'
import { z } from 'zod'
const logger = new Logger('EventbriteMCP')
class EventbriteMCPServer {
private server: Server
private eventbriteAPI: EventbriteAPI
private eventProcessor: EventProcessor
constructor() {
this.server = new Server(
{
name: 'eventbrite-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
this.eventbriteAPI = new EventbriteAPI({
apiKey: process.env.EVENTBRITE_API_KEY!,
baseUrl: process.env.EVENTBRITE_API_BASE_URL!,
})
this.eventProcessor = new EventProcessor()
this.setupToolHandlers()
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_events',
description: 'Search for events on Eventbrite',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description: 'Search query for events'
},
location: {
type: 'string',
description: 'Location to search for events (city, address, etc.)'
},
categories: {
type: 'array',
items: { type: 'string' },
description: 'Event categories to filter by'
},
start_date: {
type: 'string',
description: 'Start date filter (ISO format)'
},
end_date: {
type: 'string',
description: 'End date filter (ISO format)'
},
price: {
type: 'string',
enum: ['free', 'paid'],
description: 'Price filter'
},
sort_by: {
type: 'string',
enum: ['date', 'relevance', 'distance'],
description: 'Sort order for results'
},
limit: {
type: 'number',
minimum: 1,
maximum: 100,
description: 'Maximum number of events to return'
}
},
required: []
}
},
{
name: 'get_event_details',
description: 'Get detailed information about a specific event',
inputSchema: {
type: 'object',
properties: {
event_id: {
type: 'string',
description: 'Eventbrite event ID'
}
},
required: ['event_id']
}
},
{
name: 'get_event_categories',
description: 'Get list of available event categories',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'search_venues',
description: 'Search for venues on Eventbrite',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'Location to search for venues'
},
q: {
type: 'string',
description: 'Search query for venues'
}
},
required: []
}
}
]
}
})
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'search_events':
return await this.handleSearchEvents(args)
case 'get_event_details':
return await this.handleGetEventDetails(args)
case 'get_event_categories':
return await this.handleGetEventCategories(args)
case 'search_venues':
return await this.handleSearchVenues(args)
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
logger.error('Tool execution failed:', error)
throw error
}
})
}
private async handleSearchEvents(args: any) {
const searchParams = EventSearchSchema.parse(args)
logger.info('Searching events with params:', searchParams)
const events = await this.eventbriteAPI.searchEvents(searchParams)
const processedEvents = await this.eventProcessor.processEvents(events)
return {
content: [
{
type: 'text',
text: `Found ${processedEvents.length} events from Eventbrite`
}
],
_meta: {
source: 'eventbrite',
count: processedEvents.length,
events: processedEvents
}
}
}
private async handleGetEventDetails(args: any) {
const { event_id } = EventDetailsSchema.parse(args)
logger.info('Fetching event details for:', event_id)
const eventDetails = await this.eventbriteAPI.getEventDetails(event_id)
const processedEvent = await this.eventProcessor.processEvent(eventDetails)
return {
content: [
{
type: 'text',
text: `Event details retrieved for: ${processedEvent.title}`
}
],
_meta: {
source: 'eventbrite',
event: processedEvent
}
}
}
private async handleGetEventCategories(args: any) {
logger.info('Fetching event categories')
const categories = await this.eventbriteAPI.getCategories()
return {
content: [
{
type: 'text',
text: `Retrieved ${categories.length} event categories`
}
],
_meta: {
source: 'eventbrite',
categories: categories
}
}
}
private async handleSearchVenues(args: any) {
const searchParams = VenueSearchSchema.parse(args)
logger.info('Searching venues with params:', searchParams)
const venues = await this.eventbriteAPI.searchVenues(searchParams)
return {
content: [
{
type: 'text',
text: `Found ${venues.length} venues`
}
],
_meta: {
source: 'eventbrite',
venues: venues
}
}
}
async start() {
const transport = new StdioServerTransport()
await this.server.connect(transport)
logger.info('Eventbrite MCP Server started successfully')
}
}
// Schema definitions
const EventSearchSchema = z.object({
q: z.string().optional(),
location: z.string().optional(),
categories: z.array(z.string()).optional(),
start_date: z.string().optional(),
end_date: z.string().optional(),
price: z.enum(['free', 'paid']).optional(),
sort_by: z.enum(['date', 'relevance', 'distance']).optional(),
limit: z.number().min(1).max(100).optional()
})
const EventDetailsSchema = z.object({
event_id: z.string()
})
const VenueSearchSchema = z.object({
location: z.string().optional(),
q: z.string().optional()
})
// Start the server
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new EventbriteMCPServer()
server.start().catch(console.error)
}
export { EventbriteMCPServer }
Eventbrite API Client¶
// src/api/eventbrite-api.ts
import axios, { AxiosInstance } from 'axios'
import NodeCache from 'node-cache'
import { Logger } from '../utils/logger.js'
interface EventbriteConfig {
apiKey: string
baseUrl: string
timeout?: number
}
interface EventSearchParams {
q?: string
location?: string
categories?: string[]
start_date?: string
end_date?: string
price?: 'free' | 'paid'
sort_by?: 'date' | 'relevance' | 'distance'
limit?: number
}
interface VenueSearchParams {
location?: string
q?: string
}
export class EventbriteAPI {
private client: AxiosInstance
private cache: NodeCache
private logger: Logger
constructor(config: EventbriteConfig) {
this.logger = new Logger('EventbriteAPI')
this.client = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 30000,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
}
})
this.cache = new NodeCache({
stdTTL: parseInt(process.env.CACHE_TTL || '300'), // 5 minutes
maxKeys: parseInt(process.env.CACHE_MAX_SIZE || '1000')
})
this.setupInterceptors()
}
private setupInterceptors() {
// Request interceptor for logging
this.client.interceptors.request.use(
(config) => {
this.logger.debug(`API Request: ${config.method?.toUpperCase()} ${config.url}`)
return config
},
(error) => {
this.logger.error('API Request Error:', error)
return Promise.reject(error)
}
)
// Response interceptor for logging and error handling
this.client.interceptors.response.use(
(response) => {
this.logger.debug(`API Response: ${response.status} ${response.config.url}`)
return response
},
(error) => {
this.logger.error('API Response Error:', {
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url
})
return Promise.reject(error)
}
)
}
async searchEvents(params: EventSearchParams) {
const cacheKey = `events:${JSON.stringify(params)}`
const cached = this.cache.get(cacheKey)
if (cached) {
this.logger.debug('Returning cached events')
return cached
}
try {
const queryParams: any = {}
if (params.q) queryParams.q = params.q
if (params.location) queryParams['location.address'] = params.location
if (params.categories?.length) queryParams.categories = params.categories.join(',')
if (params.start_date) queryParams['start_date.range_start'] = params.start_date
if (params.end_date) queryParams['start_date.range_end'] = params.end_date
if (params.price === 'free') queryParams.price = 'free'
if (params.price === 'paid') queryParams.price = 'paid'
if (params.sort_by) queryParams.sort_by = params.sort_by
queryParams.expand = 'venue,category,subcategory,organizer'
queryParams.page_size = Math.min(params.limit || 50, 100)
const response = await this.client.get('/events/search/', {
params: queryParams
})
const events = response.data.events || []
this.cache.set(cacheKey, events)
this.logger.info(`Found ${events.length} events`)
return events
} catch (error) {
this.logger.error('Event search failed:', error)
throw new Error(`Failed to search events: ${error.message}`)
}
}
async getEventDetails(eventId: string) {
const cacheKey = `event:${eventId}`
const cached = this.cache.get(cacheKey)
if (cached) {
this.logger.debug('Returning cached event details')
return cached
}
try {
const response = await this.client.get(`/events/${eventId}/`, {
params: {
expand: 'venue,category,subcategory,organizer,ticket_availability,logo'
}
})
const event = response.data
this.cache.set(cacheKey, event, 600) // Cache for 10 minutes
this.logger.info(`Retrieved details for event: ${event.name?.text}`)
return event
} catch (error) {
this.logger.error('Event details fetch failed:', error)
throw new Error(`Failed to get event details: ${error.message}`)
}
}
async getCategories() {
const cacheKey = 'categories'
const cached = this.cache.get(cacheKey)
if (cached) {
this.logger.debug('Returning cached categories')
return cached
}
try {
const response = await this.client.get('/categories/')
const categories = response.data.categories || []
// Cache for 1 hour since categories don't change often
this.cache.set(cacheKey, categories, 3600)
this.logger.info(`Retrieved ${categories.length} categories`)
return categories
} catch (error) {
this.logger.error('Categories fetch failed:', error)
throw new Error(`Failed to get categories: ${error.message}`)
}
}
async searchVenues(params: VenueSearchParams) {
const cacheKey = `venues:${JSON.stringify(params)}`
const cached = this.cache.get(cacheKey)
if (cached) {
this.logger.debug('Returning cached venues')
return cached
}
try {
const queryParams: any = {}
if (params.location) queryParams['location.address'] = params.location
if (params.q) queryParams.q = params.q
const response = await this.client.get('/venues/search/', {
params: queryParams
})
const venues = response.data.venues || []
this.cache.set(cacheKey, venues)
this.logger.info(`Found ${venues.length} venues`)
return venues
} catch (error) {
this.logger.error('Venue search failed:', error)
throw new Error(`Failed to search venues: ${error.message}`)
}
}
async getVenueDetails(venueId: string) {
const cacheKey = `venue:${venueId}`
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get(`/venues/${venueId}/`)
const venue = response.data
this.cache.set(cacheKey, venue, 1800) // Cache for 30 minutes
return venue
} catch (error) {
this.logger.error('Venue details fetch failed:', error)
throw new Error(`Failed to get venue details: ${error.message}`)
}
}
}
Event Data Processing¶
// src/processors/event-processor.ts
import { Logger } from '../utils/logger.js'
interface StandardEvent {
id: string
title: string
description: string
url: string
start_date: string
end_date?: string
timezone: string
is_online: boolean
location?: {
name: string
address: string
city: string
region: string
country: string
latitude?: number
longitude?: number
}
organizer: {
name: string
description?: string
url?: string
}
category: {
id: string
name: string
short_name: string
}
subcategory?: {
id: string
name: string
}
image_url?: string
ticket_info: {
is_free: boolean
currency?: string
minimum_price?: number
maximum_price?: number
sales_status: string
}
capacity?: number
attendance_count?: number
source: string
source_id: string
created_at: string
updated_at: string
}
export class EventProcessor {
private logger: Logger
constructor() {
this.logger = new Logger('EventProcessor')
}
async processEvents(rawEvents: any[]): Promise<StandardEvent[]> {
this.logger.info(`Processing ${rawEvents.length} events`)
const processedEvents = await Promise.all(
rawEvents.map(event => this.processEvent(event))
)
return processedEvents.filter(event => event !== null) as StandardEvent[]
}
async processEvent(rawEvent: any): Promise<StandardEvent | null> {
try {
// Handle different event data structures from Eventbrite
const event = this.normalizeEventStructure(rawEvent)
const standardEvent: StandardEvent = {
id: `eventbrite_${event.id}`,
title: this.extractText(event.name),
description: this.extractText(event.description) || '',
url: event.url || '',
start_date: event.start?.utc || event.start_date,
end_date: event.end?.utc || event.end_date,
timezone: event.start?.timezone || 'UTC',
is_online: event.is_online_event || false,
location: this.processLocation(event.venue),
organizer: this.processOrganizer(event.organizer),
category: this.processCategory(event.category),
subcategory: this.processSubcategory(event.subcategory),
image_url: this.extractImageUrl(event.logo),
ticket_info: this.processTicketInfo(event),
capacity: event.capacity_integer,
attendance_count: event.attendance_count,
source: 'eventbrite',
source_id: event.id,
created_at: event.created,
updated_at: event.changed
}
this.logger.debug(`Processed event: ${standardEvent.title}`)
return standardEvent
} catch (error) {
this.logger.error('Failed to process event:', error)
return null
}
}
private normalizeEventStructure(event: any): any {
// Handle different API response formats
if (event.event) {
return event.event
}
return event
}
private extractText(textObject: any): string {
if (typeof textObject === 'string') {
return textObject
}
if (textObject?.text) {
return textObject.text
}
if (textObject?.html) {
// Strip HTML tags
return textObject.html.replace(/<[^>]*>/g, '')
}
return ''
}
private processLocation(venue: any): StandardEvent['location'] | undefined {
if (!venue) return undefined
return {
name: venue.name || '',
address: venue.address?.localized_address_display ||
venue.address?.address_1 || '',
city: venue.address?.city || '',
region: venue.address?.region || '',
country: venue.address?.country || '',
latitude: venue.latitude ? parseFloat(venue.latitude) : undefined,
longitude: venue.longitude ? parseFloat(venue.longitude) : undefined
}
}
private processOrganizer(organizer: any): StandardEvent['organizer'] {
return {
name: organizer?.name || 'Unknown Organizer',
description: this.extractText(organizer?.description),
url: organizer?.url
}
}
private processCategory(category: any): StandardEvent['category'] {
return {
id: category?.id || 'other',
name: category?.name || 'Other',
short_name: category?.short_name || category?.name || 'Other'
}
}
private processSubcategory(subcategory: any): StandardEvent['subcategory'] | undefined {
if (!subcategory) return undefined
return {
id: subcategory.id,
name: subcategory.name
}
}
private extractImageUrl(logo: any): string | undefined {
if (!logo) return undefined
// Eventbrite logos have different sizes
if (logo.crop_mask?.width && logo.crop_mask?.height) {
return logo.url
}
if (logo.original?.url) {
return logo.original.url
}
return logo.url
}
private processTicketInfo(event: any): StandardEvent['ticket_info'] {
const ticketInfo: StandardEvent['ticket_info'] = {
is_free: event.is_free || false,
sales_status: 'unknown'
}
// Process ticket availability
if (event.ticket_availability) {
ticketInfo.sales_status = event.ticket_availability.is_sold_out ? 'sold_out' : 'available'
if (!ticketInfo.is_free && event.ticket_availability.minimum_price) {
ticketInfo.currency = event.ticket_availability.minimum_price.currency
ticketInfo.minimum_price = parseFloat(event.ticket_availability.minimum_price.major_value)
}
if (event.ticket_availability.maximum_price) {
ticketInfo.maximum_price = parseFloat(event.ticket_availability.maximum_price.major_value)
}
}
return ticketInfo
}
}
๐ง Configuration & Usage¶
Environment Configuration¶
// src/config/eventbrite-config.ts
export interface EventbriteConfig {
apiKey: string
baseUrl: string
timeout: number
cache: {
ttl: number
maxSize: number
}
rateLimit: {
requests: number
window: number
}
logging: {
level: string
file?: string
}
}
export function loadConfig(): EventbriteConfig {
const requiredEnvVars = ['EVENTBRITE_API_KEY']
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`)
}
}
return {
apiKey: process.env.EVENTBRITE_API_KEY!,
baseUrl: process.env.EVENTBRITE_API_BASE_URL || 'https://www.eventbriteapi.com/v3',
timeout: parseInt(process.env.API_TIMEOUT || '30000'),
cache: {
ttl: parseInt(process.env.CACHE_TTL || '300'),
maxSize: parseInt(process.env.CACHE_MAX_SIZE || '1000')
},
rateLimit: {
requests: parseInt(process.env.RATE_LIMIT_REQUESTS || '100'),
window: parseInt(process.env.RATE_LIMIT_WINDOW || '60000')
},
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE
}
}
}
Running the Server¶
# Start the MCP server
npm start
# Run in development mode with auto-reload
npm run dev
# Run with debug logging
LOG_LEVEL=debug npm start
# Run with custom port
MCP_SERVER_PORT=3001 npm start
Integration with Inttrest¶
// Example usage in the main application
import { MCPClient } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
async function setupEventbriteMCP() {
const transport = new StdioClientTransport({
command: 'node',
args: ['./mcp_servers/eventbrite-mcp/dist/eventbrite-mcp-server.js']
})
const client = new MCPClient(
{
name: 'inttrest-client',
version: '1.0.0',
},
{
capabilities: {}
}
)
await client.connect(transport)
return client
}
async function searchEventbriteEvents(query: string, location?: string) {
const client = await setupEventbriteMCP()
const result = await client.callTool({
name: 'search_events',
arguments: {
q: query,
location: location,
limit: 50
}
})
return result._meta?.events || []
}
๐ API Endpoints & Tools¶
Available MCP Tools¶
| Tool Name | Description | Parameters |
|---|---|---|
search_events |
Search for events | q, location, categories, dates, price, etc. |
get_event_details |
Get detailed event info | event_id |
get_event_categories |
List available categories | None |
search_venues |
Search for venues | location, q |
Example Tool Calls¶
// Search for tech events in San Francisco
const techEvents = await client.callTool({
name: 'search_events',
arguments: {
q: 'technology',
location: 'San Francisco, CA',
categories: ['103'], // Technology category ID
start_date: '2024-02-01T00:00:00Z',
end_date: '2024-02-29T23:59:59Z',
limit: 20
}
})
// Get specific event details
const eventDetails = await client.callTool({
name: 'get_event_details',
arguments: {
event_id: '123456789'
}
})
// Get all categories
const categories = await client.callTool({
name: 'get_event_categories',
arguments: {}
})
๐งช Testing¶
Unit Tests¶
// tests/eventbrite-api.test.ts
import { EventbriteAPI } from '../src/api/eventbrite-api.js'
import { describe, it, expect, beforeEach } from 'vitest'
describe('EventbriteAPI', () => {
let api: EventbriteAPI
beforeEach(() => {
api = new EventbriteAPI({
apiKey: 'test-key',
baseUrl: 'https://api.test.com'
})
})
it('should search events successfully', async () => {
const events = await api.searchEvents({
q: 'test',
location: 'San Francisco'
})
expect(Array.isArray(events)).toBe(true)
})
it('should handle API errors gracefully', async () => {
await expect(
api.getEventDetails('invalid-id')
).rejects.toThrow()
})
})
Integration Tests¶
// tests/mcp-integration.test.ts
import { EventbriteMCPServer } from '../src/eventbrite-mcp-server.js'
describe('Eventbrite MCP Integration', () => {
let server: EventbriteMCPServer
beforeEach(() => {
server = new EventbriteMCPServer()
})
it('should handle search_events tool call', async () => {
const result = await server.handleSearchEvents({
q: 'test event',
location: 'New York'
})
expect(result.content).toBeDefined()
expect(result._meta?.events).toBeDefined()
})
})
๐ Performance & Monitoring¶
Metrics Collection¶
// src/utils/metrics.ts
export class EventbriteMetrics {
private static instance: EventbriteMetrics
private metrics: Map<string, number> = new Map()
static getInstance(): EventbriteMetrics {
if (!EventbriteMetrics.instance) {
EventbriteMetrics.instance = new EventbriteMetrics()
}
return EventbriteMetrics.instance
}
incrementCounter(metric: string): void {
const current = this.metrics.get(metric) || 0
this.metrics.set(metric, current + 1)
}
getMetrics(): Record<string, number> {
return Object.fromEntries(this.metrics)
}
reset(): void {
this.metrics.clear()
}
}
// Usage in API calls
const metrics = EventbriteMetrics.getInstance()
metrics.incrementCounter('api.search_events.calls')
Health Check¶
// src/health/health-check.ts
export async function checkEventbriteHealth(): Promise<{
status: 'healthy' | 'unhealthy'
details: Record<string, any>
}> {
try {
const api = new EventbriteAPI({
apiKey: process.env.EVENTBRITE_API_KEY!,
baseUrl: process.env.EVENTBRITE_API_BASE_URL!
})
// Simple test call
await api.getCategories()
return {
status: 'healthy',
details: {
api_connection: 'ok',
timestamp: new Date().toISOString()
}
}
} catch (error) {
return {
status: 'unhealthy',
details: {
api_connection: 'failed',
error: error.message,
timestamp: new Date().toISOString()
}
}
}
}
This comprehensive documentation covers the Eventbrite MCP server implementation, providing everything needed to integrate Eventbrite event data into the Inttrest platform! ๐ซ