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:
Claude
2026-04-24 18:16:56 +00:00
committed by Carlos Valente
parent e7d62f44e7
commit 0094c15da2
+184
View File
@@ -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;
}