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.
- package/LICENSE +21 -0
- package/README.ja.md +211 -0
- package/README.md +204 -0
- package/dist/api/customInstance.js +18 -0
- package/dist/cli.js +36 -0
- package/dist/core/constants.js +26 -0
- package/dist/core/errorHandling.js +19 -0
- package/dist/core/logger.js +45 -0
- package/dist/core/result.js +12 -0
- package/dist/features/prompts/handlers/td_prompts.js +131 -0
- package/dist/features/prompts/index.js +1 -0
- package/dist/features/prompts/register.js +7 -0
- package/dist/features/tools/handlers/tdTools.js +214 -0
- package/dist/features/tools/index.js +1 -0
- package/dist/features/tools/register.js +7 -0
- package/dist/features/tools/types.js +1 -0
- package/dist/features/tools/utils/toolUtils.js +1 -0
- package/dist/gen/endpoints/TouchDesignerAPI.js +250 -0
- package/dist/gen/mcp/touchDesignerAPI.zod.js +203 -0
- package/dist/index.js +3 -0
- package/dist/mock/index.js +5 -0
- package/dist/mock/node.js +3 -0
- package/dist/server/connectionManager.js +83 -0
- package/dist/server/touchDesignerServer.js +60 -0
- package/dist/tdClient/index.js +6 -0
- package/dist/tdClient/touchDesignerClient.js +150 -0
- package/package.json +76 -0
- package/src/index.ts +6 -0
- package/td/genHandlers.js +47 -0
- package/td/import_modules.py +52 -0
- package/td/mcp_webserver_base.tox +0 -0
- package/td/modules/mcp/controllers/__init__.py +9 -0
- package/td/modules/mcp/controllers/api_controller.py +623 -0
- package/td/modules/mcp/controllers/generated_handlers.py +365 -0
- package/td/modules/mcp/controllers/openapi_router.py +265 -0
- package/td/modules/mcp/services/__init__.py +8 -0
- package/td/modules/mcp/services/api_service.py +535 -0
- package/td/modules/mcp_webserver_script.py +134 -0
- package/td/modules/td_server/.dockerignore +72 -0
- package/td/modules/td_server/.openapi-generator/FILES +55 -0
- package/td/modules/td_server/.openapi-generator/VERSION +1 -0
- package/td/modules/td_server/.openapi-generator-ignore +23 -0
- package/td/modules/td_server/.travis.yml +14 -0
- package/td/modules/td_server/Dockerfile +16 -0
- package/td/modules/td_server/README.md +49 -0
- package/td/modules/td_server/git_push.sh +57 -0
- package/td/modules/td_server/openapi_server/__init__.py +0 -0
- package/td/modules/td_server/openapi_server/__main__.py +19 -0
- package/td/modules/td_server/openapi_server/controllers/__init__.py +0 -0
- package/td/modules/td_server/openapi_server/controllers/default_controller.py +160 -0
- package/td/modules/td_server/openapi_server/controllers/security_controller.py +2 -0
- package/td/modules/td_server/openapi_server/encoder.py +19 -0
- package/td/modules/td_server/openapi_server/models/__init__.py +33 -0
- package/td/modules/td_server/openapi_server/models/base_model.py +68 -0
- package/td/modules/td_server/openapi_server/models/create_node200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/create_node200_response_data.py +63 -0
- package/td/modules/td_server/openapi_server/models/create_node_request.py +123 -0
- package/td/modules/td_server/openapi_server/models/delete_node200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/delete_node200_response_data.py +91 -0
- package/td/modules/td_server/openapi_server/models/exec_node_method200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/exec_node_method200_response_data.py +65 -0
- package/td/modules/td_server/openapi_server/models/exec_node_method_request.py +153 -0
- package/td/modules/td_server/openapi_server/models/exec_node_method_request_args_inner.py +34 -0
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data.py +65 -0
- package/td/modules/td_server/openapi_server/models/exec_python_script200_response_data_result.py +63 -0
- package/td/modules/td_server/openapi_server/models/exec_python_script_request.py +65 -0
- package/td/modules/td_server/openapi_server/models/get_node_detail200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/get_nodes200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/get_nodes200_response_data.py +65 -0
- package/td/modules/td_server/openapi_server/models/get_td_info200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/get_td_info200_response_data.py +155 -0
- package/td/modules/td_server/openapi_server/models/get_td_python_class_details200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/get_td_python_classes200_response_data.py +63 -0
- package/td/modules/td_server/openapi_server/models/td_node.py +175 -0
- package/td/modules/td_server/openapi_server/models/td_node_family_type.py +44 -0
- package/td/modules/td_server/openapi_server/models/td_python_class_details.py +191 -0
- package/td/modules/td_server/openapi_server/models/td_python_class_info.py +127 -0
- package/td/modules/td_server/openapi_server/models/td_python_method_info.py +121 -0
- package/td/modules/td_server/openapi_server/models/td_python_property_info.py +123 -0
- package/td/modules/td_server/openapi_server/models/update_node200_response.py +125 -0
- package/td/modules/td_server/openapi_server/models/update_node200_response_data.py +149 -0
- package/td/modules/td_server/openapi_server/models/update_node200_response_data_failed_inner.py +91 -0
- package/td/modules/td_server/openapi_server/models/update_node_request.py +93 -0
- package/td/modules/td_server/openapi_server/openapi/openapi.yaml +966 -0
- package/td/modules/td_server/openapi_server/test/__init__.py +16 -0
- package/td/modules/td_server/openapi_server/test/test_default_controller.py +200 -0
- package/td/modules/td_server/openapi_server/typing_utils.py +30 -0
- package/td/modules/td_server/openapi_server/util.py +147 -0
- package/td/modules/td_server/requirements.txt +13 -0
- package/td/modules/td_server/setup.py +37 -0
- package/td/modules/td_server/test-requirements.txt +4 -0
- package/td/modules/td_server/tox.ini +11 -0
- package/td/modules/utils/config.py +7 -0
- package/td/modules/utils/error_handling.py +104 -0
- package/td/modules/utils/logging.py +23 -0
- package/td/modules/utils/result.py +40 -0
- package/td/modules/utils/serialization.py +57 -0
- package/td/modules/utils/types.py +33 -0
- package/td/modules/utils/utils_logging.py +60 -0
- 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,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,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,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();
|