reunion_list_communes
List all 24 communes of La Réunion with their INSEE codes, EPCI, zone d'emploi, and administrative hierarchy for territorial joins and statistical aggregation.
Instructions
List the 24 communes of La Réunion with their full INSEE administrative attributes: name, INSEE code (5 digits, "974xx"), current code (handles fusions), EPCI code and name, zone d'emploi 2020 name, bassin de vie 2022 name, department, region, year reference. Useful for territorial joins, statistical aggregation, administrative hierarchy reasoning. Use reunion_find_commune (commune module) for fuzzy commune resolution.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| epci_name | No | EPCI name prefix match. Réunion has 5 EPCIs: "CINOR" (north), "TCO" (west), "CIVIS" (south-west), "CASUD" (south), "CIREST" (east) | |
| limit | No | Max communes to return (1-100, default 50). Réunion has 24 communes total |
Implementation Reference
- src/modules/geography.ts:96-128 (handler)The 'reunion_list_communes' tool handler. It queries the 'communes-millesime-france' dataset on data.regionreunion.com, optionally filtering by EPCI name, and returns the 24 communes of La Réunion with full INSEE administrative attributes.
server.tool( 'reunion_list_communes', 'List the 24 communes of La Réunion with their full INSEE administrative attributes: name, INSEE code (5 digits, "974xx"), current code (handles fusions), EPCI code and name, zone d\'emploi 2020 name, bassin de vie 2022 name, department, region, year reference. Useful for territorial joins, statistical aggregation, administrative hierarchy reasoning. Use reunion_find_commune (commune module) for fuzzy commune resolution.', { epci_name: z.string().optional().describe('EPCI name prefix match. Réunion has 5 EPCIs: "CINOR" (north), "TCO" (west), "CIVIS" (south-west), "CASUD" (south), "CIREST" (east)'), limit: z.number().int().min(1).max(100).default(50).describe('Max communes to return (1-100, default 50). Réunion has 24 communes total'), }, async ({ epci_name, limit }) => { try { const data = await client.getRecords<RecordObject>(DATASET_COMMUNES, { where: buildWhere([epci_name ? `epci_name LIKE ${quote(`${epci_name}%`)}` : undefined]), limit, }); return jsonResult({ total_communes: data.total_count, communes: data.results.map((row) => ({ name: pickString(row, ['com_name']), insee_code: pickString(row, ['com_code']), current_code: pickString(row, ['com_current_code']), epci_code: pickString(row, ['epci_code']), epci_name: pickString(row, ['epci_name']), employment_zone: pickString(row, ['ze2020_name']), living_basin: pickString(row, ['bv2022_name']), department: pickString(row, ['dep_name']), region: pickString(row, ['reg_name']), year: pickString(row, ['year']), })), }); } catch (error) { return errorResult(error instanceof Error ? error.message : 'Failed to list communes'); } } ); - src/modules/geography.ts:100-102 (schema)Input schema for 'reunion_list_communes': optional 'epci_name' (string) and 'limit' (number, 1-100, default 50).
epci_name: z.string().optional().describe('EPCI name prefix match. Réunion has 5 EPCIs: "CINOR" (north), "TCO" (west), "CIVIS" (south-west), "CASUD" (south), "CIREST" (east)'), limit: z.number().int().min(1).max(100).default(50).describe('Max communes to return (1-100, default 50). Réunion has 24 communes total'), }, - src/modules/geography.ts:17-17 (registration)The 'reunion_list_communes' tool is registered inside the 'registerGeographyTools' function (line 17), which is called from 'registerAllTools' in src/modules/index.ts (line 43).
export function registerGeographyTools(server: McpServer): void { - src/modules/index.ts:43-43 (registration)Registration call: 'registerGeographyTools(server)' inside 'registerAllTools' which wires all tool modules to the MCP server.
registerGeographyTools(server); - src/client.ts:1-261 (helper)The ReunionClient class (singleton 'client') handles HTTP requests to the OpenDataSoft API (data.regionreunion.com). The 'getRecords' method is used by the handler to fetch commune data. The dataset 'communes-millesime-france' is cached as a referential dataset (line 17).
// src/client.ts import { CatalogResponse, DatasetMetadata, ODSQueryParams, ODSResponse, RecordObject, } from './types.js'; import { quote, normalizeText } from './utils/helpers.js'; // Datasets that are stable registries (INSEE millésimé layers, national nomenclatures). // They update at most once a year, so caching their responses per-session avoids // thousands of redundant upstream calls when the agent cross-references communes / IRIS // across many tools in a single conversation. const REFERENTIAL_DATASETS: ReadonlySet<string> = new Set([ 'communes-millesime-france', 'cantons-millesime-france', 'intercommunalites-millesime-france', 'iris-millesime-france', 'les-20-quartiers-villesaintdenis', 'quartiers-prioritaires-de-la-politique-de-la-ville-qpv', 'laposte_hexasmaldatanova', 'pnrun_2021', ]); const REFERENTIAL_TTL_MS = 24 * 60 * 60 * 1000; /** * HTTP client for OpenDataSoft API (data.regionreunion.com) * No authentication required - completely free API */ export class ReunionClient { private readonly baseUrl = 'https://data.regionreunion.com/api/explore/v2.1/'; private readonly timeout = 30000; private readonly maxRetries = 2; private readonly metadataCache = new Map<string, Promise<DatasetMetadata | undefined>>(); private readonly recordsCache = new Map<string, { value: unknown; expiresAt: number }>(); /** * Fetch records from a dataset */ async getRecords<T extends RecordObject = RecordObject>( datasetId: string, params: ODSQueryParams = {} ): Promise<ODSResponse<T>> { const url = this.buildUrl(`/catalog/datasets/${datasetId}/records`, params); if (REFERENTIAL_DATASETS.has(datasetId)) { const now = Date.now(); const cached = this.recordsCache.get(url); if (cached && cached.expiresAt > now) { return cached.value as ODSResponse<T>; } const value = await this.fetchJson<ODSResponse<T>>(url); this.recordsCache.set(url, { value, expiresAt: now + REFERENTIAL_TTL_MS }); return value; } return this.fetchJson<ODSResponse<T>>(url); } /** * Clear the in-memory caches. Intended for tests. */ clearCaches(): void { this.metadataCache.clear(); this.recordsCache.clear(); } /** * Fetch aggregated data from a dataset */ async getAggregates<T extends RecordObject = RecordObject>( datasetId: string, select: string, options: { where?: string; groupBy?: string; orderBy?: string; limit?: number; } = {} ): Promise<ODSResponse<T>> { const params: Record<string, string | number | undefined> = { select }; if (options.where) params.where = options.where; if (options.groupBy) params.group_by = options.groupBy; if (options.orderBy) params.order_by = options.orderBy; if (options.limit !== undefined) params.limit = options.limit; const url = this.buildUrl(`/catalog/datasets/${datasetId}/aggregates`, params); return this.fetchJson<ODSResponse<T>>(url); } /** * Search across all datasets */ async searchDatasets(query: string): Promise<CatalogResponse> { const url = this.buildUrl('/catalog/datasets', { where: `search(${quote(query)})`, limit: 20, }); return this.fetchJson<CatalogResponse>(url); } /** * List datasets with an optional raw ODSQL where clause. */ async listDatasets( options: { where?: string; limit?: number; offset?: number } = {} ): Promise<CatalogResponse> { const url = this.buildUrl('/catalog/datasets', { where: options.where, limit: options.limit ?? 20, offset: options.offset, }); return this.fetchJson<CatalogResponse>(url); } /** * Fetch dataset metadata from the catalog */ async getDatasetMetadata(datasetId: string): Promise<DatasetMetadata | undefined> { if (!this.metadataCache.has(datasetId)) { const promise = this.fetchJson<CatalogResponse>( this.buildUrl('/catalog/datasets', { where: `dataset_id = ${quote(datasetId)}`, limit: 1, }) ).then((data) => data.results[0]); this.metadataCache.set(datasetId, promise); } return this.metadataCache.get(datasetId); } /** * Check whether a dataset currently exists in the public catalog */ async datasetExists(datasetId: string): Promise<boolean> { return Boolean(await this.getDatasetMetadata(datasetId)); } /** * Resolve the first matching field name for a dataset */ async resolveField( datasetId: string, candidates: string[] ): Promise<string | undefined> { const metadata = await this.getDatasetMetadata(datasetId); const fields = metadata?.fields ?? []; if (fields.length === 0) { return candidates[0]; } const byNormalizedName = new Map( fields.map((field) => [normalizeText(field.name), field.name] as const) ); for (const candidate of candidates) { const direct = byNormalizedName.get(normalizeText(candidate)); if (direct) { return direct; } } const fieldNames = fields.map((field) => field.name); for (const candidate of candidates) { const normalizedCandidate = normalizeText(candidate); const partial = fieldNames.find((fieldName) => normalizeText(fieldName).includes(normalizedCandidate) ); if (partial) { return partial; } } return candidates[0]; } /** * Build URL with query parameters */ private buildUrl( path: string, params: Record<string, string | number | undefined> ): string { const normalizedPath = path.startsWith('/') ? path.slice(1) : path; const url = new URL(normalizedPath, this.baseUrl); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== '') { url.searchParams.set(key, String(value)); } } return url.toString(); } /** * Execute HTTP request with retries and timeout handling */ private async fetchJson<T>(url: string, remainingRetries = this.maxRetries): Promise<T> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json', 'User-Agent': 'mcp-reunion/1.0', }, signal: controller.signal, }); if (!response.ok) { const errorText = await response.text(); if (response.status >= 500 && remainingRetries > 0) { await this.delay(250); return this.fetchJson<T>(url, remainingRetries - 1); } throw new Error( `API error ${response.status}: ${response.statusText}. ${errorText}` ); } return (await response.json()) as T; } catch (error) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error(`Request timeout after ${this.timeout}ms`); } if (remainingRetries > 0 && this.isRetryableError(error)) { await this.delay(250); return this.fetchJson<T>(url, remainingRetries - 1); } throw error; } throw new Error('Unknown error occurred'); } finally { clearTimeout(timeoutId); } } private isRetryableError(error: Error): boolean { return /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/i.test(error.message); } private async delay(ms: number): Promise<void> { await new Promise((resolve) => setTimeout(resolve, ms)); } } // Singleton instance export const client = new ReunionClient();