touchdesigner-mcp-server 1.3.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.
package/README.ja.md CHANGED
@@ -366,13 +366,39 @@ td/
366
366
 
367
367
  ### バージョン互換性のトラブルシューティング
368
368
 
369
- - `ConnectionManager.connect` の実行中、MCPサーバーは自身のバージョンと `getTdInfo` が報告するTouchDesigner APIサーバーのバージョンを比較します。APIサーバーがバージョンを公開していない場合、またはバージョンが異なる場合(例:片方のみが更新された場合)、MCPサーバーはClaude/Codexコンソールおよび TouchDesigner ログ DAT に説明的なエラーメッセージを表示して接続を中止します。
370
- - 不一致を解決するには、TouchDesignerコンポーネントを再インストールしてください:
369
+ 柔軟な互換性チェックのために**セマンティックバージョニング**を使用しています
370
+
371
+ | MCP Server | API Server | 最小互換APIバージョン | 動作 | ステータス | 備考 |
372
+ |------------|------------|----------------|----------|--------|-------|
373
+ | 1.3.x | 1.3.0 | 1.3.0 | ✅ 正常動作 | 互換 | 推奨ベースライン構成 |
374
+ | 1.3.x | 1.4.0 | 1.3.0 | ⚠️ 警告表示、実行継続 | 警告 | 旧MCP MINORと新API、新機能未対応の可能性 |
375
+ | 1.4.0 | 1.3.x | 1.3.0 | ⚠️ 警告表示、実行継続 | 警告 | 新MCP MINORに追加機能がある可能性 |
376
+ | 1.3.2 | 1.3.1 | 1.3.2 | ❌ 実行停止 | エラー | APIが最小互換バージョン未満 |
377
+ | 2.0.0 | 1.x.x | N/A | ❌ 実行停止 | エラー | MAJORバージョン相違 = 破壊的変更 |
378
+
379
+ **互換性ルール**:
380
+
381
+ - ✅ **互換**: 同じMAJORバージョン、かつAPIバージョン ≥ 最小互換バージョン
382
+ - ⚠️ **警告**: 同じMAJOR内でMINORまたはPATCHバージョンが異なる(警告表示、実行継続)
383
+ - ❌ **エラー**: MAJORバージョンが異なる、またはAPIサーバー < 最小互換バージョン(即座に実行停止、更新が必要)
384
+
385
+ - **互換性エラーを解決するには:**
371
386
  1. リリースページから最新の [touchdesigner-mcp-td.zip](https://github.com/8beeeaaat/touchdesigner-mcp/releases/latest/download/touchdesigner-mcp-td.zip) をダウンロードします。
372
387
  2. 既存の `touchdesigner-mcp-td` フォルダを削除し、新しく展開した内容に置き換えます。
373
388
  3. TouchDesignerプロジェクトから古い `mcp_webserver_base` コンポーネントを削除し、新しいフォルダから `.tox` をインポートします。
374
389
  4. TouchDesignerとMCPサーバーを実行しているAIエージェント(例:Claude Desktop)を再起動します。
375
- - ローカルで開発している場合は、`package.json` を編集した後に `npm run gen:version` を実行してください(または単に `npm version ...` を使用してください)。これにより、Python API(`pyproject.toml` + `td/modules/utils/version.py`)、MCPバンドルマニフェスト、およびレジストリメタデータが同期され、ランタイム互換性チェックが成功するようになります。
390
+
391
+ - **開発者向け:** ローカルで開発している場合は、`package.json` を編集した後に `npm run version` を実行してください(または単に `npm version ...` を使用してください)。これにより、Python API(`pyproject.toml` + `td/modules/utils/version.py`)、MCPバンドルマニフェスト、およびレジストリメタデータが同期され、ランタイム互換性チェックが成功するようになります。
392
+
393
+ ### 接続エラーのトラブルシューティング
394
+
395
+ - `TouchDesignerClient` は接続に失敗した互換性チェック結果を **最大5秒間キャッシュ**し、その間のツール呼び出しでは同じエラーを再利用して TouchDesigner への無駄な負荷を避けます。TTL が切れると自動的に再試行します。
396
+ - MCP サーバーが TouchDesigner に接続できない場合は、次のようなガイド付きメッセージが表示されます:
397
+ - `ECONNREFUSED` / "connect refused": TouchDesigner を起動し、`mcp_webserver_base.tox` からインポートした WebServer DAT がアクティブか、ポート設定(デフォルト `9981`)が正しいか確認してください。
398
+ - `ETIMEDOUT` / "timeout": TouchDesigner の応答が遅い、またはネットワークが詰まっています。TouchDesigner/ WebServer DAT の再起動やネットワーク状況の確認を行ってください。
399
+ - `ENOTFOUND` / `getaddrinfo`: ホスト名が解決できません。特別な理由がなければ `127.0.0.1` を使用してください。
400
+ - これらの詳細なエラーテキストは `ILogger` にも出力されるため、MCP 側のログを確認すれば TouchDesigner に到達する前に止まった理由を把握できます。
401
+ - 問題を解決したら再度ツールを実行するだけで、キャッシュされたエラーがクリアされて接続チェックがやり直されます。
376
402
 
377
403
  ## 開発で貢献
378
404
 
package/README.md CHANGED
@@ -366,13 +366,39 @@ The build process (`npm run build`) runs all necessary generation steps (`npm ru
366
366
 
367
367
  ### Troubleshooting version compatibility
368
368
 
369
- - During `ConnectionManager.connect` the MCP server compares its own version with the TouchDesigner API server version reported by `getTdInfo`. If the API server does not expose a version or the versions differ (for example because only one side was updated), the MCP server aborts the connection with a descriptive error message in the Claude/Codex console and in the TouchDesigner log DAT.
370
- - To resolve the mismatch, reinstall both the TouchDesigner components
369
+ The MCP server uses **semantic versioning** for flexible compatibility checks
370
+
371
+ | MCP Server | API Server | Minimum compatible API version | Behavior | Status | Notes |
372
+ |------------|------------|----------------|----------|--------|-------|
373
+ | 1.3.x | 1.3.0 | 1.3.0 | ✅ Works normally | Compatible | Recommended baseline configuration |
374
+ | 1.3.x | 1.4.0 | 1.3.0 | ⚠️ Warning shown, continues | Warning | Older MCP MINOR with newer API may lack new features |
375
+ | 1.4.0 | 1.3.x | 1.3.0 | ⚠️ Warning shown, continues | Warning | Newer MCP MINOR may have additional features |
376
+ | 1.3.2 | 1.3.1 | 1.3.2 | ❌ Execution stops | Error | API below minimum compatible version |
377
+ | 2.0.0 | 1.x.x | N/A | ❌ Execution stops | Error | Different MAJOR = breaking changes |
378
+
379
+ **Compatibility Rules**:
380
+
381
+ - ✅ **Compatible**: Same MAJOR version AND API version ≥ 1.3.0 (minimum compatible version)
382
+ - ⚠️ **Warning**: Different MINOR or PATCH versions within the same MAJOR version (shows warning but continues execution)
383
+ - ❌ **Error**: Different MAJOR versions OR API server < 1.3.0 (execution stops immediately, update required)
384
+
385
+ - **To resolve compatibility errors:**
371
386
  1. Download the latest [touchdesigner-mcp-td.zip](https://github.com/8beeeaaat/touchdesigner-mcp/releases/latest/download/touchdesigner-mcp-td.zip) from the releases page.
372
- 2. Delete the existing \`touchdesigner-mcp-td\` folder and replace it with the newly extracted contents.
373
- 3. Remove the old \`mcp_webserver_base\` component from your TouchDesigner project and import the \`.tox\` from the new folder.
387
+ 2. Delete the existing `touchdesigner-mcp-td` folder and replace it with the newly extracted contents.
388
+ 3. Remove the old `mcp_webserver_base` component from your TouchDesigner project and import the `.tox` from the new folder.
374
389
  4. Restart TouchDesigner and the AI agent running the MCP server (e.g., Claude Desktop).
375
- - When developing locally, run `npm run gen:version` after editing `package.json` (or simply use `npm version ...`). This keeps the Python API (`pyproject.toml` + `td/modules/utils/version.py`), MCP bundle manifest, and registry metadata in sync so that the runtime compatibility check succeeds.
390
+
391
+ - **For developers:** When developing locally, run `npm run version` after editing `package.json` (or simply use `npm version ...`). This keeps the Python API (`pyproject.toml` + `td/modules/utils/version.py`), MCP bundle manifest, and registry metadata in sync so that the runtime compatibility check succeeds.
392
+
393
+ ### Troubleshooting connection errors
394
+
395
+ - `TouchDesignerClient` caches failed connection checks for **5 seconds**. Subsequent tool calls reuse the cached error to avoid spamming TouchDesigner and automatically retry after the TTL expires.
396
+ - When the MCP server cannot reach TouchDesigner, you now get guided error messages with concrete fixes:
397
+ - `ECONNREFUSED` / "connect refused": start TouchDesigner, ensure the WebServer DAT from `mcp_webserver_base.tox` is running, and confirm the configured port (default `9981`).
398
+ - `ETIMEDOUT` / "timeout": TouchDesigner is responding slowly or the network is blocked. Restart TouchDesigner/WebServer DAT or check your network connection.
399
+ - `ENOTFOUND` / `getaddrinfo`: the host name is invalid. Use `127.0.0.1` unless you explicitly changed it.
400
+ - The structured error text is also logged through `ILogger`, so you can check the MCP logs to understand why a request stopped before hitting TouchDesigner.
401
+ - Once the underlying issue is fixed, simply run the tool again—the client clears the cached error and re-verifies the connection automatically.
376
402
 
377
403
  ## Contributing
378
404
 
@@ -0,0 +1,236 @@
1
+ import semver from "semver";
2
+ import { MIN_COMPATIBLE_API_VERSION } from "./version.js";
3
+ export const COMPATIBILITY_POLICY_TYPES = {
4
+ BELOW_MIN_VERSION: "belowMinVersion",
5
+ COMPATIBLE: "compatible",
6
+ MAJOR_MISMATCH: "majorMismatch",
7
+ NEWER_MINOR: "newerMinor",
8
+ NO_VERSION: "noVersion",
9
+ OLDER_MINOR: "olderMinor",
10
+ PATCH_DIFF: "patchDiff",
11
+ };
12
+ export const COMPATIBILITY_POLICY_ERROR_LEVELS = {
13
+ ALLOW: "info",
14
+ ERROR: "error",
15
+ WARNING: "warning",
16
+ };
17
+ /**
18
+ * Compatibility policy configuration
19
+ */
20
+ const COMPATIBILITY_POLICY = {
21
+ /**
22
+ * Behavior when no version information is available
23
+ * - 'error': Stop processing with error
24
+ */
25
+ [COMPATIBILITY_POLICY_TYPES.NO_VERSION]: {
26
+ compatible: false,
27
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.ERROR,
28
+ message: generateNoVersionMessage,
29
+ },
30
+ /**
31
+ * Behavior when API server version is below minimum required version
32
+ * - 'error': Stop processing with error
33
+ */
34
+ [COMPATIBILITY_POLICY_TYPES.BELOW_MIN_VERSION]: {
35
+ compatible: false,
36
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.ERROR,
37
+ message: generateMinVersionMessage,
38
+ },
39
+ /**
40
+ * Behavior when MAJOR versions differ
41
+ * - 'error': Stop processing with error
42
+ */
43
+ [COMPATIBILITY_POLICY_TYPES.MAJOR_MISMATCH]: {
44
+ compatible: false,
45
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.ERROR,
46
+ message: generateMajorMismatchMessage,
47
+ },
48
+ /**
49
+ * Behavior when MCP server has newer MINOR version than API server
50
+ * - 'warning': Continue with warning only
51
+ */
52
+ [COMPATIBILITY_POLICY_TYPES.NEWER_MINOR]: {
53
+ compatible: true,
54
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.WARNING,
55
+ message: generateNewerMinorMessage,
56
+ },
57
+ /**
58
+ * Behavior when API server has newer MINOR version than MCP server
59
+ * - 'warning': Continue with warning
60
+ */
61
+ [COMPATIBILITY_POLICY_TYPES.OLDER_MINOR]: {
62
+ compatible: true,
63
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.WARNING,
64
+ message: generateOlderMinorMessage,
65
+ },
66
+ /**
67
+ * Behavior when PATCH versions differ
68
+ * - 'warning': Continue with warning
69
+ */
70
+ [COMPATIBILITY_POLICY_TYPES.PATCH_DIFF]: {
71
+ compatible: true,
72
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.WARNING,
73
+ message: generatePatchDiffMessage,
74
+ },
75
+ /**
76
+ * Behavior when versions are fully compatible
77
+ * - 'allow': Allow without logging
78
+ */
79
+ [COMPATIBILITY_POLICY_TYPES.COMPATIBLE]: {
80
+ compatible: true,
81
+ level: COMPATIBILITY_POLICY_ERROR_LEVELS.ALLOW,
82
+ message: generateFullyCompatibleMessage,
83
+ },
84
+ };
85
+ export const getCompatibilityPolicyType = (params) => {
86
+ const mcpSemVer = semver.coerce(params.mcpVersion);
87
+ const apiSemVer = semver.coerce(params.apiVersion);
88
+ if (!mcpSemVer || !apiSemVer) {
89
+ return COMPATIBILITY_POLICY_TYPES.NO_VERSION;
90
+ }
91
+ if (semver.lt(apiSemVer, MIN_COMPATIBLE_API_VERSION)) {
92
+ return COMPATIBILITY_POLICY_TYPES.BELOW_MIN_VERSION;
93
+ }
94
+ if (mcpSemVer.major !== apiSemVer.major) {
95
+ return COMPATIBILITY_POLICY_TYPES.MAJOR_MISMATCH;
96
+ }
97
+ if (mcpSemVer.minor > apiSemVer.minor) {
98
+ return COMPATIBILITY_POLICY_TYPES.NEWER_MINOR;
99
+ }
100
+ if (mcpSemVer.minor < apiSemVer.minor) {
101
+ return COMPATIBILITY_POLICY_TYPES.OLDER_MINOR;
102
+ }
103
+ if (mcpSemVer.patch !== apiSemVer.patch) {
104
+ return COMPATIBILITY_POLICY_TYPES.PATCH_DIFF;
105
+ }
106
+ return COMPATIBILITY_POLICY_TYPES.COMPATIBLE;
107
+ };
108
+ export const getCompatibilityPolicy = (type) => {
109
+ return COMPATIBILITY_POLICY[type];
110
+ };
111
+ /**
112
+ * Update guide template
113
+ */
114
+ const updateGuide = `
115
+ Update Guide:
116
+ 1. Download the latest release: https://github.com/8beeeaaat/touchdesigner-mcp/releases/latest
117
+ 2. Replace TouchDesigner components:
118
+ - Delete the existing touchdesigner-mcp-td folder
119
+ - Extract and import the new mcp_webserver_base.tox
120
+ 3. Restart TouchDesigner and the MCP client (e.g., Claude Desktop)
121
+
122
+ For more details, see: https://github.com/8beeeaaat/touchdesigner-mcp#troubleshooting-version-compatibility
123
+ `.trim();
124
+ /**
125
+ * Generate error message for unknown version information
126
+ */
127
+ export function generateNoVersionMessage(args) {
128
+ return `
129
+ 🚨 Version Information Missing
130
+
131
+ MCP Server: ${args.mcpVersion || "Unknown"}
132
+ API Server: ${args.apiVersion || "Unknown"}
133
+
134
+ Version information is required to ensure compatibility between the MCP server and TouchDesigner components.
135
+ Please ensure both components are updated to compatible versions.
136
+
137
+ ${updateGuide}
138
+ `.trim();
139
+ }
140
+ /**
141
+ * Generate error message for MAJOR version mismatch
142
+ */
143
+ export function generateMajorMismatchMessage(args) {
144
+ return `
145
+ 🚨 Version Incompatibility Detected
146
+
147
+ MCP Server: ${args.mcpVersion}
148
+ API Server: ${args.apiVersion}
149
+
150
+ MAJOR version mismatch indicates breaking changes.
151
+ Both components must be updated to compatible versions.
152
+
153
+ ${updateGuide}
154
+ `.trim();
155
+ }
156
+ /**
157
+ * Generate error message when API version is below minimum compatible version
158
+ */
159
+ export function generateMinVersionMessage(args) {
160
+ return `
161
+ ⚠️ TouchDesigner API Server Update Required
162
+
163
+ Current: ${args.apiVersion}
164
+ Required: ${args.minRequired}+
165
+
166
+ Your TouchDesigner components are outdated and may not support all features.
167
+
168
+ ${updateGuide}
169
+ `.trim();
170
+ }
171
+ /**
172
+ * Generate warning message when MCP server has newer MINOR version
173
+ */
174
+ export function generateNewerMinorMessage(args) {
175
+ return `
176
+ 💡 Update Recommended
177
+
178
+ MCP Server: ${args.mcpVersion}
179
+ API Server: ${args.apiVersion}
180
+
181
+ The MCP server has newer features that may not work with your TouchDesigner components.
182
+ Consider updating for the best experience.
183
+
184
+ ${updateGuide}
185
+ `.trim();
186
+ }
187
+ /**
188
+ * Generate warning message when API server has newer MINOR version
189
+ */
190
+ export function generateOlderMinorMessage(args) {
191
+ return `
192
+ 💡 Update Recommended
193
+
194
+ MCP Server: ${args.mcpVersion}
195
+ API Server: ${args.apiVersion}
196
+
197
+ Your TouchDesigner components have features that may not be supported by the MCP server.
198
+ Consider updating the MCP server for the best experience.
199
+
200
+ ${updateGuide}
201
+ `.trim();
202
+ }
203
+ /**
204
+ * Generate warning message when PATCH versions differ
205
+ */
206
+ export function generatePatchDiffMessage(args) {
207
+ return `
208
+ 💡 Patch Version Mismatch
209
+
210
+ MCP Server: ${args.mcpVersion}
211
+ API Server: ${args.apiVersion}
212
+
213
+ The MCP server and TouchDesigner components have different PATCH versions.
214
+ While generally compatible, updating both to the latest versions is recommended.
215
+
216
+ ${updateGuide}
217
+ `.trim();
218
+ }
219
+ /**
220
+ * Generate info message when versions are fully compatible
221
+ *
222
+ * @param mcpVersion MCP server version
223
+ * @param apiVersion TouchDesigner API server version
224
+ * @returns Info message
225
+ */
226
+ export function generateFullyCompatibleMessage(args) {
227
+ return `
228
+ ✅ Versions Fully Compatible
229
+
230
+ MCP Server: ${args.mcpVersion}
231
+ API Server: ${args.apiVersion}
232
+
233
+ Your MCP server and TouchDesigner components are fully compatible.
234
+ No action is needed.
235
+ `.trim();
236
+ }
@@ -14,10 +14,17 @@ export class McpLogger {
14
14
  });
15
15
  }
16
16
  catch (error) {
17
+ // Only swallow the expected "Not connected" error during startup/shutdown
17
18
  if (error instanceof Error && error.message === "Not connected") {
18
19
  return;
19
20
  }
20
- console.error(`Failed to send log to MCP server: ${error instanceof Error ? error.message : String(error)}`);
21
+ // For all other errors, log detailed information to help diagnose logging system failures
22
+ console.error("CRITICAL: Failed to send log to MCP server. Logging system may be compromised.", {
23
+ error: error instanceof Error ? error.message : String(error),
24
+ originalLogger: args.logger,
25
+ originalLogLevel: args.level,
26
+ stack: error instanceof Error ? error.stack : undefined,
27
+ });
21
28
  }
22
29
  }
23
30
  }
@@ -1,4 +1,24 @@
1
1
  import { createRequire } from "node:module";
2
2
  const requirePackage = createRequire(import.meta.url);
3
3
  const packageJson = requirePackage("../../package.json");
4
- export const PACKAGE_VERSION = packageJson.version ?? "0.0.0";
4
+ /**
5
+ * Current MCP server version
6
+ */
7
+ export const getMcpServerVersion = () => packageJson.version ?? "0.0.0";
8
+ export const MCP_SERVER_VERSION = getMcpServerVersion();
9
+ /**
10
+ * Minimum compatible TouchDesigner API Server version required by the MCP server
11
+ *
12
+ * Loaded from package.json's mcpCompatibility.minApiVersion field.
13
+ * Falls back to the current package version if undefined.
14
+ *
15
+ * API Server must be at or above this version.
16
+ * - MAJOR mismatch: Error
17
+ * - MINOR/PATCH differences: Warning or allow
18
+ *
19
+ * Update when:
20
+ * - Introducing breaking API changes
21
+ * - Making incompatible changes to OpenAPI schema
22
+ */
23
+ export const getMinCompatibleApiVersion = () => packageJson.mcpCompatibility?.minApiVersion ?? MCP_SERVER_VERSION;
24
+ export const MIN_COMPATIBLE_API_VERSION = getMinCompatibleApiVersion();
@@ -1,3 +1,4 @@
1
+ import { MCP_SERVER_VERSION } from "../../../core/version.js";
1
2
  import { finalizeFormattedText, mergeFormatterOptions, } from "./responseFormatter.js";
2
3
  export function formatTdInfo(data, options) {
3
4
  const opts = mergeFormatterOptions(options);
@@ -6,17 +7,23 @@ export function formatTdInfo(data, options) {
6
7
  context: { title: "TouchDesigner Info" },
7
8
  });
8
9
  }
9
- const summary = `Server: ${data.server}\nVersion: ${data.version}`;
10
- const osLine = data.osName
11
- ? `\nOS: ${data.osName} ${data.osVersion ?? ""}`
12
- : "";
13
- const text = opts.detailLevel === "minimal"
14
- ? `Server: ${data.server}, v${data.version}`
15
- : `${summary}${osLine}`;
10
+ const structured = {
11
+ "API Server Version": data.mcpApiVersion,
12
+ "MCP Server Version": MCP_SERVER_VERSION,
13
+ "Operating System": data.osName
14
+ ? `${data.osName} ${data.osVersion ?? ""}`.trim()
15
+ : "Unknown",
16
+ "TouchDesigner Version": data.version,
17
+ };
18
+ const text = Object.entries(structured)
19
+ .map(([key, value]) => `${key}: ${value}`)
20
+ .join("\n");
16
21
  return finalizeFormattedText(text.trim(), opts, {
17
- context: { title: "TouchDesigner Info", ...data },
18
- structured: data,
19
- template: opts.detailLevel === "detailed" ? "detailedPayload" : "default",
22
+ context: {
23
+ title: "TouchDesigner Info",
24
+ },
25
+ structured,
26
+ template: "detailedPayload",
20
27
  });
21
28
  }
22
29
  export function formatCreateNodeResult(data, options) {
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { McpLogger } from "../core/logger.js";
3
- import { PACKAGE_VERSION } from "../core/version.js";
3
+ import { MCP_SERVER_VERSION } from "../core/version.js";
4
4
  import { registerPrompts } from "../features/prompts/index.js";
5
5
  import { registerTools } from "../features/tools/index.js";
6
6
  import { createTouchDesignerClient } from "../tdClient/index.js";
@@ -19,7 +19,7 @@ export class TouchDesignerServer {
19
19
  constructor() {
20
20
  this.server = new McpServer({
21
21
  name: "TouchDesigner",
22
- version: PACKAGE_VERSION,
22
+ version: MCP_SERVER_VERSION,
23
23
  }, {
24
24
  capabilities: {
25
25
  logging: {},
@@ -1,12 +1,8 @@
1
+ import axios from "axios";
2
+ import { getCompatibilityPolicy, getCompatibilityPolicyType, } from "../core/compatibility.js";
1
3
  import { createErrorResult, createSuccessResult } from "../core/result.js";
2
- import { PACKAGE_VERSION } from "../core/version.js";
4
+ import { MCP_SERVER_VERSION, MIN_COMPATIBLE_API_VERSION, } from "../core/version.js";
3
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";
4
- const updateGuide = `
5
- 1. Download the latest [touchdesigner-mcp-td.zip](https://github.com/8beeeaaat/touchdesigner-mcp/releases/latest/download/touchdesigner-mcp-td.zip) from the releases page.
6
- 2. Delete the existing \`touchdesigner-mcp-td\` folder and replace it with the newly extracted contents.
7
- 3. Remove the old \`mcp_webserver_base\` component from your TouchDesigner project and import the \`.tox\` from the new folder.
8
- 4. Restart TouchDesigner and the AI agent running the MCP server (e.g., Claude Desktop).
9
- `;
10
6
  /**
11
7
  * Default implementation of ITouchDesignerApi using generated API clients
12
8
  */
@@ -24,6 +20,7 @@ const defaultApiClient = {
24
20
  getTdPythonClasses: apiGetTdPythonClasses,
25
21
  updateNode: apiUpdateNode,
26
22
  };
23
+ export const ERROR_CACHE_TTL_MS = 5000; // 5 seconds
27
24
  /**
28
25
  * Null logger implementation that discards all logs
29
26
  */
@@ -68,6 +65,21 @@ export class TouchDesignerClient {
68
65
  logger;
69
66
  api;
70
67
  verifiedCompatibilityError;
68
+ cachedCompatibilityCheck;
69
+ errorCacheTimestamp;
70
+ /**
71
+ * Initialize TouchDesigner client with optional dependencies
72
+ */
73
+ constructor(params = {}) {
74
+ this.logger = params.logger || nullLogger;
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
+ */
71
83
  logDebug(message, context) {
72
84
  const data = context ? { message, ...context } : { message };
73
85
  this.logger.sendLog({
@@ -76,176 +88,284 @@ export class TouchDesignerClient {
76
88
  logger: "TouchDesignerClient",
77
89
  });
78
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
+ }
79
101
  /**
80
102
  * Verify compatibility with the TouchDesigner server
81
103
  */
82
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
+ }
83
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
+ });
84
128
  throw this.verifiedCompatibilityError;
85
129
  }
86
130
  const result = await this.verifyVersionCompatibility();
87
131
  if (result.success) {
88
132
  this.verifiedCompatibilityError = null;
133
+ this.errorCacheTimestamp = null;
134
+ this.cachedCompatibilityCheck = true;
135
+ this.logDebug("Compatibility verified successfully");
89
136
  return;
90
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
+ });
91
142
  this.verifiedCompatibilityError = result.error;
143
+ this.errorCacheTimestamp = Date.now();
144
+ this.cachedCompatibilityCheck = false;
92
145
  throw result.error;
93
146
  }
