touchdesigner-mcp-server 0.2.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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +211 -0
  3. package/README.md +204 -0
  4. package/dist/api/customInstance.js +18 -0
  5. package/dist/cli.js +36 -0
  6. package/dist/core/constants.js +26 -0
  7. package/dist/core/errorHandling.js +19 -0
  8. package/dist/core/logger.js +45 -0
  9. package/dist/core/result.js +12 -0
  10. package/dist/features/prompts/handlers/td_prompts.js +131 -0
  11. package/dist/features/prompts/index.js +1 -0
  12. package/dist/features/prompts/register.js +7 -0
  13. package/dist/features/tools/handlers/tdTools.js +214 -0
  14. package/dist/features/tools/index.js +1 -0
  15. package/dist/features/tools/register.js +7 -0
  16. package/dist/features/tools/types.js +1 -0
  17. package/dist/features/tools/utils/toolUtils.js +1 -0
  18. package/dist/gen/endpoints/TouchDesignerAPI.js +250 -0
  19. package/dist/gen/mcp/touchDesignerAPI.zod.js +203 -0
  20. package/dist/index.js +3 -0
  21. package/dist/mock/index.js +5 -0
  22. package/dist/mock/node.js +3 -0
  23. package/dist/server/connectionManager.js +83 -0
  24. package/dist/server/touchDesignerServer.js +60 -0
  25. package/dist/tdClient/index.js +6 -0
  26. package/dist/tdClient/touchDesignerClient.js +150 -0
  27. package/package.json +76 -0
  28. package/src/index.ts +6 -0
  29. package/td/genHandlers.js +47 -0
  30. package/td/import_modules.py +52 -0
  31. package/td/mcp_webserver_base.tox +0 -0
  32. package/td/modules/mcp/controllers/__init__.py +9 -0
  33. package/td/modules/mcp/controllers/api_controller.py +623 -0
  34. package/td/modules/mcp/controllers/generated_handlers.py +365 -0
  35. package/td/modules/mcp/controllers/openapi_router.py +265 -0
  36. package/td/modules/mcp/services/__init__.py +8 -0
  37. package/td/modules/mcp/services/api_service.py +535 -0
  38. package/td/modules/mcp_webserver_script.py +134 -0
  39. package/td/modules/td_server/.dockerignore +72 -0
  40. package/td/modules/td_server/.openapi-generator/FILES +55 -0
  41. package/td/modules/td_server/.openapi-generator/VERSION +1 -0
  42. package/td/modules/td_server/.openapi-generator-ignore +23 -0
  43. package/td/modules/td_server/.travis.yml +14 -0
  44. package/td/modules/td_server/Dockerfile +16 -0
  45. package/td/modules/td_server/README.md +49 -0
  46. package/td/modules/td_server/git_push.sh +57 -0
  47. package/td/modules/td_server/openapi_server/__init__.py +0 -0
  48. package/td/modules/td_server/openapi_server/__main__.py +19 -0
  49. package/td/modules/td_server/openapi_server/controllers/__init__.py +0 -0
  50. package/td/modules/td_server/openapi_server/controllers/default_controller.py +160 -0
  51. package/td/modules/td_server/openapi_server/controllers/security_controller.py +2 -0
  52. package/td/modules/td_server/openapi_server/encoder.py +19 -0
  53. package/td/modules/td_server/openapi_server/models/__init__.py +33 -0
  54. package/td/modules/td_server/openapi_server/models/base_model.py +68 -0
  55. package/td/modules/td_server/openapi_server/models/create_node200_response.py +125 -0
  56. package/td/modules/td_server/openapi_server/models/create_node200_response_data.py +63 -0
  57. package/td/modules/td_server/openapi_server/models/create_node_request.py +123 -0
  58. package/td/modules/td_server/openapi_server/models/delete_node200_response.py +125 -0
  59. package/td/modules/td_server/openapi_server/models/delete_node200_response_data.py +91 -0
  60. package/td/modules/td_server/openapi_server/models/exec_node_method200_response.py +125 -0
  61. package/td/modules/td_server/openapi_server/models/exec_node_method200_response_data.py +65 -0
  62. package/td/modules/td_server/openapi_server/models/exec_node_method_request.py +153 -0
  63. package/td/modules/td_server/openapi_server/models/exec_node_method_request_args_inner.py +34 -0
  64. package/td/modules/td_server/openapi_server/models/exec_python_script200_response.py +125 -0
  65. package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data.py +65 -0
  66. package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data_result.py +63 -0
  67. package/td/modules/td_server/openapi_server/models/exec_python_script_request.py +65 -0
  68. package/td/modules/td_server/openapi_server/models/get_node_detail200_response.py +125 -0
  69. package/td/modules/td_server/openapi_server/models/get_nodes200_response.py +125 -0
  70. package/td/modules/td_server/openapi_server/models/get_nodes200_response_data.py +65 -0
  71. package/td/modules/td_server/openapi_server/models/get_td_info200_response.py +125 -0
  72. package/td/modules/td_server/openapi_server/models/get_td_info200_response_data.py +155 -0
  73. package/td/modules/td_server/openapi_server/models/get_td_python_class_details200_response.py +125 -0
  74. package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response.py +125 -0
  75. package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response_data.py +63 -0
  76. package/td/modules/td_server/openapi_server/models/td_node.py +175 -0
  77. package/td/modules/td_server/openapi_server/models/td_node_family_type.py +44 -0
  78. package/td/modules/td_server/openapi_server/models/td_python_class_details.py +191 -0
  79. package/td/modules/td_server/openapi_server/models/td_python_class_info.py +127 -0
  80. package/td/modules/td_server/openapi_server/models/td_python_method_info.py +121 -0
  81. package/td/modules/td_server/openapi_server/models/td_python_property_info.py +123 -0
  82. package/td/modules/td_server/openapi_server/models/update_node200_response.py +125 -0
  83. package/td/modules/td_server/openapi_server/models/update_node200_response_data.py +149 -0
  84. package/td/modules/td_server/openapi_server/models/update_node200_response_data_failed_inner.py +91 -0
  85. package/td/modules/td_server/openapi_server/models/update_node_request.py +93 -0
  86. package/td/modules/td_server/openapi_server/openapi/openapi.yaml +966 -0
  87. package/td/modules/td_server/openapi_server/test/__init__.py +16 -0
  88. package/td/modules/td_server/openapi_server/test/test_default_controller.py +200 -0
  89. package/td/modules/td_server/openapi_server/typing_utils.py +30 -0
  90. package/td/modules/td_server/openapi_server/util.py +147 -0
  91. package/td/modules/td_server/requirements.txt +13 -0
  92. package/td/modules/td_server/setup.py +37 -0
  93. package/td/modules/td_server/test-requirements.txt +4 -0
  94. package/td/modules/td_server/tox.ini +11 -0
  95. package/td/modules/utils/config.py +7 -0
  96. package/td/modules/utils/error_handling.py +104 -0
  97. package/td/modules/utils/logging.py +23 -0
  98. package/td/modules/utils/result.py +40 -0
  99. package/td/modules/utils/serialization.py +57 -0
  100. package/td/modules/utils/types.py +33 -0
  101. package/td/modules/utils/utils_logging.py +60 -0
  102. package/td/templates/mcp/api_controller_handlers.mustache +63 -0
