touchdesigner-mcp-server 1.2.0 → 1.3.1

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.
@@ -1,27 +1,31 @@
1
- import { createNode as apiCreateNode, deleteNode as apiDeleteNode, execNodeMethod as apiExecNodeMethod, execPythonScript as apiExecPythonScript, getNodeDetail as apiGetNodeDetail, getNodes as apiGetNodes, getTdInfo as apiGetTdInfo, getTdPythonClassDetails as apiGetTdPythonClassDetails, getTdPythonClasses as apiGetTdPythonClasses, updateNode as apiUpdateNode, } from "../gen/endpoints/TouchDesignerAPI.js";
1
+ import axios from "axios";
2
+ import { getCompatibilityPolicy, getCompatibilityPolicyType, } from "../core/compatibility.js";
3
+ import { createErrorResult, createSuccessResult } from "../core/result.js";
4
+ import { MCP_SERVER_VERSION, MIN_COMPATIBLE_API_VERSION, } from "../core/version.js";
5
+ import { createNode as apiCreateNode, deleteNode as apiDeleteNode, execNodeMethod as apiExecNodeMethod, execPythonScript as apiExecPythonScript, getModuleHelp as apiGetModuleHelp, getNodeDetail as apiGetNodeDetail, getNodeErrors as apiGetNodeErrors, getNodes as apiGetNodes, getTdInfo as apiGetTdInfo, getTdPythonClassDetails as apiGetTdPythonClassDetails, getTdPythonClasses as apiGetTdPythonClasses, updateNode as apiUpdateNode, } from "../gen/endpoints/TouchDesignerAPI.js";
2
6
  /**
3
7
  * Default implementation of ITouchDesignerApi using generated API clients
4
8
  */
5
9
  const defaultApiClient = {
10
+ createNode: apiCreateNode,
11
+ deleteNode: apiDeleteNode,
6
12
  execNodeMethod: apiExecNodeMethod,
7
13
  execPythonScript: apiExecPythonScript,
8
- getTdInfo: apiGetTdInfo,
9
- getNodes: apiGetNodes,
14
+ getModuleHelp: apiGetModuleHelp,
10
15
  getNodeDetail: apiGetNodeDetail,
11
- createNode: apiCreateNode,
12
- updateNode: apiUpdateNode,
13
- deleteNode: apiDeleteNode,
14
- getTdPythonClasses: apiGetTdPythonClasses,
16
+ getNodeErrors: apiGetNodeErrors,
17
+ getNodes: apiGetNodes,
18
+ getTdInfo: apiGetTdInfo,
15
19
  getTdPythonClassDetails: apiGetTdPythonClassDetails,
20
+ getTdPythonClasses: apiGetTdPythonClasses,
21
+ updateNode: apiUpdateNode,
16
22
  };
23
+ export const ERROR_CACHE_TTL_MS = 5000; // 5 seconds
17
24
  /**
18
25
  * Null logger implementation that discards all logs
19
26
  */
20
27
  const nullLogger = {
21
- debug: () => { },
22
- log: () => { },
23
- warn: () => { },
24
- error: () => { },
28
+ sendLog: () => { },
25
29
  };
26
30
  /**
27
31
  * Handle API error response
@@ -31,9 +35,9 @@ const nullLogger = {
31
35
  function handleError(response) {
32
36
  if (response.error) {
33
37
  const errorMessage = response.error;
34
- return { success: false, error: new Error(errorMessage) };
38
+ return { error: new Error(errorMessage), success: false };
35
39
  }
36
- return { success: false, error: new Error("Unknown error occurred") };
40
+ return { error: new Error("Unknown error occurred"), success: false };
37
41
  }
38
42
  /**
39
43
  * Handle API response and return a structured result
@@ -46,12 +50,12 @@ function handleApiResponse(response) {
46
50
  return handleError(response);
47
51
  }
48
52
  if (data === null) {
49
- return { success: false, error: new Error("No data received") };
53
+ return { error: new Error("No data received"), success: false };
50
54
  }
51
55
  if (data === undefined) {
52
- return { success: false, error: new Error("No data received") };
56
+ return { error: new Error("No data received"), success: false };
53
57
  }
54
- return { success: true, data };
58
+ return { data, success: true };
55
59
  }
56
60
  /**
57
61
  * TouchDesigner client implementation with dependency injection
@@ -60,91 +64,308 @@ function handleApiResponse(response) {
60
64
  export class TouchDesignerClient {
61
65
  logger;
62
66
  api;
67
+ verifiedCompatibilityError;
68
+ cachedCompatibilityCheck;
69
+ errorCacheTimestamp;
63
70
  /**
64
71
  * Initialize TouchDesigner client with optional dependencies
65
72
  */
