From 0094c15da2b2e4d1dab705f689dc415a615aa639 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 18:16:56 +0000 Subject: [PATCH] feat(mcp): expose rundown and schema as readable MCP resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds five resources so the agent can load context once per session instead of re-fetching via tools, and so it can ground its answers in Ontime's data model without guessing: - ontime://schema — markdown reference for the event/rundown/group model, time format (ms from midnight), cue conventions, colours, playback states - ontime://rundown/current — full currently-loaded rundown (JSON) - ontime://rundowns — every rundown in the project plus the loaded ID - ontime://project/info — project metadata - ontime://project/custom-fields — custom field definitions Declares the resources capability on the MCP server and registers resources/list and resources/read handlers that dispatch by URI. https://claude.ai/code/session_01U24MeuUacYXeQhbX3tatEe --- apps/server/src/api-mcp/mcp.router.ts | 184 ++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/apps/server/src/api-mcp/mcp.router.ts b/apps/server/src/api-mcp/mcp.router.ts index a1ce58577..419ec08c0 100644 --- a/apps/server/src/api-mcp/mcp.router.ts +++ b/apps/server/src/api-mcp/mcp.router.ts @@ -41,15 +41,111 @@ import { CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, CallToolResult, ListToolsResult, ListPromptsResult, GetPromptResult, + ListResourcesResult, + ReadResourceResult, } from '@modelcontextprotocol/sdk/types.js'; /** Active sessions indexed by session ID */ const sessions = new Map(); +// ---- Static schema document exposed as a resource ---- +const ONTIME_SCHEMA_MARKDOWN = `# Ontime data model + +A concise reference for how Ontime structures rundowns, events, and related data. Read this once per session to ground your answers in Ontime's semantics. + +## Rundown + +A rundown is an ordered list of entries rendered as a show schedule. A project can contain multiple rundowns; one is "loaded" at a time. + +\`\`\` +Rundown { + id: string + title: string + order: EntryId[] // top-level entry order + flatOrder: EntryId[] // includes entries nested in groups + entries: { [id: EntryId]: OntimeEntry } + revision: number +} +\`\`\` + +## Entries + +There are three entry types discriminated by \`type\`: + +### \`event\` — OntimeEvent (a timed show item) +\`\`\` +{ + type: 'event' + id: EntryId + cue: string // human-facing cue label, e.g. "K01" + title: string + note: string + colour: string // hex, e.g. "#4A90D9" + timeStart: number // ms from midnight (09:00 = 32400000) + timeEnd: number // ms from midnight + duration: number // ms (= timeEnd - timeStart) + delay: number // accumulated delay in ms + timerType: 'count-down' | 'count-up' | 'time-to-end' | 'clock' + endAction: 'none' | 'stop' | 'load-next' | 'play-next' + linkStart: boolean // chain start to previous event's end + countToEnd: boolean // timer counts to planned end time + skip: boolean // event is skipped during playback + timeWarning: number // ms before end to trigger 'warning' state + timeDanger: number // ms before end to trigger 'danger' state + custom: { [key: string]: string } // custom field values + triggers: AutomationTrigger[] +} +\`\`\` + +### \`delay\` — Delay (schedule shift applied to following events) +\`\`\` +{ type: 'delay', id, duration: number } +\`\`\` + +### \`group\` — Group (nested container of entries) +\`\`\` +{ type: 'group', id, title, colour, note, entries: EntryId[], targetDuration?: number } +\`\`\` + +## Time format +All time fields are **milliseconds from midnight (local)**. Examples: +- 09:00:00 = 32400000 +- 09:30:00 = 34200000 +- 14:15:00 = 51300000 +- duration of 45 min = 2700000 + +## Cue conventions (not enforced — useful when generating) +- Keynotes: K01, K02, … +- Panels: P01, P02, … +- Breaks: B01, B02, … +- Meals: M01, M02, … + +## Colours (common Ontime palette) +- Keynotes: #4A90D9 +- Panels: #7B68EE +- Breaks: #888888 +- Meals: #E8A838 + +## Custom fields +Custom fields are project-scoped name/type/colour definitions stored at \`ontime://project/custom-fields\`. Each event stores values at \`event.custom[fieldKey]\`. + +## Playback states (runtime only) +\`'stop' | 'play' | 'pause' | 'armed' | 'roll'\`. When playback is not \`stop\`, mutating tools warn that changes are visible immediately. + +## Useful resource URIs +- \`ontime://schema\` — this document +- \`ontime://rundown/current\` — the currently loaded rundown (JSON) +- \`ontime://rundowns\` — all rundowns in the project (JSON) +- \`ontime://project/info\` — project metadata (JSON) +- \`ontime://project/custom-fields\` — custom field definitions (JSON) +`; + // ---- Tool definitions ---- const TOOL_DEFINITIONS = [ { @@ -702,6 +798,7 @@ function createMcpServer(): Server { capabilities: { tools: {}, prompts: {}, + resources: {}, }, }, ); @@ -882,6 +979,93 @@ Tip: moving items in the "to" direction of the target position minimises reorder throw new Error(`Unknown prompt: ${name}`); }); + // Handle resources/list — static list of readable resources the agent can load into context + server.setRequestHandler(ListResourcesRequestSchema, async (): Promise => { + return { + resources: [ + { + uri: 'ontime://schema', + name: 'ontime-schema', + title: 'Ontime data model reference', + description: + 'Markdown reference for how Ontime structures rundowns, events, delays, groups, time fields, cue conventions, and colours. Read once per session to ground your answers.', + mimeType: 'text/markdown', + }, + { + uri: 'ontime://rundown/current', + name: 'current-rundown', + title: 'Currently loaded rundown', + description: + 'The rundown currently active in Ontime, with its full entries map and order. Re-read after any mutating call to see updated state.', + mimeType: 'application/json', + }, + { + uri: 'ontime://rundowns', + name: 'project-rundowns', + title: 'All rundowns in the project', + description: + 'List of every rundown stored in the current project file, plus the ID of the one currently loaded.', + mimeType: 'application/json', + }, + { + uri: 'ontime://project/info', + name: 'project-info', + title: 'Project metadata', + description: 'Project title, description, URL, info, logo, and custom header fields.', + mimeType: 'application/json', + }, + { + uri: 'ontime://project/custom-fields', + name: 'project-custom-fields', + title: 'Custom field definitions', + description: + 'Map of custom field keys to their label, type, and colour. Events reference these keys in their `custom` object.', + mimeType: 'application/json', + }, + ], + }; + }); + + // Handle resources/read — return the resource body for a given URI + server.setRequestHandler(ReadResourceRequestSchema, async (request): Promise => { + const uri = request.params.uri; + + if (uri === 'ontime://schema') { + return { + contents: [{ uri, mimeType: 'text/markdown', text: ONTIME_SCHEMA_MARKDOWN }], + }; + } + + if (uri === 'ontime://rundown/current') { + const rundown = getCurrentRundown(); + return { + contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(rundown) }], + }; + } + + if (uri === 'ontime://rundowns') { + const rundowns = normalisedToRundownArray(getDataProvider().getProjectRundowns()); + const loaded = getCurrentRundown().id; + return { + contents: [{ uri, mimeType: 'application/json', text: JSON.stringify({ loaded, rundowns }) }], + }; + } + + if (uri === 'ontime://project/info') { + return { + contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(getProjectData()) }], + }; + } + + if (uri === 'ontime://project/custom-fields') { + return { + contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(getProjectCustomFields()) }], + }; + } + + throw new Error(`Unknown resource URI: ${uri}`); + }); + return server; }