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,203 @@
1
+ /**
2
+ * Generated by orval v7.9.0 🍺
3
+ * Do not edit manually.
4
+ * TouchDesigner API
5
+ * OpenAPI schema for generating TouchDesigner API client code
6
+ * OpenAPI spec version: 0.2.1
7
+ */
8
+ import { z as zod } from 'zod';
9
+ /**
10
+ * @summary Delete an existing node
11
+ */
12
+ export const deleteNodeQueryParams = zod.object({
13
+ "nodePath": zod.string().describe('Path to the node to delete. e.g., \"/project1/geo1\"')
14
+ });
15
+ export const deleteNodeResponse = zod.object({
16
+ "success": zod.boolean().describe('Whether the operation was successful'),
17
+ "data": zod.object({
18
+ "deleted": zod.boolean().optional().describe('Whether the node was successfully deleted'),
19
+ "node": zod.object({
20
+ "id": zod.number(),
21
+ "opType": zod.string(),
22
+ "name": zod.string(),
23
+ "path": zod.string(),
24
+ "properties": zod.record(zod.string(), zod.any())
25
+ }).optional().describe('Information about a TouchDesigner node')
26
+ }).nullable(),
27
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
28
+ });
29
+ /**
30
+ * @summary Get nodes in the path
31
+ */
32
+ export const getNodesQueryPatternDefault = "*";
33
+ export const getNodesQueryParams = zod.object({
34
+ "parentPath": zod.string().describe('Parent path e.g., \"/project1\"'),
35
+ "pattern": zod.string().default(getNodesQueryPatternDefault).describe('Pattern to match against node names e.g., \"null*\"')
36
+ });
37
+ export const getNodesResponse = zod.object({
38
+ "success": zod.boolean().describe('Whether the operation was successful'),
39
+ "data": zod.object({
40
+ "nodes": zod.array(zod.object({
41
+ "id": zod.number(),
42
+ "opType": zod.string(),
43
+ "name": zod.string(),
44
+ "path": zod.string(),
45
+ "properties": zod.record(zod.string(), zod.any())
46
+ }).describe('Information about a TouchDesigner node')).optional().describe('Result of the execution')
47
+ }).nullable(),
48
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
49
+ });
50
+ /**
51
+ * @summary Create a new node
52
+ */
53
+ export const createNodeBody = zod.object({
54
+ "parentPath": zod.string().describe('Path to the parent node (e.g., /project1)'),
55
+ "nodeType": zod.string().describe('Type of the node to create (e.g., textTop)'),
56
+ "nodeName": zod.string().optional().describe('Name of the new node (optional)')
57
+ });
58
+ export const createNodeResponse = zod.object({
59
+ "success": zod.boolean().describe('Whether the operation was successful'),
60
+ "data": zod.object({
61
+ "result": zod.object({
62
+ "id": zod.number(),
63
+ "opType": zod.string(),
64
+ "name": zod.string(),
65
+ "path": zod.string(),
66
+ "properties": zod.record(zod.string(), zod.any())
67
+ }).optional().describe('Information about a TouchDesigner node')
68
+ }).nullable(),
69
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
70
+ });
71
+ /**
72
+ * Retrieves detailed information about a specific node including its properties, parameters and connections
73
+ * @summary Get node detail
74
+ */
75
+ export const getNodeDetailQueryParams = zod.object({
76
+ "nodePath": zod.string().describe('Node path. e.g., \"/project1/textTOP\"')
77
+ });
78
+ export const getNodeDetailResponse = zod.object({
79
+ "success": zod.boolean().describe('Whether the operation was successful'),
80
+ "data": zod.object({
81
+ "id": zod.number(),
82
+ "opType": zod.string(),
83
+ "name": zod.string(),
84
+ "path": zod.string(),
85
+ "properties": zod.record(zod.string(), zod.any())
86
+ }).describe('Information about a TouchDesigner node'),
87
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
88
+ });
89
+ /**
90
+ * @summary Update node properties
91
+ */
92
+ export const updateNodeBody = zod.object({
93
+ "nodePath": zod.string().describe('Path to the node (e.g., /project1/null1)'),
94
+ "properties": zod.record(zod.string(), zod.any())
95
+ });
96
+ export const updateNodeResponse = zod.object({
97
+ "success": zod.boolean().describe('Whether the update operation was successful'),
98
+ "data": zod.object({
99
+ "path": zod.string().optional().describe('Path of the node that was updated'),
100
+ "updated": zod.array(zod.string()).optional().describe('List of property names that were successfully updated'),
101
+ "failed": zod.array(zod.object({
102
+ "name": zod.string().optional().describe('Name of the property that failed to update'),
103
+ "reason": zod.string().optional().describe('Reason for the failure')
104
+ })).optional().describe('List of properties that failed to update'),
105
+ "message": zod.string().optional().describe('Summary message about the update operation')
106
+ }).nullable(),
107
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
108
+ });
109
+ /**
110
+ * Returns a list of Python classes, modules, and functions available in TouchDesigner
111
+ * @summary Get a list of Python classes and modules
112
+ */
113
+ export const getTdPythonClassesResponse = zod.object({
114
+ "success": zod.boolean().describe('Whether the operation was successful'),
115
+ "data": zod.object({
116
+ "classes": zod.array(zod.object({
117
+ "name": zod.string().describe('Name of the class or module'),
118
+ "type": zod.enum(['class', 'module', 'function', 'object']).describe('Type of the Python entity'),
119
+ "description": zod.string().optional().describe('Description of the class or module')
120
+ }).describe('Information about a Python class or module available in TouchDesigner')).optional()
121
+ }).nullable(),
122
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
123
+ });
124
+ /**
125
+ * Returns detailed information about a specific Python class, module, or function including methods, properties, and documentation
126
+ * @summary Get details of a specific Python class or module
127
+ */
128
+ export const getTdPythonClassDetailsParams = zod.object({
129
+ "className": zod.string().describe('Name of the class or module. e.g., \"textTOP\"')
130
+ });
131
+ export const getTdPythonClassDetailsResponse = zod.object({
132
+ "success": zod.boolean().describe('Whether the operation was successful'),
133
+ "data": zod.object({
134
+ "name": zod.string().describe('Name of the class or module'),
135
+ "type": zod.enum(['class', 'module', 'function', 'object']).describe('Type of the Python entity'),
136
+ "description": zod.string().optional().describe('Description of the class or module'),
137
+ "methods": zod.array(zod.object({
138
+ "name": zod.string().describe('Method name'),
139
+ "signature": zod.string().optional().describe('Method signature including parameters'),
140
+ "description": zod.string().optional().describe('Description of the method')
141
+ }).describe('Information about a Python method')).describe('List of methods available in the class or module'),
142
+ "properties": zod.array(zod.object({
143
+ "name": zod.string().describe('Property name'),
144
+ "type": zod.string().describe('Type of the property'),
145
+ "value": zod.object({}).nullish().describe('Current value of the property (if serializable)')
146
+ }).describe('Information about a Python property')).describe('List of properties available in the class or module')
147
+ }).describe('Detailed information about a Python class or module'),
148
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
149
+ });
150
+ /**
151
+ * Call a method on the node at the specified path (e.g., /project1).
152
+ This allows operations equivalent to TouchDesigner's Python API such as
153
+ `parent_comp = op('/project1')` and `parent_comp.create('textTOP', 'myText')`.
154
+
155
+ * @summary Call a method of the specified node
156
+ */
157
+ export const execNodeMethodBody = zod.object({
158
+ "nodePath": zod.string().describe('Path to the node (e.g., /project1/null1)'),
159
+ "method": zod.string().describe('Name of the method to call'),
160
+ "args": zod.array(zod.string().or(zod.number()).or(zod.boolean())).optional().describe('List of arguments for the method call'),
161
+ "kwargs": zod.record(zod.string(), zod.any()).optional().describe('Keyword arguments for the method call')
162
+ });
163
+ export const execNodeMethodResponse = zod.object({
164
+ "success": zod.boolean().describe('Whether the operation was successful'),
165
+ "data": zod.object({
166
+ "result": zod.object({}).describe('Result of the method call. Can be any type (equivalent to unknown in TypeScript).')
167
+ }).nullable(),
168
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
169
+ });
170
+ /**
171
+ * Execute a Python script directly in TouchDesigner.
172
+ Multiline scripts and scripts containing comments are supported.
173
+ The script can optionally set a `result` variable to explicitly return a value.
174
+ This endpoint allows you to interact with TouchDesigner nodes programmatically.
175
+
176
+ * @summary Execute python code on the server
177
+ */
178
+ export const execPythonScriptBody = zod.object({
179
+ "script": zod.string().describe('e.g., \"op(\'/project1/text_over_image\').outputConnectors[0].connect(op(\'/project1/out1\'))\"')
180
+ });
181
+ export const execPythonScriptResponse = zod.object({
182
+ "success": zod.boolean().describe('Whether the operation was successful'),
183
+ "data": zod.object({
184
+ "result": zod.object({
185
+ "value": zod.object({}).optional().describe('Return value of the executed script, can be any serializable value')
186
+ }).describe('Result of the executed script')
187
+ }).nullable(),
188
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
189
+ });
190
+ /**
191
+ * Returns information about the TouchDesigner
192
+ * @summary Get TouchDesigner information
193
+ */
194
+ export const getTdInfoResponse = zod.object({
195
+ "success": zod.boolean().describe('Whether the operation was successful'),
196
+ "data": zod.object({
197
+ "server": zod.string().describe('Server name (typically \"TouchDesigner\")'),
198
+ "version": zod.string().describe('TouchDesigner version number'),
199
+ "osName": zod.string().describe('Operating system name'),
200
+ "osVersion": zod.string().describe('Operating system version')
201
+ }).nullable(),
202
+ "error": zod.string().nullable().describe('Error message if the operation was not successful')
203
+ });
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { startServer } from "./cli.js";
2
+ export { TouchDesignerServer } from "./server/touchDesignerServer.js";
3
+ export { createTouchDesignerClient, TouchDesignerClient, } from "./tdClient/index.js";
@@ -0,0 +1,5 @@
1
+ async function initMocks() {
2
+ const { node } = await import("./node.js");
3
+ node.listen();
4
+ }
5
+ export { initMocks };
@@ -0,0 +1,3 @@
1
+ import { setupServer } from "msw/node";
2
+ import { getTouchDesignerAPIMock } from "../gen/endpoints/TouchDesignerAPI.js";
3
+ export const node = setupServer(...getTouchDesignerAPIMock());
@@ -0,0 +1,83 @@
1
+ import { createErrorResult, createSuccessResult } from "../core/result.js";
2
+ /**
3
+ * Manages the connection between TouchDesignerServer and MCP transport
4
+ */
5
+ export class ConnectionManager {
6
+ server;
7
+ logger;
8
+ tdClient;
9
+ transport = null;
10
+ constructor(server, logger, tdClient) {
11
+ this.server = server;
12
+ this.logger = logger;
13
+ this.tdClient = tdClient;
14
+ }
15
+ /**
16
+ * Connect to MCP transport
17
+ */
18
+ async connect(transport) {
19
+ if (this.isConnected()) {
20
+ this.logger.log("MCP server already connected");
21
+ return createSuccessResult(undefined);
22
+ }
23
+ this.transport = transport;
24
+ try {
25
+ await this.server.connect(transport);
26
+ this.logger.log("Server connected and ready to process requests");
27
+ const connectionResult = await this.checkTDConnection();
28
+ if (!connectionResult.success) {
29
+ throw new Error(`Failed to connect to TouchDesigner: ${connectionResult.error.message}`);
30
+ }
31
+ return createSuccessResult(undefined);
32
+ }
33
+ catch (error) {
34
+ this.transport = null;
35
+ const err = error instanceof Error ? error : new Error(String(error));
36
+ console.error("Fatal error starting server! Check TouchDesigner setup and starting webserver.", err);
37
+ return createErrorResult(err);
38
+ }
39
+ }
40
+ /**
41
+ * Disconnect from MCP transport
42
+ */
43
+ async disconnect() {
44
+ if (!this.isConnected()) {
45
+ console.log("MCP server not connected");
46
+ return createSuccessResult(undefined);
47
+ }
48
+ try {
49
+ await this.server.close();
50
+ console.log("MCP server disconnected from MCP");
51
+ this.transport = null;
52
+ return createSuccessResult(undefined);
53
+ }
54
+ catch (error) {
55
+ const err = error instanceof Error ? error : new Error(String(error));
56
+ console.error("Error disconnecting from server", err);
57
+ return createErrorResult(err);
58
+ }
59
+ }
60
+ /**
61
+ * Check if connected to MCP transport
62
+ */
63
+ isConnected() {
64
+ return this.transport !== null;
65
+ }
66
+ /**
67
+ * Check connection to TouchDesigner
68
+ */
69
+ async checkTDConnection() {
70
+ this.logger.log("Testing connection to TouchDesigner server...");
71
+ try {
72
+ const result = await this.tdClient.getTdInfo();
73
+ if (!result.success) {
74
+ throw result.error;
75
+ }
76
+ return createSuccessResult(result.data);
77
+ }
78
+ catch (error) {
79
+ const err = error instanceof Error ? error : new Error(String(error));
80
+ return createErrorResult(err);
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,60 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { McpLogger } from "../core/logger.js";
3
+ import { registerPrompts } from "../features/prompts/index.js";
4
+ import { registerTools } from "../features/tools/index.js";
5
+ import { createTouchDesignerClient } from "../tdClient/index.js";
6
+ import { ConnectionManager } from "./connectionManager.js";
7
+ /**
8
+ * TouchDesigner MCP Server implementation
9
+ */
10
+ export class TouchDesignerServer {
11
+ server;
12
+ logger;
13
+ tdClient;
14
+ connectionManager;
15
+ /**
16
+ * Initialize TouchDesignerServer with proper dependency injection
17
+ */
18
+ constructor() {
19
+ this.server = new McpServer({
20
+ name: "TouchDesigner",
21
+ version: "0.2.1",
22
+ }, {
23
+ capabilities: {
24
+ prompts: {},
25
+ logging: {},
26
+ tools: {},
27
+ },
28
+ });
29
+ this.logger = new McpLogger(this.server);
30
+ this.tdClient = createTouchDesignerClient({ logger: this.logger });
31
+ this.connectionManager = new ConnectionManager(this.server, this.logger, this.tdClient);
32
+ this.registerAllFeatures();
33
+ }
34
+ /**
35
+ * Connect to MCP transport
36
+ */
37
+ async connect(transport) {
38
+ return this.connectionManager.connect(transport);
39
+ }
40
+ /**
41
+ * Disconnect from MCP transport
42
+ */
43
+ async disconnect() {
44
+ return this.connectionManager.disconnect();
45
+ }
46
+ /**
47
+ * Check if connected to MCP transport
48
+ */
49
+ isConnectedToMCP() {
50
+ return this.connectionManager.isConnected();
51
+ }
52
+ /**
53
+ * Register all features with the server
54
+ * Only called after all dependencies are initialized
55
+ */
56
+ registerAllFeatures() {
57
+ registerPrompts(this.server, this.logger);
58
+ registerTools(this.server, this.logger, this.tdClient);
59
+ }
60
+ }
@@ -0,0 +1,6 @@
1
+ import { TouchDesignerClient } from "./touchDesignerClient.js";
2
+ export function createTouchDesignerClient(params) {
3
+ const { logger } = params;
4
+ return new TouchDesignerClient({ logger });
5
+ }
6
+ export { TouchDesignerClient } from "./touchDesignerClient.js";
@@ -0,0 +1,150 @@
1
+ import { createNode as apiCreateNode, deleteNode as apiDeleteNode, execNodeMethod as apiExecNodeMethod, execPythonScript as apiExecPythonScript, getNodeDetail as apiGetNodeDetail, getNodes as apiGetNodes, getTdInfo as apiGetTdInfo, getTdPythonClassDetails as apiGetTdPythonClassDetails, getTdPythonClasses as apiGetTdPythonClasses, updateNode as apiUpdateNode, } from "../gen/endpoints/TouchDesignerAPI.js";
2
+ /**
3
+ * Default implementation of ITouchDesignerApi using generated API clients
4
+ */
5
+ const defaultApiClient = {
6
+ execNodeMethod: apiExecNodeMethod,
7
+ execPythonScript: apiExecPythonScript,
8
+ getTdInfo: apiGetTdInfo,
9
+ getNodes: apiGetNodes,
10
+ getNodeDetail: apiGetNodeDetail,
11
+ createNode: apiCreateNode,
12
+ updateNode: apiUpdateNode,
13
+ deleteNode: apiDeleteNode,
14
+ getTdPythonClasses: apiGetTdPythonClasses,
15
+ getTdPythonClassDetails: apiGetTdPythonClassDetails,
16
+ };
17
+ /**
18
+ * Null logger implementation that discards all logs
19
+ */
20
+ const nullLogger = {
21
+ debug: () => { },
22
+ log: () => { },
23
+ warn: () => { },
24
+ error: () => { },
25
+ };
26
+ /**
27
+ * Handle API error response
28
+ * @param response - API response object
29
+ * @returns ErrorResult object indicating failure
30
+ */
31
+ function handleError(response) {
32
+ if (response.error) {
33
+ const errorMessage = response.error;
34
+ return { success: false, error: new Error(errorMessage) };
35
+ }
36
+ return { success: false, error: new Error("Unknown error occurred") };
37
+ }
38
+ /**
39
+ * Handle API response and return a structured result
40
+ * @param response - API response object
41
+ * @returns Result object indicating success or failure
42
+ */
43
+ function handleApiResponse(response) {
44
+ const { success, data } = response;
45
+ if (!success) {
46
+ return handleError(response);
47
+ }
48
+ if (data === null) {
49
+ return { success: false, error: new Error("No data received") };
50
+ }
51
+ if (data === undefined) {
52
+ return { success: false, error: new Error("No data received") };
53
+ }
54
+ return { success: true, data };
55
+ }
56
+ /**
57
+ * TouchDesigner client implementation with dependency injection
58
+ * for better testability and separation of concerns
59
+ */
60
+ export class TouchDesignerClient {
61
+ logger;
62
+ api;
63
+ /**
64
+ * Initialize TouchDesigner client with optional dependencies
65
+ */
66
+ constructor(params = {}) {
67
+ this.logger = params.logger || nullLogger;
68
+ this.api = params.httpClient || defaultApiClient;
69
+ }
70
+ /**
71
+ * Execute a node method
72
+ */
73
+ async execNodeMethod(params) {
74
+ this.logger.debug(`Executing node method: ${params.method} on ${params.nodePath}`);
75
+ const result = await this.api.execNodeMethod(params);
76
+ return handleApiResponse(result);
77
+ }
78
+ /**
79
+ * Execute a script in TouchDesigner
80
+ */
81
+ async execPythonScript(params) {
82
+ this.logger.debug(`Executing Python script: ${params}`);
83
+ const result = await this.api.execPythonScript(params);
84
+ return handleApiResponse(result);
85
+ }
86
+ /**
87
+ * Get TouchDesigner server information
88
+ */
89
+ async getTdInfo() {
90
+ this.logger.debug("Getting server info");
91
+ const result = await this.api.getTdInfo();
92
+ return handleApiResponse(result);
93
+ }
94
+ /**
95
+ * Get list of nodes
96
+ */
97
+ async getNodes(params) {
98
+ this.logger.debug(`Getting nodes for parent: ${params.parentPath}`);
99
+ const result = await this.api.getNodes(params);
100
+ return handleApiResponse(result);
101
+ }
102
+ /**
103
+ * Get node properties
104
+ */
105
+ async getNodeDetail(params) {
106
+ this.logger.debug(`Getting properties for node: ${params.nodePath}`);
107
+ const result = await this.api.getNodeDetail(params);
108
+ return handleApiResponse(result);
109
+ }
110
+ /**
111
+ * Create a new node
112
+ */
113
+ async createNode(params) {
114
+ this.logger.debug(`Creating node: ${params.nodeName} of type ${params.nodeType} under ${params.parentPath}`);
115
+ const result = await this.api.createNode(params);
116
+ return handleApiResponse(result);
117
+ }
118
+ /**
119
+ * Update node properties
120
+ */
121
+ async updateNode(params) {
122
+ this.logger.debug(`Updating node: ${params.nodePath}`);
123
+ const result = await this.api.updateNode(params);
124
+ return handleApiResponse(result);
125
+ }
126
+ /**
127
+ * Delete a node
128
+ */
129
+ async deleteNode(params) {
130
+ this.logger.debug(`Deleting node: ${params.nodePath}`);
131
+ const result = await this.api.deleteNode(params);
132
+ return handleApiResponse(result);
133
+ }
134
+ /**
135
+ * Get list of available Python classes/modules in TouchDesigner
136
+ */
137
+ async getClasses() {
138
+ this.logger.debug("Getting Python classes");
139
+ const result = await this.api.getTdPythonClasses();
140
+ return handleApiResponse(result);
141
+ }
142
+ /**
143
+ * Get details of a specific class/module
144
+ */
145
+ async getClassDetails(className) {
146
+ this.logger.debug(`Getting class details for: ${className}`);
147
+ const result = await this.api.getTdPythonClassDetails(className);
148
+ return handleApiResponse(result);
149
+ }
150
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "touchdesigner-mcp-server",
3
+ "version": "0.2.1",
4
+ "description": "MCP server for TouchDesigner",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/8beeeaaat/touchdesigner-mcp.git"
8
+ },
9
+ "keywords": [
10
+ "MCP",
11
+ "TouchDesigner"
12
+ ],
13
+ "author": "8beeeaaat",
14
+ "license": "MIT",
15
+ "bugs": {
16
+ "url": "https://github.com/8beeeaaat/touchdesigner-mcp/issues"
17
+ },
18
+ "homepage": "https://github.com/8beeeaaat/touchdesigner-mcp#readme",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.10.2",
21
+ "@mozilla/readability": "^0.6.0",
22
+ "@types/axios": "^0.14.4",
23
+ "@types/ws": "^8.18.1",
24
+ "@types/yargs": "^17.0.33",
25
+ "axios": "^1.9.0",
26
+ "dotenv": "^16.5.0",
27
+ "zod": "^3.24.3"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "1.9.4",
31
+ "@faker-js/faker": "^9.7.0",
32
+ "@types/jsdom": "^21.1.7",
33
+ "@types/node": "^22.15.2",
34
+ "@vitest/coverage-v8": "^3.1.2",
35
+ "msw": "^2.7.5",
36
+ "mustache": "^4.2.0",
37
+ "npm-run-all": "^4.1.5",
38
+ "orval": "^7.9.0",
39
+ "typescript": "^5.8.3",
40
+ "vitest": "^3.1.2",
41
+ "yaml": "^2.7.1"
42
+ },
43
+ "type": "module",
44
+ "main": "src/index.ts",
45
+ "bin": {
46
+ "mcp-server": "dist/index.js"
47
+ },
48
+ "scripts": {
49
+ "build": "run-s build:*",
50
+ "build:gen": "npm run gen",
51
+ "build:dist": "tsc",
52
+ "lint": "run-p lint:*",
53
+ "lint:biome": "biome check",
54
+ "lint:tsc": "tsc --noEmit",
55
+ "format": "biome check --fix",
56
+ "dev": "npx @modelcontextprotocol/inspector node dist/index.js --stdio",
57
+ "test": "run-p test:*",
58
+ "test:integration": "vitest run ./tests/integration",
59
+ "test:unit": "vitest run ./tests/unit",
60
+ "coverage": "vitest run --coverage",
61
+ "gen": "run-s gen:*",
62
+ "gen:webserver": "docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/src/api/index.yml -g python-flask -o /local/td/modules/td_server",
63
+ "gen:handlers": "node td/genHandlers.js",
64
+ "gen:mcp": "orval --config ./orval.config.ts"
65
+ },
66
+ "files": [
67
+ "dist/**/*",
68
+ "td/**/*",
69
+ "README*"
70
+ ],
71
+ "msw": {
72
+ "workerDirectory": [
73
+ "public"
74
+ ]
75
+ }
76
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { startServer } from "./cli.js";
2
+ export { TouchDesignerServer } from "./server/touchDesignerServer.js";
3
+ export {
4
+ createTouchDesignerClient,
5
+ TouchDesignerClient,
6
+ } from "./tdClient/index.js";
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import mustache from "mustache";
4
+ import yaml from "yaml";
5
+
6
+ const openapiPath = path.resolve(
7
+ "td/modules/td_server/openapi_server/openapi/openapi.yaml",
8
+ );
9
+ const templatePath = path.resolve(
10
+ "td/templates/mcp/api_controller_handlers.mustache",
11
+ );
12
+ const outputPath = path.resolve(
13
+ "td/modules/mcp/controllers/generated_handlers.py",
14
+ );
15
+
16
+ async function generateHandlers() {
17
+ try {
18
+ const yamlContent = await fs.readFile(openapiPath, "utf-8");
19
+ const openapiDoc = yaml.parse(yamlContent);
20
+ const operations = [];
21
+
22
+ if (openapiDoc.paths) {
23
+ for (const pathKey of Object.keys(openapiDoc.paths)) {
24
+ const methods = openapiDoc.paths[pathKey];
25
+ for (const methodKey of Object.keys(methods)) {
26
+ const operation = methods[methodKey];
27
+ if (operation.operationId) {
28
+ operations.push({ operationId: operation.operationId });
29
+ }
30
+ }
31
+ }
32
+ }
33
+ const template = await fs.readFile(templatePath, "utf-8");
34
+ const rendered = mustache.render(template, {
35
+ operations,
36
+ });
37
+
38
+ await fs.outputFile(outputPath, rendered);
39
+
40
+ console.log("✅ generated_handlers.py created successfully!");
41
+ } catch (error) {
42
+ console.error("❌ Error generating handlers:", error);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ generateHandlers();