94
147
  /**
95
- * Initialize TouchDesigner client with optional dependencies
148
+ * Wrapper for API calls that require compatibility verification
149
+ * @private
96
150
  */
97
- constructor(params = {}) {
98
- this.logger = params.logger || nullLogger;
99
- this.api = params.httpClient || defaultApiClient;
100
- this.verifiedCompatibilityError = null;
151
+ async apiCall(message, call, context) {
152
+ this.logDebug(message, context);
153
+ await this.verifyCompatibility();
154
+ const result = await call();
155
+ return handleApiResponse(result);
101
156
  }
102
157
  /**
103
158
  * Execute a node method
104
159
  */
105
160
  async execNodeMethod(params) {
106
- this.logDebug("Executing node method", {
161
+ return this.apiCall("Executing node method", () => this.api.execNodeMethod(params), {
107
162
  method: params.method,
108
163
  nodePath: params.nodePath,
109
164
  });
110
- await this.verifyCompatibility();
111
- const result = await this.api.execNodeMethod(params);
112
- return handleApiResponse(result);
113
165
  }
114
166
  /**
115
167
  * Execute a script in TouchDesigner
116
168
  */
117
169
  async execPythonScript(params) {
118
- this.logDebug("Executing Python script", { params });
119
- await this.verifyCompatibility();
120
- const result = await this.api.execPythonScript(params);
121
- return handleApiResponse(result);
170
+ return this.apiCall("Executing Python script", () => this.api.execPythonScript(params), { params });
122
171
  }
123
172
  /**
124
173
  * Get TouchDesigner server information
125
174
  */
126
175
  async getTdInfo() {
127
- this.logDebug("Getting server info");
128
- await this.verifyCompatibility();
129
- const result = await this.api.getTdInfo();
130
- return handleApiResponse(result);
176
+ return this.apiCall("Getting server info", () => this.api.getTdInfo());
131
177
  }
132
178
  /**
133
179
  * Get list of nodes
134
180
  */
135
181
  async getNodes(params) {
136
- this.logDebug("Getting nodes for parent", {
137
- parentPath: params.parentPath,
138
- });
139
- await this.verifyCompatibility();
140
- const result = await this.api.getNodes(params);
141
- return handleApiResponse(result);
182
+ return this.apiCall("Getting nodes for parent", () => this.api.getNodes(params), { parentPath: params.parentPath });
142
183
  }
143
184
  /**
144
185
  * Get node properties
145
186
  */
146
187
  async getNodeDetail(params) {
147
- this.logDebug("Getting properties for node", {
148
- nodePath: params.nodePath,
149
- });
150
- await this.verifyCompatibility();
151
- const result = await this.api.getNodeDetail(params);
152
- return handleApiResponse(result);
188
+ return this.apiCall("Getting properties for node", () => this.api.getNodeDetail(params), { nodePath: params.nodePath });
153
189
  }
154
190
  /**
155
191
  * Get node error information
156
192
  */
157
193
  async getNodeErrors(params) {
158
- this.logDebug("Checking node errors", {
159
- nodePath: params.nodePath,
160
- });
161
- await this.verifyCompatibility();
162
- const result = await this.api.getNodeErrors(params);
163
- return handleApiResponse(result);
194
+ return this.apiCall("Checking node errors", () => this.api.getNodeErrors(params), { nodePath: params.nodePath });
164
195
  }
165
196
  /**
166
197
  * Create a new node
167
198
  */
168
199
  async createNode(params) {
169
- this.logDebug("Creating node", {
200
+ return this.apiCall("Creating node", () => this.api.createNode(params), {
170
201
  nodeName: params.nodeName,
171
202
  nodeType: params.nodeType,
172
203
  parentPath: params.parentPath,
173
204
  });
174
- await this.verifyCompatibility();
175
- const result = await this.api.createNode(params);
176
- return handleApiResponse(result);
177
205
  }
178
206
  /**
179
207
  * Update node properties
180
208
  */
181
209
  async updateNode(params) {
182
- this.logDebug("Updating node", { nodePath: params.nodePath });
183
- await this.verifyCompatibility();
184
- const result = await this.api.updateNode(params);
185
- return handleApiResponse(result);
210
+ return this.apiCall("Updating node", () => this.api.updateNode(params), {
211
+ nodePath: params.nodePath,
212
+ });
186
213
  }
187
214
  /**
188
215
  * Delete a node
189
216
  */
190
217
  async deleteNode(params) {
191
- this.logDebug("Deleting node", { nodePath: params.nodePath });
192
- await this.verifyCompatibility();
193
- const result = await this.api.deleteNode(params);
194
- return handleApiResponse(result);
218
+ return this.apiCall("Deleting node", () => this.api.deleteNode(params), {
219
+ nodePath: params.nodePath,
220
+ });
195
221
  }
196
222
  /**
197
223
  * Get list of available Python classes/modules in TouchDesigner
198
224
  */
199
225
  async getClasses() {
200
- this.logDebug("Getting Python classes");
201
- await this.verifyCompatibility();
202
- const result = await this.api.getTdPythonClasses();
203
- return handleApiResponse(result);
226
+ return this.apiCall("Getting Python classes", () => this.api.getTdPythonClasses());
204
227
  }
205
228
  /**
206
229
  * Get details of a specific class/module
207
230
  */
208
231
  async getClassDetails(className) {
209
- this.logDebug("Getting class details", { className });
210
- await this.verifyCompatibility();
211
- const result = await this.api.getTdPythonClassDetails(className);
212
- return handleApiResponse(result);
232
+ return this.apiCall("Getting class details", () => this.api.getTdPythonClassDetails(className), { className });
213
233
  }
214
234
  /**
215
235
  * Retrieve Python help() documentation for modules/classes
216
236
  */
217
237
  async getModuleHelp(params) {
218
- this.logDebug("Getting module help", { moduleName: params.moduleName });
219
- await this.verifyCompatibility();
220
- const result = await this.api.getModuleHelp(params);
221
- return handleApiResponse(result);
238
+ return this.apiCall("Getting module help", () => this.api.getModuleHelp(params), { moduleName: params.moduleName });
222
239
  }
223
240
  async verifyVersionCompatibility() {
224
- const tdInfoResult = await this.api.getTdInfo();
225
- if (!tdInfoResult.success) {
226
- return createErrorResult(new Error(`Failed to retrieve TouchDesigner info for version check: ${tdInfoResult.error}`));
241
+ let tdInfoResult;
242
+ try {
243
+ tdInfoResult = await this.api.getTdInfo();
227
244
  }
228
- const apiVersion = tdInfoResult.data?.mcpApiVersion?.trim();
229
- if (!apiVersion) {
230
- return createErrorResult(new Error(`TouchDesigner API server did not report its version. Please reinstall the TouchDesigner components from the latest release.\n${updateGuide}`));
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));
231
273
  }
232
- const normalizedServerVersion = this.normalizeVersion(PACKAGE_VERSION);
233
- const normalizedApiVersion = this.normalizeVersion(apiVersion);
234
- if (normalizedServerVersion !== normalizedApiVersion) {
274
+ if (!tdInfoResult.success) {
275
+ const errorMessage = this.formatConnectionError(tdInfoResult.error);
235
276
  this.logger.sendLog({
236
- data: {
237
- message: "MCP server and TouchDesigner API server versions are incompatible",
238
- touchDesignerApiVersion: normalizedApiVersion,
239
- touchDesignerServerVersion: normalizedServerVersion,
240
- },
277
+ data: { error: tdInfoResult.error, errorType: "api_response" },
241
278
  level: "error",
242
279
  logger: "TouchDesignerClient",
243
280
  });
244
- return createErrorResult(new Error(`Version mismatch detected between MCP server (${normalizedServerVersion}) and TouchDesigner API server (${normalizedApiVersion}). Update both components to the same release.\n${updateGuide}`));
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));
245
297
  }
246
298
  return createSuccessResult(undefined);
247
299
  }
248
- normalizeVersion(version) {
249
- return version.trim().replace(/^v/i, "");
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
+ };
250
370
  }
251
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touchdesigner-mcp-server",
3
- "version": "1.3.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,13 +26,14 @@
23
26
  "touchdesigner-mcp-server": "dist/cli.js"
24
27
  },
25
28
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.23.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
33
  "@types/yargs": "^17.0.35",
31
34
  "axios": "^1.13.2",
32
35
  "mustache": "^4.2.0",
36
+ "semver": "^7.7.3",
33
37
  "yaml": "^2.8.2",
34
38
  "zod": "4.1.13"
35
39
  },
