mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 15:33:59 +00:00
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
This commit is contained in:
@@ -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<string, string>): 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: <last event id>\` 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}`);
|
||||
|
||||
@@ -55,38 +55,33 @@ export const RESOURCE_DEFINITIONS: ListResourcesResult['resources'] = [
|
||||
},
|
||||
];
|
||||
|
||||
const RESOURCE_READERS: Record<string, { mimeType: string; read: () => 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() }] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user