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,52 @@
1
+ import os
2
+ import sys
3
+
4
+ import yaml
5
+
6
+
7
+ def setup():
8
+ externaltox = parent().par.externaltox.eval()
9
+ tox_dir_path = os.path.dirname(externaltox)
10
+ modules_path = os.path.join(tox_dir_path, "modules")
11
+
12
+ if modules_path not in sys.path:
13
+ sys.path.append(modules_path)
14
+
15
+ td_server_path = os.path.join(modules_path, "td_server")
16
+ if td_server_path not in sys.path:
17
+ sys.path.append(td_server_path)
18
+
19
+ schema_path = find_openapi_schema_path(modules_path)
20
+ try:
21
+ if schema_path is None:
22
+ raise FileNotFoundError(
23
+ "OpenAPI schema file not found in any known location."
24
+ )
25
+ with open(schema_path, "r") as f:
26
+ openapi_schema = yaml.safe_load(f)
27
+ except Exception as e:
28
+ openapi_schema = {}
29
+ print("Failed to load OpenAPI schema:", e)
30
+
31
+ import mcp
32
+
33
+ mcp.openapi_schema = openapi_schema
34
+
35
+
36
+ def find_openapi_schema_path(modules_path):
37
+ candidates = [
38
+ os.path.join(
39
+ modules_path, "td_server", "openapi_server", "openapi", "openapi.yaml"
40
+ ),
41
+ os.path.join(
42
+ os.path.dirname(os.path.dirname(modules_path)),
43
+ "td_server",
44
+ "openapi_server",
45
+ "openapi",
46
+ "openapi.yaml",
47
+ ),
48
+ ]
49
+ for path in candidates:
50
+ if os.path.exists(path):
51
+ return path
52
+ return None
Binary file
@@ -0,0 +1,9 @@
1
+ """
2
+ MCP Controller package initialization
3
+ Exports controller classes for dependency injection
4
+ """
5
+
6
+ from mcp.controllers.api_controller import APIControllerOpenAPI, api_controller_openapi
7
+ from mcp.controllers.openapi_router import OpenAPIRouter
8
+
9
+ __all__ = ["APIControllerOpenAPI", "api_controller_openapi", "OpenAPIRouter"]
@@ -0,0 +1,623 @@
1
+ """
2
+ OpenAPI schema based API controller for TouchDesigner MCP Web Server
3
+
4
+ This controller uses the OpenAPIRouter to route requests based on the OpenAPI schema,
5
+ and converts between API models and internal data structures.
6
+ """
7
+
8
+ import json
9
+ import traceback
10
+ from typing import Any, Dict, List, Optional, Protocol, Tuple
11
+
12
+ from mcp.controllers.generated_handlers import *
13
+ from mcp.controllers.openapi_router import OpenAPIRouter
14
+ from utils.error_handling import ErrorCategory
15
+ from utils.logging import log_message
16
+ from utils.result import error_result
17
+ from utils.types import LogLevel, Result
18
+
19
+ try:
20
+ from td_server.openapi_server.models.create_node200_response import \
21
+ CreateNode200Response
22
+ from td_server.openapi_server.models.delete_node200_response import \
23
+ DeleteNode200Response
24
+ from td_server.openapi_server.models.exec_node_method200_response import \
25
+ ExecNodeMethod200Response
26
+ from td_server.openapi_server.models.exec_python_script200_response import \
27
+ ExecPythonScript200Response
28
+ from td_server.openapi_server.models.get_node_detail200_response import \
29
+ GetNodeDetail200Response
30
+ from td_server.openapi_server.models.get_nodes200_response import \
31
+ GetNodes200Response
32
+ from td_server.openapi_server.models.get_td_info200_response import \
33
+ GetTdInfo200Response
34
+ from td_server.openapi_server.models.update_node200_response import \
35
+ UpdateNode200Response
36
+
37
+ log_message("OpenAPI response models imported successfully", LogLevel.DEBUG)
38
+ except ImportError as e:
39
+ log_message(
40
+ f"OpenAPI models import failed, using raw dictionaries: {e}", LogLevel.WARNING
41
+ )
42
+
43
+
44
+ class ApiServiceProtocol(Protocol):
45
+ """Protocol defining the API service interface"""
46
+
47
+ def get_td_info(self) -> Result: ...
48
+
49
+ def get_nodes(self, parent_path: str, pattern: Optional[str] = None) -> Result: ...
50
+
51
+ def create_node(
52
+ self,
53
+ parent_path: str,
54
+ node_type: str,
55
+ node_name: Optional[str] = None,
56
+ parameters: Optional[Dict[str, Any]] = None,
57
+ ) -> Result: ...
58
+
59
+ def delete_node(self, node_path: str) -> Result: ...
60
+
61
+ def get_node_detail(self, node_path: str) -> Result: ...
62
+
63
+ def update_node(self, node_path: str, properties: Dict[str, Any]) -> Result: ...
64
+
65
+ def exec_script(self, script: str) -> Result: ...
66
+
67
+ def get_python_classes(self) -> Result: ...
68
+
69
+ def get_python_class_details(self, class_name: str) -> Result: ...
70
+
71
+ def call_node_method(
72
+ self,
73
+ node_path: str,
74
+ method_name: str,
75
+ args: List[Any] = None,
76
+ kwargs: Dict[str, Any] = None,
77
+ ) -> Result: ...
78
+
79
+
80
+ class RequestProcessor:
81
+ """
82
+ Responsible for processing and normalizing HTTP requests from different sources
83
+
84
+ This class helps achieve separation of concerns by isolating request processing logic
85
+ from the controller class, improving maintainability and testability.
86
+ """
87
+
88
+ @staticmethod
89
+ def normalize_request(
90
+ request: Dict[str, Any],
91
+ ) -> Tuple[str, str, Dict[str, Any], str]:
92
+ """
93
+ Normalize request object to handle different request formats
94
+
95
+ Args:
96
+ request: Request object that might be in different formats
97
+
98
+ Returns:
99
+ Tuple containing (method, path, query_params, body)
100
+ """
101
+ method = ""
102
+ path = ""
103
+ query_params = {}
104
+ body = ""
105
+
106
+ try:
107
+ method = RequestProcessor._extract_method(request)
108
+
109
+ path, uri_query_params = RequestProcessor._extract_path_and_query(request)
110
+ query_params.update(uri_query_params)
111
+
112
+ if "query" in request and isinstance(request["query"], dict):
113
+ query_params.update(request["query"])
114
+
115
+ if "pars" in request and isinstance(request["pars"], dict):
116
+ log_message(
117
+ f"Found 'pars' in request: {request['pars']}", LogLevel.DEBUG
118
+ )
119
+ query_params.update(request["pars"])
120
+
121
+ body = RequestProcessor._extract_body(request)
122
+
123
+ except Exception as e:
124
+ log_message(f"Error during request normalization: {str(e)}", LogLevel.ERROR)
125
+ log_message(traceback.format_exc(), LogLevel.DEBUG)
126
+
127
+ return method, path, query_params, body
128
+
129
+ @staticmethod
130
+ def _extract_method(request: Dict[str, Any]) -> str:
131
+ """Extract HTTP method from request"""
132
+ if "method" in request:
133
+ if isinstance(request["method"], str):
134
+ return request["method"].upper()
135
+ return ""
136
+
137
+ @staticmethod
138
+ def _extract_path_and_query(request: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
139
+ """Extract path and query parameters from request"""
140
+ path = ""
141
+ query_params = {}
142
+
143
+ uri = request.get("uri", {})
144
+
145
+ if isinstance(uri, dict):
146
+ path = uri.get("path", "")
147
+ uri_query = uri.get("query", {})
148
+ if isinstance(uri_query, dict):
149
+ query_params.update(uri_query)
150
+ elif isinstance(uri, str):
151
+ path = uri
152
+
153
+ return path, query_params
154
+
155
+ @staticmethod
156
+ def _extract_body(request: Dict[str, Any]) -> str:
157
+ """Extract body content from request"""
158
+ body = ""
159
+
160
+ body_content = request.get("body", "")
161
+
162
+ if isinstance(body_content, (str, bytes)):
163
+ body = (
164
+ body_content
165
+ if isinstance(body_content, str)
166
+ else body_content.decode("utf-8", errors="replace")
167
+ )
168
+ elif isinstance(body_content, dict):
169
+ body = json.dumps(body_content)
170
+
171
+ if not body and "data" in request:
172
+ data = request.get("data", "")
173
+ if isinstance(data, bytes):
174
+ body = data.decode("utf-8", errors="replace") if data else ""
175
+ elif isinstance(data, str):
176
+ body = data
177
+ elif isinstance(data, dict):
178
+ body = json.dumps(data)
179
+
180
+ return body
181
+
182
+
183
+ class IController(Protocol):
184
+ """
185
+ Controller interface for handling HTTP requests
186
+
187
+ All controllers should implement this interface to ensure consistency across
188
+ different controller implementations. This enforces a unified approach to
189
+ request handling throughout the application.
190
+ """
191
+
192
+ def onHTTPRequest(
193
+ self, webServerDAT: Any, request: Dict[str, Any], response: Dict[str, Any]
194
+ ) -> Dict[str, Any]:
195
+ """
196
+ Process an HTTP request from TouchDesigner WebServerDAT
197
+
198
+ Args:
199
+ webServerDAT: Reference to the WebServerDAT object
200
+ request: Dictionary containing request information
201
+ response: Dictionary for storing response information
202
+
203
+ Returns:
204
+ Updated response dictionary
205
+ """
206
+ ...
207
+
208
+
209
+ class APIControllerOpenAPI(IController):
210
+ """
211
+ API controller that uses OpenAPI schema for routing and model conversion
212
+
213
+ Implements the IController interface for consistency with other controllers.
214
+ """
215
+
216
+ def __init__(self, service: Optional[ApiServiceProtocol] = None):
217
+ """
218
+ Initialize the controller with a service implementation
219
+
220
+ Args:
221
+ service: Service implementation (uses default if None)
222
+ """
223
+ if service is None:
224
+ from mcp.services.api_service import api_service
225
+
226
+ self._service = api_service
227
+ else:
228
+ self._service = service
229
+
230
+ self.router = OpenAPIRouter()
231
+ self.register_handlers()
232
+
233
+ def _normalize_request(
234
+ self, request: Dict[str, Any]
235
+ ) -> Tuple[str, str, Dict[str, Any], str]:
236
+ """
237
+ Normalize request object to handle different request formats
238
+
239
+ Args:
240
+ request: Request object that might be in different formats
241
+
242
+ Returns:
243
+ Tuple containing (method, path, query_params, body)
244
+ """
245
+ return RequestProcessor.normalize_request(request)
246
+
247
+ def onHTTPRequest(
248
+ self, webServerDAT: Any, request: Dict[str, Any], response: Dict[str, Any]
249
+ ) -> Dict[str, Any]:
250
+ """
251
+ Handle HTTP request from TouchDesigner WebServer DAT
252
+
253
+ Implements IController interface for consistent handling across controllers.
254
+
255
+ Args:
256
+ webServerDAT: Reference to the WebServerDAT object
257
+ request: Dictionary containing request information
258
+ response: Dictionary for storing response information
259
+
260
+ Returns:
261
+ Updated response dictionary
262
+ """
263
+
264
+ if "headers" not in response:
265
+ response["headers"] = {}
266
+
267
+ response["headers"]["Access-Control-Allow-Origin"] = "*"
268
+ response["headers"][
269
+ "Access-Control-Allow-Methods"
270
+ ] = "GET, POST, PUT, DELETE, PATCH, OPTIONS"
271
+ response["headers"][
272
+ "Access-Control-Allow-Headers"
273
+ ] = "Content-Type, Authorization"
274
+ response["headers"]["Content-Type"] = "application/json"
275
+
276
+ try:
277
+ method, path, query_params, body = self._normalize_request(request)
278
+ except Exception as e:
279
+
280
+ response["statusCode"] = 500
281
+ response["statusReason"] = "Internal Server Error"
282
+ response["data"] = json.dumps(
283
+ {
284
+ "success": False,
285
+ "error": f"Request normalization error: {str(e)}",
286
+ "errorCategory": str(ErrorCategory.INTERNAL),
287
+ }
288
+ )
289
+ return response
290
+
291
+ try:
292
+ if method == "OPTIONS":
293
+ response["statusCode"] = 200
294
+ response["statusReason"] = "OK"
295
+ response["data"] = "{}"
296
+ return response
297
+
298
+ result = self.router.route_request(method, path, query_params, body)
299
+
300
+ if result["success"]:
301
+ response["statusCode"] = 200
302
+ response["statusReason"] = "OK"
303
+ response["data"] = json.dumps(result)
304
+ else:
305
+ error_category = result.get("errorCategory", ErrorCategory.VALIDATION)
306
+ response["statusCode"] = 200
307
+ response["statusReason"] = self._get_status_reason_for_error(
308
+ error_category
309
+ )
310
+ response["data"] = json.dumps(
311
+ {
312
+ "success": False,
313
+ "data" : None,
314
+ "error": result["error"],
315
+ "errorCategory": (
316
+ str(error_category)
317
+ if hasattr(error_category, "__str__")
318
+ else None
319
+ ),
320
+ }
321
+ )
322
+
323
+ except Exception as e:
324
+ log_message(f"Error handling request: {e}", LogLevel.ERROR)
325
+ log_message(traceback.format_exc(), LogLevel.DEBUG)
326
+
327
+ response["statusCode"] = 500
328
+ response["statusReason"] = "Internal Server Error"
329
+ response["data"] = json.dumps(
330
+ {
331
+ "success": False,
332
+ "error": f"Internal server error: {str(e)}",
333
+ "errorCategory": str(ErrorCategory.INTERNAL),
334
+ }
335
+ )
336
+
337
+ log_message(
338
+ f"Response status: {response['statusCode']}, {response['data']}",
339
+ LogLevel.DEBUG,
340
+ )
341
+ return response
342
+
343
+ def _get_status_code_for_error(self, error_category) -> int:
344
+ """
345
+ Map error category to HTTP status code
346
+
347
+ Args:
348
+ error_category: The error category
349
+
350
+ Returns:
351
+ Appropriate HTTP status code
352
+ """
353
+ if error_category == ErrorCategory.NOT_FOUND:
354
+ return 404
355
+ elif error_category == ErrorCategory.PERMISSION:
356
+ return 403
357
+ elif error_category == ErrorCategory.VALIDATION:
358
+ return 400
359
+ elif error_category == ErrorCategory.EXTERNAL:
360
+ return 502
361
+ else:
362
+ return 500
363
+
364
+ def _get_status_reason_for_error(self, error_category) -> str:
365
+ """
366
+ Map error category to HTTP status reason
367
+
368
+ Args:
369
+ error_category: The error category
370
+
371
+ Returns:
372
+ Status reason text
373
+ """
374
+ if error_category == ErrorCategory.NOT_FOUND:
375
+ return "Not Found"
376
+ elif error_category == ErrorCategory.PERMISSION:
377
+ return "Forbidden"
378
+ elif error_category == ErrorCategory.VALIDATION:
379
+ return "Bad Request"
380
+ elif error_category == ErrorCategory.EXTERNAL:
381
+ return "Bad Gateway"
382
+ else:
383
+ return "Internal Server Error"
384
+
385
+ def register_handlers(self) -> None:
386
+ """Register all generated handlers automatically"""
387
+ import mcp.controllers.generated_handlers as handlers
388
+
389
+ for operation_id in handlers.__all__:
390
+ handler = getattr(handlers, operation_id, None)
391
+ if callable(handler):
392
+ self.router.register_handler(operation_id, handler)
393
+ else:
394
+ log_message(f"Handler for {operation_id} not found.", LogLevel.WARNING)
395
+
396
+ def _handle_get_td_info(self, body: Optional[str] = None, **kwargs) -> Result:
397
+ """
398
+ Handle get_td_info operation
399
+
400
+ Returns server information such as version and platform.
401
+ """
402
+ service_result = self._service.get_td_info()
403
+
404
+ response_data = GetTdInfo200Response().from_dict(service_result)
405
+ return response_data.to_dict()
406
+
407
+ def _handle_get_nodes(
408
+ self,
409
+ parentPath: str,
410
+ pattern: Optional[str] = None,
411
+ body: Optional[str] = None,
412
+ **kwargs,
413
+ ) -> Result:
414
+ """
415
+ Handle get_nodes operation
416
+
417
+ Args:
418
+ parentPath: Path of the parent node to get children from
419
+ pattern: Optional pattern to filter nodes by
420
+
421
+ Returns:
422
+ List of nodes under the specified parent path
423
+ """
424
+ service_result = self._service.get_nodes(parentPath, pattern)
425
+ response_data = GetNodes200Response().from_dict(service_result)
426
+ return response_data.to_dict()
427
+
428
+ def _handle_create_node(self, body: str, **kwargs) -> Result:
429
+ """
430
+ Handle create_node operation
431
+
432
+ Args:
433
+ body: Request body containing node creation parameters
434
+
435
+ Returns:
436
+ Information about the created node
437
+ """
438
+ if not body:
439
+ return error_result("Request body is required")
440
+
441
+ try:
442
+ request_data = json.loads(body)
443
+ except json.JSONDecodeError as e:
444
+ return error_result(f"Invalid JSON in request body: {str(e)}")
445
+
446
+ parent_path = request_data.get("parentPath")
447
+ node_type = request_data.get("nodeType")
448
+ node_name = request_data.get("nodeName")
449
+ parameters = request_data.get("parameters", {})
450
+
451
+ if not parent_path:
452
+ return error_result("parentPath is required")
453
+
454
+ if not node_type:
455
+ return error_result("nodeType is required")
456
+
457
+ service_result = self._service.create_node(
458
+ parent_path, node_type, node_name, parameters
459
+ )
460
+
461
+ response_data = CreateNode200Response().from_dict(service_result)
462
+ return response_data.to_dict()
463
+
464
+ def _handle_delete_node(
465
+ self, nodePath: str, body: Optional[str] = None, **kwargs
466
+ ) -> Result:
467
+ """
468
+ Handle delete_node operation
469
+
470
+ Args:
471
+ nodePath: Path of the node to delete
472
+
473
+ Returns:
474
+ Result of the deletion operation
475
+ """
476
+ service_result = self._service.delete_node(nodePath)
477
+
478
+ response_data = DeleteNode200Response().from_dict(service_result)
479
+ return response_data.to_dict()
480
+
481
+ def _handle_get_node_detail(
482
+ self, nodePath: str, body: Optional[str] = None, **kwargs
483
+ ) -> Result:
484
+ """
485
+ Handle get_node_detail operation
486
+
487
+ Args:
488
+ nodePath: Path of the node to get properties for
489
+
490
+ Returns:
491
+ Node properties
492
+ """
493
+ service_result = self._service.get_node_detail(nodePath)
494
+ response_data = GetNodeDetail200Response().from_dict(service_result)
495
+ return response_data.to_dict()
496
+
497
+ def _handle_update_node(self, body: str, **kwargs) -> Result:
498
+ """
499
+ Handle update_node operation
500
+
501
+ Args:
502
+ nodePath: Path of the node to update
503
+ body: Request body containing properties to update
504
+
505
+ Returns:
506
+ Result of the update operation
507
+ """
508
+ if not body:
509
+ return error_result("Request body is required")
510
+
511
+ try:
512
+ request_data = json.loads(body)
513
+ except json.JSONDecodeError as e:
514
+ return error_result(f"Invalid JSON in request body: {str(e)}")
515
+
516
+ nodePath = request_data.get("nodePath", "")
517
+ if not nodePath:
518
+ return error_result("nodePath is required")
519
+
520
+ properties = request_data.get("properties", {})
521
+
522
+ if not properties or not isinstance(properties, dict):
523
+ return error_result("properties object is required")
524
+
525
+ service_result = self._service.update_node(nodePath, properties)
526
+ response_data = UpdateNode200Response().from_dict(service_result)
527
+ return response_data.to_dict()
528
+
529
+ def _handle_exec_node_method(self, body: str, **kwargs) -> Result:
530
+ """
531
+ Handle exec_node_method operation
532
+
533
+ Args:
534
+ body: Request body containing node path, method name, and arguments
535
+
536
+ Returns:
537
+ Result of the method execution
538
+ """
539
+ if not body:
540
+ return error_result("Request body is required")
541
+
542
+ try:
543
+ request_data = json.loads(body)
544
+ except json.JSONDecodeError as e:
545
+ return error_result(f"Invalid JSON in request body: {str(e)}")
546
+
547
+ node_path = request_data.get("nodePath")
548
+ method = request_data.get("method")
549
+ args = request_data.get("args", [])
550
+ kwargs = request_data.get("kwargs", {})
551
+
552
+ if not node_path:
553
+ return error_result("nodePath is required")
554
+
555
+ if not method:
556
+ return error_result("method is required")
557
+
558
+ service_result = self._service.call_node_method(node_path, method, args, kwargs)
559
+
560
+ if not service_result["success"]:
561
+ return service_result
562
+
563
+ response_data = ExecNodeMethod200Response().from_dict(service_result)
564
+ return response_data.to_dict()
565
+
566
+ def _handle_exec_python_script(self, body: str, **kwargs) -> Result:
567
+ """
568
+ Handle exec_python_script operation
569
+
570
+ Args:
571
+ body: Request body containing Python script to execute
572
+
573
+ Returns:
574
+ Result of the script execution
575
+ """
576
+ if not body:
577
+ return error_result("Request body is required")
578
+
579
+ try:
580
+ request_data = json.loads(body)
581
+ except json.JSONDecodeError as e:
582
+ return error_result(f"Invalid JSON in request body: {str(e)}")
583
+
584
+ script = request_data.get("script")
585
+
586
+ if not script:
587
+ return error_result("script is required")
588
+
589
+ service_result = self._service.exec_script(script)
590
+
591
+ if not service_result["success"]:
592
+ return service_result
593
+
594
+ response_data = ExecPythonScript200Response().from_dict(service_result)
595
+ return response_data.to_dict()
596
+
597
+ def _handle_get_td_python_classes(
598
+ self, body: Optional[str] = None, **kwargs
599
+ ) -> Result:
600
+ """
601
+ Handle get_td_python_classes operation
602
+
603
+ Returns:
604
+ List of Python classes available in TouchDesigner
605
+ """
606
+ return self._service.get_python_classes()
607
+
608
+ def _handle_get_td_python_class_details(
609
+ self, className: str, body: Optional[str] = None, **kwargs
610
+ ) -> Result:
611
+ """
612
+ Handle get_td_python_class_details operation
613
+
614
+ Args:
615
+ className: Name of the Python class to get details for
616
+
617
+ Returns:
618
+ Details of the specified Python class
619
+ """
620
+ return self._service.get_python_class_details(className)
621
+
622
+
623
+ api_controller_openapi = APIControllerOpenAPI()