From b9f387b53f092e73c5b35b506b20456e97360475 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sun, 3 May 2026 23:49:02 -0400 Subject: [PATCH] Add core MCP server implementation and Ross Talk protocol handler: index.ts --- src/index.ts | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 src/index.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..230320d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,337 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ErrorCode, + ListToolsRequestSchema, + McpError, +} from '@modelcontextprotocol/sdk/types.js'; +import { RossTalkClient } from './ross-talk.js'; +import { RossUltrixState, PanelStatus, InputStatus } from './types.js'; + +class RossUltrixMCPServer { + private server: Server; + private rossTalk: RossTalkClient; + private state: RossUltrixState = { + connected: false, + panels: {}, + inputs: {}, + lastUpdate: new Date() + }; + + constructor() { + this.server = new Server( + { + name: 'ross-ultrix-mcp', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.rossTalk = new RossTalkClient({ + host: process.env.ROSS_TALK_HOST || '192.168.1.100', + port: parseInt(process.env.ROSS_TALK_PORT || '7788'), + onStateChange: (state) => this.updateState(state) + }); + + this.setupHandlers(); + } + + private updateState(newState: Partial) { + this.state = { ...this.state, ...newState, lastUpdate: new Date() }; + } + + private setupHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'ross_connect', + description: 'Connect to Ross Ultrix system via Ross Talk protocol', + }, + { + name: 'ross_disconnect', + description: 'Disconnect from Ross Ultrix system', + }, + { + name: 'ross_get_status', + description: 'Get current system status and panel states', + }, + { + name: 'ross_switch_panel', + description: 'Switch to a specific panel on the Ultrix dashboard', + inputSchema: { + type: 'object', + properties: { + panelId: { + type: 'string', + description: 'Panel ID to switch to (e.g., "ME1", "AUX1")', + }, + }, + required: ['panelId'], + }, + }, + { + name: 'ross_take_transition', + description: 'Execute a take/cut transition on the specified mix effect', + inputSchema: { + type: 'object', + properties: { + meId: { + type: 'string', + description: 'Mix Effect ID (e.g., "ME1", "ME2")', + default: 'ME1' + }, + }, + }, + }, + { + name: 'ross_auto_transition', + description: 'Execute an auto transition with specified duration', + inputSchema: { + type: 'object', + properties: { + meId: { + type: 'string', + description: 'Mix Effect ID', + default: 'ME1' + }, + duration: { + type: 'number', + description: 'Transition duration in frames', + default: 30 + }, + }, + }, + }, + { + name: 'ross_set_preview', + description: 'Set preview source on specified mix effect', + inputSchema: { + type: 'object', + properties: { + meId: { + type: 'string', + description: 'Mix Effect ID', + default: 'ME1' + }, + sourceId: { + type: 'string', + description: 'Source ID to set on preview', + }, + }, + required: ['sourceId'], + }, + }, + { + name: 'ross_set_program', + description: 'Set program source on specified mix effect', + inputSchema: { + type: 'object', + properties: { + meId: { + type: 'string', + description: 'Mix Effect ID', + default: 'ME1' + }, + sourceId: { + type: 'string', + description: 'Source ID to set on program', + }, + }, + required: ['sourceId'], + }, + }, + { + name: 'ross_get_sources', + description: 'Get list of available video sources', + }, + { + name: 'ross_get_panels', + description: 'Get list of available control panels', + }, + { + name: 'ross_macro_run', + description: 'Execute a macro by name or ID', + inputSchema: { + type: 'object', + properties: { + macroId: { + type: 'string', + description: 'Macro name or ID to execute', + }, + }, + required: ['macroId'], + }, + }, + ], + })); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case 'ross_connect': + await this.rossTalk.connect(); + return { + content: [ + { + type: 'text', + text: `Connected to Ross Ultrix at ${this.rossTalk.getHost()}:${this.rossTalk.getPort()}`, + }, + ], + }; + + case 'ross_disconnect': + await this.rossTalk.disconnect(); + return { + content: [ + { + type: 'text', + text: 'Disconnected from Ross Ultrix', + }, + ], + }; + + case 'ross_get_status': + const status = await this.rossTalk.getSystemStatus(); + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + connected: this.state.connected, + systemStatus: status, + lastUpdate: this.state.lastUpdate, + panelCount: Object.keys(this.state.panels).length, + inputCount: Object.keys(this.state.inputs).length + }, null, 2), + }, + ], + }; + + case 'ross_switch_panel': + const { panelId } = args as { panelId: string }; + await this.rossTalk.switchPanel(panelId); + return { + content: [ + { + type: 'text', + text: `Switched to panel: ${panelId}`, + }, + ], + }; + + case 'ross_take_transition': + const { meId: takeMe = 'ME1' } = args as { meId?: string }; + await this.rossTalk.takeTransition(takeMe); + return { + content: [ + { + type: 'text', + text: `Take transition executed on ${takeMe}`, + }, + ], + }; + + case 'ross_auto_transition': + const { meId: autoMe = 'ME1', duration = 30 } = args as { meId?: string; duration?: number }; + await this.rossTalk.autoTransition(autoMe, duration); + return { + content: [ + { + type: 'text', + text: `Auto transition started on ${autoMe} with ${duration} frame duration`, + }, + ], + }; + + case 'ross_set_preview': + const { meId: prevMe = 'ME1', sourceId: prevSource } = args as { meId?: string; sourceId: string }; + await this.rossTalk.setPreview(prevMe, prevSource); + return { + content: [ + { + type: 'text', + text: `Preview set to ${prevSource} on ${prevMe}`, + }, + ], + }; + + case 'ross_set_program': + const { meId: progMe = 'ME1', sourceId: progSource } = args as { meId?: string; sourceId: string }; + await this.rossTalk.setProgram(progMe, progSource); + return { + content: [ + { + type: 'text', + text: `Program set to ${progSource} on ${progMe}`, + }, + ], + }; + + case 'ross_get_sources': + const sources = await this.rossTalk.getSources(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(sources, null, 2), + }, + ], + }; + + case 'ross_get_panels': + const panels = await this.rossTalk.getPanels(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(panels, null, 2), + }, + ], + }; + + case 'ross_macro_run': + const { macroId } = args as { macroId: string }; + await this.rossTalk.runMacro(macroId); + return { + content: [ + { + type: 'text', + text: `Macro executed: ${macroId}`, + }, + ], + }; + + default: + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${name}` + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new McpError( + ErrorCode.InternalError, + `Ross Ultrix operation failed: ${errorMessage}` + ); + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Ross Ultrix MCP server running on stdio'); + } +} + +const server = new RossUltrixMCPServer(); +server.run().catch(console.error);