From 5be596b83e464c81ea532abf31982f8fcde413b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 11:42:42 +0000 Subject: [PATCH] refactor(mcp): dispatch map for resources, userPrompt helper, enrich prompt guidance mcp.resources.ts: replace 6-branch if-chain with a RESOURCE_READERS dispatch map. Each resource is now a one-liner { mimeType, read } entry; handleReadResource collapses to a 3-line lookup with a single throw. mcp.prompts.ts: extract repeated message-wrapping boilerplate into userPrompt() helper, reducing each prompt from ~10 lines of structure to a single call. Enrich all four prompts with operational domain knowledge: - create_rundown_from_agenda: entry type selection (event/milestone/delay/group), timerType and endAction guidance, linkStart chain semantics, flag usage, colour conventions - bulk_edit_rundown: timeStrategy (lock-end vs lock-duration), endAction automation risk, clarified time-shift mechanics - validate_rundown: totalDelay/totalDays checks, linkStart chain validation, play-next chain disclosure, flag audit - restructure_rundown: group awareness (entries vs order arrays), insert order type, move-out vs move-in patterns https://claude.ai/code/session_01U24MeuUacYXeQhbX3tatEe --- apps/server/src/api-mcp/mcp.prompts.ts | 171 +++++++++++++---------- apps/server/src/api-mcp/mcp.resources.ts | 61 ++++---- 2 files changed, 129 insertions(+), 103 deletions(-) diff --git a/apps/server/src/api-mcp/mcp.prompts.ts b/apps/server/src/api-mcp/mcp.prompts.ts index d4d0ddb6f..4226de12e 100644 --- a/apps/server/src/api-mcp/mcp.prompts.ts +++ b/apps/server/src/api-mcp/mcp.prompts.ts @@ -35,17 +35,15 @@ export const PROMPT_DEFINITIONS: ListPromptsResult['prompts'] = [ }, ]; +function userPrompt(description: string, text: string): GetPromptResult { + return { description, messages: [{ role: 'user', content: { type: 'text', text } }] }; +} + export function handleGetPrompt(name: string, args: Record): GetPromptResult { if (name === 'create_rundown_from_agenda') { - const agenda = args.agenda ?? ''; - return { - description: 'Build an Ontime rundown from a plain-text agenda', - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Convert the following agenda into an Ontime rundown. + return userPrompt( + 'Build an Ontime rundown from a plain-text agenda', + `Convert the following agenda into an Ontime rundown. Read the ontime://schema resource if you need a data model reference. Steps: @@ -53,91 +51,124 @@ Steps: 2. Build an array of events in order and call ontime_create_events_batch ONCE with all of them. This is much faster than calling ontime_create_event per item. 3. If the rundown already has events, pass \`after: \` on the batch call so new events chain from the end. +Entry type guidance: +- Use \`event\` for anything with a scheduled time and duration (talks, panels, breaks, meals). +- Use \`milestone\` for non-timed markers that don't advance playback (e.g. "Doors open", "Broadcast start"). +- Use \`delay\` only when the user explicitly wants to model schedule drift that shifts all following events. +- Use \`group\` to collect related events into a named block — set \`targetDuration\` to the block's planned length. + +Timer type (timerType): +- \`count-down\` (default): counts down from duration. Use for most timed sessions. +- \`count-up\`: counts elapsed time. Use for open-ended items like Q&A or audience discussion. +- \`clock\`: shows wall-clock time. Use for broadcast-start or house-open markers. +- \`none\`: no timer shown. Use for purely informational or non-timed items. + +End action (endAction): +- \`none\` (default): stops at end; operator must manually start the next event. +- \`load-next\`: pre-arms the next event; operator triggers start. Use when a human handoff is needed. +- \`play-next\`: automatically starts the next event. Use for seamless back-to-back segments with no gap. + +Linking (linkStart): +- Set \`linkStart: true\` on events that must always follow directly after the previous event's end. +- Changing the first linked event's timeStart or duration cascades to all linked followers. +- Ideal for segments within a block where only the block's start time is managed directly. + +Flags (flag): +- Set \`flag: true\` on events that are critical operational markers (keynote starts, broadcast moments, VIP arrivals). +- The operator view shows a countdown to the next flagged event — use sparingly for maximum impact. + +Colours: +- Ask the user what colour convention they use before applying any colours. +- Common pattern: one colour per event type (keynotes, panels, breaks, meals). +- Colours are hex strings: \`#RRGGBB\`. + Agenda: -${agenda}`, - }, - }, - ], - }; +${args.agenda}`, + ); } if (name === 'bulk_edit_rundown') { - const instruction = args.instruction ?? ''; - return { - description: 'Apply a bulk change across the rundown', - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Apply the following bulk edit to the current Ontime rundown: "${instruction}" + return userPrompt( + 'Apply a bulk change across the rundown', + `Apply the following bulk edit to the current Ontime rundown: "${args.instruction}" Strategy: 1. Call ontime_get_rundown to see the current events, their IDs, and field values. 2. Determine which event IDs are affected by the instruction. 3. If every affected event receives the SAME field changes (e.g. "colour all keynotes purple", "skip all breaks"): call ontime_batch_update_events once with { ids, data }. 4. If each event needs DIFFERENT values (e.g. "shift everything 30 minutes"): check first if events use linkStart. If they do, changing the first linked event's timeStart cascades to all linked followers — you may only need to update one event. Otherwise, compute the new values per event and call ontime_update_event for each. -5. Time fields are milliseconds from midnight; compute arithmetic before calling the tools. -Confirm with the user before making destructive changes like setting skip=true on many events.`, - }, - }, - ], - }; +Time shift mechanics: +- All time fields are milliseconds from midnight; compute arithmetic before calling the tools. +- timeEnd - timeStart = duration. When shifting times, decide whether to keep duration fixed (timeEnd moves with timeStart) or keep timeEnd fixed (duration shrinks). Provide only the fields you intend to change — the server infers the strategy from which fields are present. +- For "shift everything N minutes later": update timeStart and timeEnd per event (or just timeStart on the first event of a linkStart chain). + +Automation risks: +- Setting \`endAction: 'play-next'\` on multiple events creates an automatic playback chain that removes operator control between those events. Confirm with the user before applying. +- Bulk-setting \`skip: true\` will hide events from playback. Confirm before applying to many events.`, + ); } if (name === 'validate_rundown') { - return { - description: 'Check the current rundown for common issues', - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Validate the currently loaded Ontime rundown and report issues. + return userPrompt( + 'Check the current rundown for common issues', + `Validate the currently loaded Ontime rundown and report issues. Steps: -1. Call ontime_get_rundown to read all events. -2. Call ontime_get_rundown_metadata for totals (total duration, first/last times, flagged IDs). -3. Check and report: - - Events with missing or duplicate \`cue\` - - Events with missing \`title\` - - Events with \`duration\` of 0 or negative - - Events where \`timeEnd\` is before \`timeStart\` - - Events whose \`timeStart\` overlaps the previous event's \`timeEnd\` (schedule conflict) - - Large unexplained gaps between consecutive events (> 30 min) that may indicate missing breaks - - Events flagged \`skip: true\` — confirm with the user these are intentional - - Total rundown duration and whether it matches the user's expected show length (ask if unknown) +1. Call ontime_get_rundown to read all events and their fields. +2. Call ontime_get_rundown_metadata for totals (totalDuration, totalDelay, totalDays, firstStart, lastEnd, flags). + +Check and report: + +Schedule integrity: +- Events with missing or duplicate \`cue\` +- Events with missing \`title\` +- Events with \`duration\` of 0 or negative +- Events where \`timeEnd\` < \`timeStart\` +- Events whose \`timeStart\` overlaps the previous event's \`timeEnd\` (gap < 0 means a conflict) +- Large unexplained gaps between consecutive events (> 30 min) that may indicate a missing break + +Timing and linking: +- \`metadata.totalDays > 0\`: show spans midnight — confirm this is intentional +- \`metadata.totalDelay !== 0\`: active delay entries are shifting the schedule by this many ms; report the net shift +- Events with \`linkStart: true\` that are first in the rundown (no predecessor to link to) + +Automation: +- Events with \`endAction: 'play-next'\`: these form automatic playback chains. List each chain so the user can confirm the automation is intentional. +- Events with \`flag: true\`: these are the operator's critical markers. List them so the user can verify they are correct and complete. + +Skipped events: +- Events with \`skip: true\` — confirm with the user these are intentional + +Totals: +- Total rundown duration and whether it matches the user's expected show length (ask if unknown) Present issues grouped by severity: ERROR (breaks playback), WARNING (likely mistake), INFO (worth confirming).`, - }, - }, - ], - }; + ); } if (name === 'restructure_rundown') { - const instruction = args.instruction ?? ''; - return { - description: 'Reorder events in the rundown', - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Restructure the current Ontime rundown: "${instruction}" + return userPrompt( + 'Reorder events in the rundown', + `Restructure the current Ontime rundown: "${args.instruction}" Steps: -1. Call ontime_get_rundown to see the current order and event fields. -2. Compute the target order as an array of event IDs. -3. For each event that needs to move, call ontime_reorder_event with { entryId, destinationId, order: 'before' | 'after' }. -4. Call ontime_get_rundown again at the end to confirm the new order. +1. Call ontime_get_rundown to see the current order, groups, and event fields. +2. Note which events are inside groups (check each group's \`entries\` array vs the top-level \`order\` array). +3. Compute the target arrangement as a sequence of moves. +4. For each event that needs to move, call ontime_reorder_event: + - \`order: 'before'\` or \`'after'\` — places the event as a sibling next to destinationId + - \`order: 'insert'\` — places the event inside a group (destinationId must be the group's ID) +5. Call ontime_get_rundown again to confirm the new order. -Tip: moving items in the "to" direction of the target position minimises reorder calls. Plan the sequence of moves to avoid moving the same event twice.`, - }, - }, - ], - }; +Group awareness: +- Events inside a group appear in the group's \`entries\` array, not in the top-level \`order\`. +- To move an event out of a group, reorder it before/after a top-level entry. +- To move an event into a group, use \`order: 'insert'\` with the group as destinationId. +- A group's \`targetDuration\` is a planning hint only — moving events in or out does not break anything. + +Efficiency tip: plan moves in the direction of the target position to minimise reorder calls. Avoid moving the same event twice — compute the full target sequence before issuing any calls.`, + ); } throw new Error(`Unknown prompt: ${name}`); diff --git a/apps/server/src/api-mcp/mcp.resources.ts b/apps/server/src/api-mcp/mcp.resources.ts index cdc237216..523648a84 100644 --- a/apps/server/src/api-mcp/mcp.resources.ts +++ b/apps/server/src/api-mcp/mcp.resources.ts @@ -55,38 +55,33 @@ export const RESOURCE_DEFINITIONS: ListResourcesResult['resources'] = [ }, ]; +const RESOURCE_READERS: Record string }> = { + 'ontime://schema': { mimeType: 'text/markdown', read: () => ONTIME_SCHEMA_MARKDOWN }, + 'ontime://docs': { mimeType: 'text/markdown', read: () => ONTIME_DOCS_MARKDOWN }, + 'ontime://rundown/current': { + mimeType: 'application/json', + read: () => JSON.stringify(getCurrentRundown()), + }, + 'ontime://rundowns': { + mimeType: 'application/json', + read: () => { + const rundowns = normalisedToRundownArray(getDataProvider().getProjectRundowns()); + const loaded = getCurrentRundown().id; + return JSON.stringify({ loaded, rundowns }); + }, + }, + 'ontime://project/info': { + mimeType: 'application/json', + read: () => JSON.stringify(getProjectData()), + }, + 'ontime://project/custom-fields': { + mimeType: 'application/json', + read: () => JSON.stringify(getProjectCustomFields()), + }, +}; + export function handleReadResource(uri: string): ReadResourceResult { - if (uri === 'ontime://schema') { - return { contents: [{ uri, mimeType: 'text/markdown', text: ONTIME_SCHEMA_MARKDOWN }] }; - } - - if (uri === 'ontime://rundown/current') { - return { - contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(getCurrentRundown()) }], - }; - } - - 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()) }], - }; - } - - if (uri === 'ontime://docs') { - return { contents: [{ uri, mimeType: 'text/markdown', text: ONTIME_DOCS_MARKDOWN }] }; - } - - throw new Error(`Unknown resource URI: ${uri}`); + const reader = RESOURCE_READERS[uri]; + if (!reader) throw new Error(`Unknown resource URI: ${uri}`); + return { contents: [{ uri, mimeType: reader.mimeType, text: reader.read() }] }; }