touchdesigner-mcp-server 0.4.0-alpha.0 → 0.4.0-alpha.2

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 (82) hide show
  1. package/README.ja.md +4 -4
  2. package/README.md +4 -3
  3. package/dist/cli.js +0 -0
  4. package/dist/gen/endpoints/TouchDesignerAPI.js +1 -1
  5. package/dist/gen/mcp/touchDesignerAPI.zod.js +1 -1
  6. package/dist/index.js +0 -0
  7. package/dist/server/touchDesignerServer.js +1 -1
  8. package/package.json +14 -12
  9. package/td/genHandlers.js +0 -47
  10. package/td/import_modules.py +0 -52
  11. package/td/mcp_webserver_base.tox +0 -0
  12. package/td/modules/mcp/controllers/__init__.py +0 -9
  13. package/td/modules/mcp/controllers/api_controller.py +0 -637
  14. package/td/modules/mcp/controllers/generated_handlers.py +0 -365
  15. package/td/modules/mcp/controllers/openapi_router.py +0 -265
  16. package/td/modules/mcp/services/__init__.py +0 -8
  17. package/td/modules/mcp/services/api_service.py +0 -555
  18. package/td/modules/mcp_webserver_script.py +0 -134
  19. package/td/modules/td_server/.dockerignore +0 -72
  20. package/td/modules/td_server/.openapi-generator/FILES +0 -55
  21. package/td/modules/td_server/.openapi-generator/VERSION +0 -1
  22. package/td/modules/td_server/.openapi-generator-ignore +0 -23
  23. package/td/modules/td_server/.travis.yml +0 -14
  24. package/td/modules/td_server/Dockerfile +0 -16
  25. package/td/modules/td_server/README.md +0 -49
  26. package/td/modules/td_server/git_push.sh +0 -57
  27. package/td/modules/td_server/openapi_server/__init__.py +0 -0
  28. package/td/modules/td_server/openapi_server/__main__.py +0 -19
  29. package/td/modules/td_server/openapi_server/controllers/__init__.py +0 -0
  30. package/td/modules/td_server/openapi_server/controllers/default_controller.py +0 -162
  31. package/td/modules/td_server/openapi_server/controllers/security_controller.py +0 -2
  32. package/td/modules/td_server/openapi_server/encoder.py +0 -19
  33. package/td/modules/td_server/openapi_server/models/__init__.py +0 -33
  34. package/td/modules/td_server/openapi_server/models/base_model.py +0 -68
  35. package/td/modules/td_server/openapi_server/models/create_node200_response.py +0 -125
  36. package/td/modules/td_server/openapi_server/models/create_node200_response_data.py +0 -63
  37. package/td/modules/td_server/openapi_server/models/create_node_request.py +0 -123
  38. package/td/modules/td_server/openapi_server/models/delete_node200_response.py +0 -125
  39. package/td/modules/td_server/openapi_server/models/delete_node200_response_data.py +0 -91
  40. package/td/modules/td_server/openapi_server/models/exec_node_method200_response.py +0 -125
  41. package/td/modules/td_server/openapi_server/models/exec_node_method200_response_data.py +0 -65
  42. package/td/modules/td_server/openapi_server/models/exec_node_method_request.py +0 -153
  43. package/td/modules/td_server/openapi_server/models/exec_node_method_request_args_inner.py +0 -34
  44. package/td/modules/td_server/openapi_server/models/exec_python_script200_response.py +0 -125
  45. package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data.py +0 -65
  46. package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data_result.py +0 -63
  47. package/td/modules/td_server/openapi_server/models/exec_python_script_request.py +0 -65
  48. package/td/modules/td_server/openapi_server/models/get_node_detail200_response.py +0 -125
  49. package/td/modules/td_server/openapi_server/models/get_nodes200_response.py +0 -125
  50. package/td/modules/td_server/openapi_server/models/get_nodes200_response_data.py +0 -65
  51. package/td/modules/td_server/openapi_server/models/get_td_info200_response.py +0 -125
  52. package/td/modules/td_server/openapi_server/models/get_td_info200_response_data.py +0 -155
  53. package/td/modules/td_server/openapi_server/models/get_td_python_class_details200_response.py +0 -125
  54. package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response.py +0 -125
  55. package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response_data.py +0 -63
  56. package/td/modules/td_server/openapi_server/models/td_node.py +0 -175
  57. package/td/modules/td_server/openapi_server/models/td_node_family_type.py +0 -44
  58. package/td/modules/td_server/openapi_server/models/td_python_class_details.py +0 -191
  59. package/td/modules/td_server/openapi_server/models/td_python_class_info.py +0 -127
  60. package/td/modules/td_server/openapi_server/models/td_python_method_info.py +0 -121
  61. package/td/modules/td_server/openapi_server/models/td_python_property_info.py +0 -123
  62. package/td/modules/td_server/openapi_server/models/update_node200_response.py +0 -125
  63. package/td/modules/td_server/openapi_server/models/update_node200_response_data.py +0 -149
  64. package/td/modules/td_server/openapi_server/models/update_node200_response_data_failed_inner.py +0 -91
  65. package/td/modules/td_server/openapi_server/models/update_node_request.py +0 -93
  66. package/td/modules/td_server/openapi_server/openapi/openapi.yaml +0 -975
  67. package/td/modules/td_server/openapi_server/test/__init__.py +0 -16
  68. package/td/modules/td_server/openapi_server/test/test_default_controller.py +0 -201
  69. package/td/modules/td_server/openapi_server/typing_utils.py +0 -30
  70. package/td/modules/td_server/openapi_server/util.py +0 -147
  71. package/td/modules/td_server/requirements.txt +0 -13
  72. package/td/modules/td_server/setup.py +0 -37
  73. package/td/modules/td_server/test-requirements.txt +0 -4
  74. package/td/modules/td_server/tox.ini +0 -11
  75. package/td/modules/utils/config.py +0 -7
  76. package/td/modules/utils/error_handling.py +0 -104
  77. package/td/modules/utils/logging.py +0 -23
  78. package/td/modules/utils/result.py +0 -40
  79. package/td/modules/utils/serialization.py +0 -57
  80. package/td/modules/utils/types.py +0 -33
  81. package/td/modules/utils/utils_logging.py +0 -60
  82. package/td/templates/mcp/api_controller_handlers.mustache +0 -63