66
73
  constructor(params = {}) {
67
74
  this.logger = params.logger || nullLogger;
68
75
  this.api = params.httpClient || defaultApiClient;
76
+ this.verifiedCompatibilityError = null;
77
+ this.cachedCompatibilityCheck = false;
78
+ this.errorCacheTimestamp = null;
79
+ }
80
+ /**
81
+ * Log debug message
82
+ */
83
+ logDebug(message, context) {
84
+ const data = context ? { message, ...context } : { message };
85
+ this.logger.sendLog({
86
+ data,
87
+ level: "debug",
88
+ logger: "TouchDesignerClient",
89
+ });
90
+ }
91
+ /**
92
+ * Check if the cached error should be cleared (TTL expired)
93
+ */
94
+ shouldClearErrorCache() {
95
+ if (!this.errorCacheTimestamp) {
96
+ return false;
97
+ }
98
+ const now = Date.now();
99
+ return now - this.errorCacheTimestamp >= ERROR_CACHE_TTL_MS;
100
+ }
101
+ /**
102
+ * Verify compatibility with the TouchDesigner server
103
+ */
104
+ async verifyCompatibility() {
105
+ // If we've already verified compatibility successfully, skip re-verification
106
+ if (this.cachedCompatibilityCheck && !this.verifiedCompatibilityError) {
107
+ return;
108
+ }
109
+ // Clear cached error if TTL has expired
110
+ if (this.verifiedCompatibilityError && this.shouldClearErrorCache()) {
111
+ this.logDebug("Clearing cached connection error (TTL expired), retrying...");
112
+ this.verifiedCompatibilityError = null;
113
+ this.errorCacheTimestamp = null;
114
+ this.cachedCompatibilityCheck = false;
115
+ }
116
+ if (this.verifiedCompatibilityError) {
117
+ // Re-log the cached error so users know it's still failing
118
+ const ttlRemaining = this.errorCacheTimestamp
119
+ ? Math.max(0, Math.ceil((ERROR_CACHE_TTL_MS - (Date.now() - this.errorCacheTimestamp)) /
120
+ 1000))
121
+ : 0;
122
+ this.logDebug(`Using cached connection error (retry in ${ttlRemaining} seconds)`, {
123
+ cacheAge: this.errorCacheTimestamp
124
+ ? Date.now() - this.errorCacheTimestamp
125
+ : 0,
126
+ cachedError: this.verifiedCompatibilityError.message,
127
+ });
128
+ throw this.verifiedCompatibilityError;
129
+ }
130
+ const result = await this.verifyVersionCompatibility();
131
+ if (result.success) {
132
+ this.verifiedCompatibilityError = null;
133
+ this.errorCacheTimestamp = null;
134
+ this.cachedCompatibilityCheck = true;
135
+ this.logDebug("Compatibility verified successfully");
136
+ return;
137
+ }
138
+ // Log when we're caching a NEW error
139
+ this.logDebug(`Caching connection error for ${ERROR_CACHE_TTL_MS / 1000} seconds`, {
140
+ error: result.error.message,
141
+ });
142
+ this.verifiedCompatibilityError = result.error;
143
+ this.errorCacheTimestamp = Date.now();
144
+ this.cachedCompatibilityCheck = false;
145
+ throw result.error;
146
+ }
147
+ /**
148
+ * Wrapper for API calls that require compatibility verification
149
+ * @private
150
+ */
151
+ async apiCall(message, call, context) {
152
+ this.logDebug(message, context);
153
+ await this.verifyCompatibility();
154
+ const result = await call();
155
+ return handleApiResponse(result);
69
156
  }
70
157
  /**
71
158
  * Execute a node method
72
159
  */
73
160
  async execNodeMethod(params) {
74
- this.logger.debug(`Executing node method: ${params.method} on ${params.nodePath}`);
75
- const result = await this.api.execNodeMethod(params);
76
- return handleApiResponse(result);
161
+ return this.apiCall("Executing node method", () => this.api.execNodeMethod(params), {
162
+ method: params.method,
163
+ nodePath: params.nodePath,
164
+ });
77
165
  }
78
166
  /**
79
167
  * Execute a script in TouchDesigner
80
168
  */
81
169
  async execPythonScript(params) {
82
- this.logger.debug(`Executing Python script: ${params}`);
83
- const result = await this.api.execPythonScript(params);
84
- return handleApiResponse(result);
170
+ return this.apiCall("Executing Python script", () => this.api.execPythonScript(params), { params });
85
171
  }
