337 lines
9.7 KiB
JavaScript
337 lines
9.7 KiB
JavaScript
#!/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<RossUltrixState>) {
|
|
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);
|