For developers
Build an MCP server on Kidcation
Kidcation is built for AI agents as first-class consumers. Family fit is captured as language-neutral, filterable data — so an agent can discover kid-friendly places, read honest structured tips, and interpret every field, all over three read endpoints. No scraping required; please use them within the fair-use limits below.
The three endpoints
All are public, cacheable GETs. Base URL https://kidcation.app. Start from llms.txt, which maps the site and links the machine-readable attribute registry and sitemap (every place URL).
GET/api/search
Natural-language discovery. Parses the query into family-fit constraints + geo + a semantic part, ranks matching places, and returns each with per-signal score contributions and the attribute keys it confirmed.
- q
- Free-text query, e.g. "quiet resort near Lisbon with a kids club". Max 500 chars.
- must
- Attribute key promoted to a hard filter — a place must confirm it. Repeat for several (must=kids_club&must=step_free).
Rate-limited per IP (20/min) and robots-disallowed: it runs a paid parse+embed per call, so point your MCP server straight at it rather than crawling. Provide at least q or one must.
GET/api/places/{id}
One place as schema.org JSON-LD (LodgingBusiness / TouristDestination) — the identical shape embedded in the HTML page, so HTML and API never drift. Tips are schema.org Review with inLanguage; family-fit is additionalProperty → PropertyValue. There is deliberately no AggregateRating.
GET/api/attributes
The family-fit vocabulary: every attribute key, its type, allowed values, and English label. This is the dictionary that lets you interpret the language-neutral propertyID keys returned by the other two endpoints.
Interpreting the data
Places return schema.org JSON-LD. Family fit rides on additionalProperty as PropertyValue entries: propertyID carries the language-neutral attribute key, name the English label, and value the consensus from published tips. Resolve any propertyID against /api/attributes to get its type, allowed values, and label. Tips are Review objects with inLanguage and are never translated; there is no aggregate star rating by design.
Example: search
curl "https://kidcation.app/api/search?q=beach+hotel+with+a+kids+club&must=kids_club"{
"query": "beach hotel with a kids club",
"mustHave": ["kids_club"],
"results": [
{
"id": "…",
"kind": "hotel",
"name": "…",
"matchScore": 0.82,
"contributions": { "semantic": 0.39, "attribute": 0.34, "corroboration": 0.09 },
"matched": ["kids_club", "beachfront"],
"apiUrl": "https://kidcation.app/api/places/…"
}
]
}matched and contributions make ranking explainable — you can tell an agent why a place ranked where it did, not just that it did.
Wrapping it as MCP
Map one MCP tool to each endpoint — each tool is a thin fetch, no data reshaping needed:
| MCP tool | Endpoint |
|---|---|
| search_places(query, must?) | GET /api/search |
| get_place(id) | GET /api/places/{id} |
| list_attributes() | GET /api/attributes |
A minimal server with the official TypeScript SDK:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { z } from "zod"
const BASE = "https://kidcation.app"
const server = new McpServer({ name: "kidcation", version: "1.0.0" })
server.tool(
"search_places",
{ query: z.string(), must: z.array(z.string()).optional() },
async ({ query, must = [] }) => {
const url = new URL(BASE + "/api/search")
url.searchParams.set("q", query)
for (const key of must) url.searchParams.append("must", key)
const res = await fetch(url)
return { content: [{ type: "text", text: await res.text() }] }
},
)
server.tool("get_place", { id: z.string() }, async ({ id }) => {
const res = await fetch(`${BASE}/api/places/${id}`)
return { content: [{ type: "text", text: await res.text() }] }
})
server.tool("list_attributes", {}, async () => {
const res = await fetch(BASE + "/api/attributes")
return { content: [{ type: "text", text: await res.text() }] }
})Fair use
Search runs live AI parsing per call, so it is rate-limited to 20 requests/minute per IP — cache results and query it only when you actually need fresh discovery. Place and attribute reads are CDN-cached (~1 hour); cache the attribute registry on your side and refresh occasionally rather than per request. Content is community-contributed — attribute your source as Kidcation. Building something heavier, or a place is missing? Reach out via the contact form.