reunion_list_festivals
List festivals in La Réunion filtered by discipline or commune. Returns name, location, contact, and artistic discipline details from the Ministère de la Culture census.
Instructions
List festivals taking place in La Réunion: music, performing arts, cinema/audiovisual, books and literature, visual and digital arts. Returns festival name, territorial scope, host commune, postal code, address, website, email, founding year, main period of occurrence, dominant discipline, sub-categories per discipline (music genre, cinema type, etc.). Source: Ministère de la Culture festival census via data.regionreunion.com.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| discipline | No | Dominant discipline prefix match. Examples: "Musique", "Spectacle vivant", "Cinéma", "Livre", "Arts visuels" | |
| commune | No | Host commune name prefix match | |
| limit | No | Max festivals to return (1-100, default 50) |
Implementation Reference
- src/modules/culture.ts:123-163 (handler)Handler function for the reunion_list_festivals tool. Queries the 'liste-des-festivals-a-la-reunion' dataset with optional filters for discipline and commune, maps results to a structured festival response.
server.tool( 'reunion_list_festivals', 'List festivals taking place in La Réunion: music, performing arts, cinema/audiovisual, books and literature, visual and digital arts. Returns festival name, territorial scope, host commune, postal code, address, website, email, founding year, main period of occurrence, dominant discipline, sub-categories per discipline (music genre, cinema type, etc.). Source: Ministère de la Culture festival census via data.regionreunion.com.', { discipline: z.string().optional().describe('Dominant discipline prefix match. Examples: "Musique", "Spectacle vivant", "Cinéma", "Livre", "Arts visuels"'), commune: z.string().optional().describe('Host commune name prefix match'), limit: z.number().int().min(1).max(100).default(50).describe('Max festivals to return (1-100, default 50)'), }, async ({ discipline, commune, limit }) => { try { const data = await client.getRecords<RecordObject>(DATASET_FESTIVALS, { where: buildWhere([ discipline ? `discipline_dominante LIKE ${quote(`${discipline}%`)}` : undefined, commune ? `commune_principale_de_deroulement LIKE ${quote(`${commune}%`)}` : undefined, ]), limit, }); return jsonResult({ total_festivals: data.total_count, festivals: data.results.map((row) => ({ name: pickString(row, ['nom_du_festival']), scope: pickString(row, ['envergure_territoriale']), commune: pickString(row, ['commune_principale_de_deroulement']), postal_code: pickString(row, ['code_postal_de_la_commune_principale_de_deroulement']), address: pickString(row, ['adresse_postale']), website: pickString(row, ['site_internet_du_festival']), email: pickString(row, ['adresse_e_mail']), created_year: pickNumber(row, ['annee_de_creation_du_festival']), period: pickString(row, ['periode_principale_de_deroulement_du_festival']), discipline: pickString(row, ['discipline_dominante']), sub_category_music: pickString(row, ['sous_categorie_musique']), sub_category_cinema: pickString(row, ['sous_categorie_cinema_et_audiovisuel']), sub_category_books: pickString(row, ['sous_categorie_livre_et_litterature']), sub_category_visual_arts: pickString(row, ['sous_categorie_arts_visuels_et_arts_numeriques']), })), }); } catch (error) { return errorResult(error instanceof Error ? error.message : 'Failed to list festivals'); } } ); - src/modules/culture.ts:126-130 (schema)Input schema for reunion_list_festivals: optional discipline and commune prefix filters, and optional limit (default 50, max 100).
{ discipline: z.string().optional().describe('Dominant discipline prefix match. Examples: "Musique", "Spectacle vivant", "Cinéma", "Livre", "Arts visuels"'), commune: z.string().optional().describe('Host commune name prefix match'), limit: z.number().int().min(1).max(100).default(50).describe('Max festivals to return (1-100, default 50)'), }, - src/modules/culture.ts:123-163 (registration)Registration of the reunion_list_festivals tool via McpServer.server.tool() in registerCultureTools.
server.tool( 'reunion_list_festivals', 'List festivals taking place in La Réunion: music, performing arts, cinema/audiovisual, books and literature, visual and digital arts. Returns festival name, territorial scope, host commune, postal code, address, website, email, founding year, main period of occurrence, dominant discipline, sub-categories per discipline (music genre, cinema type, etc.). Source: Ministère de la Culture festival census via data.regionreunion.com.', { discipline: z.string().optional().describe('Dominant discipline prefix match. Examples: "Musique", "Spectacle vivant", "Cinéma", "Livre", "Arts visuels"'), commune: z.string().optional().describe('Host commune name prefix match'), limit: z.number().int().min(1).max(100).default(50).describe('Max festivals to return (1-100, default 50)'), }, async ({ discipline, commune, limit }) => { try { const data = await client.getRecords<RecordObject>(DATASET_FESTIVALS, { where: buildWhere([ discipline ? `discipline_dominante LIKE ${quote(`${discipline}%`)}` : undefined, commune ? `commune_principale_de_deroulement LIKE ${quote(`${commune}%`)}` : undefined, ]), limit, }); return jsonResult({ total_festivals: data.total_count, festivals: data.results.map((row) => ({ name: pickString(row, ['nom_du_festival']), scope: pickString(row, ['envergure_territoriale']), commune: pickString(row, ['commune_principale_de_deroulement']), postal_code: pickString(row, ['code_postal_de_la_commune_principale_de_deroulement']), address: pickString(row, ['adresse_postale']), website: pickString(row, ['site_internet_du_festival']), email: pickString(row, ['adresse_e_mail']), created_year: pickNumber(row, ['annee_de_creation_du_festival']), period: pickString(row, ['periode_principale_de_deroulement_du_festival']), discipline: pickString(row, ['discipline_dominante']), sub_category_music: pickString(row, ['sous_categorie_musique']), sub_category_cinema: pickString(row, ['sous_categorie_cinema_et_audiovisuel']), sub_category_books: pickString(row, ['sous_categorie_livre_et_litterature']), sub_category_visual_arts: pickString(row, ['sous_categorie_arts_visuels_et_arts_numeriques']), })), }); } catch (error) { return errorResult(error instanceof Error ? error.message : 'Failed to list festivals'); } } ); - src/modules/index.ts:37-37 (registration)Tool module registration is called from registerAllTools in modules/index.ts.
registerCultureTools(server); - src/utils/helpers.ts:1-176 (helper)Supporting utilities used by the reunion_list_festivals handler (buildWhere, quote, pickString, pickNumber, jsonResult, errorResult).
// src/utils/helpers.ts import { RecordObject, ToolResult } from '../types.js'; /** * Format data as JSON tool result */ export function jsonResult(data: unknown): ToolResult { return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * Format error as tool result */ export function errorResult(message: string): ToolResult { return { content: [ { type: 'text', text: JSON.stringify({ error: message }, null, 2), }, ], }; } /** * Build ODSQL WHERE clause from conditions */ export function buildWhere( conditions: Array<string | undefined | null | false> ): string | undefined { const valid = conditions.filter((condition): condition is string => Boolean(condition)); return valid.length > 0 ? valid.join(' AND ') : undefined; } /** * Escape a string literal for ODSQL */ export function escapeOdSqlString(value: string): string { return value.replace(/'/g, "''"); } /** * Quote an ODSQL string literal */ export function quote(value: string): string { return `'${escapeOdSqlString(value)}'`; } /** * Format date as YYYY-MM-DD */ export function formatDate(date: Date): string { return date.toISOString().split('T')[0]; } /** * Get today's date as YYYY-MM-DD */ export function today(): string { return formatDate(new Date()); } /** * Calculate days between two dates */ export function daysBetween(date1: string, date2: string): number { const d1 = new Date(date1); const d2 = new Date(date2); const diffTime = d2.getTime() - d1.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Pick the first defined value from a record */ export function pickValue<T = unknown>( record: RecordObject, candidates: string[] ): T | undefined { for (const candidate of candidates) { if (candidate in record) { const value = record[candidate]; if (value !== undefined && value !== null) { // OpenDataSoft v2.1 wraps some text fields as single-element arrays // (e.g. com_name → ["Saint-Denis"]). Unwrap so downstream pickers // see the scalar they expect. if (Array.isArray(value) && value.length === 1) { return value[0] as T; } return value as T; } } } return undefined; } /** * Pick the first string-like value from a record */ export function pickString( record: RecordObject, candidates: string[] ): string | undefined { const value = pickValue(record, candidates); if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } return undefined; } /** * Pick the first numeric value from a record */ export function pickNumber( record: RecordObject, candidates: string[] ): number | undefined { const value = pickValue(record, candidates); if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } return undefined; } /** * Pick the first boolean-like value from a record */ export function pickBoolean( record: RecordObject, candidates: string[] ): boolean | undefined { const value = pickValue(record, candidates); if (typeof value === 'boolean') { return value; } if (typeof value === 'number') { return value !== 0; } if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); if (['true', '1', 'oui', 'yes'].includes(normalized)) { return true; } if (['false', '0', 'non', 'no'].includes(normalized)) { return false; } } return undefined; } /** * Normalize a string for case-insensitive comparisons */ export function normalizeText(value: string): string { return value .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); }