# History Cake — MCP Integration Reference History Cake is a platform for geo-triggered audio tours: a "tour" contains ordered "stories" (narrated stops with coordinates and a trigger radius). This document is the complete reference for building tours through the History Cake MCP server. ## Connect Remote (Streamable HTTP, stateless): POST https://historycake.app/api/mcp Authorization: Bearer hc_mcp_... Accept: application/json, text/event-stream stdio bridge for clients without remote MCP support: npx -y @history-cake/mcp env: HISTORY_CAKE_API_KEY=hc_mcp_... (HISTORY_CAKE_URL optional) Claude Code: claude mcp add --transport http history-cake https://historycake.app/api/mcp --header "Authorization: Bearer hc_mcp_..." API keys are created in Studio → Settings → API tokens (creator or admin accounts only). Keys are shown once, stored hashed, and can be revoked or expired; role and ban status are re-checked on every request. ## Rules and limits - 120 requests per minute per token. - You can only manage tours you own (admins can manage all). - Media inputs are public HTTPS URLs; the server imports them into the History Cake media library (S3). No data: URLs, no private hosts. - A media URL that fails to import does not fail create_tour_bundle or create_story: it comes back in mediaErrors and the tour or story is still created. Re-import it with import_media_url; do not resend the call. - No AI generation over MCP: write story text and narration scripts yourself, and supply narration audio as HTTPS files (or upload narration audio later in Studio). The audioScript field stores your narration script. - Publishing requires: every story has narration audio, the tour has a primary (cover) image, and non-flying tours have at least one "route" story. check_tour_publishable reports the exact blockers. ## Recommended workflow (from a researched document) 1. create_tour_bundle — tour fields + all stories (title, latitude, longitude, description, audioScript, role route/poi) in one call. Media URLs that fail to import come back in mediaErrors (the tour is still created); any other failure rolls the whole bundle back. 2. import_media_url + update_tour — set the cover image (primaryImageId). 3. import_media_url + update_story — set per-story narration audio (audioId) and images (primaryImageId). 4. check_tour_publishable — fix anything it lists. 5. publish_tour — the tour goes live at its public URL. ## Tools ### create_tour_bundle Create a complete History Cake tour in one call: tour fields, stories (with locations, narration scripts, role/POI status), tags, and media imported from HTTPS URLs. Created as a draft unless status is 'published' AND the tour passes publish validation (every route stop needs narration audio, the tour needs a cover image, and non-flying tours need at least one route stop; audio is optional for points of interest) — otherwise it is saved as a draft and publishBlockers lists what's missing. A media URL that cannot be imported (rate limit, timeout, unsupported type) does not fail the call: the tour is created without it and mediaErrors lists each one (scope, storyIndex, field, url, error) so you can re-import it with import_media_url and attach it with update_tour/update_story or attach_media_to_*. Any other failure rolls the whole bundle back so nothing is saved, and it is safe to send again — do not resend a bundle that succeeded. This is the fastest way to turn a researched text document into a tour. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tour": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "shortDescription": { "type": "string", "maxLength": 500 }, "description": { "type": "string", "maxLength": 30000 }, "status": { "default": "draft", "type": "string", "enum": [ "draft", "published" ] }, "navigationType": { "default": "driving", "type": "string", "enum": [ "driving", "walking", "flying" ] }, "storyOrderMode": { "default": "ordered", "type": "string", "enum": [ "ordered", "unordered" ] }, "estimatedDurationMinutes": { "type": "integer", "minimum": 0, "maximum": 10000 }, "tags": { "maxItems": 25, "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 100 } }, "media": { "type": "object", "properties": { "primaryImageUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "primaryVideoUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "backgroundMusicUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "introAudioUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "outroAudioUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "gallery": { "maxItems": 50, "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 }, "role": { "type": "string", "minLength": 1, "maxLength": 80 }, "orderIndex": { "type": "integer", "minimum": 0, "maximum": 1000 } }, "required": [ "url" ] } } } } }, "required": [ "name" ] }, "stories": { "maxItems": 50, "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 200 }, "shortDescription": { "type": "string", "maxLength": 500 }, "description": { "type": "string", "maxLength": 20000 }, "audioScript": { "type": "string", "maxLength": 12000 }, "latitude": { "type": "number", "minimum": -90, "maximum": 90 }, "longitude": { "type": "number", "minimum": -180, "maximum": 180 }, "city": { "type": "string", "maxLength": 120 }, "state": { "type": "string", "maxLength": 120 }, "zipCode": { "type": "string", "maxLength": 20 }, "triggerRadius": { "default": 500, "type": "number", "minimum": 10, "maximum": 10000 }, "historicalDate": { "type": "string", "maxLength": 100 }, "eventStartDate": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, "eventEndDate": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, "orderIndex": { "type": "integer", "minimum": 0, "maximum": 1000 }, "role": { "default": "route", "type": "string", "enum": [ "route", "poi" ] }, "poiCategory": { "type": "string", "enum": [ "bar", "restaurant", "trailhead", "viewpoint", "parking", "other" ] }, "tags": { "maxItems": 25, "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 100 } }, "media": { "type": "object", "properties": { "primaryImageUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "primaryVideoUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "audioUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "gallery": { "maxItems": 50, "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 }, "role": { "type": "string", "minLength": 1, "maxLength": 80 }, "orderIndex": { "type": "integer", "minimum": 0, "maximum": 1000 } }, "required": [ "url" ] } } } } }, "required": [ "title", "latitude", "longitude" ] } } }, "required": [ "tour" ] } ``` ### import_media_url Import a single HTTPS image, audio, or video URL into the History Cake media library (downloads to S3). Returns a mediaId you can set as a tour cover, story image, or narration audio via the update tools. Only public HTTPS URLs are allowed. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ``` ### create_story Add one story to a tour: route stops auto-play as travelers arrive; points of interest (role "poi") appear on the map and are played manually. Takes location, trigger radius (route stops only), text, narration script, media from URLs, and tags. New stories are appended to the tour unless orderIndex says otherwise. A media URL that cannot be imported (rate limit, timeout, unsupported type) does not fail the call: the story is created without it and mediaErrors lists each one (scope, field, galleryIndex, url, error) so you can re-import it with import_media_url and attach it with update_story or attach_media_to_story — do not resend a create_story that succeeded, or the tour gets a second copy of the story. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "title": { "type": "string", "minLength": 1, "maxLength": 200 }, "shortDescription": { "type": "string", "maxLength": 500 }, "description": { "type": "string", "maxLength": 20000 }, "audioScript": { "type": "string", "maxLength": 12000 }, "latitude": { "type": "number", "minimum": -90, "maximum": 90 }, "longitude": { "type": "number", "minimum": -180, "maximum": 180 }, "city": { "type": "string", "maxLength": 120 }, "state": { "type": "string", "maxLength": 120 }, "zipCode": { "type": "string", "maxLength": 20 }, "triggerRadius": { "default": 500, "type": "number", "minimum": 10, "maximum": 10000 }, "historicalDate": { "type": "string", "maxLength": 100 }, "eventStartDate": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, "eventEndDate": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, "orderIndex": { "type": "integer", "minimum": 0, "maximum": 1000 }, "role": { "default": "route", "type": "string", "enum": [ "route", "poi" ] }, "poiCategory": { "type": "string", "enum": [ "bar", "restaurant", "trailhead", "viewpoint", "parking", "other" ] }, "tags": { "maxItems": 25, "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 100 } }, "media": { "type": "object", "properties": { "primaryImageUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "primaryVideoUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "audioUrl": { "anyOf": [ { "type": "string", "maxLength": 2048, "format": "uri" }, { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 } }, "required": [ "url" ] } ] }, "gallery": { "maxItems": 50, "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 }, "role": { "type": "string", "minLength": 1, "maxLength": 80 }, "orderIndex": { "type": "integer", "minimum": 0, "maximum": 1000 } }, "required": [ "url" ] } } } }, "tourId": { "type": "string", "minLength": 1 } }, "required": [ "title", "latitude", "longitude", "tourId" ] } ``` ### update_tour Patch fields on a tour you own: name, descriptions, navigation type, story order mode, duration estimate, primary media (by mediaId from import_media_url), and tags (replaces all tags when provided; tags are created by name as needed). The public URL slug never changes after creation. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "shortDescription": { "anyOf": [ { "type": "string", "maxLength": 500 }, { "type": "null" } ] }, "description": { "anyOf": [ { "type": "string", "maxLength": 30000 }, { "type": "null" } ] }, "navigationType": { "type": "string", "enum": [ "driving", "walking", "flying" ] }, "storyOrderMode": { "type": "string", "enum": [ "ordered", "unordered" ] }, "estimatedDurationMinutes": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" } ] }, "primaryImageId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "primaryVideoId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "backgroundMusicId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "introAudioId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "outroAudioId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "tags": { "maxItems": 25, "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 100 } } }, "required": [ "tourId" ] } ``` ### update_story Patch fields on a story you own: text, narration script (audioScript), location, trigger radius, dates, primary media (by mediaId), and tags (replaces all tags when provided). Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "storyId": { "type": "string", "minLength": 1 }, "title": { "type": "string", "minLength": 1, "maxLength": 200 }, "shortDescription": { "anyOf": [ { "type": "string", "maxLength": 500 }, { "type": "null" } ] }, "description": { "anyOf": [ { "type": "string", "maxLength": 20000 }, { "type": "null" } ] }, "audioScript": { "anyOf": [ { "type": "string", "maxLength": 12000 }, { "type": "null" } ] }, "latitude": { "type": "number", "minimum": -90, "maximum": 90 }, "longitude": { "type": "number", "minimum": -180, "maximum": 180 }, "city": { "anyOf": [ { "type": "string", "maxLength": 120 }, { "type": "null" } ] }, "state": { "anyOf": [ { "type": "string", "maxLength": 120 }, { "type": "null" } ] }, "zipCode": { "anyOf": [ { "type": "string", "maxLength": 20 }, { "type": "null" } ] }, "triggerRadius": { "type": "number", "minimum": 10, "maximum": 10000 }, "historicalDate": { "anyOf": [ { "type": "string", "maxLength": 100 }, { "type": "null" } ] }, "eventStartDate": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, { "type": "null" } ] }, "eventEndDate": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" }, { "type": "null" } ] }, "primaryImageId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "primaryVideoId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "audioId": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] }, "tags": { "maxItems": 25, "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 100 } } }, "required": [ "storyId" ] } ``` ### get_tour Fetch full detail for a tour you own: all fields, tags, every story (with order, role, and whether it has audio/image/script yet), plus publishReadiness telling you exactly what still blocks publishing. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 } }, "required": [ "tourId" ] } ``` ### get_my_tours List tours owned by the authenticated token's user (admins see all): id, slug, status, story count, and public URL. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "limit": { "default": 20, "type": "integer", "minimum": 1, "maximum": 100 } } } ``` ### get_story Fetch one story's complete content by id: full WYSIWYG `description` text, the private `audioScript` narration, short description, exact location and trigger radius, dates, resolved primary image/video/audio URLs, gallery items, tags, and every tour it belongs to (with its per-tour order and role). Use this to read or audit a story's existing wording before editing it with update_story — list_stories and get_tour only report whether content exists, not the text itself. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "storyId": { "type": "string", "minLength": 1 } }, "required": [ "storyId" ] } ``` ### list_stories List every story in a tour — route stops in listening order first, then points of interest (map-shown, played manually) — with role (route/POI), location, short description, and whether each has narration audio, a primary image, or an audio script. Pass includeContent:true to also return each story's full description text and audioScript so you can audit a whole tour's writing in one call. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 }, "includeContent": { "default": false, "type": "boolean" } }, "required": [ "tourId" ] } ``` ### reorder_stories Set the listening order of a tour's stories. Pass every story currently in the tour (use list_stories first) as an ordered array of storyIds. Route stops keep the relative order you pass; points of interest always seat after the route stops (they never auto-play and have no place on the walked route). Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 }, "storyIds": { "minItems": 1, "maxItems": 500, "type": "array", "items": { "type": "string", "minLength": 1 } } }, "required": [ "tourId", "storyIds" ] } ``` ### set_story_role Mark a story, within one tour, as a route stop (auto-plays as travelers arrive) or an off-path point of interest (shown on the map and played manually, never auto-triggered; with a category like viewpoint/restaurant/trailhead). Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 }, "storyId": { "type": "string", "minLength": 1 }, "role": { "type": "string", "enum": [ "route", "poi" ] }, "poiCategory": { "type": "string", "enum": [ "bar", "restaurant", "trailhead", "viewpoint", "parking", "other" ] } }, "required": [ "tourId", "storyId", "role" ] } ``` ### remove_story_from_tour Detach a story from a tour (the story itself is kept; it must remain in at least one tour). Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 }, "storyId": { "type": "string", "minLength": 1 } }, "required": [ "tourId", "storyId" ] } ``` ### attach_media_to_story Attach an existing media item (mediaId) or import an HTTPS URL into a story's gallery, with optional caption and order. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "mediaId": { "type": "string", "minLength": 1 }, "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 }, "orderIndex": { "default": 0, "type": "integer", "minimum": 0, "maximum": 1000 }, "storyId": { "type": "string", "minLength": 1 } }, "required": [ "storyId" ] } ``` ### attach_media_to_tour Attach an existing media item (mediaId) or import an HTTPS URL into a tour's gallery, with optional caption, role, and order. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "mediaId": { "type": "string", "minLength": 1 }, "url": { "type": "string", "maxLength": 2048, "format": "uri" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "type": "string", "maxLength": 1000 }, "altText": { "type": "string", "maxLength": 500 }, "caption": { "type": "string", "maxLength": 500 }, "orderIndex": { "default": 0, "type": "integer", "minimum": 0, "maximum": 1000 }, "tourId": { "type": "string", "minLength": 1 }, "role": { "default": "gallery", "type": "string", "minLength": 1, "maxLength": 80 } }, "required": [ "tourId" ] } ``` ### list_tags Browse existing content tags (optionally filtered by search) so you can reuse them instead of creating near-duplicates. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "search": { "type": "string", "maxLength": 100 }, "limit": { "default": 100, "type": "integer", "minimum": 1, "maximum": 200 } } } ``` ### check_tour_publishable Dry-run the publish validation for a tour. Returns ok plus the exact blocking errors (e.g. STORY_NO_AUDIO, NO_PRIMARY_IMAGE) and non-blocking warnings, without changing anything. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 } }, "required": [ "tourId" ] } ``` ### publish_tour Validate and publish a tour you own. Fails with the list of blocking reasons if the tour isn't complete (run check_tour_publishable first). On success the tour goes live at its public URL. Input schema (JSON Schema): ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "tourId": { "type": "string", "minLength": 1 } }, "required": [ "tourId" ] } ```