Kidcation

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 toolEndpoint
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.