mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 00:43:54 +00:00
feat(mcp): add custom field management tools
Adds three new MCP tools for managing project-level custom field definitions: - ontime_create_custom_field: create a field with label, type, colour; key is auto-derived from label - ontime_update_custom_field: rename or recolour an existing field (cascades key renames to all entries) - ontime_delete_custom_field: remove a field and its values from all entries across all rundowns Also improves the assertKnownCustomFields error to guide the agent toward creating the missing field rather than just reporting it as unknown, and adds a manage_custom_fields prompt for guided field management workflows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011u3XRF6QwHZNB5o69xJ7UB
This commit is contained in:
@@ -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 {
|
||||
@@ -203,5 +214,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}`);
|
||||
}
|
||||
|
||||
@@ -177,8 +177,15 @@ Examples:
|
||||
## 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]\`.
|
||||
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
|
||||
Entry read/write tools accept an optional \`rundownId\`.
|
||||
|
||||
@@ -17,7 +17,10 @@ import { getCurrentRundown, getCurrentRundownId, getProjectCustomFields } from '
|
||||
import {
|
||||
addEntry,
|
||||
batchEditEntries,
|
||||
createCustomField,
|
||||
deleteCustomField,
|
||||
deleteEntries,
|
||||
editCustomField,
|
||||
editEntry,
|
||||
groupEntries,
|
||||
reorderEntry,
|
||||
@@ -103,8 +106,15 @@ export function assertKnownCustomFields(...customValues: Array<EntryFieldArgs['c
|
||||
}
|
||||
|
||||
if (unknownKeys.size > 0) {
|
||||
const keys = [...unknownKeys].join(', ');
|
||||
throw new Error(`Unknown custom field key(s): ${keys}. Call ontime_get_custom_fields to list available keys.`);
|
||||
const missing = [...unknownKeys].join(', ');
|
||||
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);
|
||||
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 {
|
||||
batchCreateEntriesForMcp,
|
||||
batchUpdateEntriesForMcp,
|
||||
createCustomFieldForMcp,
|
||||
createEntryForMcp,
|
||||
deleteCustomFieldForMcp,
|
||||
deleteEntriesForMcp,
|
||||
findEntry,
|
||||
getRundownById,
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
reorderEntryForMcp,
|
||||
toRundownList,
|
||||
ungroupEntryForMcp,
|
||||
updateCustomFieldForMcp,
|
||||
updateEntryForMcp,
|
||||
type BatchCreateEntryArgs,
|
||||
type CreateEntryArgs,
|
||||
@@ -371,6 +374,60 @@ export const TOOL_DEFINITIONS = [
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
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 ---
|
||||
{
|
||||
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_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_load_project: async (args) => {
|
||||
|
||||
Reference in New Issue
Block a user