# Directory Structure ``` ├── nuke_bridge.py ├── package-lock.json ├── package.json ├── README.md └── src ├── index.js └── server.js ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # nuke-mcp-2 2 | nuke-mcp 3 | ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { server } from './server.js'; 4 | 5 | console.log('Starting Nuke MCP server...'); 6 | 7 | // Start receiving messages on stdin and sending messages on stdout 8 | const transport = new StdioServerTransport(); 9 | await server.connect(transport); 10 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-nuke-server", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "MCP server for Nuke integration", 6 | "main": "src/index.js", 7 | "bin": { 8 | "mcp-nuke-server": "src/index.js" 9 | }, 10 | "scripts": { 11 | "start": "node src/index.js", 12 | "dev": "node --watch src/index.js", 13 | "inspect": "npx -y @modelcontextprotocol/inspector node src/index.js" 14 | }, 15 | "dependencies": { 16 | "@modelcontextprotocol/sdk": "^1.0.0", 17 | "zod": "^3.22.4" 18 | } 19 | } 20 | ``` -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- ```javascript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { exec } from 'child_process'; 4 | import { promisify } from 'util'; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | // Create an MCP server for Nuke integration 9 | const server = new McpServer({ 10 | name: "Nuke MCP Server", 11 | version: "1.0.0", 12 | description: "MCP server for interacting with Nuke" 13 | }); 14 | 15 | // Helper function to execute Python bridge script 16 | async function executeBridge(command, args = {}) { 17 | try { 18 | // Convert args to JSON string and escape quotes for command line 19 | const argsJson = JSON.stringify(args).replace(/"/g, '\\"'); 20 | const { stdout, stderr } = await execAsync(`python nuke_bridge.py ${command} "${argsJson}"`); 21 | 22 | if (stderr) { 23 | console.error(`Bridge script error: ${stderr}`); 24 | return { 25 | content: [{ type: "text", text: `Error: ${stderr}` }], 26 | isError: true 27 | }; 28 | } 29 | 30 | try { 31 | const result = JSON.parse(stdout); 32 | if (result.error) { 33 | return { 34 | content: [{ type: "text", text: `Error: ${result.error}` }], 35 | isError: true 36 | }; 37 | } 38 | return { 39 | content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] 40 | }; 41 | } catch (parseError) { 42 | console.error(`Error parsing bridge output: ${parseError.message}`); 43 | return { 44 | content: [{ type: "text", text: `Error parsing bridge output: ${parseError.message}\nRaw output: ${stdout}` }], 45 | isError: true 46 | }; 47 | } 48 | } catch (error) { 49 | console.error(`Error executing bridge script: ${error.message}`); 50 | return { 51 | content: [{ type: "text", text: `Error executing bridge script: ${error.message}` }], 52 | isError: true 53 | }; 54 | } 55 | } 56 | 57 | // 1. createNode tool 58 | server.tool( 59 | "createNode", 60 | { 61 | nodeType: z.string().describe("Type of node to create (e.g., 'Read', 'Merge2')"), 62 | name: z.string().optional().describe("Optional name for the node"), 63 | inputs: z.array(z.string()).optional().describe("Optional array of input node names") 64 | }, 65 | async ({ nodeType, name, inputs }) => { 66 | return await executeBridge("createNode", { nodeType, name, inputs }); 67 | }, 68 | { description: "Creates a node of the specified type in a Nuke script" } 69 | ); 70 | 71 | // 2. setKnobValue tool 72 | server.tool( 73 | "setKnobValue", 74 | { 75 | nodeName: z.string().describe("Name of the node"), 76 | knobName: z.string().describe("Name of the knob to set"), 77 | value: z.union([z.string(), z.number()]).describe("Value to set the knob to") 78 | }, 79 | async ({ nodeName, knobName, value }) => { 80 | return await executeBridge("setKnobValue", { nodeName, knobName, value }); 81 | }, 82 | { description: "Sets a knob on the specified node to the provided value" } 83 | ); 84 | 85 | // 3. getNode tool 86 | server.tool( 87 | "getNode", 88 | { 89 | nodeName: z.string().describe("Name of the node to get information about") 90 | }, 91 | async ({ nodeName }) => { 92 | return await executeBridge("getNode", { nodeName }); 93 | }, 94 | { description: "Returns basic info about a node (type, knob values, etc.)" } 95 | ); 96 | 97 | // 4. execute tool 98 | server.tool( 99 | "execute", 100 | { 101 | writeNodeName: z.string().describe("Name of the Write node to render"), 102 | frameRangeStart: z.number().describe("Start frame for rendering"), 103 | frameRangeEnd: z.number().describe("End frame for rendering") 104 | }, 105 | async ({ writeNodeName, frameRangeStart, frameRangeEnd }) => { 106 | return await executeBridge("execute", { writeNodeName, frameRangeStart, frameRangeEnd }); 107 | }, 108 | { description: "Renders the specified Write node from start to end frames" } 109 | ); 110 | 111 | export { server }; 112 | ``` -------------------------------------------------------------------------------- /nuke_bridge.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python 2 | import sys 3 | import json 4 | import traceback 5 | 6 | # Function to handle response formatting 7 | def respond(success=True, data=None, error=None): 8 | response = { 9 | "success": success, 10 | "data": data 11 | } 12 | if error: 13 | response["error"] = error 14 | print(json.dumps(response)) 15 | 16 | # Main function to process commands 17 | def main(): 18 | if len(sys.argv) < 2: 19 | respond(False, None, "No command specified") 20 | return 21 | 22 | command = sys.argv[1] 23 | args = {} 24 | 25 | # Parse arguments if provided 26 | if len(sys.argv) > 2: 27 | try: 28 | args = json.loads(sys.argv[2]) 29 | except json.JSONDecodeError as e: 30 | respond(False, None, f"Invalid JSON arguments: {str(e)}") 31 | return 32 | 33 | try: 34 | # Import nuke here to avoid errors when running outside of Nuke 35 | try: 36 | import nuke 37 | except ImportError: 38 | respond(False, None, "Failed to import nuke module. Make sure this script is run within Nuke.") 39 | return 40 | 41 | # Process the command 42 | if command == "createNode": 43 | create_node(args, nuke) 44 | elif command == "setKnobValue": 45 | set_knob_value(args, nuke) 46 | elif command == "getNode": 47 | get_node(args, nuke) 48 | elif command == "execute": 49 | execute(args, nuke) 50 | else: 51 | respond(False, None, f"Unknown command: {command}") 52 | except Exception as e: 53 | respond(False, None, f"Error executing command: {str(e)}\n{traceback.format_exc()}") 54 | 55 | # Command implementations 56 | def create_node(args, nuke): 57 | if "nodeType" not in args: 58 | respond(False, None, "nodeType parameter is required") 59 | return 60 | 61 | node_type = args["nodeType"] 62 | name = args.get("name") 63 | inputs = args.get("inputs", []) 64 | 65 | try: 66 | # Create the node 67 | node = nuke.createNode(node_type, inpanel=False) 68 | 69 | # Set name if provided 70 | if name: 71 | node["name"].setValue(name) 72 | 73 | # Connect inputs if provided 74 | for i, input_name in enumerate(inputs): 75 | input_node = nuke.toNode(input_name) 76 | if input_node: 77 | node.setInput(i, input_node) 78 | else: 79 | respond(False, None, f"Input node not found: {input_name}") 80 | return 81 | 82 | # Return node info 83 | node_info = { 84 | "name": node.name(), 85 | "type": node.Class(), 86 | "position": {"x": node.xpos(), "y": node.ypos()} 87 | } 88 | respond(True, node_info) 89 | except Exception as e: 90 | respond(False, None, f"Error creating node: {str(e)}") 91 | 92 | def set_knob_value(args, nuke): 93 | if "nodeName" not in args: 94 | respond(False, None, "nodeName parameter is required") 95 | return 96 | if "knobName" not in args: 97 | respond(False, None, "knobName parameter is required") 98 | return 99 | if "value" not in args: 100 | respond(False, None, "value parameter is required") 101 | return 102 | 103 | node_name = args["nodeName"] 104 | knob_name = args["knobName"] 105 | value = args["value"] 106 | 107 | try: 108 | node = nuke.toNode(node_name) 109 | if not node: 110 | respond(False, None, f"Node not found: {node_name}") 111 | return 112 | 113 | if knob_name not in node.knobs(): 114 | respond(False, None, f"Knob not found: {knob_name}") 115 | return 116 | 117 | node[knob_name].setValue(value) 118 | 119 | respond(True, { 120 | "node": node_name, 121 | "knob": knob_name, 122 | "value": value 123 | }) 124 | except Exception as e: 125 | respond(False, None, f"Error setting knob value: {str(e)}") 126 | 127 | def get_node(args, nuke): 128 | if "nodeName" not in args: 129 | respond(False, None, "nodeName parameter is required") 130 | return 131 | 132 | node_name = args["nodeName"] 133 | 134 | try: 135 | node = nuke.toNode(node_name) 136 | if not node: 137 | respond(False, None, f"Node not found: {node_name}") 138 | return 139 | 140 | # Collect basic node info 141 | node_info = { 142 | "name": node.name(), 143 | "type": node.Class(), 144 | "position": {"x": node.xpos(), "y": node.ypos()}, 145 | "knobs": {} 146 | } 147 | 148 | # Collect knob values (only for basic types that can be serialized) 149 | for knob in node.knobs(): 150 | try: 151 | k = node[knob] 152 | # Handle different knob types 153 | if k.Class() in ["String_Knob", "File_Knob", "Text_Knob"]: 154 | node_info["knobs"][knob] = k.value() 155 | elif k.Class() in ["Int_Knob", "Double_Knob", "Boolean_Knob", "XY_Knob"]: 156 | node_info["knobs"][knob] = k.value() 157 | # Skip complex knobs that can't be easily serialized 158 | except: 159 | pass 160 | 161 | respond(True, node_info) 162 | except Exception as e: 163 | respond(False, None, f"Error getting node info: {str(e)}") 164 | 165 | def execute(args, nuke): 166 | if "writeNodeName" not in args: 167 | respond(False, None, "writeNodeName parameter is required") 168 | return 169 | if "frameRangeStart" not in args: 170 | respond(False, None, "frameRangeStart parameter is required") 171 | return 172 | if "frameRangeEnd" not in args: 173 | respond(False, None, "frameRangeEnd parameter is required") 174 | return 175 | 176 | write_node_name = args["writeNodeName"] 177 | frame_start = args["frameRangeStart"] 178 | frame_end = args["frameRangeEnd"] 179 | 180 | try: 181 | write_node = nuke.toNode(write_node_name) 182 | if not write_node: 183 | respond(False, None, f"Write node not found: {write_node_name}") 184 | return 185 | 186 | if write_node.Class() != "Write": 187 | respond(False, None, f"Node {write_node_name} is not a Write node") 188 | return 189 | 190 | # Execute the write node 191 | nuke.execute(write_node, int(frame_start), int(frame_end)) 192 | 193 | respond(True, { 194 | "node": write_node_name, 195 | "rendered": True, 196 | "frameRange": {"start": frame_start, "end": frame_end}, 197 | "outputPath": write_node["file"].value() 198 | }) 199 | except Exception as e: 200 | respond(False, None, f"Error executing write node: {str(e)}") 201 | 202 | if __name__ == "__main__": 203 | main() 204 | ```