86
172
  /**
87
173
  * Get TouchDesigner server information
88
174
  */
89
175
  async getTdInfo() {
90
- this.logger.debug("Getting server info");
91
- const result = await this.api.getTdInfo();
92
- return handleApiResponse(result);
176
+ return this.apiCall("Getting server info", () => this.api.getTdInfo());
93
177
  }
94
178
  /**
95
179
  * Get list of nodes
96
180
  */
97
181
  async getNodes(params) {
98
- this.logger.debug(`Getting nodes for parent: ${params.parentPath}`);
99
- const result = await this.api.getNodes(params);
100
- return handleApiResponse(result);
182
+ return this.apiCall("Getting nodes for parent", () => this.api.getNodes(params), { parentPath: params.parentPath });
101
183
  }
102
184
  /**
103
185
  * Get node properties
104
186
  */
105
187
  async getNodeDetail(params) {
106
- this.logger.debug(`Getting properties for node: ${params.nodePath}`);
107
- const result = await this.api.getNodeDetail(params);
108
- return handleApiResponse(result);
188
+ return this.apiCall("Getting properties for node", () => this.api.getNodeDetail(params), { nodePath: params.nodePath });
189
+ }
190
+ /**
191
+ * Get node error information
192
+ */
193
+ async getNodeErrors(params) {
194
+ return this.apiCall("Checking node errors", () => this.api.getNodeErrors(params), { nodePath: params.nodePath });
109
195
  }
110
196
  /**
111
197
  * Create a new node
112
198
  */
113
199
  async createNode(params) {
114
- this.logger.debug(`Creating node: ${params.nodeName} of type ${params.nodeType} under ${params.parentPath}`);
115
- const result = await this.api.createNode(params);
116
- return handleApiResponse(result);
200
+ return this.apiCall("Creating node", () => this.api.createNode(params), {
201
+ nodeName: params.nodeName,
202
+ nodeType: params.nodeType,
203
+ parentPath: params.parentPath,
204
+ });
117
205
  }
118
206
  /**
119
207
  * Update node properties
120
208
  */
121
209
  async updateNode(params) {
122
- this.logger.debug(`Updating node: ${params.nodePath}`);
123
- const result = await this.api.updateNode(params);
124
- return handleApiResponse(result);
210
+ return this.apiCall("Updating node", () => this.api.updateNode(params), {
211
+ nodePath: params.nodePath,
212
+ });
125
213
  }
126
214
  /**
127
215
  * Delete a node
128
216
  */
129
217
  async deleteNode(params) {
130
- this.logger.debug(`Deleting node: ${params.nodePath}`);
131
- const result = await this.api.deleteNode(params);
132
- return handleApiResponse(result);
218
+ return this.apiCall("Deleting node", () => this.api.deleteNode(params), {
219
+ nodePath: params.nodePath,
220
+ });
133
221
  }
134
222
  /**
135
223
  * Get list of available Python classes/modules in TouchDesigner
136
224
  */
137
225
  async getClasses() {
138
- this.logger.debug("Getting Python classes");
139
- const result = await this.api.getTdPythonClasses();
140
- return handleApiResponse(result);
226
+ return this.apiCall("Getting Python classes", () => this.api.getTdPythonClasses());
141
227
  }
142
228
  /**
143
229
  * Get details of a specific class/module
144
230
  */