@@ -0,0 +1,535 @@
1
+ """
2
+ TouchDesigner MCP Web Server API Service Implementation
3
+ Provides API functionality related to TouchDesigner
4
+ """
5
+
6
+ import contextlib
7
+ import inspect
8
+ import io
9
+ from typing import Any, Dict, List, Optional, Protocol
10
+
11
+ from utils.logging import log_message
12
+ from utils.result import error_result, success_result
13
+ from utils.serialization import safe_serialize
14
+ from utils.types import LogLevel, Result
15
+
16
+ import td
17
+
18
+
19
+ class IApiService(Protocol):
20
+ """API service interface"""
21
+
22
+ def get_td_info(self) -> Result: ...
23
+ def get_td_python_classes(self) -> Result: ...
24
+ def get_td_python_class_details(self, class_name: str) -> Result: ...
25
+ def get_node_detail(self, node_path: str) -> Result: ...
26
+ def update_node(self, node_path: str, properties: Dict[str, Any]) -> Result: ...
27
+ def exec_node_method(
28
+ self, node_path: str, method: str, args: List, kwargs: Dict
29
+ ) -> Result: ...
30
+
31
+
32
+ class TouchDesignerApiService(IApiService):
33
+ """Implementation of the TouchDesigner API service"""
34
+
35
+ def get_td_info(self) -> Result:
36
+ """Get information about the TouchDesigner server"""
37
+
38
+ version = td.app.version
39
+ build = td.app.build
40
+
41
+ server_info = {
42
+ "server": f"TouchDesigner {version}.{build}",
43
+ "version": f"{version}.{build}",
44
+ "osName": td.app.osName,
45
+ "osVersion": td.app.osVersion,
46
+ }
47
+
48
+ return success_result(server_info)
49
+
50
+ def get_td_python_classes(self) -> Result:
51
+ """Get list of Python classes and modules available in TouchDesigner"""
52
+ classes = []
53
+
54
+ for name, obj in inspect.getmembers(td):
55
+ if name.startswith("_"):
56
+ continue
57
+
58
+ description = inspect.getdoc(obj) or ""
59
+ class_info = {
60
+ "name": name,
61
+ "description": description,
62
+ }
63
+
64
+ classes.append(class_info)
65
+
66
+ return success_result({"classes": classes})
67
+
68
+ def get_td_python_class_details(self, class_name: str) -> Result:
69
+ """Get detailed information about a specific Python class or module"""
70
+
71
+ obj = None
72
+ if hasattr(td, class_name):
73
+ obj = getattr(td, class_name)
74
+ log_message(f"Found {class_name} in td module", LogLevel.DEBUG)
75
+ else:
76
+ log_message(f"Class not found: {class_name}", LogLevel.WARNING)
77
+ raise error_result(f"Class or module not found: {class_name}")
78
+
79
+ methods = []
80
+ properties = []
81
+
82
+ for name, member in inspect.getmembers(obj):
83
+ if name.startswith("_"):
84
+ continue
85
+
86
+ try:
87
+ info = {
88
+ "name": name,
89
+ "description": inspect.getdoc(member) or "",
90
+ "type": type(member).__name__,
91
+ }
92
+ if (
93
+ inspect.isfunction(member)
94
+ or inspect.ismethod(member)
95
+ or inspect.ismethoddescriptor(member)
96
+ ):
97
+ methods.append(info)
98
+ else:
99
+ properties.append(info)
100
+ except Exception as e:
101
+ log_message(
102
+ f"Error processing member {name}: {str(e)}", LogLevel.WARNING
103
+ )
104
+
105
+ if inspect.isclass(obj):
106
+ type_info = inspect.classify_class_attrs(obj)[0].kind
107
+ else:
108
+ type_info = type(obj).__name__
109
+
110
+ class_details = {
111
+ "name": class_name,
112
+ "type": type_info,
113
+ "description": inspect.getdoc(obj) or "",
114
+ "methods": methods,
115
+ "properties": properties,
116
+ }
117
+
118
+ return success_result(class_details)
119
+
120
+ def get_node(self, node_path: str) -> Result:
121
+ """Alias for get_node_detail for backwards compatibility"""
122
+ return self.get_node_detail(node_path)
123
+
124
+ def get_node_detail(self, node_path: str) -> Result:
125
+ """Get node at the specified path"""
126
+
127
+ node = td.op(node_path)
128
+
129
+ if node is None or not node.valid:
130
+ raise error_result(f"Node not found at path: {node_path}")
131
+
132
+ node_info = self._get_node_summary(node)
133
+ return success_result(node_info)
134
+
135
+ def get_nodes(self, parent_path: str, pattern: Optional[str] = None) -> Result:
136
+ """Get nodes under the specified parent path, optionally filtered by pattern
137
+
138
+ Args:
139
+ parent_path: Path to the parent node
140
+ pattern: Pattern to filter nodes by name (e.g. "text*" for all nodes starting with "text")
141
+
142
+ Returns:
143
+ Result: Success with list of nodes or error
144
+ """
145
+
146
+ parent_node = td.op(parent_path)
147
+ if parent_node is None or not parent_node.valid:
148
+ raise error_result(f"Parent node not found at path: {parent_path}")
149
+
150
+ if pattern:
151
+ log_message(
152
+ f"Calling parent_node.findChildren(name='{pattern}')",
153
+ LogLevel.DEBUG,
154
+ )
155
+ nodes = parent_node.findChildren(name=pattern)
156
+ else:
157
+ log_message("Calling parent_node.findChildren(depth=1)", LogLevel.DEBUG)
158
+ nodes = parent_node.findChildren(depth=1)
159
+
160
+ node_summaries = [self._get_node_summary(node) for node in nodes]
161
+
162
+ return success_result({"nodes": node_summaries})
163
+
164
+ def create_node(
165
+ self,
166
+ parent_path: str,
167
+ node_type: str,
168
+ node_name: Optional[str] = None,
169
+ parameters: Optional[Dict[str, Any]] = None,
170
+ ) -> Result:
171
+ """Create a new node under the specified parent path"""
172
+
173
+ parent_node = td.op(parent_path)
174
+ if parent_node is None or not parent_node.valid:
175
+ return error_result(
176
+ f"Parent node not found at path: {parent_path}",
177
+ )
178
+
179
+ new_node = parent_node.create(node_type, node_name)
180
+
181
+ if new_node is None or not new_node.valid:
182
+ return error_result(
183
+ f"Failed to create node of type {node_type} under {parent_path}"
184
+ )
185
+
186
+ if parameters and isinstance(parameters, dict):
187
+ for prop_name, prop_value in parameters.items():
188
+ try:
189
+ if hasattr(new_node.par, prop_name):
190
+ par = getattr(new_node.par, prop_name)
191
+ if hasattr(par, "val"):
192
+ par.val = prop_value
193
+ elif hasattr(new_node, prop_name):
194
+ prop = getattr(new_node, prop_name)
195
+ if isinstance(prop, (int, float, str)):
196
+ setattr(new_node, prop_name, prop_value)
197
+ except Exception as e:
198
+ log_message(
199
+ f"Error setting parameter {prop_name} on new node: {str(e)}",
200
+ LogLevel.WARNING,
201
+ )
202
+
203
+ node_info = self._get_node_summary(new_node)
204
+ return success_result({"result": node_info})
205
+
206
+ def delete_node(self, node_path: str) -> Result:
207
+ """Delete the node at the specified path"""
208
+
209
+ node = td.op(node_path)
210
+ if node is None or not node.valid:
211
+ return error_result(f"Node not found at path: {node_path}")
212
+
213
+ node_info = self._get_node_summary(node)
214
+ node.destroy()
215
+
216
+ if td.op(node_path) is None:
217
+ log_message(f"Node deleted successfully: {node_path}", LogLevel.DEBUG)
218
+ return success_result({"deleted": True, "node": node_info})
219
+ else:
220
+ log_message(
221
+ f"Failed to verify node deletion: {node_path}", LogLevel.WARNING
222
+ )
223
+ return error_result(f"Failed to delete node: {node_path}")
224
+
225
+ def exec_node_method(
226
+ self, node_path: str, method: str, args: List, kwargs: Dict
227
+ ) -> Result:
228
+ """Call method on the specified node"""
229
+
230
+ node = td.op(node_path)
231
+ if node is None or not node.valid:
232
+ raise error_result(f"Node not found at path: {node_path}")
233
+
234
+ if not hasattr(node, method):
235
+ raise error_result(f"Method {method} not found on node {node_path}")
236
+
237
+ method = getattr(node, method)
238
+ if not callable(method):
239
+ raise error_result(f"{method} is not a callable method")
240
+
241
+ result = method(*args, **kwargs)
242
+
243
+ log_message(
244
+ f"Method: {method}, args: {args}, kwargs: {kwargs}, result: {result}",
245
+ LogLevel.DEBUG,
246
+ )
247
+ log_message(
248
+ f"Method execution complete, result type: {type(result).__name__}",
249
+ LogLevel.DEBUG,
250
+ )
251
+
252
+ processed_result = self._process_method_result(result)
253
+
254
+ return success_result({"result": processed_result})
255
+
256
+ def exec_python_script(self, script: str) -> Result:
257
+ """Execute a Python script directly in TouchDesigner
258
+
259
+ Args:
260
+ script (str): The Python script to execute
261
+
262
+ Returns:
263
+ Result: Success result with execution output or error result with message
264
+ """
265
+
266
+ local_vars = {
267
+ "op": td.op,
268
+ "ops": td.ops,
269
+ "me": td.op.me if hasattr(td, "op") and hasattr(td.op, "me") else None,
270
+ "parent": (td.op("..").path if hasattr(td, "op") and td.op("..") else None),
271
+ "project": td.project if hasattr(td, "project") else None,
272
+ "td": td,
273
+ "result": None,
274
+ }
275
+
276
+ stdout_capture = io.StringIO()
277
+ stderr_capture = io.StringIO()
278
+
279
+ with contextlib.redirect_stdout(stdout_capture), contextlib.redirect_stderr(
280
+ stderr_capture
281
+ ):
282
+ if "\n" not in script and ";" not in script:
283
+ try:
284
+ result = eval(script, globals(), local_vars)
285
+ local_vars["result"] = result
286
+ processed_result = self._process_method_result(result)
287
+
288
+ log_message(
289
+ f"Script evaluated. Raw result: {repr(result)}",
290
+ LogLevel.DEBUG,
291
+ )
292
+
293
+ stdout_val = stdout_capture.getvalue()
294
+ stderr_val = stderr_capture.getvalue()
295
+
296
+ return success_result(
297
+ {
298
+ "result": processed_result,
299
+ "stdout": stdout_val,
300
+ "stderr": stderr_val,
301
+ }
302
+ )
303
+ except SyntaxError:
304
+ pass
305
+
306
+ try:
307
+ exec(script, globals(), local_vars)
308
+
309
+ if "result" not in local_vars:
310
+ lines = script.strip().split("\n")
311
+ if lines:
312
+ last_expr = lines[-1].strip()
313
+ if last_expr and not last_expr.startswith(
314
+ (
315
+ "import",
316
+ "from",
317
+ "#",
318
+ "if",
319
+ "def",
320
+ "class",
321
+ "for",
322
+ "while",
323
+ )
324
+ ):
325
+ try:
326
+ local_vars["result"] = eval(
327
+ last_expr, globals(), local_vars
328
+ )
329
+ log_message(
330
+ f"Extracted result from last line: {last_expr}",
331
+ LogLevel.DEBUG,
332
+ )
333
+ except Exception:
334
+ pass
335
+
336
+ result = local_vars.get("result", None)
337
+ processed_result = self._process_method_result(result)
338
+
339
+ stdout_val = stdout_capture.getvalue()
340
+ stderr_val = stderr_capture.getvalue()
341
+
342
+ return success_result(
343
+ {
344
+ "result": processed_result,
345
+ "stdout": stdout_val,
346
+ "stderr": stderr_val,
347
+ }
348
+ )
349
+ except Exception as exec_error:
350
+ raise Exception(f"Script execution failed: {str(exec_error)}")
351
+
352
+ def update_node(self, node_path: str, properties: Dict[str, Any]) -> Result:
353
+ """Update properties of the node at the specified path"""
354
+
355
+ node = td.op(node_path)
356
+
357
+ if node is None or not node.valid:
358
+ raise error_result(f"Node not found at path: {node_path}")
359
+
360
+ node_detail = self.get_node_detail(node_path)
361
+ current_properties = node_detail.get("data", {})
362
+ updated_properties = []
363
+ failed_properties = []
364
+
365
+ for prop_name, prop_value in properties.items():
366
+ try:
367
+ if hasattr(node.par, prop_name):
368
+ par = getattr(node.par, prop_name)
369
+ if hasattr(par, "val"):
370
+ par.val = prop_value
371
+ updated_properties.append(prop_name)
372
+ else:
373
+ failed_properties.append(
374
+ {
375
+ "name": prop_name,
376
+ "reason": "Not a settable parameter",
377
+ }
378
+ )
379
+ elif hasattr(node, prop_name):
380
+ prop = getattr(node, prop_name)
381
+ if isinstance(prop, (int, float, str)):
382
+ setattr(node, prop_name, prop_value)
383
+ updated_properties.append(prop_name)
384
+ else:
385
+ failed_properties.append(
386
+ {
387
+ "name": prop_name,
388
+ "reason": "Not a settable property",
389
+ }
390
+ )
391
+ else:
392
+ failed_properties.append(
393
+ {"name": prop_name, "reason": "Property not found on node"}
394
+ )
395
+ except Exception as e:
396
+ log_message(
397
+ f"Error updating property {prop_name}: {str(e)}", LogLevel.ERROR
398
+ )
399
+ failed_properties.append({"name": prop_name, "reason": str(e)})
400
+
401
+ result = {
402
+ "path": node_path,
403
+ "updated": updated_properties,
404
+ "failed": failed_properties,
405
+ "message": f"Updated {len(updated_properties)} properties",
406
+ }
407
+
408
+ if updated_properties:
409
+ log_message(
410
+ f"Successfully updated properties: {updated_properties}",
411
+ LogLevel.DEBUG,
412
+ )
413
+ return success_result(result)
414
+ else:
415
+ log_message(
416
+ f"No properties were updated. Failed: {failed_properties}",
417
+ LogLevel.WARNING,
418
+ )
419
+ if failed_properties:
420
+ raise error_result("Failed to update any properties")
421
+ else:
422
+ raise error_result("No matching properties to update")
423
+
424
+ def _get_node_properties(self, node):
425
+ params_dict = {}
426
+ for par in node.pars("*"):
427
+ try:
428
+ value = par.eval()
429
+ if isinstance(value, td.OP):
430
+ value = value.path
431
+ params_dict[par.name] = value
432
+ except Exception as e:
433
+ log_message(
434
+ f"Error evaluating parameter {par.name}: {str(e)}", LogLevel.DEBUG
435
+ )
436
+ params_dict[par.name] = f"<Error: {str(e)}>"
437
+
438
+ return params_dict
439
+
440
+ def _get_node_summary(self, node) -> Dict:
441
+ """Get detailed information about a node"""
442
+ try:
443
+ node_info = {
444
+ "id": node.id,
445
+ "name": node.name,
446
+ "path": node.path,
447
+ "opType": node.OPType,
448
+ "properties": self._get_node_properties(node),
449
+ }
450
+
451
+ return node_info
452
+ except Exception as e:
453
+ log_message(
454
+ f"Error collecting node information: {str(e)}", LogLevel.WARNING
455
+ )
456
+ return {"name": node.name if hasattr(node, "name") else "unknown"}
457
+
458
+ def _process_method_result(self, result: Any) -> Any:
459
+ """
460
+ Process method result based on its type to make it JSON serializable
461
+
462
+ Args:
463
+ result: Result value to process
464
+
465
+ Returns:
466
+ Processed value that can be serialized to JSON
467
+ """
468
+ if isinstance(result, (int, float, str, bool)) or result is None:
469
+ return result
470
+
471
+ if isinstance(result, (list, tuple)):
472
+ processed_list = []
473
+ for item in result:
474
+ processed_list.append(self._process_item(item))
475
+ return processed_list
476
+
477
+ if isinstance(result, dict):
478
+ processed_dict = {}
479
+ for key, value in result.items():
480
+ processed_dict[key] = self._process_item(value)
481
+ return processed_dict
482
+
483
+ try:
484
+ result_dict = {}
485
+ for item in result:
486
+ processed = self._process_item(item)
487
+ if hasattr(item, "name"):
488
+ result_dict[item.name] = processed
489
+ else:
490
+ result_dict[f"item_{len(result_dict)}"] = processed
491
+ return result_dict
492
+ except TypeError:
493
+ return self._process_item(result)
494
+
495
+ def _process_item(self, item: Any) -> Any:
496
+ """
497
+ Process individual item from a result for JSON serialization
498
+
499
+ Args:
500
+ item: Item to process
501
+
502
+ Returns:
503
+ Processed item that can be serialized to JSON
504
+ """
505
+ if isinstance(item, (int, float, str, bool)) or item is None:
506
+ return item
507
+
508
+ if hasattr(td, "op") and callable(td.op):
509
+ node = td.op(item)
510
+ if node and hasattr(node, "valid") and node.valid:
511
+ return self._get_node_summary(node)
512
+
513
+ if not callable(item) and hasattr(item, "name"):
514
+ return str(item)
515
+
516
+ if hasattr(item, "eval") and callable(item.eval):
517
+ try:
518
+ value = item.eval()
519
+ if hasattr(td, "OP") and isinstance(value, td.OP):
520
+ return value.path
521
+ return value
522
+ except Exception as e:
523
+ log_message(
524
+ f"Error evaluating parameter {item.name if hasattr(item, 'name') else 'unknown'}: {str(e)}",
525
+ LogLevel.DEBUG,
526
+ )
527
+ return f"<Error: {str(e)}>"
528
+
529
+ try:
530
+ return safe_serialize(item)
531
+ except Exception:
532
+ return str(item)
533
+
534
+
535
+ api_service = TouchDesignerApiService()
@@ -0,0 +1,134 @@
1
+ """
2
+ TouchDesigner MCP Web Server Script
3
+ Implements and handles API endpoints
4
+
5
+ This file serves as the entry point for modularized components in TouchDesigner.
6
+ Actual implementations are separated into modules within the mcp package.
7
+ """
8
+
9
+ import traceback
10
+ from typing import Any, Dict
11
+
12
+ try:
13
+ import import_modules
14
+
15
+ import_modules.setup()
16
+ except Exception as e:
17
+ print(f"[ERROR] Failed to setup modules: {str(e)}")
18
+
19
+
20
+ def onServerStart(webServerDAT):
21
+ print("HTTP server started")
22
+ """Called when the web server starts"""
23
+ print("======================================================")
24
+ print("=========== HTTP SERVER STARTED ===========")
25
+ print("======================================================")
26
+ return
27
+
28
+
29
+ def onServerStop(webServerDAT):
30
+ """Called when the web server stops"""
31
+ print("HTTP server stopped")
32
+ return
33
+
34
+
35
+ class ModuleFactory:
36
+ """
37
+ Factory for importing and providing MCP modules.
38
+ Uses lazy loading and provides fallbacks for unavailable modules.
39
+ """
40
+
41
+ def __init__(self):
42
+ self._modules = {}
43
+ self._import_status = {}
44
+
45
+ def get_module(self, module_name: str) -> Any:
46
+ """Get a module by name with lazy loading"""
47
+ if module_name not in self._modules:
48
+ self._load_module(module_name)
49
+ return self._modules.get(module_name)
50
+
51
+ def is_module_available(self, module_name: str) -> bool:
52
+ """Check if a module is available"""
53
+ if module_name not in self._import_status:
54
+ self._load_module(module_name)
55
+ return self._import_status.get(module_name, False)
56
+
57
+ def _load_module(self, module_name: str) -> None:
58
+ """Load a module and track its import status"""
59
+ try:
60
+ module = __import__(module_name, fromlist=["*"])
61
+ self._modules[module_name] = module
62
+ self._import_status[module_name] = True
63
+ print(f"MCP: Successfully loaded module: {module_name}")
64
+ except ImportError as e:
65
+ self._modules[module_name] = None
66
+ self._import_status[module_name] = False
67
+ print(f"MCP: Failed to import module {module_name}: {str(e)}")
68
+
69
+
70
+ class ControllerManager:
71
+ """
72
+ Manages API controller with priority-based processing.
73
+ Implements the Composite and Chain of Responsibility patterns.
74
+ """
75
+
76
+ def __init__(self, module_factory: ModuleFactory):
77
+ print("MCP: Initializing ControllerManager")
78
+ from mcp.controllers.api_controller import api_controller_openapi
79
+
80
+ self.module_factory = module_factory
81
+ self.controller = api_controller_openapi
82
+
83
+ def handle_request(
84
+ self, webServerDAT: Any, request: Dict[str, Any], response: Dict[str, Any]
85
+ ) -> Dict[str, Any]:
86
+ """
87
+ Handle HTTP request with fallback chain
88
+ """
89
+
90
+ try:
91
+ if self.controller is None:
92
+ print("[ERROR] Controller not initialized")
93
+ response["statusCode"] = 500
94
+ response["statusReason"] = "Server Error"
95
+ response["body"] = '{"error": "Controller not initialized"}'
96
+ return response
97
+ return self.controller.onHTTPRequest(webServerDAT, request, response)
98
+ except Exception as e:
99
+ print(f"MCP: Error handling request: {str(e)}")
100
+ traceback.print_exc()
101
+
102
+ response["statusCode"] = 500
103
+ response["statusReason"] = "Internal Server Error"
104
+ response["headers"] = {"Content-Type": "application/json"}
105
+ response["body"] = '{"error": "API controller failed to handle the request"}'
106
+ return response
107
+
108
+
109
+ _module_factory = ModuleFactory()
110
+ _controller_manager = ControllerManager(_module_factory)
111
+
112
+
113
+ def onHTTPRequest(webServerDAT, request, response):
114
+ """
115
+ HTTP request handler for TouchDesigner WebServerDAT
116
+
117
+ Args:
118
+ webServerDAT: Reference to the WebServer DAT
119
+ request: Request object from WebServer DAT
120
+ response: Response object to be filled and returned
121
+
122
+ Returns:
123
+ Completed response object
124
+ """
125
+ return _controller_manager.handle_request(webServerDAT, request, response)
126
+
127
+
128
+ log_module = _module_factory.get_module("utils.logging")
129
+ if log_module:
130
+ types_module = _module_factory.get_module("utils.types")
131
+ log_level = types_module.LogLevel.INFO if types_module else "INFO"
132
+ log_module.log_message("TouchDesigner MCP WebServer Script initialized", log_level)
133
+
134
+ print("TouchDesigner MCP WebServer Script (entry point) initialization completed")