mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
feat(mcp): expose rundown and schema as readable MCP resources
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
This commit is contained in:
@@ -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<string, StreamableHTTPServerTransport>();
|
||||
|
||||
// ---- 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<ListResourcesResult> => {
|
||||
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<ReadResourceResult> => {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user