145
231
  async getClassDetails(className) {
146
- this.logger.debug(`Getting class details for: ${className}`);
147
- const result = await this.api.getTdPythonClassDetails(className);
148
- return handleApiResponse(result);
232
+ return this.apiCall("Getting class details", () => this.api.getTdPythonClassDetails(className), { className });
233
+ }
234
+ /**
235
+ * Retrieve Python help() documentation for modules/classes
236
+ */
237
+ async getModuleHelp(params) {
238
+ return this.apiCall("Getting module help", () => this.api.getModuleHelp(params), { moduleName: params.moduleName });
239
+ }
240
+ async verifyVersionCompatibility() {
241
+ let tdInfoResult;
242
+ try {
243
+ tdInfoResult = await this.api.getTdInfo();
244
+ }
245
+ catch (error) {
246
+ // Use axios.isAxiosError() for robust network/HTTP error detection
247
+ // AxiosError includes connection refused, timeout, network errors, etc.
248
+ // All other errors (TypeError, etc.) are programming errors and should propagate
249
+ if (!axios.isAxiosError(error)) {
250
+ // This is a programming error (e.g., TypeError, ReferenceError), not a connection error
251
+ const errorMessage = error instanceof Error ? error.message : String(error);
252
+ const errorStack = error instanceof Error ? error.stack : undefined;
253
+ this.logger.sendLog({
254
+ data: {
255
+ error: errorMessage,
256
+ errorType: "programming_error",
257
+ stack: errorStack,
258
+ },
259
+ level: "error",
260
+ logger: "TouchDesignerClient",
261
+ });
262
+ throw error;
263
+ }
264
+ // Handle AxiosError (network/HTTP errors)
265
+ const rawMessage = error.message || "Unknown network error";
266
+ const errorMessage = this.formatConnectionError(rawMessage);
267
+ this.logger.sendLog({
268
+ data: { error: rawMessage, errorType: "connection" },
269
+ level: "error",
270
+ logger: "TouchDesignerClient",
271
+ });
272
+ return createErrorResult(new Error(errorMessage));
273
+ }
274
+ if (!tdInfoResult.success) {
275
+ const errorMessage = this.formatConnectionError(tdInfoResult.error);
276
+ this.logger.sendLog({
277
+ data: { error: tdInfoResult.error, errorType: "api_response" },
278
+ level: "error",
279
+ logger: "TouchDesignerClient",
280
+ });
281
+ return createErrorResult(new Error(errorMessage));
282
+ }
283
+ const apiVersionRaw = tdInfoResult.data?.mcpApiVersion?.trim() || "";
284
+ const result = this.checkVersionCompatibility(MCP_SERVER_VERSION, apiVersionRaw);
285
+ this.logger.sendLog({
286
+ data: {
287
+ apiVersion: result.details.apiVersion,
288
+ mcpVersion: result.details.mcpVersion,
289
+ message: result.message,
290
+ minRequired: result.details.minRequired,
291
+ },
292
+ level: result.level,
293
+ logger: "TouchDesignerClient",
294
+ });
295
+ if (result.level === "error") {
296
+ return createErrorResult(new Error(result.message));
297
+ }
298
+ return createSuccessResult(undefined);
299
+ }
300
+ /**
301
+ * Format connection errors with helpful messages
302
+ */
303
+ formatConnectionError(error) {
304
+ if (!error) {
305
+ return "Failed to connect to TouchDesigner API server (unknown error)";
306
+ }
307
+ // Check for common connection errors
308
+ if (error.includes("ECONNREFUSED") ||
309
+ error.toLowerCase().includes("connect refused")) {
310
+ return `🔌 TouchDesigner Connection Failed
311
+
312
+ Cannot connect to TouchDesigner API server at the configured address.
313
+
314
+ Possible causes:
315
+ 1. TouchDesigner is not running
316
+ → Please start TouchDesigner
317
+
318
+ 2. WebServer DAT is not active
319
+ → Import 'mcp_webserver_base.tox' and ensure it's active
320
+
321
+ 3. Wrong port configuration
322
+ → Default port is 9981, check your configuration
323
+
324
+ For setup instructions, visit:
325
+ https://github.com/8beeeaaat/touchdesigner-mcp/releases/latest
326
+
327
+ Original error: ${error}`;
328
+ }
329
+ if (error.includes("ETIMEDOUT") || error.includes("timeout")) {
330
+ return `⏱️ TouchDesigner Connection Timeout
331
+
332
+ The connection to TouchDesigner timed out.
333
+
334
+ Possible causes:
335
+ 1. TouchDesigner is slow to respond
336
+ 2. Network issues
337
+ 3. WebServer DAT is overloaded
338
+
339
+ Try restarting TouchDesigner or check the network connection.
340
+
341
+ Original error: ${error}`;
342
+ }
343
+ if (error.includes("ENOTFOUND") || error.includes("getaddrinfo")) {
344
+ return `🌐 Invalid Host Configuration
345
+
346
+ Cannot resolve the TouchDesigner API server hostname.
347
+
348
+ Please check your host configuration (default: 127.0.0.1)
349
+
350
+ Original error: ${error}`;
351
+ }
352
+ // Generic error message
353
+ return `Failed to connect to TouchDesigner API server: ${error}`;
354
+ }
355
+ checkVersionCompatibility(mcpVersion, apiVersion) {
356
+ const policyType = getCompatibilityPolicyType({ apiVersion, mcpVersion });
357
+ const policy = getCompatibilityPolicy(policyType);
358
+ const details = {
359
+ apiVersion,
360
+ mcpVersion,
361
+ minRequired: MIN_COMPATIBLE_API_VERSION,
362
+ };
363
+ const message = policy.message(details);
364
+ return {
365
+ compatible: policy.compatible,
366
+ details,
367
+ level: policy.level,
368
+ message,
369
+ };
149
370
  }
