mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 23:49:15 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3134e5027a | |||
| 94024a7cdf |
@@ -33,6 +33,17 @@ export const PROMPT_DEFINITIONS: ListPromptsResult['prompts'] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'manage_custom_fields',
|
||||||
|
description: 'List, create, rename, recolour, or delete project-level custom field definitions',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'instruction',
|
||||||
|
description: 'What to do, e.g. "add a Speaker field" or "delete the Camera field"',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function userPrompt(description: string, text: string): GetPromptResult {
|
function userPrompt(description: string, text: string): GetPromptResult {
|
||||||
@@ -79,9 +90,11 @@ Count to end (countToEnd):
|
|||||||
- Do not set \`countToEnd: true\` unless the user explicitly asks for "count to end", "count to scheduled end", or confirms after you explain this behaviour.
|
- Do not set \`countToEnd: true\` unless the user explicitly asks for "count to end", "count to scheduled end", or confirms after you explain this behaviour.
|
||||||
|
|
||||||
End action (endAction):
|
End action (endAction):
|
||||||
- \`none\` (default): stops at end; operator must manually start the next event.
|
- Leave \`endAction\` unset unless the user explicitly asks for automatic cueing or chaining. It defaults to \`none\`.
|
||||||
- \`load-next\`: pre-arms the next event; operator triggers start. Use when a human handoff is needed.
|
- \`load-next\` and \`play-next\` create playback automations that remove operator control between events and can surprise operators. Do NOT add them just because events are scheduled back-to-back — a seamless schedule does not imply automated playback.
|
||||||
- \`play-next\`: automatically starts the next event. Use for seamless back-to-back segments with no gap.
|
- \`none\` (default): stops at end; operator manually starts the next event. Use this for essentially every event unless told otherwise.
|
||||||
|
- \`load-next\`: pre-arms the next event; operator triggers start. Only set when the user explicitly asks for it.
|
||||||
|
- \`play-next\`: automatically starts the next event. Only set when the user explicitly asks for an automatic playback chain; confirm they understand it removes operator control between those events.
|
||||||
|
|
||||||
Linking (linkStart):
|
Linking (linkStart):
|
||||||
- \`linkStart\` controls schedule-change propagation through the rundown.
|
- \`linkStart\` controls schedule-change propagation through the rundown.
|
||||||
@@ -203,5 +216,36 @@ Efficiency tip: plan moves in the direction of the target position to minimise r
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'manage_custom_fields') {
|
||||||
|
return userPrompt(
|
||||||
|
'List, create, rename, recolour, or delete custom field definitions',
|
||||||
|
`Manage the project-level custom field definitions for this Ontime project: "${args.instruction}"
|
||||||
|
|
||||||
|
Custom fields are project-scoped. They appear as columns in the cuesheet and can be set on any event, milestone, or group. Changes apply to all rundowns in the project.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Call ontime_get_custom_fields to see what fields already exist. This is mandatory before creating — to avoid duplicates such as "Cam", "camera", and "Cameras".
|
||||||
|
2. Carry out the instruction using the tools below. Confirm destructive changes (rename, delete) with the user before calling.
|
||||||
|
|
||||||
|
Creating a field:
|
||||||
|
- Call ontime_create_custom_field with { label, type, colour }.
|
||||||
|
- label: human-readable name (alphanumeric with spaces). The key is auto-derived: spaces → underscores (e.g. "Camera Angle" → "Camera_Angle"). Confirm the derived key with the user before creating.
|
||||||
|
- type: "text" for short string values, "image" for image URLs. Cannot be changed after creation.
|
||||||
|
- colour: hex colour (#RRGGBB) used to visually identify this column in the cuesheet. Ask the user what colour to use if not specified.
|
||||||
|
- Returns: { key, customFields } — use the returned key when setting values on entries.
|
||||||
|
|
||||||
|
Renaming or recolouring a field:
|
||||||
|
- Call ontime_update_custom_field with { key, label?, colour? }.
|
||||||
|
- Warning: changing the label changes the derived key and cascades to all entry references in all rundowns. Confirm with the user before renaming.
|
||||||
|
- The field type cannot be changed.
|
||||||
|
|
||||||
|
Deleting a field:
|
||||||
|
- Call ontime_delete_custom_field with { key }.
|
||||||
|
- Warning: this permanently removes the field definition and its values from every entry in all rundowns. Confirm with the user before deleting.
|
||||||
|
|
||||||
|
After any mutation, call ontime_get_custom_fields again to confirm the result and show the user the updated field list with their keys.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Unknown prompt: ${name}`);
|
throw new Error(`Unknown prompt: ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export const EVENT_TIMER_FIELDS = {
|
|||||||
endAction: {
|
endAction: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['none', 'load-next', 'play-next'],
|
enum: ['none', 'load-next', 'play-next'],
|
||||||
description: 'Action when event ends: none = stop, load-next = cue next event, play-next = auto-start next event',
|
description:
|
||||||
|
'Action when the event ends. Defaults to none. load-next and play-next create playback automations (load-next arms the next event, play-next auto-starts it) that remove operator control between events and can surprise operators. Only set load-next or play-next when the user explicitly asks for automatic cueing or chaining; otherwise omit this field and leave it as none. Do not add end actions just because events are back-to-back.',
|
||||||
},
|
},
|
||||||
linkStart: {
|
linkStart: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -116,7 +117,7 @@ There are four entry types discriminated by \`type\`:
|
|||||||
dayOffset: number // runtime calculated day offset from the rundown start schedule, increments when the rundown crosses midnight
|
dayOffset: number // runtime calculated day offset from the rundown start schedule, increments when the rundown crosses midnight
|
||||||
gap: number // schedule gap between sequential playable events; negative gap means overlap
|
gap: number // schedule gap between sequential playable events; negative gap means overlap
|
||||||
timerType: 'count-down' | 'count-up' | 'clock' | 'none'
|
timerType: 'count-down' | 'count-up' | 'clock' | 'none'
|
||||||
endAction: 'none' | 'load-next' | 'play-next'
|
endAction: 'none' | 'load-next' | 'play-next' // load-next/play-next create playback automations; leave as none unless the user explicitly asks for cueing/chaining
|
||||||
linkStart: boolean // chain start to previous event's end
|
linkStart: boolean // chain start to previous event's end
|
||||||
countToEnd: boolean // advanced mode: counts to scheduled timeEnd instead of duration; confirm before enabling
|
countToEnd: boolean // advanced mode: counts to scheduled timeEnd instead of duration; confirm before enabling
|
||||||
skip: boolean // event is skipped during playback
|
skip: boolean // event is skipped during playback
|
||||||
@@ -177,8 +178,15 @@ Examples:
|
|||||||
## Custom fields
|
## Custom fields
|
||||||
Custom fields are project-scoped definitions. Get definitions at \`ontime://project/custom-fields\` or with \`ontime_get_custom_fields\`.
|
Custom fields are project-scoped definitions. Get definitions at \`ontime://project/custom-fields\` or with \`ontime_get_custom_fields\`.
|
||||||
Events, milestones and groups store values at \`entry.custom[fieldKey]\`.
|
Events, milestones and groups store values at \`entry.custom[fieldKey]\`.
|
||||||
Only use existing field keys when setting \`custom\` values. Adding, renaming, or deleting custom field definitions is a separate project-level operation.
|
|
||||||
When the user wants to assign custom values, show the existing custom field list first if there is any ambiguity, so you do not create duplicate concepts such as \`Cam\`, \`camera\`, and \`Cameras\`.
|
Managing field definitions:
|
||||||
|
- Read: \`ontime_get_custom_fields\` — returns \`{ [key]: { label, type, colour } }\`
|
||||||
|
- Create: \`ontime_create_custom_field { label, type, colour }\` — key is auto-derived from label (spaces → underscores). Confirm the derived key before creating. Returns \`{ key, customFields }\`.
|
||||||
|
- Rename/recolour: \`ontime_update_custom_field { key, label?, colour? }\` — renaming the label changes the derived key and cascades to all entry references across all rundowns. Type cannot be changed.
|
||||||
|
- Delete: \`ontime_delete_custom_field { key }\` — removes the field definition and its values from every entry in all rundowns. Destructive, confirm first.
|
||||||
|
|
||||||
|
When setting \`custom\` values on entries, only use existing field keys. If a key does not exist, create it with \`ontime_create_custom_field\` first.
|
||||||
|
Show the existing field list before creating to avoid duplicates such as \`Cam\`, \`camera\`, and \`Cameras\`.
|
||||||
|
|
||||||
## Targeting rundowns
|
## Targeting rundowns
|
||||||
Entry read/write tools accept an optional \`rundownId\`.
|
Entry read/write tools accept an optional \`rundownId\`.
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import { getCurrentRundown, getCurrentRundownId, getProjectCustomFields } from '
|
|||||||
import {
|
import {
|
||||||
addEntry,
|
addEntry,
|
||||||
batchEditEntries,
|
batchEditEntries,
|
||||||
|
createCustomField,
|
||||||
|
deleteCustomField,
|
||||||
deleteEntries,
|
deleteEntries,
|
||||||
|
editCustomField,
|
||||||
editEntry,
|
editEntry,
|
||||||
groupEntries,
|
groupEntries,
|
||||||
reorderEntry,
|
reorderEntry,
|
||||||
@@ -103,8 +106,15 @@ export function assertKnownCustomFields(...customValues: Array<EntryFieldArgs['c
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (unknownKeys.size > 0) {
|
if (unknownKeys.size > 0) {
|
||||||
const keys = [...unknownKeys].join(', ');
|
const missing = [...unknownKeys].join(', ');
|
||||||
throw new Error(`Unknown custom field key(s): ${keys}. Call ontime_get_custom_fields to list available keys.`);
|
const available = Object.keys(customFields);
|
||||||
|
const hint = available.length > 0 ? `Available keys: ${available.join(', ')}.` : 'No custom fields are defined yet.';
|
||||||
|
throw new Error(
|
||||||
|
`Unknown custom field key(s): ${missing}. ${hint} ` +
|
||||||
|
`Call ontime_create_custom_field with { label, type, colour } to create a missing field — ` +
|
||||||
|
`the key is auto-derived from the label (spaces → underscores, e.g. label "Camera" → key "Camera"). ` +
|
||||||
|
`Call ontime_get_custom_fields to list existing keys.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,3 +330,21 @@ export async function batchUpdateEntriesForMcp(args: TargetRundownArgs & { ids:
|
|||||||
const rundown = await batchEditEntries(rundownId, args.ids, args.data);
|
const rundown = await batchEditEntries(rundownId, args.ids, args.data);
|
||||||
return { target: getTargetMeta(rundownId), updated: args.ids, order: rundown.order };
|
return { target: getTargetMeta(rundownId), updated: args.ids, order: rundown.order };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createCustomFieldForMcp(args: { label: string; type: 'text' | 'image'; colour: string }) {
|
||||||
|
const updated = await createCustomField({ label: args.label, type: args.type, colour: args.colour });
|
||||||
|
const key = Object.keys(updated).find((k) => updated[k].label === args.label) ?? args.label;
|
||||||
|
return { key, customFields: updated };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomFieldForMcp(args: { key: string; label?: string; colour?: string }) {
|
||||||
|
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||||
|
const updated = await editCustomField(args.key, { label: args.label, colour: args.colour }, projectRundowns);
|
||||||
|
return { customFields: updated };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCustomFieldForMcp(args: { key: string }) {
|
||||||
|
const projectRundowns = getDataProvider().getProjectRundowns();
|
||||||
|
const updated = await deleteCustomField(args.key, projectRundowns);
|
||||||
|
return { customFields: updated };
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import { EVENT_WRITABLE_FIELDS, RUNDOWN_TARGET_FIELD } from './mcp.schema.js';
|
|||||||
import {
|
import {
|
||||||
batchCreateEntriesForMcp,
|
batchCreateEntriesForMcp,
|
||||||
batchUpdateEntriesForMcp,
|
batchUpdateEntriesForMcp,
|
||||||
|
createCustomFieldForMcp,
|
||||||
createEntryForMcp,
|
createEntryForMcp,
|
||||||
|
deleteCustomFieldForMcp,
|
||||||
deleteEntriesForMcp,
|
deleteEntriesForMcp,
|
||||||
findEntry,
|
findEntry,
|
||||||
getRundownById,
|
getRundownById,
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
reorderEntryForMcp,
|
reorderEntryForMcp,
|
||||||
toRundownList,
|
toRundownList,
|
||||||
ungroupEntryForMcp,
|
ungroupEntryForMcp,
|
||||||
|
updateCustomFieldForMcp,
|
||||||
updateEntryForMcp,
|
updateEntryForMcp,
|
||||||
type BatchCreateEntryArgs,
|
type BatchCreateEntryArgs,
|
||||||
type CreateEntryArgs,
|
type CreateEntryArgs,
|
||||||
@@ -371,6 +374,60 @@ export const TOOL_DEFINITIONS = [
|
|||||||
inputSchema: { type: 'object', properties: {} },
|
inputSchema: { type: 'object', properties: {} },
|
||||||
annotations: READ,
|
annotations: READ,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ontime_create_custom_field',
|
||||||
|
description:
|
||||||
|
'Create a new project-level custom field definition. Custom fields add typed columns to every entry in all rundowns. The key is auto-derived from the label (spaces → underscores, e.g. "Camera Angle" → "Camera_Angle"). After creation, use the key in entry.custom when creating or updating entries.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['label', 'type', 'colour'],
|
||||||
|
properties: {
|
||||||
|
label: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Human-readable label (alphanumeric with spaces, e.g. "Camera"). Determines the key — ask the user to confirm before creating to avoid duplicates like "Cam", "camera", "Cameras".',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['text', 'image'],
|
||||||
|
description: 'Field type — cannot be changed after creation. Use "text" for short text values; "image" for image URLs.',
|
||||||
|
},
|
||||||
|
colour: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Hex colour (#RRGGBB) used to visually identify this column in the cuesheet.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
annotations: WRITE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ontime_update_custom_field',
|
||||||
|
description:
|
||||||
|
'Update a custom field label or colour. Changing the label renames the derived key (spaces → underscores) and updates all entry references across all rundowns. Field type cannot be changed.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['key'],
|
||||||
|
properties: {
|
||||||
|
key: { type: 'string', description: 'Current field key (from ontime_get_custom_fields)' },
|
||||||
|
label: { type: 'string', description: 'New human-readable label (optional). Changes the derived key and cascades to all entries.' },
|
||||||
|
colour: { type: 'string', description: 'New hex colour (#RRGGBB) (optional)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
annotations: WRITE_IDEM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ontime_delete_custom_field',
|
||||||
|
description:
|
||||||
|
'Delete a custom field definition and remove its values from all entries in all rundowns. Destructive and cannot be undone — confirm with the user before calling.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['key'],
|
||||||
|
properties: {
|
||||||
|
key: { type: 'string', description: 'Field key to delete (from ontime_get_custom_fields)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
annotations: WRITE_DESTRUCTIVE,
|
||||||
|
},
|
||||||
// --- Project file management ---
|
// --- Project file management ---
|
||||||
{
|
{
|
||||||
name: 'ontime_list_projects',
|
name: 'ontime_list_projects',
|
||||||
@@ -571,6 +628,18 @@ const TOOL_HANDLERS: Record<ToolName, (args: Record<string, unknown>) => Promise
|
|||||||
|
|
||||||
ontime_get_custom_fields: async () => ok(getProjectCustomFields()),
|
ontime_get_custom_fields: async () => ok(getProjectCustomFields()),
|
||||||
|
|
||||||
|
ontime_create_custom_field: async (args) => {
|
||||||
|
return ok(await createCustomFieldForMcp(args as { label: string; type: 'text' | 'image'; colour: string }));
|
||||||
|
},
|
||||||
|
|
||||||
|
ontime_update_custom_field: async (args) => {
|
||||||
|
return ok(await updateCustomFieldForMcp(args as { key: string; label?: string; colour?: string }));
|
||||||
|
},
|
||||||
|
|
||||||
|
ontime_delete_custom_field: async (args) => {
|
||||||
|
return ok(await deleteCustomFieldForMcp(args as { key: string }));
|
||||||
|
},
|
||||||
|
|
||||||
ontime_list_projects: async () => ok(await getProjectList()),
|
ontime_list_projects: async () => ok(await getProjectList()),
|
||||||
|
|
||||||
ontime_load_project: async (args) => {
|
ontime_load_project: async (args) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user