touchdesigner-mcp-server 1.4.12 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ export declare const TOOL_NAMES: {
19
19
  readonly GET_TD_NODE_ERRORS: "get_td_node_errors";
20
20
  readonly GET_TD_NODE_PARAMETERS: "get_td_node_parameters";
21
21
  readonly GET_TD_NODES: "get_td_nodes";
22
+ readonly GET_TOP_IMAGE: "get_top_image";
22
23
  readonly UPDATE_TD_NODE_PARAMETERS: "update_td_node_parameters";
23
24
  };
24
25
  export declare const REFERENCE_COMMENT = "Check reference resources: https://docs.derivative.ca/Python_Classes_and_Modules";
@@ -19,6 +19,7 @@ export const TOOL_NAMES = {
19
19
  GET_TD_NODE_ERRORS: "get_td_node_errors",
20
20
  GET_TD_NODE_PARAMETERS: "get_td_node_parameters",
21
21
  GET_TD_NODES: "get_td_nodes",
22
+ GET_TOP_IMAGE: "get_top_image",
22
23
  UPDATE_TD_NODE_PARAMETERS: "update_td_node_parameters",
23
24
  };
24
25
  export const REFERENCE_COMMENT = `Check reference resources: ${TD_PYTHON_CLASS_REFERENCE_INDEX_URL}`;
