Skip to main content
Glama

LacyLights MCP Server

by bbernstein
index.ts70.2 kB
#!/usr/bin/env node import { Server, StdioServerTransport, CallToolRequestSchema, ListToolsRequestSchema, } from "./mcp-imports"; import { LacyLightsGraphQLClient } from "./services/graphql-client-simple"; import { RAGService } from "./services/rag-service-simple"; import { AILightingService } from "./services/ai-lighting"; import { FixtureTools } from "./tools/fixture-tools"; import { SceneTools } from "./tools/scene-tools"; import { CueTools } from "./tools/cue-tools"; import { ProjectTools } from "./tools/project-tools"; import { logger } from "./utils/logger"; class LacyLightsMCPServer { private server: Server; private graphqlClient: LacyLightsGraphQLClient; private ragService: RAGService; private aiLightingService: AILightingService; private fixtureTools: FixtureTools; private sceneTools: SceneTools; private cueTools: CueTools; private projectTools: ProjectTools; constructor() { this.server = new Server( { name: "lacylights-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); // Initialize services const graphqlEndpoint = process.env.LACYLIGHTS_GRAPHQL_ENDPOINT || "http://localhost:4000/graphql"; this.graphqlClient = new LacyLightsGraphQLClient(graphqlEndpoint); this.ragService = new RAGService(); this.aiLightingService = new AILightingService(this.ragService); // Initialize tools this.fixtureTools = new FixtureTools(this.graphqlClient); this.sceneTools = new SceneTools( this.graphqlClient, this.ragService, this.aiLightingService, ); this.cueTools = new CueTools( this.graphqlClient, this.ragService, this.aiLightingService, ); this.projectTools = new ProjectTools(this.graphqlClient); this.setupHandlers(); } private setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Project Tools { name: "list_projects", description: "List all available lighting projects", inputSchema: { type: "object", properties: { includeDetails: { type: "boolean", default: false, description: "Include fixture and scene counts", }, }, }, }, { name: "create_project", description: "Create a new lighting project", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name", }, description: { type: "string", description: "Project description", }, }, required: ["name"], }, }, { name: "get_project_details", description: "Get detailed information about a specific project", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to get details for", }, }, required: ["projectId"], }, }, { name: "delete_project", description: "Delete a project and all its data", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to delete", }, confirmDelete: { type: "boolean", default: false, description: "Confirm deletion of project and all its data", }, }, required: ["projectId", "confirmDelete"], }, }, { name: "qlc_import_guidance", description: "Get information about importing QLC+ (.qxw) files into LacyLights", inputSchema: { type: "object", properties: {}, }, }, // Fixture Tools { name: "get_fixture_inventory", description: "Get available lighting fixtures and their capabilities for a project or globally", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Optional project ID to filter fixtures", }, fixtureType: { type: "string", enum: ["LED_PAR", "MOVING_HEAD", "STROBE", "DIMMER", "OTHER"], description: "Optional fixture type filter", }, includeDefinitions: { type: "boolean", default: true, description: "Include available fixture definitions", }, }, }, }, { name: "analyze_fixture_capabilities", description: "Analyze specific fixtures to understand their lighting capabilities", inputSchema: { type: "object", properties: { fixtureId: { type: "string", description: "Single fixture ID to analyze", }, fixtureIds: { type: "array", items: { type: "string" }, description: "Multiple fixture IDs to analyze", }, analysisType: { type: "string", enum: ["color_mixing", "positioning", "effects", "general"], default: "general", description: "Type of capability analysis", }, }, }, }, { name: "create_fixture_instance", description: "Create a new fixture instance in a project with manufacturer/model details", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to add fixture to", }, name: { type: "string", description: "Name for this fixture instance", }, description: { type: "string", description: "Description of where this fixture is placed or its purpose", }, manufacturer: { type: "string", description: 'Fixture manufacturer (e.g., "Chauvet", "Martin", "ETC")', }, model: { type: "string", description: "Fixture model name", }, mode: { type: "string", description: "Specific mode if the fixture has multiple modes", }, universe: { type: "number", default: 1, description: "DMX universe number (typically 1-4)", }, startChannel: { type: "number", description: "Starting DMX channel (1-512). If not provided, will auto-assign", }, tags: { type: "array", items: { type: "string" }, default: [], description: 'Tags for organization (e.g., ["front", "wash", "blue"])', }, channelAssignment: { type: "string", enum: ["auto", "manual", "suggest"], default: "auto", description: "How to assign channels: auto=find next available, manual=use provided startChannel, suggest=recommend placement", }, }, required: ["projectId", "name", "manufacturer", "model"], }, }, { name: "get_channel_map", description: "Get the DMX channel usage map for a project", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to analyze", }, universe: { type: "number", description: "Specific universe to analyze (if not provided, shows all)", }, }, required: ["projectId"], }, }, { name: "suggest_channel_assignment", description: "Suggest optimal channel assignments for multiple fixtures", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID", }, fixtureSpecs: { type: "array", items: { type: "object", properties: { name: { type: "string" }, manufacturer: { type: "string" }, model: { type: "string" }, mode: { type: "string" }, channelCount: { type: "number", description: "Number of channels (if known)", }, }, required: ["name", "manufacturer", "model"], }, description: "List of fixtures to assign channels for", }, universe: { type: "number", default: 1, description: "Universe to assign channels in", }, startingChannel: { type: "number", default: 1, description: "Channel to start assignments from", }, groupingStrategy: { type: "string", enum: ["sequential", "by_type", "by_function"], default: "sequential", description: "How to group fixture assignments", }, }, required: ["projectId", "fixtureSpecs"], }, }, { name: "update_fixture_instance", description: "Update an existing fixture instance with new properties", inputSchema: { type: "object", properties: { fixtureId: { type: "string", description: "ID of the fixture instance to update", }, name: { type: "string", description: "New name for the fixture", }, description: { type: "string", description: "New description for the fixture", }, manufacturer: { type: "string", description: "New manufacturer (will find/create new definition if changed)", }, model: { type: "string", description: "New model (will find/create new definition if changed)", }, mode: { type: "string", description: "New mode name", }, universe: { type: "number", description: "New DMX universe number", }, startChannel: { type: "number", description: "New starting DMX channel", }, tags: { type: "array", items: { type: "string" }, description: "New tags array", }, }, required: ["fixtureId"], }, }, { name: "delete_fixture_instance", description: "Delete a fixture instance from a project (will remove it from all scenes)", inputSchema: { type: "object", properties: { fixtureId: { type: "string", description: "ID of the fixture instance to delete", }, confirmDelete: { type: "boolean", description: "Confirm deletion (required to be true for safety)", }, }, required: ["fixtureId", "confirmDelete"], }, }, { name: "bulk_update_fixtures", description: "Update multiple fixture instances in a single atomic operation. All updates succeed or fail together. Useful for batch renaming, repositioning, or re-tagging fixtures.", inputSchema: { type: "object", properties: { fixtures: { type: "array", items: { type: "object", properties: { fixtureId: { type: "string", description: "ID of the fixture instance to update", }, name: { type: "string", description: "New name for the fixture", }, description: { type: "string", description: "New description", }, universe: { type: "number", description: "New DMX universe number", }, startChannel: { type: "number", description: "New starting DMX channel", }, tags: { type: "array", items: { type: "string" }, description: "New tags array", }, layoutX: { type: "number", description: "X position (0-1 normalized)", }, layoutY: { type: "number", description: "Y position (0-1 normalized)", }, layoutRotation: { type: "number", description: "Rotation in degrees", }, }, required: ["fixtureId"], }, description: "Array of fixture updates to apply", }, }, required: ["fixtures"], }, }, { name: "bulk_create_fixtures", description: "Create multiple fixture instances with best-effort approach. Each fixture is processed individually, allowing partial success if some fixtures fail. Returns detailed success/failure information for each fixture. Automatically assigns DMX channels if not specified. Validates channel availability before creation.", inputSchema: { type: "object", properties: { fixtures: { type: "array", items: { type: "object", properties: { projectId: { type: "string", description: "Project ID to add fixture to", }, name: { type: "string", description: "Name for this fixture instance", }, description: { type: "string", description: "Description of where this fixture is placed or its purpose", }, manufacturer: { type: "string", description: 'Fixture manufacturer (e.g., "Chauvet", "Martin", "ETC")', }, model: { type: "string", description: "Fixture model name", }, mode: { type: "string", description: "Specific mode if the fixture has multiple modes", }, universe: { type: "number", default: 1, description: "DMX universe number (typically 1-4)", }, startChannel: { type: "number", description: "Starting DMX channel (1-512). If not provided, will auto-assign", }, tags: { type: "array", items: { type: "string" }, default: [], description: 'Tags for organization (e.g., ["front", "wash", "blue"])', }, }, required: ["projectId", "name", "manufacturer", "model"], }, description: "Array of fixtures to create", }, }, required: ["fixtures"], }, }, // Scene Tools { name: "generate_scene", description: "Generate a lighting scene based on script context and design preferences", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to create scene in", }, sceneDescription: { type: "string", description: "Description of the scene to light", }, scriptContext: { type: "string", description: "Optional script context for the scene", }, sceneType: { type: "string", enum: ["full", "additive"], default: "full", description: "Type of scene: 'full' uses all fixtures (default), 'additive' only modifies specified fixtures", }, designPreferences: { type: "object", properties: { colorPalette: { type: "array", items: { type: "string" }, description: "Preferred colors for the scene", }, mood: { type: "string", description: "Mood or atmosphere for the scene", }, intensity: { type: "string", enum: ["subtle", "moderate", "dramatic"], description: "Overall intensity level", }, focusAreas: { type: "array", items: { type: "string" }, description: "Stage areas to emphasize", }, }, }, fixtureFilter: { type: "object", properties: { includeTypes: { type: "array", items: { type: "string", enum: [ "LED_PAR", "MOVING_HEAD", "STROBE", "DIMMER", "OTHER", ], }, }, excludeTypes: { type: "array", items: { type: "string", enum: [ "LED_PAR", "MOVING_HEAD", "STROBE", "DIMMER", "OTHER", ], }, }, includeTags: { type: "array", items: { type: "string" }, }, }, }, activate: { type: "boolean", description: "Automatically activate the scene after creation", }, }, required: ["projectId", "sceneDescription"], }, }, { name: "analyze_script", description: "Analyze a theatrical script to extract lighting-relevant information", inputSchema: { type: "object", properties: { scriptText: { type: "string", description: "The theatrical script text to analyze", }, extractLightingCues: { type: "boolean", default: true, description: "Extract specific lighting cues from the script", }, suggestScenes: { type: "boolean", default: true, description: "Generate scene suggestions based on analysis", }, }, required: ["scriptText"], }, }, { name: "optimize_scene", description: "Optimize an existing scene for specific goals", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to optimize", }, projectId: { type: "string", description: "Project ID containing the scene", }, optimizationGoals: { type: "array", items: { type: "string", enum: [ "energy_efficiency", "color_accuracy", "dramatic_impact", "technical_simplicity", ], }, default: ["dramatic_impact"], description: "Goals for optimization", }, }, required: ["sceneId", "projectId"], }, }, { name: "update_scene", description: "Update an existing scene with new values", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to update", }, name: { type: "string", description: "Optional new name for the scene", }, description: { type: "string", description: "Optional new description for the scene", }, fixtureValues: { type: "array", items: { type: "object", properties: { fixtureId: { type: "string", description: "Fixture ID to update", }, channelValues: { type: "array", items: { type: "number", minimum: 0, maximum: 255, }, description: "Array of channel values (0-255)", }, }, required: ["fixtureId", "channelValues"], }, description: "Optional fixture values to update", }, }, required: ["sceneId"], }, }, { name: "activate_scene", description: "Activate a lighting scene by name or ID", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Optional project ID to search within", }, sceneId: { type: "string", description: "Scene ID to activate", }, sceneName: { type: "string", description: "Scene name to activate (searches across projects if projectId not provided)", }, }, }, }, { name: "fade_to_black", description: "Fade all lights to black (turn off)", inputSchema: { type: "object", properties: { fadeOutTime: { type: "number", default: 3.0, description: "Time in seconds to fade out (default: 3.0)", }, }, }, }, { name: "get_current_active_scene", description: "Get the currently active scene if any", inputSchema: { type: "object", properties: {}, }, }, // Safe Scene Management Tools { name: "add_fixtures_to_scene", description: "Safely add fixtures to a scene without affecting existing fixtures", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to add fixtures to", }, fixtureValues: { type: "array", items: { type: "object", properties: { fixtureId: { type: "string", description: "Fixture ID to add", }, channelValues: { type: "array", items: { type: "number", minimum: 0, maximum: 255, }, description: "Array of channel values (0-255)", }, sceneOrder: { type: "number", description: "Optional order in scene", }, }, required: ["fixtureId", "channelValues"], }, description: "Fixtures to add to the scene", }, overwriteExisting: { type: "boolean", default: false, description: "Whether to overwrite existing fixture values", }, }, required: ["sceneId", "fixtureValues"], }, }, { name: "remove_fixtures_from_scene", description: "Safely remove specific fixtures from a scene", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to remove fixtures from", }, fixtureIds: { type: "array", items: { type: "string" }, description: "Array of fixture IDs to remove", }, }, required: ["sceneId", "fixtureIds"], }, }, { name: "get_scene_fixture_values", description: "Get current fixture values for a scene (read-only)", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to get fixture values for", }, }, required: ["sceneId"], }, }, { name: "ensure_fixtures_in_scene", description: "Ensure specific fixtures exist in a scene with given values, adding only if missing", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to ensure fixtures in", }, fixtureValues: { type: "array", items: { type: "object", properties: { fixtureId: { type: "string", description: "Fixture ID to ensure", }, channelValues: { type: "array", items: { type: "number", minimum: 0, maximum: 255, }, description: "Array of channel values (0-255)", }, sceneOrder: { type: "number", description: "Optional order in scene", }, }, required: ["fixtureId", "channelValues"], }, description: "Fixtures to ensure exist in the scene", }, }, required: ["sceneId", "fixtureValues"], }, }, { name: "update_scene_partial", description: "Safely update scene metadata and optionally merge fixture values", inputSchema: { type: "object", properties: { sceneId: { type: "string", description: "Scene ID to update", }, name: { type: "string", description: "Optional new name for the scene", }, description: { type: "string", description: "Optional new description for the scene", }, fixtureValues: { type: "array", items: { type: "object", properties: { fixtureId: { type: "string", description: "Fixture ID to update", }, channelValues: { type: "array", items: { type: "number", minimum: 0, maximum: 255, }, description: "Array of channel values (0-255)", }, sceneOrder: { type: "number", description: "Optional order in scene", }, }, required: ["fixtureId", "channelValues"], }, description: "Optional fixture values to merge/update", }, mergeFixtures: { type: "boolean", default: true, description: "Whether to merge fixtures (true) or replace all (false)", }, }, required: ["sceneId"], }, }, // Cue Tools { name: "create_cue_sequence", description: "Create a sequence of lighting cues from existing scenes", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to create cue sequence in", }, scriptContext: { type: "string", description: "Script context for the cue sequence", }, sceneIds: { type: "array", items: { type: "string" }, description: "Scene IDs to include in sequence", }, sequenceName: { type: "string", description: "Name for the cue sequence", }, transitionPreferences: { type: "object", properties: { defaultFadeIn: { type: "number", default: 3 }, defaultFadeOut: { type: "number", default: 3 }, followCues: { type: "boolean", default: false }, autoAdvance: { type: "boolean", default: false }, }, }, }, required: [ "projectId", "scriptContext", "sceneIds", "sequenceName", ], }, }, { name: "generate_act_cues", description: "Generate cue suggestions for an entire act based on script analysis", inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Project ID to work with", }, actNumber: { type: "number", description: "Act number to generate cues for", }, scriptText: { type: "string", description: "Script text for the act", }, existingScenes: { type: "array", items: { type: "string" }, description: "Optional existing scene IDs to reference", }, cueListName: { type: "string", description: "Optional name for the cue list", }, }, required: ["projectId", "actNumber", "scriptText"], }, }, { name: "optimize_cue_timing", description: "Optimize the timing of cues in a cue list", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to optimize", }, projectId: { type: "string", description: "Project ID containing the cue list", }, optimizationStrategy: { type: "string", enum: [ "smooth_transitions", "dramatic_timing", "technical_precision", "energy_conscious", ], default: "smooth_transitions", description: "Optimization strategy to apply", }, }, required: ["cueListId", "projectId"], }, }, { name: "analyze_cue_structure", description: "Analyze the structure and timing of a cue list", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to analyze", }, projectId: { type: "string", description: "Project ID containing the cue list", }, includeRecommendations: { type: "boolean", default: true, description: "Include improvement recommendations", }, }, required: ["cueListId", "projectId"], }, }, // Cue List Management Tools { name: "update_cue_list", description: "Update cue list name or description", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to update", }, name: { type: "string", description: "New name for the cue list", }, description: { type: "string", description: "New description for the cue list", }, loop: { type: "boolean", description: "Whether to loop the cue list (restart from first cue after last cue finishes)", }, }, required: ["cueListId"], }, }, { name: "add_cue_to_list", description: "Add a new cue to an existing cue list", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to add cue to", }, name: { type: "string", description: "Name for the new cue", }, cueNumber: { type: "number", description: "Cue number (e.g., 1.5, 2.0)", }, sceneId: { type: "string", description: "Scene ID to use for this cue", }, fadeInTime: { type: "number", default: 3, description: "Fade in time in seconds", }, fadeOutTime: { type: "number", default: 3, description: "Fade out time in seconds", }, followTime: { type: "number", description: "Auto-follow time in seconds (null for manual)", }, notes: { type: "string", description: "Notes or description for the cue", }, position: { type: "string", enum: ["before", "after"], description: "Position relative to reference cue", }, referenceCueNumber: { type: "number", description: "Cue number to insert before/after", }, }, required: ["cueListId", "name", "cueNumber", "sceneId"], }, }, { name: "remove_cue_from_list", description: "Remove a cue from a cue list", inputSchema: { type: "object", properties: { cueId: { type: "string", description: "ID of the cue to remove", }, }, required: ["cueId"], }, }, { name: "update_cue", description: "Update properties of an existing cue", inputSchema: { type: "object", properties: { cueId: { type: "string", description: "ID of the cue to update", }, name: { type: "string", description: "New name for the cue", }, cueNumber: { type: "number", description: "New cue number", }, sceneId: { type: "string", description: "New scene ID", }, fadeInTime: { type: "number", description: "New fade in time in seconds", }, fadeOutTime: { type: "number", description: "New fade out time in seconds", }, followTime: { type: "number", description: "New follow time (null to remove auto-follow)", }, notes: { type: "string", description: "New notes or description", }, }, required: ["cueId"], }, }, { name: "bulk_update_cues", description: "Update fade times and follow times for multiple cues in a single operation", inputSchema: { type: "object", properties: { cueIds: { type: "array", items: { type: "string" }, description: "Array of cue IDs to update" }, fadeInTime: { type: "number", description: "New fade in time in seconds (applies to all selected cues)" }, fadeOutTime: { type: "number", description: "New fade out time in seconds (applies to all selected cues)" }, followTime: { type: "number", description: "New follow time in seconds (applies to all selected cues, null to remove auto-follow)" }, easingType: { type: "string", description: "Easing type for transitions" } }, required: ["cueIds"] } }, { name: "reorder_cues", description: "Reorder multiple cues by assigning new cue numbers", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID containing the cues", }, cueReordering: { type: "array", items: { type: "object", properties: { cueId: { type: "string", description: "ID of the cue to reorder", }, newCueNumber: { type: "number", description: "New cue number for this cue", }, }, required: ["cueId", "newCueNumber"], }, description: "Array of cue ID and new number pairs", }, }, required: ["cueListId", "cueReordering"], }, }, { name: "get_cue_list_details", description: "Query and analyze cues in a cue list with filtering, sorting, and lookup tables", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to query", }, includeSceneDetails: { type: "boolean", default: true, description: "Include detailed scene information for each cue", }, sortBy: { type: "string", enum: ["cueNumber", "name", "sceneName"], default: "cueNumber", description: "Sort cues by cue number, name, or scene name", }, filterBy: { type: "object", properties: { cueNumberRange: { type: "object", properties: { min: { type: "number" }, max: { type: "number" }, }, description: "Filter by cue number range", }, nameContains: { type: "string", description: "Filter cues where name contains this text (case-insensitive)", }, sceneNameContains: { type: "string", description: "Filter cues where scene name contains this text (case-insensitive)", }, hasFollowTime: { type: "boolean", description: "Filter cues that have (true) or don't have (false) follow times", }, fadeTimeRange: { type: "object", properties: { min: { type: "number" }, max: { type: "number" }, }, description: "Filter by fade in time range", }, }, description: "Optional filters to apply to the cue list", }, }, required: ["cueListId"], }, }, { name: "delete_cue_list", description: "Delete a cue list and all its cues", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to delete", }, confirmDelete: { type: "boolean", default: false, description: "Confirm deletion of cue list and all its cues (required to be true for safety)", }, }, required: ["cueListId", "confirmDelete"], }, }, // Cue List Playback Tools { name: "start_cue_list", description: "Start playing a cue list from the beginning or a specific cue", inputSchema: { type: "object", properties: { cueListId: { type: "string", description: "Cue list ID to play", }, cueListName: { type: "string", description: "Cue list name to search for (alternative to ID)", }, projectId: { type: "string", description: "Project ID to search within (optional when using cueListName)", }, startFromCue: { type: "number", description: "Cue number to start from (optional, defaults to first cue)", }, }, }, }, { name: "next_cue", description: "Advance to the next cue in the currently playing cue list", inputSchema: { type: "object", properties: { fadeInTime: { type: "number", description: "Override fade in time in seconds (optional)", }, }, }, }, { name: "previous_cue", description: "Go back to the previous cue in the currently playing cue list", inputSchema: { type: "object", properties: { fadeInTime: { type: "number", description: "Override fade in time in seconds (optional)", }, }, }, }, { name: "go_to_cue", description: "Jump to a specific cue by number or name in the currently playing cue list", inputSchema: { type: "object", properties: { cueNumber: { type: "number", description: "Cue number to jump to", }, cueName: { type: "string", description: "Cue name to search for (alternative to cueNumber)", }, fadeInTime: { type: "number", description: "Override fade in time in seconds (optional)", }, }, }, }, { name: "stop_cue_list", description: "Stop the currently playing cue list", inputSchema: { type: "object", properties: {}, }, }, { name: "get_cue_list_status", description: "Get information about the currently playing cue list and navigation options", inputSchema: { type: "object", properties: {}, }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Log all incoming tool calls logger.info(`Tool call: ${name}`, { args }); try { switch (name) { // Project Tools case "list_projects": return { content: [ { type: "text", text: JSON.stringify( await this.projectTools.listProjects(args as any), null, 2, ), }, ], }; case "create_project": return { content: [ { type: "text", text: JSON.stringify( await this.projectTools.createProject(args as any), null, 2, ), }, ], }; case "get_project_details": return { content: [ { type: "text", text: JSON.stringify( await this.projectTools.getProjectDetails(args as any), null, 2, ), }, ], }; case "delete_project": return { content: [ { type: "text", text: JSON.stringify( await this.projectTools.deleteProject(args as any), null, 2, ), }, ], }; case "qlc_import_guidance": return { content: [ { type: "text", text: JSON.stringify({ guidance: "QLC+ Import via Web Interface", message: "QLC+ file import is available in the LacyLights web interface due to file size constraints that make it impractical for AI chat interfaces.", instructions: [ "1. Open the LacyLights web application", "2. Click 'Manage Projects' in the main interface", "3. Use the 'Import QLC+ Project' feature to upload .qxw files directly", "4. The web interface supports drag-and-drop file upload and provides detailed import feedback" ], why_not_here: "QLC+ files are typically 10-100KB+ of XML content, which exceeds practical limits for AI chat context windows. The web UI is optimized for file handling.", export_available: "QLC+ export is still available via this MCP interface and can generate .qxw files for download." }, null, 2), }, ], }; // Fixture Tools case "get_fixture_inventory": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.getFixtureInventory(args as any), null, 2, ), }, ], }; case "analyze_fixture_capabilities": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.analyzeFixtureCapabilities( args as any, ), null, 2, ), }, ], }; case "create_fixture_instance": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.createFixtureInstance(args as any), null, 2, ), }, ], }; case "get_channel_map": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.getChannelMap(args as any), null, 2, ), }, ], }; case "suggest_channel_assignment": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.suggestChannelAssignment( args as any, ), null, 2, ), }, ], }; case "update_fixture_instance": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.updateFixtureInstance(args as any), null, 2, ), }, ], }; case "delete_fixture_instance": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.deleteFixtureInstance(args as any), null, 2, ), }, ], }; case "bulk_update_fixtures": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.bulkUpdateFixtures(args as any), null, 2, ), }, ], }; case "bulk_create_fixtures": return { content: [ { type: "text", text: JSON.stringify( await this.fixtureTools.bulkCreateFixtures(args as any), null, 2, ), }, ], }; // Scene Tools case "generate_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.generateScene(args as any), null, 2, ), }, ], }; case "analyze_script": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.analyzeScript(args as any), null, 2, ), }, ], }; case "optimize_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.optimizeScene(args as any), null, 2, ), }, ], }; case "update_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.updateScene(args as any), null, 2, ), }, ], }; case "activate_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.activateScene(args as any), null, 2, ), }, ], }; case "fade_to_black": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.fadeToBlack(args as any), null, 2, ), }, ], }; case "get_current_active_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.getCurrentActiveScene(), null, 2, ), }, ], }; // Safe Scene Management Tools case "add_fixtures_to_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.addFixturesToScene(args as any), null, 2, ), }, ], }; case "remove_fixtures_from_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.removeFixturesFromScene(args as any), null, 2, ), }, ], }; case "get_scene_fixture_values": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.getSceneFixtureValues(args as any), null, 2, ), }, ], }; case "ensure_fixtures_in_scene": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.ensureFixturesInScene(args as any), null, 2, ), }, ], }; case "update_scene_partial": return { content: [ { type: "text", text: JSON.stringify( await this.sceneTools.updateScenePartial(args as any), null, 2, ), }, ], }; // Cue Tools case "create_cue_sequence": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.createCueSequence(args as any), null, 2, ), }, ], }; case "generate_act_cues": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.generateActCues(args as any), null, 2, ), }, ], }; case "optimize_cue_timing": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.optimizeCueTiming(args as any), null, 2, ), }, ], }; case "analyze_cue_structure": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.analyzeCueStructure(args as any), null, 2, ), }, ], }; // Cue List Management case "update_cue_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.updateCueList(args as any), null, 2, ), }, ], }; case "add_cue_to_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.addCueToCueList(args as any), null, 2, ), }, ], }; case "remove_cue_from_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.removeCueFromList(args as any), null, 2, ), }, ], }; case "update_cue": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.updateCue(args as any), null, 2, ), }, ], }; case "bulk_update_cues": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.bulkUpdateCues(args as any), null, 2, ), }, ], }; case "reorder_cues": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.reorderCues(args as any), null, 2, ), }, ], }; case "get_cue_list_details": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.getCueListDetails(args as any), null, 2, ), }, ], }; case "delete_cue_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.deleteCueList(args as any), null, 2, ), }, ], }; // Cue List Playback Tools case "start_cue_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.startCueList(args as any), null, 2, ), }, ], }; case "next_cue": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.nextCue(args as any), null, 2, ), }, ], }; case "previous_cue": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.previousCue(args as any), null, 2, ), }, ], }; case "go_to_cue": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.goToCue(args as any), null, 2, ), }, ], }; case "stop_cue_list": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.stopCueList(args as any), null, 2, ), }, ], }; case "get_cue_list_status": return { content: [ { type: "text", text: JSON.stringify( await this.cueTools.getCueListStatus(args as any), null, 2, ), }, ], }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; // Log the error with full context logger.error(`Tool call failed: ${name}`, { args, error: errorMessage, stack: errorStack, }); return { content: [ { type: "text", text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); } async run() { // Log server startup logger.info('LacyLights MCP Server initializing', { logFile: logger.getLogPath(), graphqlEndpoint: process.env.LACYLIGHTS_GRAPHQL_ENDPOINT || "http://localhost:4000/graphql", }); // Initialize RAG service try { await this.ragService.initializeCollection(); logger.info('RAG service initialized'); } catch (error) { logger.warn('RAG service initialization failed (optional)', { error: error instanceof Error ? error.message : String(error), }); } const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('MCP Server connected and ready'); } } async function main() { const server = new LacyLightsMCPServer(); await server.run(); } // Run the server main().catch((error) => { // Log fatal startup error logger.error('Fatal server startup error', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); // Exit with error code but don't log to stderr (interferes with MCP protocol) process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bbernstein/lacylights-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server