# 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 |
```