150
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touchdesigner-mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server for TouchDesigner",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,9 @@
13
13
  "author": "8beeeaaat",
14
14
  "license": "MIT",
15
15
  "mcpName": "io.github.8beeeaaat/touchdesigner-mcp-server",
16
+ "mcpCompatibility": {
17
+ "minApiVersion": "1.3.0"
18
+ },
16
19
  "bugs": {
17
20
  "url": "https://github.com/8beeeaaat/touchdesigner-mcp/issues"
18
21
  },
@@ -23,30 +26,33 @@
23
26
  "touchdesigner-mcp-server": "dist/cli.js"
24
27
  },
25
28
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.18.0",
29
+ "@modelcontextprotocol/sdk": "^1.24.3",
27
30
  "@mozilla/readability": "^0.6.0",
28
31
  "@types/axios": "^0.14.4",
29
32
  "@types/ws": "^8.18.1",
30
- "@types/yargs": "^17.0.33",
31
- "axios": "^1.12.2",
33
+ "@types/yargs": "^17.0.35",
34
+ "axios": "^1.13.2",
32
35
  "mustache": "^4.2.0",
33
- "yaml": "^2.8.1",
34
- "zod": "3.25.76"
36
+ "semver": "^7.7.3",
37
+ "yaml": "^2.8.2",
38
+ "zod": "4.1.13"
35
39
  },
36
40
  "devDependencies": {
37
- "@biomejs/biome": "2.2.4",
38
- "@openapitools/openapi-generator-cli": "^2.23.1",
39
- "@types/jsdom": "^21.1.7",
41
+ "@biomejs/biome": "2.3.8",
42
+ "@openapitools/openapi-generator-cli": "^2.25.2",
43
+ "@types/jsdom": "^27.0.0",
40
44
  "@types/mustache": "^4.2.6",
41
- "@types/node": "^24.4.0",
42
- "@vitest/coverage-v8": "^3.2.4",
45
+ "@types/node": "^24.10.1",
46
+ "@types/semver": "^7.7.1",
47
+ "@vitest/coverage-v8": "^4.0.15",
43
48
  "archiver": "^7.0.1",
44
- "msw": "^2.11.2",
49
+ "msw": "^2.12.4",
45
50
  "npm-run-all": "^4.1.5",
46
- "orval": "^7.11.2",
51
+ "orval": "^7.17.0",
52
+ "prettier": "^3.7.4",
47
53
  "shx": "^0.4.0",
48
- "typescript": "^5.9.2",
49
- "vitest": "^3.2.4"
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.15"
50
56
  },
51
57
  "type": "module",
52
58
  "exports": {
@@ -67,12 +73,20 @@
67
73
  "lint": "run-p lint:*",
68
74
  "lint:biome": "biome check",
69
75
  "lint:tsc": "tsc --noEmit",
70
- "format": "biome check --fix",
76
+ "lint:python": "ruff check td/",
77
+ "lint:yaml": "prettier --check \"**/*.{yml,yaml}\"",
78
+ "format": "run-p format:*",
79
+ "format:biome": "biome check --fix",
80
+ "format:python": "ruff format td/ && ruff check --fix td/",
81
+ "format:yaml": "prettier --write \"**/*.{yml,yaml}\"",
71
82
  "dev": "npx @modelcontextprotocol/inspector node dist/cli.js --stdio",
72
83
  "test": "run-p test:*",
73
84
  "test:integration": "vitest run ./tests/integration",
74
85
  "test:unit": "vitest run ./tests/unit",
75
86
  "coverage": "vitest run --coverage",
87
+ "version": "run-p version:*",
88
+ "version:api": "node ./scripts/syncApiServerVersions.ts",
89
+ "version:mcp": "node ./scripts/syncMcpServerVersions.ts",
76
90
  "gen": "run-s gen:*",
77
91
  "gen:webserver": "openapi-generator-cli generate -i ./src/api/index.yml -g python-flask -o ./td/modules/td_server",
78
92
  "gen:handlers": "node td/genHandlers.js",
@@ -87,6 +101,6 @@
87
101
  ]
88
102
  },
89
103
  "overrides": {
90
- "axios": "^1.12.2"
104
+ "axios": "^1.13.2"
91
105
  }
92
106
  }