One typed core, three surfaces: a REST API with an OpenAPI 3.1 spec, a remote MCP server, and an agent-readable layer. All three read the same data as the site itself — no upstream call happens in a request path, and every value carries its source, vintage and extraction date. Missing data comes back asnull with a reason, never a zero or an imputed average.
Base URL: https://www.expatliving.it/api/v1. Spec: /api/v1/openapi.json (OpenAPI 3.1, importable unmodified as a Custom GPT Action).
curl https://www.expatliving.it/api/v1/comuni/075029
curl https://www.expatliving.it/api/v1/datasetsAnonymous, no key required. Responses are cached for a day at the edge — the data changes on an ETL run, not per request.
Streamable HTTP at https://www.expatliving.it/api/v1/mcp. Read-only, no auth. One tool today — get_comune — generated from the same schema as the REST endpoint above, so the two can never drift apart.
{
"mcpServers": {
"expatliving-data": {
"url": "https://www.expatliving.it/api/v1/mcp"
}
}
}Paste that into Claude Desktop's or Claude Code's MCP config, or any client that speaks Streamable HTTP MCP.
/llms.txt — what this site is, what it holds, and where the API and MCP endpoints are.
GET /v1/comuni/{istatCode} and GET /v1/datasets. The full build order — search/filter, province and nationality endpoints, tax eligibility, JSON twins on every page, JSON-LD, and bulk snapshots — is tracked in docs/data-api-brief.md.