@@ -17,8 +17,8 @@ export function registerTdTools(server, logger, tdClient) {
17
17
  for (const definition of TOOL_DEFINITIONS) {
18
18
  server.tool(definition.name, definition.description, definition.schema.strict().shape, async (params = {}) => {
19
19
  try {
20
- const text = await definition.run({ logger, params, tdClient });
21
- return createToolResult(tdClient, text);
20
+ const output = await definition.run({ logger, params, tdClient });
21
+ return createToolResult(tdClient, output);
22
22
  }
23
23
  catch (error) {
24
24
  return handleToolError(error, logger, definition.name, definition.errorComment);
@@ -57,10 +57,16 @@ export function registerTdTools(server, logger, tdClient) {
57
57
  }
58
58
  });
59
59
  }
60
- const createToolResult = (tdClient, text) => {
61
- const content = [
62
- { text, type: "text" },
63
- ];
60
+ const createToolResult = (tdClient, output) => {
61
+ const content = typeof output === "string"
62
+ ? [{ text: output, type: "text" }]
63
+ : output.content.map((block) => block.type === "image"
64
+ ? {
65
+ data: block.data,
66
+ mimeType: block.mimeType,
67
+ type: "image",
68
+ }
69
+ : { text: block.text, type: "text" });
64
70
  const additionalContents = tdClient.getAdditionalToolResultContents();
65
71
  if (additionalContents) {
66
72
  content.push(...additionalContents);
@@ -12,7 +12,8 @@ export interface ScriptResultData {
12
12
  success?: boolean;
13
13
  data?: {
14
14
  result?: unknown;
15
- output?: string;
15
+ stdout?: string;
16
+ stderr?: string;
16
17
  error?: string;
17
18
  };
18
19
  [key: string]: unknown;
@@ -16,7 +16,8 @@ export function formatScriptResult(data, scriptSnippet, options) {
16
16
  }
17
17
  const success = data.success ?? true;
18
18
  const result = data.data?.result;
19
- const output = data.data?.output;
19
+ const stdout = data.data?.stdout;
20
+ const stderr = data.data?.stderr;
20
21
  const error = data.data?.error;
21
22
  // Error case - always show full details
22
23
  if (!success || error) {
@@ -30,10 +31,10 @@ export function formatScriptResult(data, scriptSnippet, options) {
30
31
  switch (opts.detailLevel) {
31
32
  case "minimal":
32
33
  formattedText = formatMinimal(result);
33
- context = buildScriptContext(scriptSnippet, result, output);
34
+ context = buildScriptContext(scriptSnippet, result, stdout, stderr);
34
35
  break;
35
36
  case "summary": {
36
- const summary = formatSummary(result, output, scriptSnippet);
37
+ const summary = formatSummary(result, stdout, stderr, scriptSnippet);
37
38
  formattedText = summary.text;
38
39
  context = summary.context;
39
40
  break;
@@ -80,7 +81,7 @@ function formatMinimal(result) {
80
81
  /**
81
82
  * Summary mode: Result with context
82
83
  */
83
- function formatSummary(result, output, scriptSnippet) {
84
+ function formatSummary(result, stdout, stderr, scriptSnippet) {
84
85
  let formatted = "✓ Script executed successfully\n\n";
85
86
  const snippet = scriptSnippet ? truncateScript(scriptSnippet) : "";
86
87
  if (snippet) {
@@ -91,34 +92,49 @@ function formatSummary(result, output, scriptSnippet) {
91
92
  resultPreview = formatResultValue(result, 500);
92
93
  formatted += `Result: ${resultPreview}\n`;
93
94
  }
94
- let outputPreview;
95
- if (output?.trim()) {
96
- outputPreview =
97
- output.length > 200 ? `${output.substring(0, 200)}...` : output;
95
+ const outputPreview = truncateOutput(stdout);
96
+ if (outputPreview) {
98
97
  formatted += `\nOutput:\n${outputPreview}`;
99
98
  }
99
+ const stderrPreview = truncateOutput(stderr);
100
+ if (stderrPreview) {
101
+ formatted += `\nStderr:\n${stderrPreview}`;
102
+ }
100
103
  return {
101
104
  context: {
102
105
  hasOutput: Boolean(outputPreview),
106
+ hasStderr: Boolean(stderrPreview),
103
107
  outputPreview,
104
- outputType: getValueType(output),
108
+ outputType: getValueType(stdout),
105
109
  resultPreview,
106
110
  resultType: getValueType(result),
107
111
  snippet,
112
+ stderrPreview,
108
113
  },
109
114
  text: formatted,
110
115
  };
111
116
  }
112
- function buildScriptContext(scriptSnippet, result, output) {
117
+ function buildScriptContext(scriptSnippet, result, stdout, stderr) {
113
118
  return {
114
- hasOutput: Boolean(output?.trim()),
115
- outputPreview: output,
116
- outputType: getValueType(output),
119
+ hasOutput: Boolean(stdout?.trim()),
120
+ hasStderr: Boolean(stderr?.trim()),
121
+ outputPreview: stdout,
122
+ outputType: getValueType(stdout),
117
123
  resultPreview: formatResultValue(result ?? "", 200),
118
124
  resultType: getValueType(result),
119
125
  snippet: scriptSnippet ? truncateScript(scriptSnippet) : "",
126
+ stderrPreview: stderr,
120
127
  };
121
128
  }
129
+ /**
130
+ * Truncate captured stdout/stderr for display
131
+ */
132
+ function truncateOutput(output) {
133
+ if (!output?.trim()) {
134
+ return undefined;
135
+ }
136
+ return output.length > 200 ? `${output.substring(0, 200)}...` : output;
137
+ }
122
138
  /**
123
139
  * Detailed mode: Full JSON
124
140
  */
@@ -9,7 +9,16 @@
9
9
  ```
10
10
 
11
11
  {{#outputPreview}}
12
+ Output:
13
+
12
14
  ```text
13
15
  {{{outputPreview}}}
14
16
  ```
15
17
  {{/outputPreview}}
18
+ {{#stderrPreview}}
19
+ Stderr:
20
+
21
+ ```text
22
+ {{{stderrPreview}}}
23
+ ```
24
+ {{/stderrPreview}}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Python script builder for GET_TOP_IMAGE.
3
+ *
4
+ * `get_top_image` has no dedicated API endpoint: it reuses the same
5
+ * `execute_python_script` channel as EXECUTE_PYTHON_SCRIPT. This module only
6
+ * builds the script text that gets sent over that channel.
7
+ */
8
+ export interface GetTopImageScriptParams {
9
+ nodePath: string;
10
+ maxSize?: number;
11
+ }
12
+ /**
13
+ * Builds a Python script that captures the current output of a TOP node as a
14
+ * base64-encoded JPEG and assigns it to `result` (the variable the TD-side
15
+ * script executor extracts as the return value).
16
+ *
17
+ * When `maxSize` is set and the TOP's longer dimension exceeds it, a
18
+ * temporary `td.resolutionTOP` is chained onto the node to downscale (aspect
19
+ * preserved) before capture, then destroyed in a `finally` block so the
20
+ * project is left unmodified.
21
+ *
22
+ * OP type constants are referenced via `td.resolutionTOP` (not the bare
23
+ * `resolutionTOP` global) because the bare global is only injected into the
24
+ * exec namespace by TD-side packages that include #185's global injection
25
+ * fix; `import td` + `td.resolutionTOP` works against both older and newer
26
+ * deployed `mcp_webserver_base.tox` packages.
27
+ */
28
+ export declare function buildGetTopImageScript({ nodePath, maxSize, }: GetTopImageScriptParams): string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Python script builder for GET_TOP_IMAGE.
3
+ *
4
+ * `get_top_image` has no dedicated API endpoint: it reuses the same
5
+ * `execute_python_script` channel as EXECUTE_PYTHON_SCRIPT. This module only
6
+ * builds the script text that gets sent over that channel.
7
+ */
8
+ /**
9
+ * Builds a Python script that captures the current output of a TOP node as a
10
+ * base64-encoded JPEG and assigns it to `result` (the variable the TD-side
11
+ * script executor extracts as the return value).
12
+ *
13
+ * When `maxSize` is set and the TOP's longer dimension exceeds it, a
14
+ * temporary `td.resolutionTOP` is chained onto the node to downscale (aspect
15
+ * preserved) before capture, then destroyed in a `finally` block so the
16
+ * project is left unmodified.
17
+ *
18
+ * OP type constants are referenced via `td.resolutionTOP` (not the bare
19
+ * `resolutionTOP` global) because the bare global is only injected into the
20
+ * exec namespace by TD-side packages that include #185's global injection
21
+ * fix; `import td` + `td.resolutionTOP` works against both older and newer
22
+ * deployed `mcp_webserver_base.tox` packages.
23
+ */
24
+ export function buildGetTopImageScript({ nodePath, maxSize, }) {
25
+ // JSON string literals are valid Python string literals for the characters
26
+ // that can appear in a node path, so this is a safe way to embed user input.
27
+ const nodePathLiteral = JSON.stringify(nodePath);
28
+ const maxSizeLiteral = maxSize === undefined ? "None" : JSON.stringify(maxSize);
29
+ return `import base64
30
+ import td
31
+
32
+ node_path = ${nodePathLiteral}
33
+ max_size = ${maxSizeLiteral}
34
+
35
+ node = op(node_path)
36
+ if node is None:
37
+ raise Exception(f"Node not found at path: {node_path}")
38
+ if node.family != 'TOP':
39
+ raise Exception(f"Node at {node_path} is not a TOP (family={node.family})")
40
+
41
+ tmp_top = None
42
+ source_top = node
43
+ try:
44
+ if max_size is not None:
45
+ width = node.width
46
+ height = node.height
47
+ longest = max(width, height)
48
+ if longest > max_size:
49
+ if width >= height:
50
+ new_w = max_size
51
+ new_h = max(1, round(height * max_size / width))
52
+ else:
53
+ new_h = max_size
54
+ new_w = max(1, round(width * max_size / height))
55
+
56
+ parent = node.parent()
57
+ tmp_name = '__mcp_tmp_res__' + node.name
58
+ existing = parent.op(tmp_name)
59
+ if existing is not None:
60
+ existing.destroy()
61
+ tmp_top = parent.create(td.resolutionTOP, tmp_name)
62
+ tmp_top.inputConnectors[0].connect(node)
63
+ tmp_top.par.outputresolution = 'custom'
64
+ tmp_top.par.resolutionw = new_w
65
+ tmp_top.par.resolutionh = new_h
66
+ source_top = tmp_top
67
+
68
+ byte_array = source_top.saveByteArray('.jpg')
69
+ if not byte_array:
70
+ raise Exception(f"Failed to capture image from {node_path}: saveByteArray returned no data")
71
+
72
+ result = base64.b64encode(bytes(byte_array)).decode('ascii')
73
+ finally:
74
+ if tmp_top is not None:
75
+ tmp_top.destroy()
76
+ `;
77
+ }
@@ -1,8 +1,29 @@
1
- import type { z } from "zod";
1
+ import { z } from "zod";
2
2
  import type { ILogger } from "../../core/logger.js";
3
3
  import type { TouchDesignerClient } from "../../tdClient/touchDesignerClient.js";
4
4
  import type { ToolNames } from "./index.js";
5
5
  export type ToolCategory = "system" | "python" | "nodes" | "classes" | "state";
6
+ /** MCP text content block. */
7
+ export interface ToolTextContent {
8
+ type: "text";
9
+ text: string;
10
+ }
11
+ /** MCP image content block (base64-encoded). */
12
+ export interface ToolImageContent {
13
+ type: "image";
14
+ data: string;
15
+ mimeType: string;
16
+ }
17
+ export type ToolContent = ToolTextContent | ToolImageContent;
18
+ /**
19
+ * A tool's `run` normally returns formatter-ready text, which is wrapped in a
20
+ * single text content block. Tools that need to return non-text content
21
+ * (e.g. an image) return `{ content }` with the full content block list
22
+ * instead.
23
+ */
24
+ export type ToolRunResult = string | {
25
+ content: ToolContent[];
26
+ };
6
27
  /**
7
28
  * Single source of truth for a TouchDesigner MCP tool.
8
29
  *
@@ -27,8 +48,8 @@ export interface ToolDefinition {
27
48
  notes?: string;
28
49
  /** Optional reference comment appended to error output. */
29
50
  errorComment?: string;
30
- /** Executes the tool and returns formatter-ready text. */
31
- run: (ctx: ToolRunContext) => Promise<string>;
51
+ /** Executes the tool and returns formatter-ready text (or explicit content blocks). */
52
+ run: (ctx: ToolRunContext) => Promise<ToolRunResult>;
32
53
  }
33
54
  export interface ToolRunContext {
34
55
  params: Record<string, unknown>;
@@ -1,6 +1,8 @@
1
+ import { z } from "zod";
1
2
  import { REFERENCE_COMMENT, TOOL_NAMES } from "../../core/constants.js";
2
3
  import { CreateNodeBody, DeleteNodeQueryParams, ExecNodeMethodBody, ExecPythonScriptBody, GetModuleHelpQueryParams, GetNodeDetailQueryParams, GetNodeErrorsQueryParams, GetNodesQueryParams, GetTdPythonClassDetailsParams, UpdateNodeBody, } from "../../gen/mcp/touchDesignerAPI.zod.js";
3
4
  import { formatClassDetails, formatClassList, formatCreateNodeResult, formatDeleteNodeResult, formatExecNodeMethodResult, formatModuleHelp, formatNodeDetails, formatNodeErrors, formatNodeList, formatScriptResult, formatTdInfo, formatUpdateNodeResult, } from "./presenter/index.js";
5
+ import { buildGetTopImageScript } from "./pythonScripts/getTopImageScript.js";
4
6
  import { detailOnlyFormattingSchema, formattingOptionsSchema, } from "./types.js";
5
7
  /**
6
8
  * Authoring helper that infers `params` from the tool's schema while erasing to
@@ -13,6 +15,24 @@ function defineTool(def) {
13
15
  // because params are validated by the MCP SDK before reaching run().
14
16
  return def;
15
17
  }
18
+ /**
19
+ * `get_top_image` has no OpenAPI-backed endpoint (see architecture note on
20
+ * the tool definition below), so its params schema is defined locally
21
+ * instead of being derived from the generated Zod schemas.
22
+ */
23
+ const GetTopImageParams = z.object({
24
+ maxSize: z
25
+ .number()
26
+ .int()
27
+ .positive()
28
+ .max(4096)
29
+ .optional()
30
+ .describe("Maximum length of the image's longer side in pixels. The TOP is downscaled (aspect ratio preserved) only if it exceeds this value; omit to capture at native resolution."),
31
+ nodePath: z
32
+ .string()
33
+ .min(1)
34
+ .describe("Path to the TOP node to capture, e.g. '/project1/moviefilein1'"),
35
+ });
16
36
  export const TOOL_DEFINITIONS = [
17
37
  defineTool({
18
38
  category: "system",
@@ -314,4 +334,37 @@ console.log(docs.helpText?.slice(0, 200));`,
314
334
  },
315
335
  schema: GetModuleHelpQueryParams.extend(detailOnlyFormattingSchema.shape),
316
336
  }),
337
+ defineTool({
338
+ category: "nodes",
339
+ description: "Capture the current output of a TOP node as an image so it can be viewed directly (maxSize optionally downscales the longer side)",
340
+ errorComment: REFERENCE_COMMENT,
341
+ example: `import { getTopImage } from './servers/touchdesigner/getTopImage';
342
+
343
+ const image = await getTopImage({ nodePath: '/project1/moviefilein1', maxSize: 512 });`,
344
+ name: TOOL_NAMES.GET_TOP_IMAGE,
345
+ notes: "Routed through the same Python execution channel as execute_python_script; no dedicated API endpoint exists. When maxSize forces a downscale, a temporary resolutionTOP is created next to the node and always destroyed afterward, so the project is left unmodified.",
346
+ returns: "An image content block (base64-encoded JPEG) with the captured TOP output.",
347
+ run: async ({ params, tdClient }) => {
348
+ const { nodePath, maxSize } = params;
349
+ const script = buildGetTopImageScript({ maxSize, nodePath });
350
+ const result = await tdClient.execPythonScript({ script });
351
+ if (!result.success) {
352
+ throw result.error;
353
+ }
354
+ const base64Data = result.data.result;
355
+ if (typeof base64Data !== "string" || base64Data.length === 0) {
356
+ throw new Error(`get_top_image: expected a base64 string result for ${nodePath}, got ${typeof base64Data}`);
357
+ }
358
+ return {
359
+ content: [
360
+ { data: base64Data, mimeType: "image/jpeg", type: "image" },
361
+ {
362
+ text: `Captured TOP image from ${nodePath}${maxSize ? ` (maxSize=${maxSize}px)` : ""}.`,
363
+ type: "text",
364
+ },
365
+ ],
366
+ };
367
+ },
368
+ schema: GetTopImageParams,
369
+ }),
317
370
  ];
@@ -3,7 +3,7 @@
3
3
  * Do not edit manually.
4
4
  * TouchDesigner API
5
5
  * OpenAPI schema for generating TouchDesigner API client code
6
- * OpenAPI spec version: 1.4.3
6
+ * OpenAPI spec version: 1.5.0
7
7
  */
8
8
  import { customInstance } from '../../api/customInstance.js';
9
9
  import type { BodyType } from '../../api/customInstance.js';
@@ -395,6 +395,10 @@ export type ExecPythonScript200DataResult = {
395
395
  export type ExecPythonScript200Data = {
396
396
  /** Result of the executed script */
397
397
  result: ExecPythonScript200DataResult;
398
+ /** Captured standard output (e.g. print statements) emitted while the script ran */
399
+ stdout?: string;
400
+ /** Captured standard error output emitted while the script ran */
401
+ stderr?: string;
398
402
  } | null;
399
403
  export type ExecPythonScript200 = {
400
404
  /** Whether the operation was successful */
@@ -3,7 +3,7 @@
3
3
  * Do not edit manually.
4
4
  * TouchDesigner API
5
5
  * OpenAPI schema for generating TouchDesigner API client code
6
- * OpenAPI spec version: 1.4.3
6
+ * OpenAPI spec version: 1.5.0
7
7
  */
8
8
  import { customInstance } from '../../api/customInstance.js';
9
9
  export const TdNodeFamilyType = {
@@ -3,7 +3,7 @@
3
3
  * Do not edit manually.
4
4
  * TouchDesigner API
5
5
  * OpenAPI schema for generating TouchDesigner API client code
6
- * OpenAPI spec version: 1.4.3
6
+ * OpenAPI spec version: 1.5.0
7
7
  */
8
8
  import * as zod from 'zod';
9
9
  /**
@@ -233,6 +233,8 @@ export declare const ExecPythonScriptResponse: zod.ZodObject<{
233
233
  result: zod.ZodObject<{
234
234
  value: zod.ZodOptional<zod.ZodUnknown>;
235
235
  }, zod.z.core.$strip>;
236
+ stdout: zod.ZodOptional<zod.ZodString>;
237
+ stderr: zod.ZodOptional<zod.ZodString>;
236
238
  }, zod.z.core.$strip>>;
237
239
  error: zod.ZodNullable<zod.ZodString>;
238
240
  }, zod.z.core.$strip>;
@@ -3,7 +3,7 @@
3
3
  * Do not edit manually.
4
4
  * TouchDesigner API
5
5
  * OpenAPI schema for generating TouchDesigner API client code
6
- * OpenAPI spec version: 1.4.3
6
+ * OpenAPI spec version: 1.5.0
7
7
  */
8
8
  import * as zod from 'zod';
9
9
  /**
@@ -222,7 +222,9 @@ export const ExecPythonScriptResponse = zod.object({
222
222
  "data": zod.object({
223
223
  "result": zod.object({
224
224
  "value": zod.unknown().optional().describe('Return value of the executed script, can be any serializable value')
225
- }).describe('Result of the executed script')
225
+ }).describe('Result of the executed script'),
226
+ "stdout": zod.string().optional().describe('Captured standard output (e.g. print statements) emitted while the script ran'),
227
+ "stderr": zod.string().optional().describe('Captured standard error output emitted while the script ran')
226
228
  }).nullable(),
227
229
  "error": zod.string().nullable().describe('Error message if the operation was not successful')
228
230
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touchdesigner-mcp-server",
3
- "version": "1.4.12",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for TouchDesigner",
5
5
  "repository": {
6
6
  "type": "git",