{
  "protocolVersion": "2025-11-25",
  "serverInfo": {
    "name": "livedot",
    "title": "livedot MCP",
    "version": "1.0.0",
    "description": "Where someone is right now in a live running or cycling race, with a live map; running and cycling clubs near you; coaches and training groups near you; and race-day weather and course flyovers. Windline for cycling, RunFast for running. Free.",
    "websiteUrl": "https://livedot.me/mcp",
    "icons": [
      {
        "src": "https://livedot.me/brand/icon.svg",
        "mimeType": "image/svg+xml",
        "sizes": [
          "any"
        ]
      },
      {
        "src": "https://livedot.me/brand/png/icon-512.png",
        "mimeType": "image/png",
        "sizes": [
          "512x512"
        ]
      }
    ]
  },
  "description": "Where someone is right now in a live running or cycling race, with a live map; running and cycling clubs near you; coaches and training groups near you; and race-day weather and course flyovers. Windline for cycling, RunFast for running. Free.",
  "transport": {
    "type": "streamable-http",
    "endpoint": "https://livedot.me/mcp"
  },
  "capabilities": {
    "tools": {
      "listChanged": false
    },
    "resources": {
      "listChanged": false
    },
    "prompts": {
      "listChanged": false
    }
  },
  "instructions": "livedot: live race tracking and route weather for runners and cyclists (Windline for cycling, RunFast for running). When the user shares a GPX file, a route link or a route's points and asks about the weather, wind, heat, a flyover or a video, call plan_route (pass an attached file as gpx_file; or, if you can read it, its text as gpx or its points as points). Never treat a file name as an event name. When they ask where someone is in a race, call find_person, then where_is with the person id; if several people match, ask one short question and never guess. For how a race, a club or a coach's athletes are doing, use event_status or club_in_race (with coach for \"Coach Nadia's runners in the Comrades\"); for a race's weather, event_weather; to fly a course, fly_route; for a cycling tour, tour_route (tour_notes for the organiser's notes); for clubs or events near a place, find_clubs or find_events; for a coach, trainer, coaching or training group, find_coaches, never find_clubs. Each answer has a card; don't restate the whole card. Only public data: findable people on public races. Organisers can sign in (OAuth) to draft their own events; nothing is published without the user's yes.",
  "authentication": {
    "required": false,
    "schemes": [
      "oauth2"
    ],
    "note": "Every read tool works without an account; organisers sign in (OAuth 2.1) for the event tools.",
    "protectedResourceMetadata": "https://livedot.me/.well-known/oauth-protected-resource/mcp"
  },
  "tools": [
    {
      "name": "find_person",
      "title": "Find someone live",
      "description": "Finds a person who is live on livedot right now (cycling on Windline, running on RunFast) and answers where they are. Use it when the user asks where someone is or how they're doing in a race (\"Where's Megan in the Comrades?\"), or gives a livedot handle (@megan, livedot.me/megan) or a follow link. Inputs: name_or_handle as the user wrote it; event if they named the race; surname, bib, club, place or wave to narrow. Returns: for one clear match, the live answer (position, time against goal, finish estimate, the weather there) with a live map; for several matches, the 5 most likely and the total, so the user can pick (then call where_is with the chosen person id, or call again with what they add). Limits: only people who chose to be findable on public races can be found by name; a ride or run of their own (Ride live, Run live) only by their handle or follow link, never showing where it starts or ends. Read-only.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name_or_handle": {
            "type": "string",
            "description": "The person's name as the user said it (\"Megan\", \"Megan Naidoo\"), a livedot handle exactly as typed (\"@megan\", \"livedot.me/megan\"; a capitalised name like \"Megan\" is searched as a name), or a follow link (livedot.me/megan, windline.tech/f/…, runfast.tech/f/…)."
          },
          "event": {
            "type": "string",
            "description": "The race, if named: \"Comrades\", \"Coast Classic\". Narrows the search to that race."
          },
          "surname": {
            "type": "string",
            "description": "A surname or its first letter, to narrow many matches."
          },
          "bib": {
            "type": "string",
            "description": "A bib number, if the user gave one."
          },
          "club": {
            "type": "string",
            "description": "A club, if the user gave one."
          },
          "place": {
            "type": "string",
            "description": "A home town or country, if the user gave one."
          },
          "wave": {
            "type": "string",
            "description": "A start wave or start time, if the user gave one."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "name_or_handle"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Find someone live",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Finding them in the race…",
        "openai/toolInvocation/invoked": "Found",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "One match: the person's live answer (view \"person\"). Several: the 5 most likely (view \"picker\").",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "person",
              "picker",
              "club",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "id": {
            "type": "string",
            "description": "The person id to pass to where_is (\"h:<handle>\", \"<app>:f:<code>\" or \"<app>:p:<id>\")."
          },
          "name": {
            "type": "string",
            "description": "Their name as the public live page shows it."
          },
          "app": {
            "type": "string",
            "enum": [
              "windline",
              "runfast"
            ],
            "description": "windline (cycling) or runfast (running)."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "status": {
            "type": "string",
            "description": "live, delayed, nosignal, paused, waiting, finished or stopped."
          },
          "race": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race (or ride) its members are in."
          },
          "team": {
            "description": "Their club and coach, as they chose to show them: { club: { slug, name, logo, url } | null, coach: { slug, name, url, demo? } | null }, or null (none set, or a private race)."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their profile picture (from windline.tech or runfast.tech), only for someone shown by their full name (Findable)."
          },
          "can_tap": {
            "type": "boolean",
            "description": "A tap on the back would reach them now (taps on; live or in the day after the finish; this asker may tap)."
          },
          "follow_url": {
            "type": "string",
            "description": "The public Follow live link."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "The units the text answer uses."
          },
          "position": {
            "type": "object",
            "description": "Only when the public live page shows where they are: km, mile, of_km, percent, seen (ISO time)."
          },
          "goal": {
            "type": "object",
            "description": "Their goal time and how far behind (positive) or ahead (negative) of it they are, in seconds."
          },
          "pace": {
            "type": "object",
            "description": "Running: per_km_s; cycling: kmh; with words."
          },
          "conditions": {
            "type": "object",
            "description": "The weather where they are: temp_c, feels_c, wind_kmh, gust_kmh, wind_from_deg, along_road_kmh, effect (head, tail, cross, calm), rain_mm_h."
          },
          "ahead": {
            "type": "array",
            "description": "The next stretches of the course: from_km, to_km, at (ISO), temp_c, effect, along_road_kmh, wind_kmh (running: heat_stress)."
          },
          "road_ahead": {
            "type": "object",
            "description": "The Windline forecast engine's rating of the road ahead: rating (good, fair, tough), why."
          },
          "forecast_updated": {
            "type": "string",
            "description": "When the forecast was fetched (ISO)."
          },
          "finished": {
            "type": "object",
            "description": "When they have finished: time (h:mm:ss) and at (ISO)."
          },
          "eta": {
            "type": "object",
            "description": "The finish time their pace points to: at (ISO), local, tz."
          },
          "total": {
            "type": "string",
            "description": "Several matches: how many (\"24\", or \"20+\" when capped)."
          },
          "capped": {
            "type": "boolean",
            "description": "Several matches: true when there are more than the count shows."
          },
          "people": {
            "type": "array",
            "description": "Several matches: the 5 most likely, each with id, name, sport, race, status, where, pace, goal, bib, club. Ask the user to pick; never guess."
          },
          "club": {
            "type": "object",
            "description": "The club: name, app (windline or runfast), slug, url (its page)."
          },
          "count": {
            "type": "number",
            "description": "How many of the club's people are on its live map in that race."
          },
          "finished_count": {
            "type": "number",
            "description": "How many of them have finished."
          },
          "on_course": {
            "type": "number",
            "description": "How many are on the course."
          },
          "members": {
            "type": "array",
            "description": "Each member on the club's live map (Findable): id (for where_is), name, photo, status, sub (km and goal delta, or the finish time), km (null near the start or finish), goal_delta, can_tap."
          },
          "can_tap_team": {
            "type": "boolean",
            "description": "A team tap would reach at least one of them now."
          },
          "club_url": {
            "type": "string",
            "description": "The club's live map."
          },
          "clubs": {
            "type": "array",
            "description": "Several clubs matched: name, app, people. Ask which one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "where_is",
      "title": "Where someone is, live",
      "description": "Where someone is right now, in a live race or on a ride or run of their own (Ride live, Run live), and the weather they're in: km and mile on the course, time against their goal, pace, the finish time it points to, the temperature and wind there (headwind, tailwind or side-on) with the Windline forecast engine's rating, the wind and heat over the next few km, and a live map with a Follow live button. Use it with the person id find_person returned, or straight away with a livedot handle or follow link the user gave. It answers only what the public live page shows: if they're paused, stopped, near the start or finish, or not sharing, it says it can't share where they are; say so plainly.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "person": {
            "type": "string",
            "description": "A person id from find_person, e.g. \"runfast:p:pdemo00megan\" or \"h:megan\"."
          },
          "handle": {
            "type": "string",
            "description": "A livedot handle (\"megan\") or link (livedot.me/megan)."
          },
          "code": {
            "type": "string",
            "description": "A follow link or its code (windline.tech/f/<code>, runfast.tech/f/<code>). \"demo\" with app gives the demo."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "additionalProperties": false
      },
      "annotations": {
        "title": "Where someone is, live",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Getting their live position…",
        "openai/toolInvocation/invoked": "Live position ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "The person's live answer: where they are, time against their goal, the finish it points to and the weather there.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "person",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "id": {
            "type": "string",
            "description": "The person id to pass to where_is (\"h:<handle>\", \"<app>:f:<code>\" or \"<app>:p:<id>\")."
          },
          "name": {
            "type": "string",
            "description": "Their name as the public live page shows it."
          },
          "app": {
            "type": "string",
            "enum": [
              "windline",
              "runfast"
            ],
            "description": "windline (cycling) or runfast (running)."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "status": {
            "type": "string",
            "description": "live, delayed, nosignal, paused, waiting, finished or stopped."
          },
          "race": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race they are in, or null for a ride of their own."
          },
          "team": {
            "description": "Their club and coach, as they chose to show them: { club: { slug, name, logo, url } | null, coach: { slug, name, url, demo? } | null }, or null (none set, or a private race)."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their profile picture (from windline.tech or runfast.tech), only for someone shown by their full name (Findable)."
          },
          "can_tap": {
            "type": "boolean",
            "description": "A tap on the back would reach them now (taps on; live or in the day after the finish; this asker may tap)."
          },
          "follow_url": {
            "type": "string",
            "description": "The public Follow live link."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "The units the text answer uses."
          },
          "position": {
            "type": "object",
            "description": "Only when the public live page shows where they are: km, mile, of_km, percent, seen (ISO time)."
          },
          "goal": {
            "type": "object",
            "description": "Their goal time and how far behind (positive) or ahead (negative) of it they are, in seconds."
          },
          "pace": {
            "type": "object",
            "description": "Running: per_km_s; cycling: kmh; with words."
          },
          "conditions": {
            "type": "object",
            "description": "The weather where they are: temp_c, feels_c, wind_kmh, gust_kmh, wind_from_deg, along_road_kmh, effect (head, tail, cross, calm), rain_mm_h."
          },
          "ahead": {
            "type": "array",
            "description": "The next stretches of the course: from_km, to_km, at (ISO), temp_c, effect, along_road_kmh, wind_kmh (running: heat_stress)."
          },
          "road_ahead": {
            "type": "object",
            "description": "The Windline forecast engine's rating of the road ahead: rating (good, fair, tough), why."
          },
          "forecast_updated": {
            "type": "string",
            "description": "When the forecast was fetched (ISO)."
          },
          "finished": {
            "type": "object",
            "description": "When they have finished: time (h:mm:ss) and at (ISO)."
          },
          "eta": {
            "type": "object",
            "description": "The finish time their pace points to: at (ISO), local, tz."
          },
          "total": {
            "type": "string",
            "description": "Several matches: how many (\"24\", or \"20+\" when capped)."
          },
          "capped": {
            "type": "boolean",
            "description": "Several matches: true when there are more than the count shows."
          },
          "people": {
            "type": "array",
            "description": "Several matches: the 5 most likely, each with id, name, sport, race, status, where, pace, goal, bib, club. Ask the user to pick; never guess."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "event_status",
      "title": "How a live race is going",
      "description": "Shows how a live race on livedot is going. Use it when the user asks how a race is going or who is leading (\"How's the Comrades going?\", \"Who's leading the Coast Classic?\"). Inputs: event (the race's name). Returns: whether it's live, how long it has been under way, how many people are on the public map, how many have finished, and the leaders the public race map shows, with a live map. If several races match, it lists them so the user can choose. Limits: public races only; only what the public race map shows. Read-only.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "description": "The race's name, e.g. \"Comrades\", \"Coast Classic demo\"."
          },
          "club": {
            "type": "string",
            "description": "A club or team, if the user asked about one in this race (as club_in_race)."
          },
          "coach": {
            "type": "string",
            "description": "A coach, if the user asked about their athletes in this race (as club_in_race)."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "event"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "How a live race is going",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Reading the race…",
        "openai/toolInvocation/invoked": "Race ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "How a live race is going.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "event",
              "club",
              "picker",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "club": {
            "type": "object",
            "description": "The club: name, app (windline or runfast), slug, url (its page)."
          },
          "race": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race (or ride) its members are in."
          },
          "count": {
            "type": "number",
            "description": "How many of the club's people are on its live map in that race."
          },
          "finished_count": {
            "type": "number",
            "description": "How many of them have finished."
          },
          "on_course": {
            "type": "number",
            "description": "How many are on the course."
          },
          "members": {
            "type": "array",
            "description": "Each member on the club's live map (Findable): id (for where_is), name, photo, status, sub (km and goal delta, or the finish time), km (null near the start or finish), goal_delta, can_tap."
          },
          "can_tap_team": {
            "type": "boolean",
            "description": "A team tap would reach at least one of them now."
          },
          "club_url": {
            "type": "string",
            "description": "The club's live map."
          },
          "clubs": {
            "type": "array",
            "description": "Several clubs matched: name, app, people. Ask which one."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race's name."
          },
          "app": {
            "type": "string",
            "enum": [
              "windline",
              "runfast"
            ]
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "started": {
            "type": [
              "string",
              "null"
            ],
            "description": "When it got under way (ISO)."
          },
          "on_map": {
            "description": "How many people are on livedot's map."
          },
          "finished": {
            "type": "number",
            "description": "How many have finished."
          },
          "leaders": {
            "type": "array",
            "description": "The leaders the public race map shows: name, and finished (time) or km and where (null km near the start or finish)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race's live page."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "upcoming": {
            "type": "array",
            "description": "Not live yet: upcoming races with name, start, url."
          },
          "races": {
            "type": "array",
            "description": "Several races matched: name, app, people. Ask which one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "club_in_race",
      "title": "How a club is doing in a race",
      "description": "Shows how a club, team or coach's athletes are doing in a live race. Use it when the user asks about a club or team in a race (\"How is the UCT team doing in the Comrades?\") or a coach's athletes (\"How are Coach Nadia's runners doing in the Comrades?\"). Inputs: club (its name or page address, windline.tech/c/<club> or runfast.tech/c/<club>) or coach; event if named. Returns: each member's km, time against goal or finish time, and a map with just their dots; each person opens their live card. If several clubs or coaches match, or their people are live in several races, it lists them so the user can choose. Limits: only what the club's public live map shows: members who chose to be findable, never a private run, never a position near the start or finish. Read-only.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "club": {
            "type": "string",
            "description": "The club or team as the user said it (\"UCT\", \"the UCT team\", \"Coast Riders\"), or its page address (runfast.tech/c/uct)."
          },
          "coach": {
            "type": "string",
            "description": "Instead of club: a coach as the user said it (\"Coach Nadia\", \"Nadia Pillay\"), or the coach id a picker gave (\"runfast:coach-nadia-pillay\")."
          },
          "event": {
            "type": "string",
            "description": "The race, if named: \"Comrades\", \"Coast Classic\"."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "additionalProperties": false
      },
      "annotations": {
        "title": "How a club is doing in a race",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Finding the team on the course…",
        "openai/toolInvocation/invoked": "Team ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "A club or team in a race: its people on the club's live map, with their km and goal deltas.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "club",
              "picker",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "club": {
            "type": "object",
            "description": "The club: name, app (windline or runfast), slug, url (its page)."
          },
          "race": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race (or ride) its members are in."
          },
          "count": {
            "type": "number",
            "description": "How many of the club's people are on its live map in that race."
          },
          "finished_count": {
            "type": "number",
            "description": "How many of them have finished."
          },
          "on_course": {
            "type": "number",
            "description": "How many are on the course."
          },
          "members": {
            "type": "array",
            "description": "Each member on the club's live map (Findable): id (for where_is), name, photo, status, sub (km and goal delta, or the finish time), km (null near the start or finish), goal_delta, can_tap."
          },
          "can_tap_team": {
            "type": "boolean",
            "description": "A team tap would reach at least one of them now."
          },
          "club_url": {
            "type": "string",
            "description": "The club's live map."
          },
          "clubs": {
            "type": "array",
            "description": "Several clubs matched: name, app, people. Ask which one."
          },
          "coach": {
            "description": "A coach's athletes (instead of club): name, app, slug, url, demo."
          },
          "pick": {
            "description": "A picker: club, coach or race."
          },
          "options": {
            "description": "A picker: each club, coach or race to choose from (id to pass back, name, app, sport, area, live_count)."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "event_weather",
      "title": "Race-day weather for an event",
      "description": "What the weather will be like for a cycling or running event, for the people taking part: \"What will the weather be like for the NYC Marathon?\", \"Weather for the Comrades demo?\". Within 10 days of the start: the live forecast (Apple Weather) along the course at the times the riders or runners get to each stretch, with the Windline forecast engine's rating (Good, Fair, Tough) and why, the headwind and tailwind stretches, heat stress and rain. Further out: 10 years of weather on that date (Copernicus ERA5), clearly labelled as history, not a forecast. With a map of the course coloured by the wind or the heat, and a link to the event's livedot page. If several events match it lists them: ask which one. Only for named events and races. Not for the user's own route or GPX file; for those use plan_route.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "description": "The event's name, e.g. \"NYC Marathon\", \"Gravel Burn\", \"Comrades demo\"."
          },
          "year": {
            "type": "integer",
            "description": "The edition's year, if the user named one."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "event"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Race-day weather for an event",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Reading the weather on the course…",
        "openai/toolInvocation/invoked": "Race-day weather ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "The weather for an event along its course, rated by the Windline forecast engine.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "event_weather",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's name."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "basis": {
            "type": "string",
            "enum": [
              "forecast",
              "historic",
              "mixed"
            ],
            "description": "forecast (Apple Weather, within 10 days), historic (10 years of Copernicus ERA5 on that date) or mixed."
          },
          "timed_for": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the times assume (the start, the pace)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's page."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "days": {
            "type": "array",
            "description": "Each day: name, date (ISO), basis, rating (good, fair, tough), why, start_temp_c, finish_temp_c, effort_pct (cycling), heat_stress and wbgt_c (running), rain_mm or rain_days_in_10 and usual_high_c."
          },
          "events": {
            "type": "array",
            "description": "Several events matched: their names. Ask the user which one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "fly_route",
      "title": "Fly the course",
      "description": "Fly over a race course inline: \"Fly the Comrades\", \"Show me the course\", \"What does the NYC Marathon route look like?\". Returns a flyover of the course on a map that plays in the chat, coloured by the weather along it (the live forecast within 10 days of the start, 10 years of weather on that date beyond it, as event_weather), with a \"Fly it in 3D\" link to the full Fly the route and the event's livedot page. If several events match it lists them: ask which one. For a route the user gave as a GPX file or link (their own route), use plan_route: it flies over that route too.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event_or_route": {
            "type": "string",
            "description": "The event or route, e.g. \"Comrades\", \"NYC Marathon\", \"Gravel Burn\"."
          },
          "year": {
            "type": "integer",
            "description": "The edition's year, if named."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "event_or_route"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Fly the course",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Preparing the flyover…",
        "openai/toolInvocation/invoked": "Flyover ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "A flyover of the course, with the weather along it.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "fly_route",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's name."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "basis": {
            "type": "string",
            "enum": [
              "forecast",
              "historic",
              "mixed"
            ],
            "description": "forecast (Apple Weather, within 10 days), historic (10 years of Copernicus ERA5 on that date) or mixed."
          },
          "timed_for": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the times assume (the start, the pace)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's page."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "days": {
            "type": "array",
            "description": "Each day: name, date (ISO), basis, rating (good, fair, tough), why, start_temp_c, finish_temp_c, effort_pct (cycling), heat_stress and wbgt_c (running), rain_mm or rain_days_in_10 and usual_high_c."
          },
          "events": {
            "type": "array",
            "description": "Several events matched: their names. Ask the user which one."
          },
          "fly_url": {
            "type": "string",
            "description": "Fly it in 3D."
          },
          "km": {
            "type": "number",
            "description": "The course's length in km."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "tour_route",
      "title": "A cycling tour's route",
      "description": "A Windline cycling tour's route, day by day: \"What's the Durban Tour 2027 route?\", \"Give me the GPX files for Alistair's tour\", \"Where is Alistair's Durban tour?\". On the tour's days its card offers a tap on the back for the organiser (tap_on_the_back with tour). To fly over day 3 of a tour (\"Give me the flyover for day 3 of TSF 2026\"), pass day: 3. Finds a published tour by its name, its address or its organiser's or club's name, and returns each day (from and to, distance, climbing, start time), a map of every day, and the weather (the live forecast within 10 days, 10 years of weather on the date beyond). Route files (GPX, Garmin .fit) only when the organiser lets riders download them; otherwise say \"The organiser hasn't made the route files available.\" Never make up a file link. If several tours match, ask which.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "tour": {
            "type": "string",
            "description": "The tour's name, address or organiser, e.g. \"ProLend 2027 Durban Tour\", \"AllyCat\"."
          },
          "day": {
            "type": "integer",
            "minimum": 1,
            "description": "One day of the tour to fly over (1 for day 1), when the user asks for a day: the flyover of that day and its Fly it in 3D link."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "tour"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "A cycling tour's route",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Reading the tour…",
        "openai/toolInvocation/invoked": "Tour ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "A Windline cycling tour's route, day by day.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "tour_route",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "can_tap": {
            "type": "boolean",
            "description": "A tap on the back for the tour would reach its organiser now (its days; their switches on)."
          },
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tour's address (for tap_on_the_back tour)."
          },
          "tour": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tour's name."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tour's page."
          },
          "day": {
            "type": "number",
            "description": "With day: the day flown over (1 for day 1)."
          },
          "day_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "With day: that day's page."
          },
          "fly_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "With day: Fly it in 3D, Windline's Fly the route for that day."
          },
          "route_files": {
            "type": "boolean",
            "description": "Whether the organiser lets riders download the route files."
          },
          "gpx": {
            "type": "array",
            "description": "When route_files: each day's GPX link (day, url). Never make one up."
          },
          "days": {
            "type": "array",
            "description": "Each day: label, from, to, km, climb_m, start (ISO), and its weather."
          },
          "tours": {
            "type": "array",
            "description": "Several tours matched: their names. Ask which one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "tour_notes",
      "title": "A cycling tour's notes for riders (beta)",
      "description": "Beta, cycling tours only: the organiser's own notes for riders on a Windline tour (what to pack, how the tour runs, meeting points), for questions like \"What should I pack for the Durban tour?\". Answer only from the notes: quote and summarise them as the organiser's words, never invent anything, and say so when they don't cover the question.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "tour": {
            "type": "string",
            "description": "The tour's name, address or organiser."
          },
          "question": {
            "type": "string",
            "description": "The rider's question, if any."
          }
        },
        "required": [
          "tour"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "A cycling tour's notes for riders (beta)",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ],
        "openai/toolInvocation/invoking": "Reading the organiser's notes…",
        "openai/toolInvocation/invoked": "Notes ready"
      },
      "outputSchema": {
        "type": "object",
        "description": "A cycling tour's notes for riders, as the organiser wrote them.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "tour_notes",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "beta": {
            "type": "boolean",
            "description": "tour_notes is in beta."
          },
          "tour": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tour's name."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "The organiser's notes for riders, in their words (markdown); null when there are none."
          },
          "notes_updated": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the notes last changed (ISO)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tour's page."
          },
          "tours": {
            "type": "array",
            "description": "Several tours matched: their names. Ask which one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "tap_on_the_back",
      "title": "Give someone a tap on the back",
      "description": "Sends a tap on the back: a short buzz on the person's phone, with the sender's name if signed in (or a first name they give). In the day after a finish it arrives as a congratulation. Use it only when the user explicitly asks to give someone a tap on the back; one tap per request. Inputs: person (an id from find_person), or a livedot handle or follow link; or tour (the tap goes to the tour's organiser on its days); or club with event (one tap to each member on the club's live map who accepts taps). Side effects: sends a notification to the person (or to each club member, and it reports how many). Limits: follows each person's own settings; if taps are off, they aren't live, or they only accept taps from people with their link, it sends nothing and says so. Returns the live map.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "person": {
            "type": "string",
            "description": "A person id from find_person, e.g. \"runfast:p:pdemo00megan\" or \"h:megan\"."
          },
          "handle": {
            "type": "string",
            "description": "A livedot handle (\"megan\") or link (livedot.me/megan)."
          },
          "code": {
            "type": "string",
            "description": "A follow link or its code (windline.tech/f/<code>, runfast.tech/f/<code>)."
          },
          "tour": {
            "type": "string",
            "description": "A Windline tour, by its name, address or organiser (\"Alistair's Durban tour\"): the tap goes to the tour's organiser, who reads them out at night."
          },
          "club": {
            "type": "string",
            "description": "A club or team, by its name or page address, or the club id a club card gave (\"runfast:demo\"): one tap to each of its members on the map."
          },
          "coach": {
            "type": "string",
            "description": "A coach, or the coach id a team card gave (\"runfast:coach-nadia-pillay\"): one tap to each of their athletes on the map."
          },
          "event": {
            "type": "string",
            "description": "With club or coach: the race they are in, if named."
          },
          "name": {
            "type": "string",
            "description": "The first name the user wants on the tap (\"Sam\"), if they gave one. A signed-in user's tap shows their own public name instead."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "additionalProperties": false
      },
      "annotations": {
        "title": "Give someone a tap on the back",
        "readOnlyHint": false,
        "destructiveHint": false,
        "idempotentHint": false,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Sending the tap…",
        "openai/toolInvocation/invoked": "Tap sent",
        "securitySchemes": [
          {
            "type": "noauth"
          },
          {
            "type": "oauth2",
            "scopes": []
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        },
        {
          "type": "oauth2",
          "scopes": []
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "The tap, and their live answer (or, for a tour, who reads it out).",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "tap",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "id": {
            "type": "string",
            "description": "The person id to pass to where_is (\"h:<handle>\", \"<app>:f:<code>\" or \"<app>:p:<id>\")."
          },
          "name": {
            "type": "string",
            "description": "Their name as the public live page shows it."
          },
          "app": {
            "type": "string",
            "enum": [
              "windline",
              "runfast"
            ],
            "description": "windline (cycling) or runfast (running)."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "status": {
            "type": "string",
            "description": "live, delayed, nosignal, paused, waiting, finished or stopped."
          },
          "race": {
            "type": [
              "string",
              "null"
            ],
            "description": "The race (or ride) its members are in."
          },
          "team": {
            "description": "A team tap: the club's name. A person's tap: their club and coach, as where_is."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their profile picture (from windline.tech or runfast.tech), only for someone shown by their full name (Findable)."
          },
          "can_tap": {
            "type": "boolean",
            "description": "A tap on the back would reach them now (taps on; live or in the day after the finish; this asker may tap)."
          },
          "follow_url": {
            "type": "string",
            "description": "The public Follow live link."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "The units the text answer uses."
          },
          "position": {
            "type": "object",
            "description": "Only when the public live page shows where they are: km, mile, of_km, percent, seen (ISO time)."
          },
          "goal": {
            "type": "object",
            "description": "Their goal time and how far behind (positive) or ahead (negative) of it they are, in seconds."
          },
          "pace": {
            "type": "object",
            "description": "Running: per_km_s; cycling: kmh; with words."
          },
          "conditions": {
            "type": "object",
            "description": "The weather where they are: temp_c, feels_c, wind_kmh, gust_kmh, wind_from_deg, along_road_kmh, effect (head, tail, cross, calm), rain_mm_h."
          },
          "ahead": {
            "type": "array",
            "description": "The next stretches of the course: from_km, to_km, at (ISO), temp_c, effect, along_road_kmh, wind_kmh (running: heat_stress)."
          },
          "road_ahead": {
            "type": "object",
            "description": "The Windline forecast engine's rating of the road ahead: rating (good, fair, tough), why."
          },
          "forecast_updated": {
            "type": "string",
            "description": "When the forecast was fetched (ISO)."
          },
          "finished": {
            "type": "boolean",
            "description": "They'd finished: it landed as a congratulation."
          },
          "eta": {
            "type": "object",
            "description": "The finish time their pace points to: at (ISO), local, tz."
          },
          "total": {
            "type": "string",
            "description": "Several matches: how many (\"24\", or \"20+\" when capped)."
          },
          "capped": {
            "type": "boolean",
            "description": "Several matches: true when there are more than the count shows."
          },
          "people": {
            "type": "array",
            "description": "Several matches: the 5 most likely, each with id, name, sport, race, status, where, pace, goal, bib, club. Ask the user to pick; never guess."
          },
          "sent": {
            "type": "boolean",
            "description": "The tap went through."
          },
          "named": {
            "type": "boolean",
            "description": "The tap carried a name."
          },
          "taps": {
            "description": "Their taps so far, with this one."
          },
          "tour": {
            "description": "A tour's tap: the tour (the tap went to its organiser)."
          },
          "club": {
            "type": "object",
            "description": "The club: name, app (windline or runfast), slug, url (its page)."
          },
          "count": {
            "type": "number",
            "description": "How many of the club's people are on its live map in that race."
          },
          "finished_count": {
            "type": "number",
            "description": "How many of them have finished."
          },
          "on_course": {
            "type": "number",
            "description": "How many are on the course."
          },
          "members": {
            "type": "array",
            "description": "Each member on the club's live map (Findable): id (for where_is), name, photo, status, sub (km and goal delta, or the finish time), km (null near the start or finish), goal_delta, can_tap."
          },
          "can_tap_team": {
            "type": "boolean",
            "description": "A team tap would reach at least one of them now."
          },
          "club_url": {
            "type": "string",
            "description": "The club's live map."
          },
          "clubs": {
            "type": "array",
            "description": "Several clubs matched: name, app, people. Ask which one."
          },
          "taps_sent": {
            "type": "number",
            "description": "A team tap: how many members got one."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "find_clubs",
      "title": "Find a club near a place",
      "description": "Find running and cycling clubs near a place, e.g. best running club to join in Fourways, beginner cycling group near Sandton. Lists approved clubs and organisers on Windline (cycling) and RunFast (running) nearest first, with their weekly sessions, whether beginners are welcome, whether anyone can join (Open to anyone) or it's members only, their pace groups, their next session with its weather, the club's own links page when it has one, and a Join link to the club's page. Set beginners for \"beginner friendly\", \"new to running\", \"for beginners\"; set open for \"open to anyone\", \"anyone can join\", \"no membership\", \"don't need to be a member\". Only clubs that have added their area show; never a person. Say what each club chose to share, and nothing more. For coaches or personal trainers, use find_coaches.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "place": {
            "type": "string",
            "description": "The suburb, town or city, as the user said it, e.g. \"Fourways\", \"Durban North\", \"Cape Town\"."
          },
          "sport": {
            "type": "string",
            "enum": [
              "cycling",
              "running",
              "road",
              "gravel",
              "mtb",
              "road-run",
              "trail-run",
              "tours"
            ],
            "description": "cycling or running, or one sport: road, gravel, mtb, road-run, trail-run, tours (multi-day tours). Leave it out for both."
          },
          "beginners": {
            "type": "boolean",
            "description": "true when the user wants a beginner-friendly club (\"beginner friendly\", \"new to cycling\")."
          },
          "open": {
            "type": "boolean",
            "description": "true for only clubs open to anyone (no membership needed): \"open to anyone\", \"anyone can join\", \"no membership\"."
          },
          "km": {
            "type": "number",
            "description": "How far from the place, in km (1 to 100). Leave it out for 25 km."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "place"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Find a club near a place",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Finding clubs near there…",
        "openai/toolInvocation/invoked": "Clubs ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "Clubs and organisers near a place, nearest first.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "clubs",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "add_club": {
            "description": "Where a club that isn't listed asks to be added: { cycling (Windline's page application), running (RunFast's Add your club) }."
          },
          "place": {
            "type": "string",
            "description": "The place searched, as found on the map."
          },
          "km": {
            "type": "number",
            "description": "The radius searched, in km."
          },
          "sport": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sport asked for, or null for all."
          },
          "app": {
            "type": "string",
            "description": "windline, or runfast when every row is a running club or event."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "beginners": {
            "type": "boolean",
            "description": "Only beginner-friendly clubs."
          },
          "open": {
            "type": "boolean",
            "description": "Only clubs open to anyone (no membership needed)."
          },
          "clubs": {
            "type": "array",
            "description": "Nearest first: id, app, name, kind (club or organiser), url (its page), join (Join: the page, sign-in in the same step), photo, km, area, sports, sessions (one line), beginners_welcome, open_to_all (true: Open to anyone, false: Members only, null: not said), links ({ url, biotree }: the club's own links page, or null), pace_groups, who_for, next ({ start, place, kind, weather, weather_basis: forecast or typical })."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "find_events",
      "title": "Find rides, runs, races and tours near a place",
      "description": "Find public cycling and running events near a place and when: races, multi-day tours, club rides and club runs on Windline and RunFast, e.g. trail runs near Cape Town in November, cycle tours around Johannesburg. Each with its date, where it starts, the weather (the forecast within 10 days, typical weather beyond), its club or organiser and an I'm in link to its page. Only published public events of approved clubs and organisers.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "place": {
            "type": "string",
            "description": "The suburb, town or city, as the user said it, e.g. \"Fourways\", \"Durban North\", \"Cape Town\"."
          },
          "sport": {
            "type": "string",
            "enum": [
              "cycling",
              "running",
              "road",
              "gravel",
              "mtb",
              "road-run",
              "trail-run",
              "tours"
            ],
            "description": "cycling or running, or one sport: road, gravel, mtb, road-run, trail-run, tours (multi-day tours). Leave it out for both."
          },
          "when": {
            "type": "string",
            "description": "When, as the user said it: \"November\", \"this weekend\", \"next month\", \"2026-11-14\", or \"2026-11-01 to 2026-11-30\". Leave it out for the next four months."
          },
          "kind": {
            "type": "string",
            "enum": [
              "race",
              "tour",
              "club ride",
              "club run"
            ],
            "description": "Only races, only multi-day tours, or only club rides or runs."
          },
          "km": {
            "type": "number",
            "description": "How far from the place, in km (1 to 100). Leave it out for 50 km."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "place"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Find events near a place",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Finding events near there…",
        "openai/toolInvocation/invoked": "Events ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "Public rides, runs, races and tours near a place, soonest first.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "events",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "add_club": {
            "description": "Where a club that isn't listed asks to be added: { cycling (Windline's page application), running (RunFast's Add your club) }."
          },
          "place": {
            "type": "string",
            "description": "The place searched, as found on the map."
          },
          "km": {
            "type": "number",
            "description": "The radius searched, in km."
          },
          "sport": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sport asked for, or null for all."
          },
          "app": {
            "type": "string",
            "description": "windline, or runfast when every row is a running club or event."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "when": {
            "description": "The window searched: { from, to } (YYYY-MM-DD), or null for the next four months."
          },
          "events": {
            "type": "array",
            "description": "Soonest first: id, app, name, kind (ride, run, race, tour), url (its page), join (I'm in), photo, km, start (ISO), area, club, sports, beginners_welcome, weather, weather_basis."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "find_coaches",
      "title": "Find a coach near a place",
      "description": "Find cycling and running coaches and training groups near a place, e.g. personal cycling trainer in Johannesburg, running coach near Sandton, coaches near Fourways. Use it for any coach, personal trainer, coaching or training group question (never find_clubs, which lists clubs). Lists coaches and training groups on Windline (cycling) and RunFast (running) nearest first, with what they coach (road, MTB, gravel, triathlon, beginners, racing, marathon, ultra), how (1-on-1, small group, online), their next group session with its weather, whether beginners are welcome, the qualifications they state in their own words (livedot doesn't check them), a Book / contact link (their own) and their page. Only coaches who listed themselves and added their area; never anyone else. Say what each coach chose to share, and nothing more.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "place": {
            "type": "string",
            "description": "The suburb, town or city, as the user said it, e.g. \"Fourways\", \"Durban North\", \"Cape Town\"."
          },
          "sport": {
            "type": "string",
            "enum": [
              "cycling",
              "running"
            ],
            "description": "cycling or running. Leave it out for both."
          },
          "specialty": {
            "type": "string",
            "enum": [
              "road",
              "mtb",
              "gravel",
              "triathlon",
              "beginners",
              "racing",
              "marathon",
              "ultra"
            ],
            "description": "What the coach coaches, if the user said: road, mtb, gravel, triathlon, beginners, racing, marathon or ultra."
          },
          "format": {
            "type": "string",
            "enum": [
              "one-to-one",
              "group",
              "online"
            ],
            "description": "one-to-one for a personal trainer or private coaching, group for a training group or small group, online for remote coaching."
          },
          "beginners": {
            "type": "boolean",
            "description": "true when the user is new to the sport (\"for beginners\", \"new to running\")."
          },
          "km": {
            "type": "number",
            "description": "How far from the place, in km (1 to 100). Leave it out for 25 km."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "place"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Find a coach near a place",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Finding coaches near there…",
        "openai/toolInvocation/invoked": "Coaches ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ]
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "outputSchema": {
        "type": "object",
        "description": "Coaches and training groups near a place, nearest first.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "coaches",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "add_club": {
            "description": "Where a club that isn't listed asks to be added: { cycling (Windline's page application), running (RunFast's Add your club) }."
          },
          "place": {
            "type": "string",
            "description": "The place searched, as found on the map."
          },
          "km": {
            "type": "number",
            "description": "The radius searched, in km."
          },
          "sport": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sport asked for, or null for all."
          },
          "app": {
            "type": "string",
            "description": "windline, or runfast when every row is a running club or event."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "add_coach": {
            "description": "Where a coach who isn't listed gets listed free: { cycling (Windline's page application, I'm a coach), running (RunFast's Add your club, I'm a coach) }."
          },
          "specialty": {
            "type": [
              "string",
              "null"
            ],
            "description": "The specialty asked for, or null."
          },
          "format": {
            "type": [
              "string",
              "null"
            ],
            "description": "one-to-one, group or online, or null."
          },
          "beginners": {
            "type": "boolean",
            "description": "Only coaches who welcome beginners."
          },
          "coaches": {
            "type": "array",
            "description": "Nearest first: id, app, name, url (their livedot.me page), book (Book / contact: their own link, or their page), photo, km, area, sports, specialties, format, qualifications (their own words, never checked), beginners_welcome, sessions (one line), next ({ start, place, kind, weather, weather_basis })."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "plan_route",
      "title": "Weather along your route",
      "description": "Weather and a flyover for a GPX file or route the user shares. If the user attached a GPX or route file, call plan_route and pass the file as gpx_file; if you can read the file's contents yourself, you may instead pass its text as gpx or its points as points. Never treat a file name as an event name. Also a Ride with GPS or GPX link, or another app's route points: the weather along it at the time they'd ride or run it, and a flyover of the route (a flyby, fly over my route) that plays in the chat, coloured by the weather: the forecast within 10 days, 10 years of weather on that date beyond it, rated by Windline's forecast engine, with the headwind and tailwind stretches, heat and rain. No account needed. For \"What will the wind be like on this route on Saturday at 6?\" or \"Show me the weather for this GPX and a flyover\". The route stays theirs: nothing is published. For a 3D flight and a route video, offer to save it as a private ride or run with create_event (visibility private). Several GPX files are one multi-day tour: pass them all as files (ordered by the numbers in their names, else by the map; one day after another from start), never one call per file. A route can also come from another app's tool as its points or an encoded polyline (for example a route or activity map from a fitness app): pass it as polyline or points. Never ask the user to export a GPX if you already have the route's points.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "gpx": {
            "type": "string",
            "description": "The GPX file's text (up to 5 MB): if the user pasted it, or attached a file you can read yourself (for example with code)."
          },
          "url": {
            "type": "string",
            "description": "A link to the route: a Ride with GPS route, ride or collection (a collection gives one day per route), or any public link to a GPX file. Not Strava."
          },
          "polyline": {
            "type": "string",
            "description": "The route as an encoded polyline (Google's format, precision 5 or 6), as another app's tool gives it (a route's or an activity's map)."
          },
          "points": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            },
            "description": "The route as its points: [[lat, lon], …], with elevation in metres as a third number if known. Up to 50,000. Use it for points another app's tool gave, or that you read from an attached file (GPX, FIT, TCX) with code."
          },
          "gpx_file": {
            "type": "object",
            "description": "The GPX or route file the user attached (ChatGPT passes it here).",
            "properties": {
              "download_url": {
                "type": "string",
                "description": "The file's download link."
              },
              "file_id": {
                "type": "string",
                "description": "The file's id."
              },
              "mime_type": {
                "type": "string",
                "description": "Its type, if known."
              },
              "file_name": {
                "type": "string",
                "description": "Its name, if known (the route's name; never an event's)."
              }
            },
            "required": [
              "download_url",
              "file_id"
            ]
          },
          "gpx_files": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One attached GPX or route file.",
              "properties": {
                "download_url": {
                  "type": "string",
                  "description": "The file's download link."
                },
                "file_id": {
                  "type": "string",
                  "description": "The file's id."
                },
                "mime_type": {
                  "type": "string",
                  "description": "Its type, if known."
                },
                "file_name": {
                  "type": "string",
                  "description": "Its name, if known (the route's name; never an event's)."
                }
              },
              "required": [
                "download_url",
                "file_id"
              ]
            },
            "description": "Several GPX or route files the user attached (ChatGPT passes them here): one per day of a tour, ordered by the numbers in their names, else by the map."
          },
          "start": {
            "type": "string",
            "description": "When they start, \"YYYY-MM-DD HH:MM\" local to the route. Ask if they didn't say."
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "A GPX file (its text, or a link to it) or a route's polyline or points, and its name (the name helps order the days).",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "The file's name, e.g. \"day-2.gpx\"."
                },
                "gpx": {
                  "type": "string",
                  "description": "The GPX text."
                },
                "url": {
                  "type": "string",
                  "description": "A link to the GPX or route (Ride with GPS; not Strava)."
                },
                "polyline": {
                  "type": "string",
                  "description": "The day as an encoded polyline (another app's route or activity map)."
                },
                "points": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "description": "The day as its points, [[lat, lon], …]."
                },
                "download_url": {
                  "type": "string",
                  "description": "ChatGPT: the attached file's download link."
                },
                "file_id": {
                  "type": "string",
                  "description": "ChatGPT: the attached file's id."
                }
              }
            },
            "description": "Several GPX files (a multi-day tour): ordered by the numbers in their names, else by the map; one day after another from start."
          },
          "rest_days": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With files: dates with no riding, \"YYYY-MM-DD\", only if the user named rest days."
          },
          "starts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Each day's own local start, \"YYYY-MM-DD HH:MM\", when they differ."
          },
          "name": {
            "type": "string",
            "description": "What to call the plan, if the user named it."
          },
          "tz": {
            "type": "string",
            "description": "The route's IANA time zone, if known (else worked out from where it starts)."
          },
          "sport": {
            "type": "string",
            "enum": [
              "ride",
              "run"
            ],
            "description": "ride (default) or run."
          },
          "pace": {
            "type": "number",
            "description": "Riding speed in km/h (default 25), or running pace in minutes per km (default 6)."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ],
            "description": "Pass the asker's units: imperial for users in the US, UK road distances, Liberia or Myanmar, or when they ask in miles/°F; otherwise metric."
          }
        },
        "required": [
          "start"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Weather along your route",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": true
      },
      "securitySchemes": [
        {
          "type": "noauth"
        }
      ],
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Reading your route and the weather…",
        "openai/toolInvocation/invoked": "Weather on your route ready",
        "securitySchemes": [
          {
            "type": "noauth"
          }
        ],
        "openai/fileParams": [
          "gpx_file",
          "gpx_files"
        ]
      },
      "outputSchema": {
        "type": "object",
        "description": "The weather along a route at the time you'd ride or run it.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "plan_route",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's name."
          },
          "sport": {
            "type": "string",
            "description": "cycling or running."
          },
          "basis": {
            "type": "string",
            "enum": [
              "forecast",
              "historic",
              "mixed"
            ],
            "description": "forecast (Apple Weather, within 10 days), historic (10 years of Copernicus ERA5 on that date) or mixed."
          },
          "timed_for": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the times assume (the start, the pace)."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's page."
          },
          "units": {
            "type": "string",
            "enum": [
              "metric",
              "imperial"
            ]
          },
          "days": {
            "type": "array",
            "description": "Each day: name, date (ISO), basis, rating (good, fair, tough), why, start_temp_c, finish_temp_c, effort_pct (cycling), heat_stress and wbgt_c (running), rain_mm or rain_days_in_10 and usual_high_c."
          },
          "events": {
            "type": "array",
            "description": "Several events matched: their names. Ask the user which one."
          },
          "tz": {
            "type": "string",
            "description": "The route's IANA time zone."
          },
          "km": {
            "type": "number",
            "description": "The route's length in km."
          },
          "order": {
            "description": "Several files: how the days were ordered (numbers, map or upload)."
          },
          "order_note": {
            "description": "When the file numbers and the map disagree: what was chosen."
          },
          "files": {
            "description": "Several files: the file of each day, in order."
          },
          "flyover": {
            "type": "boolean",
            "description": "The map flies over the route in the chat."
          },
          "fly_3d": {
            "description": "A link to the full 3D flight, only for a route with a public page; else null."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "list_my_events",
      "title": "My events",
      "description": "The signed-in organiser's own events on Windline (rides, tours, races) or RunFast (runs, races, and their clubs' runs): each with its id, status (draft or published), who it's for (private or shared), dates, distance, whether it's finished, and its dashboard, public or private links; and the club and organiser pages they run (pages on Windline, clubs on RunFast). Call it before making a new event, to know whether to ask who it's for; and to find an event id before changing one.",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "annotations": {
        "title": "My events",
        "readOnlyHint": true,
        "destructiveHint": false,
        "idempotentHint": true,
        "openWorldHint": false
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.read"
          ]
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.read"
            ]
          }
        ],
        "openai/toolInvocation/invoking": "Reading your events…",
        "openai/toolInvocation/invoked": "Your events"
      },
      "outputSchema": {
        "type": "object",
        "description": "The signed-in organiser's events.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "my_events",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "create_event",
      "title": "Create a draft event",
      "description": "Create an event in the signed-in account from GPX files or links: a cycling ride, tour or race (Windline) or a run or running race (RunFast). Pass every GPX the user gave as files: several files make a multi-day tour by themselves (never ask for the type when it's obvious), ordered by the numbers in their names (1, 01, \"day-2\", \"Stage 3\"), else by the map (each day starts where the last ended); day names, km, climbing and a suggested name come from the files, and the days follow one another from start, at the same time, skipping rest_days. The first call (without confirm) only reads the files and returns the plan: show the user that summary (days, order, dates, km, private or for a group) in one message and get one yes, then call again with the same files and confirm: true. visibility: private is just for them (Windline My Rides with its own private link, or a RunFast private run: never published, never on a page); shared is for a group, published later with publish_event. A personal ride or run someone is planning for themselves is private. If the signed-in person runs any club or organiser page (list_my_events lists them) and hasn't said who it's for, ask one short question first: is this a private ride or run for you, or a group ride or run for <club name>? Then use post_club_ride for the club, or create_event with visibility private for themselves. Tour notes for riders the organiser pasted or attached (text, markdown, or a document's text) go in notes, up to 10,000 characters (Windline tours, beta). A route can also come from another app's tool as its points or an encoded polyline (for example a route or activity map from a fitness app): pass it as polyline or points. Never ask the user to export a GPX if you already have the route's points.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "ride",
              "tour",
              "race",
              "run"
            ],
            "description": "ride, tour or race on Windline (cycling); run on RunFast (a running race: type run with race: true). Leave it out when it's obvious: several files make a tour, one file a ride (or a run on a RunFast account)."
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "A GPX file (its text, or a link to it) or a route's polyline or points, and its name (the name helps order the days).",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "The file's name, e.g. \"day-2.gpx\"."
                },
                "gpx": {
                  "type": "string",
                  "description": "The GPX text."
                },
                "url": {
                  "type": "string",
                  "description": "A link to the GPX or route (Ride with GPS; not Strava)."
                },
                "polyline": {
                  "type": "string",
                  "description": "The day as an encoded polyline (another app's route or activity map)."
                },
                "points": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "description": "The day as its points, [[lat, lon], …]."
                },
                "download_url": {
                  "type": "string",
                  "description": "ChatGPT: the attached file's download link."
                },
                "file_id": {
                  "type": "string",
                  "description": "ChatGPT: the attached file's id."
                }
              }
            },
            "description": "Every GPX the user attached or linked, in the order they came: one per day of a tour, one for a ride or run."
          },
          "start": {
            "type": "string",
            "description": "With files: day 1's local start, \"YYYY-MM-DD HH:MM\" (the same time every day unless starts says otherwise). Ask if they didn't say."
          },
          "rest_days": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With files: dates with no riding, \"YYYY-MM-DD\", only if the user named rest days."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "shared"
            ],
            "description": "private: just for them (My Rides; a private run). shared: for a group, published later. Ask if they run a club and didn't say."
          },
          "confirm": {
            "type": "boolean",
            "description": "With files: true only after the user said yes to the plan this tool returned."
          },
          "name": {
            "type": "string",
            "description": "The event's name; with files, left out to use the suggested one."
          },
          "race": {
            "type": "boolean",
            "description": "RunFast: a race (once, with a gun time) rather than a run."
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "gpx": {
                  "type": "string",
                  "description": "The GPX file's text (up to 5 MB): if the user pasted it, or attached a file you can read yourself (for example with code)."
                },
                "url": {
                  "type": "string",
                  "description": "A link to the route: a Ride with GPS route, ride or collection (a collection gives one day per route), or any public link to a GPX file. Not Strava."
                },
                "polyline": {
                  "type": "string",
                  "description": "The route as an encoded polyline (Google's format, precision 5 or 6), as another app's tool gives it (a route's or an activity's map)."
                },
                "points": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    }
                  },
                  "description": "The route as its points: [[lat, lon], …], with elevation in metres as a third number if known. Up to 50,000. Use it for points another app's tool gave, or that you read from an attached file (GPX, FIT, TCX) with code."
                },
                "start": {
                  "type": "string",
                  "description": "Local start, \"YYYY-MM-DD HH:MM\" in the route's own time zone."
                },
                "label": {
                  "type": "string",
                  "description": "e.g. \"Day 1\", \"Stage 2\"."
                }
              },
              "additionalProperties": false
            },
            "description": "One per day (a ride or a run: one). Or give url with a Ride with GPS collection and starts."
          },
          "url": {
            "type": "string",
            "description": "A Ride with GPS collection for a multi-day tour (one day per route), used with starts."
          },
          "starts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Each day's own local start, \"YYYY-MM-DD HH:MM\", when they differ (with url or files)."
          },
          "weekly": {
            "type": "object",
            "description": "A weekly ride or run: { days: [0-6, Sunday is 0], time: \"HH:MM\" }.",
            "properties": {
              "days": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "time": {
                "type": "string"
              }
            }
          },
          "tz": {
            "type": "string",
            "description": "IANA time zone; else worked out from where the route starts."
          },
          "description": {
            "type": "string",
            "description": "What the event is, for its public page."
          },
          "notes": {
            "type": "string",
            "description": "Tours: the organiser's notes for riders (what to pack, meeting points), as they pasted or attached them, markdown, up to 10,000 characters. RunFast: a short note (600 characters)."
          },
          "place": {
            "type": "string",
            "description": "RunFast: where it starts, in words."
          },
          "pace": {
            "type": "number",
            "description": "Riding km/h, or running seconds per km."
          },
          "page": {
            "type": "string",
            "description": "Windline: the club or organiser page address to show it on (one they run)."
          },
          "club": {
            "type": "string",
            "description": "RunFast: the club address (one they manage)."
          },
          "polyline": {
            "type": "string",
            "description": "The route as an encoded polyline (Google's format, precision 5 or 6), as another app's tool gives it (a route's or an activity's map). One route: a ride, a run or a one-day race (with start)."
          },
          "points": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            },
            "description": "The route as its points: [[lat, lon], …], with elevation in metres as a third number if known. Up to 50,000. Use it for points another app's tool gave, or that you read from an attached file (GPX, FIT, TCX) with code."
          },
          "gpx_file": {
            "type": "object",
            "description": "The GPX or route file the user attached (ChatGPT passes it here).",
            "properties": {
              "download_url": {
                "type": "string",
                "description": "The file's download link."
              },
              "file_id": {
                "type": "string",
                "description": "The file's id."
              },
              "mime_type": {
                "type": "string",
                "description": "Its type, if known."
              },
              "file_name": {
                "type": "string",
                "description": "Its name, if known (the route's name; never an event's)."
              }
            },
            "required": [
              "download_url",
              "file_id"
            ]
          },
          "gpx_files": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "One attached GPX or route file.",
              "properties": {
                "download_url": {
                  "type": "string",
                  "description": "The file's download link."
                },
                "file_id": {
                  "type": "string",
                  "description": "The file's id."
                },
                "mime_type": {
                  "type": "string",
                  "description": "Its type, if known."
                },
                "file_name": {
                  "type": "string",
                  "description": "Its name, if known (the route's name; never an event's)."
                }
              },
              "required": [
                "download_url",
                "file_id"
              ]
            },
            "description": "Several GPX or route files the user attached (ChatGPT passes them here): one per day of a tour, ordered by the numbers in their names, else by the map."
          }
        },
        "additionalProperties": false
      },
      "annotations": {
        "title": "Create a draft event",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": false
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.write"
          ]
        }
      ],
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Creating the draft…",
        "openai/toolInvocation/invoked": "Draft ready",
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.write"
            ]
          }
        ],
        "openai/fileParams": [
          "gpx_file",
          "gpx_files"
        ]
      },
      "outputSchema": {
        "type": "object",
        "description": "The plan to check with the user (view tour_plan, nothing made yet), or the new event and its links.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "created",
              "tour_plan",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "update_event",
      "title": "Change an event",
      "description": "Change one of the signed-in organiser's events: its name, description or notes, or start dates and times. Never a finished event (finished events stay as they were). Tell the user exactly what will change and get a yes first.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "From list_my_events."
          },
          "name": {
            "type": "string",
            "description": "A new name."
          },
          "description": {
            "type": "string",
            "description": "A new description for its public page."
          },
          "notes": {
            "type": "string",
            "description": "RunFast runs only (tours: add_tour_notes)."
          },
          "place": {
            "type": "string",
            "description": "RunFast: where it starts, in words."
          },
          "days": {
            "type": "array",
            "description": "Windline tours and races: [{ day: 2, date?: \"YYYY-MM-DD\", time?: \"HH:MM\" }].",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "integer"
                },
                "date": {
                  "type": "string"
                },
                "time": {
                  "type": "string"
                }
              }
            }
          },
          "date": {
            "type": "string",
            "description": "A one-off ride or run: its new date, YYYY-MM-DD."
          },
          "time": {
            "type": "string",
            "description": "A ride or run: its new start time, HH:MM."
          }
        },
        "required": [
          "event_id"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Change an event",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": true
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.write"
          ]
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.write"
            ]
          }
        ],
        "openai/toolInvocation/invoking": "Saving the change…",
        "openai/toolInvocation/invoked": "Change saved"
      },
      "outputSchema": {
        "type": "object",
        "description": "What changed on the event.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "update_event",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "add_tour_notes",
      "title": "Write a tour's notes for riders",
      "description": "Set or add to a Windline cycling tour's notes for riders (what to pack, how the tour runs, meeting points; markdown, up to 10,000 characters), shown on the tour's page. Never a finished tour. Show the user the notes and get a yes first.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "The tour's id, from list_my_events."
          },
          "notes": {
            "type": "string",
            "description": "The notes for riders, in markdown (up to 10,000 characters)."
          },
          "mode": {
            "type": "string",
            "enum": [
              "replace",
              "append"
            ],
            "description": "replace (default) or append."
          }
        },
        "required": [
          "event_id",
          "notes"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Write a tour's notes",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": true
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.write"
          ]
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.write"
            ]
          }
        ],
        "openai/toolInvocation/invoking": "Saving the tour notes…",
        "openai/toolInvocation/invoked": "Tour notes saved"
      },
      "outputSchema": {
        "type": "object",
        "description": "The tour's notes, saved.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "add_tour_notes",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "publish_event",
      "title": "Publish an event",
      "description": "Publish one of the signed-in organiser's draft events, so anyone with its link can see it (Windline: at tour., event. or ride.windline.tech/<address>). Private events can't be published. ALWAYS ask the user to confirm first, then call with confirm: true.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "The draft's id, from list_my_events or create_event."
          },
          "address": {
            "type": "string",
            "description": "Windline: the public address, lowercase letters, numbers and hyphens, e.g. \"cape-coast-2027\"."
          },
          "confirm": {
            "type": "boolean",
            "description": "true only after the user said yes."
          }
        },
        "required": [
          "event_id",
          "confirm"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Publish an event",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": true
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.publish"
          ]
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.publish"
            ]
          }
        ],
        "openai/toolInvocation/invoking": "Publishing…",
        "openai/toolInvocation/invoked": "Published"
      },
      "outputSchema": {
        "type": "object",
        "description": "The event, now public, and its link.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "publish_event",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "post_club_ride",
      "title": "Post a club ride or run",
      "description": "Post a DRAFT group ride (Windline) or club run (RunFast) on a club or organiser page the signed-in person runs, from a GPX route or link, once or weekly: always for the group, never private. Publish it with publish_event. Confirm the details with the user first. If the signed-in person runs any club or organiser page (list_my_events lists them) and hasn't said who it's for, ask one short question first: is this a private ride or run for you, or a group ride or run for <club name>? Then use post_club_ride for the club, or create_event with visibility private for themselves. A route can also come from another app's tool as its points or an encoded polyline (for example a route or activity map from a fitness app): pass it as polyline or points. Never ask the user to export a GPX if you already have the route's points.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "string",
            "description": "The club or organiser page address (livedot.me/<address>)."
          },
          "name": {
            "type": "string",
            "description": "The ride's or run's name, e.g. \"Saturday coffee ride\"."
          },
          "gpx": {
            "type": "string",
            "description": "The GPX file's text (up to 5 MB): if the user pasted it, or attached a file you can read yourself (for example with code)."
          },
          "url": {
            "type": "string",
            "description": "A link to the route: a Ride with GPS route, ride or collection (a collection gives one day per route), or any public link to a GPX file. Not Strava."
          },
          "polyline": {
            "type": "string",
            "description": "The route as an encoded polyline (Google's format, precision 5 or 6), as another app's tool gives it (a route's or an activity's map)."
          },
          "points": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            },
            "description": "The route as its points: [[lat, lon], …], with elevation in metres as a third number if known. Up to 50,000. Use it for points another app's tool gave, or that you read from an attached file (GPX, FIT, TCX) with code."
          },
          "start": {
            "type": "string",
            "description": "A one-off: \"YYYY-MM-DD HH:MM\" local. Weekly: the first one."
          },
          "weekly": {
            "type": "object",
            "description": "Weekly: { days: [0-6, Sunday is 0], time: \"HH:MM\" }.",
            "properties": {
              "days": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "time": {
                "type": "string"
              }
            }
          },
          "tz": {
            "type": "string",
            "description": "IANA time zone; else worked out from where the route starts."
          },
          "description": {
            "type": "string",
            "description": "What the ride or run is, for the club page."
          },
          "place": {
            "type": "string",
            "description": "RunFast: where it starts, in words."
          },
          "gpx_file": {
            "type": "object",
            "description": "The GPX or route file the user attached (ChatGPT passes it here).",
            "properties": {
              "download_url": {
                "type": "string",
                "description": "The file's download link."
              },
              "file_id": {
                "type": "string",
                "description": "The file's id."
              },
              "mime_type": {
                "type": "string",
                "description": "Its type, if known."
              },
              "file_name": {
                "type": "string",
                "description": "Its name, if known (the route's name; never an event's)."
              }
            },
            "required": [
              "download_url",
              "file_id"
            ]
          }
        },
        "required": [
          "page",
          "name",
          "start"
        ],
        "additionalProperties": false
      },
      "annotations": {
        "title": "Post a club ride or run",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": false
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "clubs.post"
          ]
        }
      ],
      "_meta": {
        "ui": {
          "resourceUri": "ui://livedot/live-map.a21553cf02.html",
          "visibility": [
            "model",
            "app"
          ]
        },
        "ui/resourceUri": "ui://livedot/live-map.a21553cf02.html",
        "openai/outputTemplate": "ui://livedot/live-map.a21553cf02.html",
        "openai/widgetAccessible": true,
        "openai/toolInvocation/invoking": "Creating the draft…",
        "openai/toolInvocation/invoked": "Draft ready",
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "clubs.post"
            ]
          }
        ],
        "openai/fileParams": [
          "gpx_file"
        ]
      },
      "outputSchema": {
        "type": "object",
        "description": "The club ride, posted on the club's page.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "club_ride",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "events": {
            "description": "list_my_events: each event with id, name, status (draft or published), dates, km, finished, and its dashboard and public links."
          },
          "id": {
            "description": "The event's id."
          },
          "status": {
            "description": "draft, published, or private."
          },
          "visibility": {
            "description": "private (just for them) or shared (for a group)."
          },
          "link": {
            "description": "A private event: its private link (Windline) or where the app opens it (RunFast)."
          },
          "pages": {
            "description": "list_my_events on Windline: the club and organiser pages they run ({ slug, name, type }). Ask who a new ride is for when there are any."
          },
          "clubs": {
            "description": "list_my_events on RunFast: the clubs they run ({ slug, name })."
          },
          "order": {
            "description": "The plan (view tour_plan): how the days were ordered."
          },
          "note": {
            "description": "The plan: when the file numbers and the map disagree."
          },
          "days": {
            "description": "The plan: each day with its file, from, to, km, climbing and start."
          },
          "dashboard": {
            "description": "The organiser's dashboard link."
          },
          "url": {
            "description": "The public link, once published."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    },
    {
      "name": "invite_members",
      "title": "Invite members by email",
      "description": "Invite a club's members, or a tour's riders, by email, from a list the organiser gives (a CSV, a spreadsheet's text, or names and addresses typed in): for a club or organiser page the signed-in person runs (page on Windline, club on RunFast) or a published tour of theirs (event). Each person gets one email from livedot: \"<club> added you as a member\" (or \"<organiser> added you to <tour>\") with Confirm; nobody joins until they confirm, and nobody is invited twice. The first call (without confirm) only reads the list and returns who would be invited and who's skipped (listed twice, invited before, unsubscribed, no usable email): show the organiser that, get a yes, then call again with confirm: true. A day's limit: 500 for club admins and managers, 50 for a tour's organiser, 20 for an account under a week old; the preview says how many go today.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "page": {
            "type": "string",
            "description": "Windline: the club page address they run (livedot.me/<address> or windline.tech/c/<address>)."
          },
          "club": {
            "type": "string",
            "description": "RunFast: the club address they manage (runfast.tech/c/<address>)."
          },
          "event": {
            "type": "string",
            "description": "A published Windline tour of theirs, by its id from list_my_events."
          },
          "csv": {
            "type": "string",
            "description": "The list as the organiser gave it: a CSV or spreadsheet's text (any delimiter, with or without a header row), up to 2,000 people."
          },
          "people": {
            "type": "array",
            "description": "Or the people as a list: [{ name, email }].",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Their name."
                },
                "email": {
                  "type": "string",
                  "description": "Their email address."
                }
              }
            }
          },
          "confirm": {
            "type": "boolean",
            "description": "true only after the organiser said yes to the preview this tool returned."
          },
          "csv_file": {
            "type": "object",
            "description": "The CSV or spreadsheet the organiser attached (ChatGPT passes it here); or pass its text as csv.",
            "properties": {
              "download_url": {
                "type": "string",
                "description": "The file's download link."
              },
              "file_id": {
                "type": "string",
                "description": "The file's id."
              },
              "mime_type": {
                "type": "string",
                "description": "Its type, if known."
              },
              "file_name": {
                "type": "string",
                "description": "Its name, if known (the route's name; never an event's)."
              }
            },
            "required": [
              "download_url",
              "file_id"
            ]
          }
        },
        "additionalProperties": false
      },
      "annotations": {
        "title": "Invite members by email",
        "readOnlyHint": false,
        "openWorldHint": true,
        "idempotentHint": false,
        "destructiveHint": false
      },
      "securitySchemes": [
        {
          "type": "oauth2",
          "scopes": [
            "events.write"
          ]
        }
      ],
      "_meta": {
        "securitySchemes": [
          {
            "type": "oauth2",
            "scopes": [
              "events.write"
            ]
          }
        ],
        "openai/fileParams": [
          "csv_file"
        ],
        "openai/toolInvocation/invoking": "Reading the list…",
        "openai/toolInvocation/invoked": "Invites ready"
      },
      "outputSchema": {
        "type": "object",
        "description": "Who would get an invite (preview), or who got one.",
        "properties": {
          "view": {
            "type": "string",
            "enum": [
              "invites",
              "none"
            ],
            "description": "What this answer is; \"none\" when there is nothing to show (see message)."
          },
          "app": {
            "type": [
              "string",
              "null"
            ],
            "description": "The account's app: windline or runfast."
          },
          "state": {
            "description": "preview (nothing sent yet) or sent."
          },
          "target": {
            "description": "The club or tour: { kind, name, url }."
          },
          "to_send": {
            "description": "How many would be (or were) invited."
          },
          "sent": {
            "description": "How many emails went out."
          },
          "failed": {
            "description": "How many couldn't be sent just now."
          },
          "skipped": {
            "description": "Skipped: { invalid, duplicate, already, unsubscribed, over_limit }."
          },
          "people": {
            "description": "The first 25 who'd get an invite: { name, email }."
          },
          "invalid": {
            "description": "Rows without a usable email (the first 10)."
          },
          "left_today": {
            "description": "Invites left today (500 a day)."
          },
          "subject": {
            "description": "The email's subject."
          },
          "sponsor_line": {
            "type": "string",
            "description": "The line every answer ends with: who brings livedot's live tracking free."
          },
          "message": {
            "type": "string",
            "description": "When view is \"none\": what to tell the user (no match, not live yet, a question back)."
          },
          "error": {
            "type": "string",
            "description": "When the call failed: what went wrong, in words for the user."
          },
          "signup": {
            "description": "Without a sign-in: { label, url } to make a free Windline or RunFast account (?ref=mcp)."
          }
        },
        "required": [
          "view"
        ],
        "additionalProperties": true
      }
    }
  ],
  "prompts": [
    {
      "name": "track_someone",
      "title": "Track someone in a race",
      "description": "Where someone is in a live cycling or running race right now, the weather they're in, and when they'll finish, with a live map.",
      "arguments": [
        {
          "name": "person",
          "description": "Their name or livedot handle, e.g. \"Megan\" or \"@megan\".",
          "required": true
        },
        {
          "name": "race",
          "description": "The race, if you know it, e.g. \"Comrades\".",
          "required": false
        }
      ]
    },
    {
      "name": "race_day_weather",
      "title": "Race-day weather",
      "description": "What the weather will be like along an event's course on race day: the forecast within 10 days, 10 years of weather on that date beyond it.",
      "arguments": [
        {
          "name": "event",
          "description": "The event, e.g. \"NYC Marathon\", \"Gravel Burn\", \"Comrades demo\".",
          "required": true
        },
        {
          "name": "year",
          "description": "The edition's year, if not the next one.",
          "required": false
        }
      ]
    },
    {
      "name": "fly_a_course",
      "title": "Fly a course",
      "description": "Fly over a race course in the chat, coloured by the weather along it.",
      "arguments": [
        {
          "name": "event",
          "description": "The event or route, e.g. \"Comrades\", \"NYC Marathon\".",
          "required": true
        }
      ]
    }
  ],
  "resources": [
    {
      "uri": "ui://livedot/live-map.a21553cf02.html",
      "name": "livedot live map",
      "title": "livedot live map",
      "mimeType": "text/html;profile=mcp-app",
      "description": "The live map: the course, where they are, the wind and heat on the road ahead, and Follow live.",
      "_meta": {
        "ui": {
          "csp": {
            "connectDomains": [
              "https://tiles.openfreemap.org",
              "https://s3.amazonaws.com"
            ],
            "resourceDomains": [
              "https://livedot.me",
              "https://unpkg.com",
              "https://tiles.openfreemap.org",
              "https://s3.amazonaws.com",
              "https://fonts.googleapis.com",
              "https://fonts.gstatic.com",
              "https://windline.tech",
              "https://runfast.tech",
              "https://weatherkit.apple.com",
              "https://firebasestorage.googleapis.com",
              "https://storage.googleapis.com"
            ]
          },
          "prefersBorder": true
        },
        "openai/widgetCSP": {
          "connect_domains": [
            "https://tiles.openfreemap.org",
            "https://s3.amazonaws.com"
          ],
          "resource_domains": [
            "https://livedot.me",
            "https://unpkg.com",
            "https://tiles.openfreemap.org",
            "https://s3.amazonaws.com",
            "https://fonts.googleapis.com",
            "https://fonts.gstatic.com",
            "https://windline.tech",
            "https://runfast.tech",
            "https://weatherkit.apple.com",
            "https://firebasestorage.googleapis.com",
            "https://storage.googleapis.com"
          ]
        },
        "openai/widgetPrefersBorder": true,
        "openai/widgetDescription": "A live map of the person on the course with the wind and heat ahead. The text answer already has the facts; don't repeat the map."
      }
    },
    {
      "uri": "livedot://guide",
      "name": "livedot guide",
      "title": "How to use livedot MCP",
      "mimeType": "text/markdown",
      "description": "Which tool to use when, the demo races that always work, and what livedot will and won't share."
    }
  ]
}