@@ -1,555 +0,0 @@
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, include_properties: bool = False) -> 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
- include_properties: Whether to include full node properties (default False for better performance)
142
-
143
- Returns:
144
- Result: Success with list of nodes or error
145
- """
146
-
147
- parent_node = td.op(parent_path)
148
- if parent_node is None or not parent_node.valid:
149
- raise error_result(f"Parent node not found at path: {parent_path}")
150
-
151
- if pattern:
152
- log_message(
153
- f"Calling parent_node.findChildren(name='{pattern}')",
154
- LogLevel.DEBUG,
155
- )
156
- nodes = parent_node.findChildren(name=pattern)
157
- else:
158
- log_message("Calling parent_node.findChildren(depth=1)", LogLevel.DEBUG)
159
- nodes = parent_node.findChildren(depth=1)
160
-
161
- if include_properties:
162
- node_summaries = [self._get_node_summary(node) for node in nodes]
163
- else:
164
- node_summaries = [self._get_node_summary_light(node) for node in nodes]
165
-
166
- return success_result({"nodes": node_summaries})
167
-
168
- def create_node(
169
- self,
170
- parent_path: str,
171
- node_type: str,
172
- node_name: Optional[str] = None,
173
- parameters: Optional[Dict[str, Any]] = None,
174
- ) -> Result:
175
- """Create a new node under the specified parent path"""
176
-
177
- parent_node = td.op(parent_path)
178
- if parent_node is None or not parent_node.valid:
179
- return error_result(
180
- f"Parent node not found at path: {parent_path}",
181
- )
182
-
183
- new_node = parent_node.create(node_type, node_name)
184
-
185
- if new_node is None or not new_node.valid:
186
- return error_result(
187
- f"Failed to create node of type {node_type} under {parent_path}"
188
- )
189
-
190
- if parameters and isinstance(parameters, dict):
191
- for prop_name, prop_value in parameters.items():
192
- try:
193
- if hasattr(new_node.par, prop_name):
194
- par = getattr(new_node.par, prop_name)
195
- if hasattr(par, "val"):
196
- par.val = prop_value
197
- elif hasattr(new_node, prop_name):
198
- prop = getattr(new_node, prop_name)
199
- if isinstance(prop, (int, float, str)):
200
- setattr(new_node, prop_name, prop_value)
201
- except Exception as e:
202
- log_message(
203
- f"Error setting parameter {prop_name} on new node: {str(e)}",
204
- LogLevel.WARNING,
205
- )
206
-
207
- node_info = self._get_node_summary(new_node)
208
- return success_result({"result": node_info})
209
-
210
- def delete_node(self, node_path: str) -> Result:
211
- """Delete the node at the specified path"""
212
-
213
- node = td.op(node_path)
214
- if node is None or not node.valid:
215
- return error_result(f"Node not found at path: {node_path}")
216
-
217
- node_info = self._get_node_summary(node)
218
- node.destroy()
219
-
220
- if td.op(node_path) is None:
221
- log_message(f"Node deleted successfully: {node_path}", LogLevel.DEBUG)
222
- return success_result({"deleted": True, "node": node_info})
223
- else:
224
- log_message(
225
- f"Failed to verify node deletion: {node_path}", LogLevel.WARNING
226
- )
227
- return error_result(f"Failed to delete node: {node_path}")
228
-
229
- def exec_node_method(
230
- self, node_path: str, method: str, args: List, kwargs: Dict
231
- ) -> Result:
232
- """Call method on the specified node"""
233
-
234
- node = td.op(node_path)
235
- if node is None or not node.valid:
236
- raise error_result(f"Node not found at path: {node_path}")
237
-
238
- if not hasattr(node, method):
239
- raise error_result(f"Method {method} not found on node {node_path}")
240
-
241
- method = getattr(node, method)
242
- if not callable(method):
243
- raise error_result(f"{method} is not a callable method")
244
-
245
- result = method(*args, **kwargs)
246
-
247
- log_message(
248
- f"Method: {method}, args: {args}, kwargs: {kwargs}, result: {result}",
249
- LogLevel.DEBUG,
250
- )
251
- log_message(
252
- f"Method execution complete, result type: {type(result).__name__}",
253
- LogLevel.DEBUG,
254
- )
255
-
256
- processed_result = self._process_method_result(result)
257
-
258
- return success_result({"result": processed_result})
259
-
260
- def exec_python_script(self, script: str) -> Result:
261
- """Execute a Python script directly in TouchDesigner
262
-
263
- Args:
264
- script (str): The Python script to execute
265
-
266
- Returns:
267
- Result: Success result with execution output or error result with message
268
- """
269
-
270
- local_vars = {
271
- "op": td.op,
272
- "ops": td.ops,
273
- "me": td.op.me if hasattr(td, "op") and hasattr(td.op, "me") else None,
274
- "parent": (td.op("..").path if hasattr(td, "op") and td.op("..") else None),
275
- "project": td.project if hasattr(td, "project") else None,
276
- "td": td,
277
- "result": None,
278
- }
279
-
280
- stdout_capture = io.StringIO()
281
- stderr_capture = io.StringIO()
282
-
283
- with contextlib.redirect_stdout(stdout_capture), contextlib.redirect_stderr(
284
- stderr_capture
285
- ):
286
- if "\n" not in script and ";" not in script:
287
- try:
288
- result = eval(script, globals(), local_vars)
289
- local_vars["result"] = result
290
- processed_result = self._process_method_result(result)
291
-
292
- log_message(
293
- f"Script evaluated. Raw result: {repr(result)}",
294
- LogLevel.DEBUG,
295
- )
296
-
297
- stdout_val = stdout_capture.getvalue()
298
- stderr_val = stderr_capture.getvalue()
299
-
300
- return success_result(
301
- {
302
- "result": processed_result,
303
- "stdout": stdout_val,
304
- "stderr": stderr_val,
305
- }
306
- )
307
- except SyntaxError:
308
- pass
309
-
310
- try:
311
- exec(script, globals(), local_vars)
312
-
313
- if "result" not in local_vars:
314
- lines = script.strip().split("\n")
315
- if lines:
316
- last_expr = lines[-1].strip()
317
- if last_expr and not last_expr.startswith(
318
- (
319
- "import",
320
- "from",
321
- "#",
322
- "if",
323
- "def",
324
- "class",
325
- "for",
326
- "while",
327
- )
328
- ):
329
- try:
330
- local_vars["result"] = eval(
331
- last_expr, globals(), local_vars
332
- )
333
- log_message(
334
- f"Extracted result from last line: {last_expr}",
335
- LogLevel.DEBUG,
336
- )
337
- except Exception:
338
- pass
339
-
340
- result = local_vars.get("result", None)
341
- processed_result = self._process_method_result(result)
342
-
343
- stdout_val = stdout_capture.getvalue()
344
- stderr_val = stderr_capture.getvalue()
345
-
346
- return success_result(
347
- {
348
- "result": processed_result,
349
- "stdout": stdout_val,
350
- "stderr": stderr_val,
351
- }
352
- )
353
- except Exception as exec_error:
354
- raise Exception(f"Script execution failed: {str(exec_error)}")
355
-
356
- def update_node(self, node_path: str, properties: Dict[str, Any]) -> Result:
357
- """Update properties of the node at the specified path"""
358
-
359
- node = td.op(node_path)
360
-
361
- if node is None or not node.valid:
362
- raise error_result(f"Node not found at path: {node_path}")
363
-
364
- updated_properties = []
365
- failed_properties = []
366
-
367
- for prop_name, prop_value in properties.items():
368
- try:
369
- if hasattr(node.par, prop_name):
370
- par = getattr(node.par, prop_name)
371
- if hasattr(par, "val"):
372
- par.val = prop_value
373
- updated_properties.append(prop_name)
374
- else:
375
- failed_properties.append(
376
- {
377
- "name": prop_name,
378
- "reason": "Not a settable parameter",
379
- }
380
- )
381
- elif hasattr(node, prop_name):
382
- prop = getattr(node, prop_name)
383
- if isinstance(prop, (int, float, str)):
384
- setattr(node, prop_name, prop_value)
385
- updated_properties.append(prop_name)
386
- else:
387
- failed_properties.append(
388
- {
389
- "name": prop_name,
390
- "reason": "Not a settable property",
391
- }
392
- )
393
- else:
394
- failed_properties.append(
395
- {"name": prop_name, "reason": "Property not found on node"}
396
- )
397
- except Exception as e:
398
- log_message(
399
- f"Error updating property {prop_name}: {str(e)}", LogLevel.ERROR
400
- )
401
- failed_properties.append({"name": prop_name, "reason": str(e)})
402
-
403
- result = {
404
- "path": node_path,
405
- "updated": updated_properties,
406
- "failed": failed_properties,
407
- "message": f"Updated {len(updated_properties)} properties",
408
- }
409
-
410
- if updated_properties:
411
- log_message(
412
- f"Successfully updated properties: {updated_properties}",
413
- LogLevel.DEBUG,
414
- )
415
- return success_result(result)
416
- else:
417
- log_message(
418
- f"No properties were updated. Failed: {failed_properties}",
419
- LogLevel.WARNING,
420
- )
421
- if failed_properties:
422
- raise error_result("Failed to update any properties")
423
- else:
424
- raise error_result("No matching properties to update")
425
-
426
- def _get_node_properties(self, node):
427
- params_dict = {}
428
- for par in node.pars("*"):
429
- try:
430
- value = par.eval()
431
- if isinstance(value, td.OP):
432
- value = value.path
433
- params_dict[par.name] = value
434
- except Exception as e:
435
- log_message(
436
- f"Error evaluating parameter {par.name}: {str(e)}", LogLevel.DEBUG
437
- )
438
- params_dict[par.name] = f"<Error: {str(e)}>"
439
-
440
- return params_dict
441
-
442
- def _get_node_summary_light(self, node) -> Dict:
443
- """Get lightweight information about a node (without properties for better performance)"""
444
- try:
445
- node_info = {
446
- "id": node.id,
447
- "name": node.name,
448
- "path": node.path,
449
- "opType": node.OPType,
450
- "properties": {}, # Empty properties for lightweight response
451
- }
452
-
453
- return node_info
454
- except Exception as e:
455
- log_message(
456
- f"Error collecting node information: {str(e)}", LogLevel.WARNING
457
- )
458
- return {"name": node.name if hasattr(node, "name") else "unknown"}
459
-
460
- def _get_node_summary(self, node) -> Dict:
461
- """Get detailed information about a node"""
462
- try:
463
- node_info = {
464
- "id": node.id,
465
- "name": node.name,
466
- "path": node.path,
467
- "opType": node.OPType,
468
- "properties": self._get_node_properties(node),
469
- }
470
-
471
- return node_info
472
- except Exception as e:
473
- log_message(
474
- f"Error collecting node information: {str(e)}", LogLevel.WARNING
475
- )
476
- return {"name": node.name if hasattr(node, "name") else "unknown"}
477
-
478
- def _process_method_result(self, result: Any) -> Any:
479
- """
480
- Process method result based on its type to make it JSON serializable
481
-
482
- Args:
483
- result: Result value to process
484
-
485
- Returns:
486
- Processed value that can be serialized to JSON
487
- """
488
- if isinstance(result, (int, float, str, bool)) or result is None:
489
- return result
490
-
491
- if isinstance(result, (list, tuple)):
492
- processed_list = []
493
- for item in result:
494
- processed_list.append(self._process_item(item))
495
- return processed_list
496
-
497
- if isinstance(result, dict):
498
- processed_dict = {}
499
- for key, value in result.items():
500
- processed_dict[key] = self._process_item(value)
501
- return processed_dict
502
-
503
- try:
504
- result_dict = {}
505
- for item in result:
506
- processed = self._process_item(item)
507
- if hasattr(item, "name"):
508
- result_dict[item.name] = processed
509
- else:
510
- result_dict[f"item_{len(result_dict)}"] = processed
511
- return result_dict
512
- except TypeError:
513
- return self._process_item(result)
514
-
515
- def _process_item(self, item: Any) -> Any:
516
- """
517
- Process individual item from a result for JSON serialization
518
-
519
- Args:
520
- item: Item to process
521
-
522
- Returns:
523
- Processed item that can be serialized to JSON
524
- """
525
- if isinstance(item, (int, float, str, bool)) or item is None:
526
- return item
527
-
528
- if hasattr(td, "op") and callable(td.op):
529
- node = td.op(item)
530
- if node and hasattr(node, "valid") and node.valid:
531
- return self._get_node_summary(node)
532
-
533
- if not callable(item) and hasattr(item, "name"):
534
- return str(item)
535
-
536
- if hasattr(item, "eval") and callable(item.eval):
537
- try:
538
- value = item.eval()
539
- if hasattr(td, "OP") and isinstance(value, td.OP):
540
- return value.path
541
- return value
542
- except Exception as e:
543
- log_message(
544
- f"Error evaluating parameter {item.name if hasattr(item, 'name') else 'unknown'}: {str(e)}",
545
- LogLevel.DEBUG,
546
- )
547
- return f"<Error: {str(e)}>"
548
-
549
- try:
550
- return safe_serialize(item)
551
- except Exception:
552
- return str(item)
553
-
554
-
555
- api_service = TouchDesignerApiService()
@@ -1,134 +0,0 @@
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")