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.
- package/README.ja.md +4 -4
- package/README.md +4 -3
- package/dist/cli.js +0 -0
- package/dist/gen/endpoints/TouchDesignerAPI.js +1 -1
- package/dist/gen/mcp/touchDesignerAPI.zod.js +1 -1
- package/dist/index.js +0 -0
- package/dist/server/touchDesignerServer.js +1 -1
- package/package.json +14 -12
- package/td/genHandlers.js +0 -47
- package/td/import_modules.py +0 -52
- package/td/mcp_webserver_base.tox +0 -0
- package/td/modules/mcp/controllers/__init__.py +0 -9
- package/td/modules/mcp/controllers/api_controller.py +0 -637
- package/td/modules/mcp/controllers/generated_handlers.py +0 -365
- package/td/modules/mcp/controllers/openapi_router.py +0 -265
- package/td/modules/mcp/services/__init__.py +0 -8
- package/td/modules/mcp/services/api_service.py +0 -555
- package/td/modules/mcp_webserver_script.py +0 -134
- package/td/modules/td_server/.dockerignore +0 -72
- package/td/modules/td_server/.openapi-generator/FILES +0 -55
- package/td/modules/td_server/.openapi-generator/VERSION +0 -1
- package/td/modules/td_server/.openapi-generator-ignore +0 -23
- package/td/modules/td_server/.travis.yml +0 -14
- package/td/modules/td_server/Dockerfile +0 -16
- package/td/modules/td_server/README.md +0 -49
- package/td/modules/td_server/git_push.sh +0 -57
- package/td/modules/td_server/openapi_server/__init__.py +0 -0
- package/td/modules/td_server/openapi_server/__main__.py +0 -19
- package/td/modules/td_server/openapi_server/controllers/__init__.py +0 -0
- package/td/modules/td_server/openapi_server/controllers/default_controller.py +0 -162
- package/td/modules/td_server/openapi_server/controllers/security_controller.py +0 -2
- package/td/modules/td_server/openapi_server/encoder.py +0 -19
- package/td/modules/td_server/openapi_server/models/__init__.py +0 -33
- package/td/modules/td_server/openapi_server/models/base_model.py +0 -68
- package/td/modules/td_server/openapi_server/models/create_node200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/create_node200_response_data.py +0 -63
- package/td/modules/td_server/openapi_server/models/create_node_request.py +0 -123
- package/td/modules/td_server/openapi_server/models/delete_node200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/delete_node200_response_data.py +0 -91
- package/td/modules/td_server/openapi_server/models/exec_node_method200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/exec_node_method200_response_data.py +0 -65
- package/td/modules/td_server/openapi_server/models/exec_node_method_request.py +0 -153
- package/td/modules/td_server/openapi_server/models/exec_node_method_request_args_inner.py +0 -34
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data.py +0 -65
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data_result.py +0 -63
- package/td/modules/td_server/openapi_server/models/exec_python_script_request.py +0 -65
- package/td/modules/td_server/openapi_server/models/get_node_detail200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/get_nodes200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/get_nodes200_response_data.py +0 -65
- package/td/modules/td_server/openapi_server/models/get_td_info200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/get_td_info200_response_data.py +0 -155
- package/td/modules/td_server/openapi_server/models/get_td_python_class_details200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response_data.py +0 -63
- package/td/modules/td_server/openapi_server/models/td_node.py +0 -175
- package/td/modules/td_server/openapi_server/models/td_node_family_type.py +0 -44
- package/td/modules/td_server/openapi_server/models/td_python_class_details.py +0 -191
- package/td/modules/td_server/openapi_server/models/td_python_class_info.py +0 -127
- package/td/modules/td_server/openapi_server/models/td_python_method_info.py +0 -121
- package/td/modules/td_server/openapi_server/models/td_python_property_info.py +0 -123
- package/td/modules/td_server/openapi_server/models/update_node200_response.py +0 -125
- package/td/modules/td_server/openapi_server/models/update_node200_response_data.py +0 -149
- package/td/modules/td_server/openapi_server/models/update_node200_response_data_failed_inner.py +0 -91
- package/td/modules/td_server/openapi_server/models/update_node_request.py +0 -93
- package/td/modules/td_server/openapi_server/openapi/openapi.yaml +0 -975
- package/td/modules/td_server/openapi_server/test/__init__.py +0 -16
- package/td/modules/td_server/openapi_server/test/test_default_controller.py +0 -201
- package/td/modules/td_server/openapi_server/typing_utils.py +0 -30
- package/td/modules/td_server/openapi_server/util.py +0 -147
- package/td/modules/td_server/requirements.txt +0 -13
- package/td/modules/td_server/setup.py +0 -37
- package/td/modules/td_server/test-requirements.txt +0 -4
- package/td/modules/td_server/tox.ini +0 -11
- package/td/modules/utils/config.py +0 -7
- package/td/modules/utils/error_handling.py +0 -104
- package/td/modules/utils/logging.py +0 -23
- package/td/modules/utils/result.py +0 -40
- package/td/modules/utils/serialization.py +0 -57
- package/td/modules/utils/types.py +0 -33
- package/td/modules/utils/utils_logging.py +0 -60
- 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")
|