@@ -39,15 +43,16 @@
39
43
  "@types/jsdom": "^27.0.0",
40
44
  "@types/mustache": "^4.2.6",
41
45
  "@types/node": "^24.10.1",
42
- "@vitest/coverage-v8": "^4.0.14",
46
+ "@types/semver": "^7.7.1",
47
+ "@vitest/coverage-v8": "^4.0.15",
43
48
  "archiver": "^7.0.1",
44
- "msw": "^2.12.3",
49
+ "msw": "^2.12.4",
45
50
  "npm-run-all": "^4.1.5",
46
51
  "orval": "^7.17.0",
47
- "prettier": "^3.7.3",
52
+ "prettier": "^3.7.4",
48
53
  "shx": "^0.4.0",
49
54
  "typescript": "^5.9.3",
50
- "vitest": "^4.0.14"
55
+ "vitest": "^4.0.15"
51
56
  },
52
57
  "type": "module",
53
58
  "exports": {
@@ -79,8 +84,10 @@
79
84
  "test:integration": "vitest run ./tests/integration",
80
85
  "test:unit": "vitest run ./tests/unit",
81
86
  "coverage": "vitest run --coverage",
87
+ "version": "run-p version:*",
88
+ "version:api": "node ./scripts/syncApiServerVersions.ts",
89
+ "version:mcp": "node ./scripts/syncMcpServerVersions.ts",
82
90
  "gen": "run-s gen:*",
83
- "gen:version": "node ./scripts/syncVersions.ts",
84
91
  "gen:webserver": "openapi-generator-cli generate -i ./src/api/index.yml -g python-flask -o ./td/modules/td_server",
85
92
  "gen:handlers": "node td/genHandlers.js",
86
93
  "gen:mcp": "orval --config ./orval.config.ts"