threlte-mcp 1.4.0
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.md +306 -0
- package/dist/bridge-server.d.ts +101 -0
- package/dist/bridge-server.d.ts.map +1 -0
- package/dist/bridge-server.js +155 -0
- package/dist/bridge-server.js.map +1 -0
- package/dist/camera-presets.d.ts +62 -0
- package/dist/camera-presets.d.ts.map +1 -0
- package/dist/camera-presets.js +114 -0
- package/dist/camera-presets.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +134 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/MCPBridge.d.ts +93 -0
- package/dist/client/MCPBridge.d.ts.map +1 -0
- package/dist/client/MCPBridge.js +580 -0
- package/dist/client/MCPBridge.js.map +1 -0
- package/dist/client/MCPBridge.svelte +59 -0
- package/dist/client/index.d.ts +18 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/gltf-io.d.ts +11 -0
- package/dist/gltf-io.d.ts.map +1 -0
- package/dist/gltf-io.js +64 -0
- package/dist/gltf-io.js.map +1 -0
- package/dist/gltf-tools.d.ts +120 -0
- package/dist/gltf-tools.d.ts.map +1 -0
- package/dist/gltf-tools.js +360 -0
- package/dist/gltf-tools.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +808 -0
- package/dist/index.js.map +1 -0
- package/dist/svelte-generator.d.ts +17 -0
- package/dist/svelte-generator.d.ts.map +1 -0
- package/dist/svelte-generator.js +154 -0
- package/dist/svelte-generator.js.map +1 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Threlte MCP Server
|
|
4
|
+
*
|
|
5
|
+
* An MCP server that exposes Three.js/Threlte scenes to AI agents
|
|
6
|
+
* for inspection, debugging, and manipulation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx threlte-mcp
|
|
10
|
+
*
|
|
11
|
+
* Prerequisites:
|
|
12
|
+
* 1. Game must be running with MCPBridge component
|
|
13
|
+
* 2. MCPBridge connects to ws://localhost:8082
|
|
14
|
+
*/
|
|
15
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
import { BridgeServer } from './bridge-server.js';
|
|
19
|
+
import { analyzeGltf, optimizeGltf, validateGltf } from './gltf-tools.js';
|
|
20
|
+
import { cameraPresets } from './camera-presets.js';
|
|
21
|
+
import { exportToSvelte } from './svelte-generator.js';
|
|
22
|
+
// Tool definitions
|
|
23
|
+
const TOOLS = [
|
|
24
|
+
// Scene Inspection
|
|
25
|
+
{
|
|
26
|
+
name: 'get_scene_state',
|
|
27
|
+
description: 'Get the full scene hierarchy with all named objects, positions, and transforms',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
maxDepth: { type: 'number', description: 'Maximum depth to traverse (default: 3)' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'find_objects',
|
|
37
|
+
description: 'Search for objects by name, type, or userData properties',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
name: { type: 'string', description: 'Exact object name to find' },
|
|
42
|
+
nameContains: { type: 'string', description: 'Partial name match' },
|
|
43
|
+
type: { type: 'string', description: 'Object type (Mesh, Group, etc.)' },
|
|
44
|
+
hasUserData: { type: 'string', description: 'UserData property key that must exist' }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'get_object_position',
|
|
50
|
+
description: 'Get the position of a specific object by name',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
name: { type: 'string', description: 'Object name' }
|
|
55
|
+
},
|
|
56
|
+
required: ['name']
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'log_positions',
|
|
61
|
+
description: 'Log all object positions in copy-paste format for code',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
filter: { type: 'string', description: 'Optional name filter' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
// Camera Control
|
|
70
|
+
{
|
|
71
|
+
name: 'set_camera_position',
|
|
72
|
+
description: 'Set camera position with optional lookAt target and lens settings',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
name: { type: 'string', description: 'Optional camera object name' },
|
|
77
|
+
position: { type: 'array', items: { type: 'number' }, description: '[x, y, z] position' },
|
|
78
|
+
lookAt: { type: 'array', items: { type: 'number' }, description: '[x, y, z] look target' },
|
|
79
|
+
fov: { type: 'number', description: 'Field of view in degrees (PerspectiveCamera)' },
|
|
80
|
+
near: { type: 'number', description: 'Near clipping plane' },
|
|
81
|
+
far: { type: 'number', description: 'Far clipping plane' }
|
|
82
|
+
},
|
|
83
|
+
required: ['position']
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'save_camera_preset',
|
|
88
|
+
description: 'Save current camera position as a named preset for quick recall',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
name: { type: 'string', description: 'Preset name (e.g., "overhead", "closeup")' },
|
|
93
|
+
description: { type: 'string', description: 'Optional description of this view' }
|
|
94
|
+
},
|
|
95
|
+
required: ['name']
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'load_camera_preset',
|
|
100
|
+
description: 'Load a saved camera preset and optionally animate to it',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
name: { type: 'string', description: 'Preset name to load' },
|
|
105
|
+
animate: { type: 'boolean', description: 'Smoothly animate to position (default: false)' },
|
|
106
|
+
duration: { type: 'number', description: 'Animation duration in ms (default: 1000)' }
|
|
107
|
+
},
|
|
108
|
+
required: ['name']
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'list_camera_presets',
|
|
113
|
+
description: 'List all available camera presets',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'delete_camera_preset',
|
|
121
|
+
description: 'Delete a saved camera preset',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
name: { type: 'string', description: 'Preset name to delete' }
|
|
126
|
+
},
|
|
127
|
+
required: ['name']
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'animate_camera_presets',
|
|
132
|
+
description: 'Animate the camera through a sequence of saved presets',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
presets: { type: 'array', items: { type: 'string' }, description: 'Ordered list of preset names' },
|
|
137
|
+
duration: { type: 'number', description: 'Transition duration per preset in ms (default: 1000)' },
|
|
138
|
+
hold: { type: 'number', description: 'Hold time per preset in ms (default: 0)' },
|
|
139
|
+
repeat: { type: 'number', description: 'Number of times to repeat the sequence (default: 1)' }
|
|
140
|
+
},
|
|
141
|
+
required: ['presets']
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
// Hierarchy Management
|
|
145
|
+
{
|
|
146
|
+
name: 'spawn_entity',
|
|
147
|
+
description: 'Spawn a new primitive entity (box, sphere, plane, etc.)',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
type: { type: 'string', description: 'Primitive type: box, sphere, plane, cylinder, cone, torus' },
|
|
152
|
+
name: { type: 'string', description: 'Name for the new entity' },
|
|
153
|
+
position: { type: 'array', items: { type: 'number' }, description: '[x, y, z] position' },
|
|
154
|
+
color: { type: 'string', description: 'Hex color (e.g., #ff0000)' },
|
|
155
|
+
parentName: { type: 'string', description: 'Optional parent object name' }
|
|
156
|
+
},
|
|
157
|
+
required: ['type', 'name']
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'destroy_entity',
|
|
162
|
+
description: 'Remove an entity from the scene',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
name: { type: 'string', description: 'Entity name to destroy' }
|
|
167
|
+
},
|
|
168
|
+
required: ['name']
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'move_object',
|
|
173
|
+
description: 'Move an object to a new position',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
name: { type: 'string', description: 'Object name or path' },
|
|
178
|
+
position: { type: 'array', items: { type: 'number' }, description: '[x, y, z] position' }
|
|
179
|
+
},
|
|
180
|
+
required: ['name', 'position']
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'set_transform',
|
|
185
|
+
description: 'Set position, rotation, and/or scale of an object',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
name: { type: 'string', description: 'Object name' },
|
|
190
|
+
position: { type: 'array', items: { type: 'number' }, description: '[x, y, z]' },
|
|
191
|
+
rotation: { type: 'array', items: { type: 'number' }, description: '[x, y, z] in radians' },
|
|
192
|
+
scale: { type: 'array', items: { type: 'number' }, description: '[x, y, z]' }
|
|
193
|
+
},
|
|
194
|
+
required: ['name']
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'set_visibility',
|
|
199
|
+
description: 'Show or hide an object',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
name: { type: 'string', description: 'Object name' },
|
|
204
|
+
visible: { type: 'boolean', description: 'Visibility state' }
|
|
205
|
+
},
|
|
206
|
+
required: ['name', 'visible']
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'rename_entity',
|
|
211
|
+
description: 'Rename an object in the scene',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
oldName: { type: 'string', description: 'Current name' },
|
|
216
|
+
newName: { type: 'string', description: 'New name' }
|
|
217
|
+
},
|
|
218
|
+
required: ['oldName', 'newName']
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'duplicate_entity',
|
|
223
|
+
description: 'Clone an object with optional position offset',
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
name: { type: 'string', description: 'Object to clone' },
|
|
228
|
+
newName: { type: 'string', description: 'Name for the clone' },
|
|
229
|
+
offset: { type: 'array', items: { type: 'number' }, description: '[x, y, z] offset from original' }
|
|
230
|
+
},
|
|
231
|
+
required: ['name', 'newName']
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
// Physics
|
|
235
|
+
{
|
|
236
|
+
name: 'make_physical',
|
|
237
|
+
description: 'Add physics body to an object',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
name: { type: 'string', description: 'Object name' },
|
|
242
|
+
type: { type: 'string', description: 'Body type: dynamic, kinematic, static' },
|
|
243
|
+
colliders: { type: 'string', description: 'Collider type: cuboid, ball, hull, trimesh, auto' }
|
|
244
|
+
},
|
|
245
|
+
required: ['name']
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'remove_physics',
|
|
250
|
+
description: 'Remove physics body from an object',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
name: { type: 'string', description: 'Object name' }
|
|
255
|
+
},
|
|
256
|
+
required: ['name']
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'apply_impulse',
|
|
261
|
+
description: 'Apply an impulse force to a physics object',
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
name: { type: 'string', description: 'Object name' },
|
|
266
|
+
vector: { type: 'array', items: { type: 'number' }, description: '[x, y, z] impulse' }
|
|
267
|
+
},
|
|
268
|
+
required: ['name', 'vector']
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'set_gravity',
|
|
273
|
+
description: 'Set global gravity vector',
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
vector: { type: 'array', items: { type: 'number' }, description: '[x, y, z] gravity' }
|
|
278
|
+
},
|
|
279
|
+
required: ['vector']
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
// Materials & Assets
|
|
283
|
+
{
|
|
284
|
+
name: 'analyze_gltf',
|
|
285
|
+
description: 'Analyze a GLTF/GLB file for meshes, materials, textures, and animations',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
path: { type: 'string', description: 'Local path or file:// URL to a .gltf/.glb file' }
|
|
290
|
+
},
|
|
291
|
+
required: ['path']
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'validate_asset',
|
|
296
|
+
description: 'Validate a GLTF/GLB file for structural and performance issues',
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
path: { type: 'string', description: 'Local path or file:// URL to a .gltf/.glb file' },
|
|
301
|
+
limits: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
maxDrawCalls: { type: 'number', description: 'Max draw calls before warning' },
|
|
305
|
+
maxTriangles: { type: 'number', description: 'Max triangles before warning' },
|
|
306
|
+
maxVertices: { type: 'number', description: 'Max vertices before warning' },
|
|
307
|
+
maxTextures: { type: 'number', description: 'Max textures before warning' },
|
|
308
|
+
maxMaterials: { type: 'number', description: 'Max materials before warning' },
|
|
309
|
+
maxAnimations: { type: 'number', description: 'Max animations before warning' }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
required: ['path']
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'optimize_gltf',
|
|
318
|
+
description: 'Optimize a GLTF/GLB file with mesh simplification and texture compression',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
path: { type: 'string', description: 'Local path or file:// URL to a .gltf/.glb file' },
|
|
323
|
+
output: { type: 'string', description: 'Output path for optimized file' },
|
|
324
|
+
options: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
dedup: { type: 'boolean', description: 'Remove duplicate accessors/materials (default: true)' },
|
|
328
|
+
prune: { type: 'boolean', description: 'Remove unused data (default: true)' },
|
|
329
|
+
weld: { type: 'boolean', description: 'Weld shared vertices (default: true)' },
|
|
330
|
+
quantize: { type: 'boolean', description: 'Quantize vertex data (default: true)' },
|
|
331
|
+
simplify: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
ratio: { type: 'number', description: 'Target ratio (0-1) of vertices to keep' },
|
|
335
|
+
error: { type: 'number', description: 'Maximum simplification error' },
|
|
336
|
+
lockBorder: { type: 'boolean', description: 'Preserve mesh borders' }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
textures: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
format: { type: 'string', description: 'Target texture format: jpeg, png, webp, avif' },
|
|
343
|
+
resize: {
|
|
344
|
+
description: 'Resize textures [width,height] or power-of-two preset',
|
|
345
|
+
type: ['array', 'string'],
|
|
346
|
+
items: { type: 'number' }
|
|
347
|
+
},
|
|
348
|
+
quality: { type: 'number', description: 'Compression quality (1-100)' },
|
|
349
|
+
useSharp: { type: 'boolean', description: 'Use sharp encoder when available' }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
required: ['path']
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'export_to_svelte',
|
|
360
|
+
description: 'Generate a Threlte/Svelte component from a GLTF/GLB file',
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: 'object',
|
|
363
|
+
properties: {
|
|
364
|
+
path: { type: 'string', description: 'Local path or file:// URL to a .gltf/.glb file' },
|
|
365
|
+
output: { type: 'string', description: 'Output .svelte file path (optional)' },
|
|
366
|
+
componentName: { type: 'string', description: 'Component name override' },
|
|
367
|
+
assetUrl: { type: 'string', description: 'Asset URL to load in the generated component' },
|
|
368
|
+
mode: { type: 'string', description: 'Export mode: nodes or primitive' }
|
|
369
|
+
},
|
|
370
|
+
required: ['path']
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'load_asset',
|
|
375
|
+
description: 'Load a GLTF/GLB model into the scene',
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
url: { type: 'string', description: 'URL or path to the asset' },
|
|
380
|
+
name: { type: 'string', description: 'Name for the loaded model' },
|
|
381
|
+
position: { type: 'array', items: { type: 'number' }, description: '[x, y, z]' },
|
|
382
|
+
scale: { type: 'array', items: { type: 'number' }, description: '[x, y, z]' }
|
|
383
|
+
},
|
|
384
|
+
required: ['url', 'name']
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: 'apply_material',
|
|
389
|
+
description: 'Apply a material to an object',
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: 'object',
|
|
392
|
+
properties: {
|
|
393
|
+
name: { type: 'string', description: 'Object name' },
|
|
394
|
+
type: { type: 'string', description: 'Material type: Standard, Physical, Basic, Toon' },
|
|
395
|
+
color: { type: 'string', description: 'Hex color' },
|
|
396
|
+
preset: { type: 'string', description: 'Material preset: cyberpunk, gold, glass, cartoon' }
|
|
397
|
+
},
|
|
398
|
+
required: ['name']
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: 'set_environment',
|
|
403
|
+
description: 'Set the scene environment/skybox',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
preset: { type: 'string', description: 'Environment preset: sunset, dawn, night, warehouse, forest, apartment, studio, city, park, lobby' },
|
|
408
|
+
blur: { type: 'number', description: 'Background blur amount' },
|
|
409
|
+
background: { type: 'boolean', description: 'Show environment as background' }
|
|
410
|
+
},
|
|
411
|
+
required: ['preset']
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
// Vibe & Atmosphere
|
|
415
|
+
{
|
|
416
|
+
name: 'apply_vibe',
|
|
417
|
+
description: 'Apply a visual vibe/mood preset to the scene',
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
properties: {
|
|
421
|
+
vibe: { type: 'string', description: 'Vibe name: cozy, spooky, neon, retro, minimal, chaos' }
|
|
422
|
+
},
|
|
423
|
+
required: ['vibe']
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
// Bridge Status
|
|
427
|
+
{
|
|
428
|
+
name: 'get_bridge_status',
|
|
429
|
+
description: 'Get the connection status of the bridge server',
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
];
|
|
436
|
+
const LOCAL_ONLY_TOOLS = new Set([
|
|
437
|
+
'get_bridge_status',
|
|
438
|
+
'analyze_gltf',
|
|
439
|
+
'validate_asset',
|
|
440
|
+
'optimize_gltf',
|
|
441
|
+
'export_to_svelte',
|
|
442
|
+
'list_camera_presets',
|
|
443
|
+
'delete_camera_preset',
|
|
444
|
+
]);
|
|
445
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
446
|
+
// Create MCP server
|
|
447
|
+
const server = new Server({
|
|
448
|
+
name: 'threlte-mcp',
|
|
449
|
+
version: '1.4.0',
|
|
450
|
+
}, {
|
|
451
|
+
capabilities: {
|
|
452
|
+
tools: {},
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
// Bridge server for WebSocket communication
|
|
456
|
+
const bridge = new BridgeServer();
|
|
457
|
+
// List available tools
|
|
458
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
459
|
+
return { tools: TOOLS };
|
|
460
|
+
});
|
|
461
|
+
// Handle tool calls
|
|
462
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
463
|
+
const { name, arguments: args } = request.params;
|
|
464
|
+
if (!bridge.isConnected() && !LOCAL_ONLY_TOOLS.has(name)) {
|
|
465
|
+
try {
|
|
466
|
+
await bridge.connect();
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: 'text',
|
|
473
|
+
text: `❌ Failed to connect to game. Make sure:\n1. Game is running (npm run dev)\n2. MCPBridge is added to your scene (auto-connects in dev mode)\n3. For production: Set VITE_MCP_ENABLED=true in .env file`,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
isError: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
switch (name) {
|
|
482
|
+
case 'get_scene_state': {
|
|
483
|
+
const maxDepth = args?.maxDepth ?? 3;
|
|
484
|
+
const result = await bridge.sendCommand({ action: 'getFullSceneState', maxDepth });
|
|
485
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
486
|
+
}
|
|
487
|
+
case 'find_objects': {
|
|
488
|
+
const { name: objName, nameContains, type, hasUserData } = args;
|
|
489
|
+
const result = await bridge.sendCommand({
|
|
490
|
+
action: 'findObjects',
|
|
491
|
+
name: objName,
|
|
492
|
+
filter: { nameContains, type, hasUserData },
|
|
493
|
+
});
|
|
494
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
495
|
+
}
|
|
496
|
+
case 'get_object_position': {
|
|
497
|
+
const { name: objName } = args;
|
|
498
|
+
const result = await bridge.sendCommand({ action: 'findObjects', name: objName });
|
|
499
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
500
|
+
}
|
|
501
|
+
case 'set_camera_position': {
|
|
502
|
+
const { name: cameraName, position, lookAt, fov, near, far } = args;
|
|
503
|
+
await bridge.sendCommand({
|
|
504
|
+
action: 'setCameraPosition',
|
|
505
|
+
name: cameraName,
|
|
506
|
+
position,
|
|
507
|
+
lookAt,
|
|
508
|
+
fov,
|
|
509
|
+
near,
|
|
510
|
+
far,
|
|
511
|
+
});
|
|
512
|
+
const target = cameraName ? `camera "${cameraName}"` : 'camera';
|
|
513
|
+
return {
|
|
514
|
+
content: [{
|
|
515
|
+
type: 'text',
|
|
516
|
+
text: `OK. Set ${target} position to [${position.join(', ')}]`
|
|
517
|
+
}]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
case 'save_camera_preset': {
|
|
521
|
+
const { name: presetName, description } = args;
|
|
522
|
+
// Get current camera position from scene
|
|
523
|
+
const currentCamera = await bridge.sendCommand({ action: 'getCameraState' });
|
|
524
|
+
const preset = {
|
|
525
|
+
name: presetName,
|
|
526
|
+
position: currentCamera.position,
|
|
527
|
+
lookAt: currentCamera.lookAt,
|
|
528
|
+
fov: currentCamera.fov,
|
|
529
|
+
near: currentCamera.near,
|
|
530
|
+
far: currentCamera.far,
|
|
531
|
+
description
|
|
532
|
+
};
|
|
533
|
+
cameraPresets.savePreset(preset);
|
|
534
|
+
return {
|
|
535
|
+
content: [{
|
|
536
|
+
type: 'text',
|
|
537
|
+
text: `OK. Saved camera preset "${presetName}"`
|
|
538
|
+
}]
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
case 'load_camera_preset': {
|
|
542
|
+
const { name: presetName, animate, duration } = args;
|
|
543
|
+
const preset = cameraPresets.loadPreset(presetName);
|
|
544
|
+
if (!preset) {
|
|
545
|
+
return {
|
|
546
|
+
content: [{
|
|
547
|
+
type: 'text',
|
|
548
|
+
text: `Error: Preset "${presetName}" not found`
|
|
549
|
+
}]
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
await bridge.sendCommand({
|
|
553
|
+
action: 'setCameraPosition',
|
|
554
|
+
position: preset.position,
|
|
555
|
+
lookAt: preset.lookAt,
|
|
556
|
+
fov: preset.fov,
|
|
557
|
+
near: preset.near,
|
|
558
|
+
far: preset.far,
|
|
559
|
+
animate: animate || false,
|
|
560
|
+
duration: duration || 1000
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
content: [{
|
|
564
|
+
type: 'text',
|
|
565
|
+
text: `OK. Loaded camera preset "${presetName}"${animate ? ' with animation' : ''}`
|
|
566
|
+
}]
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
case 'list_camera_presets': {
|
|
570
|
+
const presets = cameraPresets.listPresets();
|
|
571
|
+
if (presets.length === 0) {
|
|
572
|
+
return {
|
|
573
|
+
content: [{
|
|
574
|
+
type: 'text',
|
|
575
|
+
text: 'No camera presets saved yet. Default presets: overhead, front, side, perspective, closeup, wideangle'
|
|
576
|
+
}]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const list = presets.map(p => `• ${p.name}: [${p.position.join(', ')}]${p.description ? ` - ${p.description}` : ''}`).join('\n');
|
|
580
|
+
return {
|
|
581
|
+
content: [{
|
|
582
|
+
type: 'text',
|
|
583
|
+
text: `Camera Presets:\n${list}`
|
|
584
|
+
}]
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
case 'delete_camera_preset': {
|
|
588
|
+
const { name: presetName } = args;
|
|
589
|
+
const deleted = cameraPresets.deletePreset(presetName);
|
|
590
|
+
if (deleted) {
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: `OK. Deleted preset "${presetName}"`
|
|
595
|
+
}]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
return {
|
|
600
|
+
content: [{
|
|
601
|
+
type: 'text',
|
|
602
|
+
text: `Error: Preset "${presetName}" not found`
|
|
603
|
+
}]
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
case 'animate_camera_presets': {
|
|
608
|
+
const { presets, duration, hold, repeat } = args;
|
|
609
|
+
if (!Array.isArray(presets) || presets.length < 2) {
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: 'text',
|
|
613
|
+
text: 'Error: Provide at least two preset names to animate.'
|
|
614
|
+
}],
|
|
615
|
+
isError: true,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const missing = presets.filter((presetName) => !cameraPresets.loadPreset(presetName));
|
|
619
|
+
if (missing.length > 0) {
|
|
620
|
+
return {
|
|
621
|
+
content: [{
|
|
622
|
+
type: 'text',
|
|
623
|
+
text: `Error: Missing presets: ${missing.join(', ')}`
|
|
624
|
+
}],
|
|
625
|
+
isError: true,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const sequence = presets.map((presetName) => cameraPresets.loadPreset(presetName));
|
|
629
|
+
const stepDuration = typeof duration === 'number' && Number.isFinite(duration) ? duration : 1000;
|
|
630
|
+
const holdDuration = typeof hold === 'number' && Number.isFinite(hold) ? hold : 0;
|
|
631
|
+
const repeats = typeof repeat === 'number' && Number.isFinite(repeat) ? Math.max(1, Math.floor(repeat)) : 1;
|
|
632
|
+
for (let loop = 0; loop < repeats; loop += 1) {
|
|
633
|
+
for (const preset of sequence) {
|
|
634
|
+
await bridge.sendCommand({
|
|
635
|
+
action: 'setCameraPosition',
|
|
636
|
+
position: preset.position,
|
|
637
|
+
lookAt: preset.lookAt,
|
|
638
|
+
fov: preset.fov,
|
|
639
|
+
near: preset.near,
|
|
640
|
+
far: preset.far,
|
|
641
|
+
animate: true,
|
|
642
|
+
duration: stepDuration,
|
|
643
|
+
});
|
|
644
|
+
if (holdDuration > 0) {
|
|
645
|
+
await sleep(holdDuration);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
content: [{
|
|
651
|
+
type: 'text',
|
|
652
|
+
text: `OK. Animated camera through ${presets.length} preset(s) x${repeats}.`
|
|
653
|
+
}]
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
case 'move_object': {
|
|
657
|
+
const { name: objName, position } = args;
|
|
658
|
+
const result = await bridge.sendCommand({ action: 'moveSceneObject', path: objName, position });
|
|
659
|
+
return {
|
|
660
|
+
content: [{
|
|
661
|
+
type: 'text',
|
|
662
|
+
text: result ? `✅ Moved "${objName}" to [${position.join(', ')}]` : `❌ Object "${objName}" not found`
|
|
663
|
+
}]
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
case 'spawn_entity': {
|
|
667
|
+
const { type, name: entityName, position, color, parentName } = args;
|
|
668
|
+
await bridge.sendCommand({
|
|
669
|
+
action: 'addPrimitive',
|
|
670
|
+
type: type.toLowerCase(),
|
|
671
|
+
name: entityName,
|
|
672
|
+
position: position || [0, 0, 0],
|
|
673
|
+
material: color ? { color } : undefined,
|
|
674
|
+
parent: parentName,
|
|
675
|
+
});
|
|
676
|
+
return {
|
|
677
|
+
content: [{
|
|
678
|
+
type: 'text',
|
|
679
|
+
text: `✅ Spawned "${entityName}" (${type}) at [${(position || [0, 0, 0]).join(', ')}]`
|
|
680
|
+
}]
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
case 'destroy_entity': {
|
|
684
|
+
const { name: entityName } = args;
|
|
685
|
+
const result = await bridge.sendCommand({ action: 'removeObject', id: entityName, name: entityName });
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: 'text',
|
|
689
|
+
text: result ? `✅ Destroyed "${entityName}"` : `❌ Object "${entityName}" not found`
|
|
690
|
+
}]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
case 'set_transform': {
|
|
694
|
+
const { name: objName, position, rotation, scale } = args;
|
|
695
|
+
const updates = [];
|
|
696
|
+
if (position) {
|
|
697
|
+
await bridge.sendCommand({ action: 'moveSceneObject', path: objName, position });
|
|
698
|
+
updates.push(`position: [${position.join(', ')}]`);
|
|
699
|
+
}
|
|
700
|
+
if (rotation) {
|
|
701
|
+
await bridge.sendCommand({ action: 'setRotation', id: objName, name: objName, rotation });
|
|
702
|
+
updates.push(`rotation: [${rotation.join(', ')}]`);
|
|
703
|
+
}
|
|
704
|
+
if (scale) {
|
|
705
|
+
await bridge.sendCommand({ action: 'setScale', id: objName, name: objName, scale });
|
|
706
|
+
updates.push(`scale: [${scale.join(', ')}]`);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
content: [{
|
|
710
|
+
type: 'text',
|
|
711
|
+
text: updates.length > 0 ? `✅ Updated "${objName}": ${updates.join(', ')}` : `⚠️ No transform properties specified`
|
|
712
|
+
}]
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
case 'set_visibility': {
|
|
716
|
+
const { name: objName, visible } = args;
|
|
717
|
+
await bridge.sendCommand({ action: 'setVisibility', name: objName, visible });
|
|
718
|
+
return { content: [{ type: 'text', text: `✅ ${visible ? 'Showed' : 'Hid'} "${objName}"` }] };
|
|
719
|
+
}
|
|
720
|
+
case 'apply_vibe': {
|
|
721
|
+
const { vibe } = args;
|
|
722
|
+
await bridge.sendCommand({ action: 'applyVibe', vibe });
|
|
723
|
+
return { content: [{ type: 'text', text: `✅ Applied vibe "${vibe}"` }] };
|
|
724
|
+
}
|
|
725
|
+
case 'set_environment': {
|
|
726
|
+
const { preset } = args;
|
|
727
|
+
await bridge.sendCommand({ action: 'setEnvironment', preset });
|
|
728
|
+
return { content: [{ type: 'text', text: `✅ Set environment to "${preset}"` }] };
|
|
729
|
+
}
|
|
730
|
+
case 'apply_impulse': {
|
|
731
|
+
const { name: objName, vector } = args;
|
|
732
|
+
await bridge.sendCommand({ action: 'applyImpulse', name: objName, vector });
|
|
733
|
+
return { content: [{ type: 'text', text: `✅ Applied impulse [${vector.join(', ')}] to "${objName}"` }] };
|
|
734
|
+
}
|
|
735
|
+
case 'set_gravity': {
|
|
736
|
+
const { vector } = args;
|
|
737
|
+
await bridge.sendCommand({ action: 'setGravity', vector });
|
|
738
|
+
return { content: [{ type: 'text', text: `✅ Set global gravity to [${vector.join(', ')}]` }] };
|
|
739
|
+
}
|
|
740
|
+
case 'analyze_gltf': {
|
|
741
|
+
const { path } = args;
|
|
742
|
+
const result = await analyzeGltf(path);
|
|
743
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
744
|
+
}
|
|
745
|
+
case 'validate_asset': {
|
|
746
|
+
const { path, limits } = args;
|
|
747
|
+
const result = await validateGltf(path, limits);
|
|
748
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
749
|
+
}
|
|
750
|
+
case 'optimize_gltf': {
|
|
751
|
+
const { path, output, options } = args;
|
|
752
|
+
const result = await optimizeGltf(path, {
|
|
753
|
+
outputPath: output,
|
|
754
|
+
dedup: options?.dedup,
|
|
755
|
+
prune: options?.prune,
|
|
756
|
+
weld: options?.weld,
|
|
757
|
+
quantize: options?.quantize,
|
|
758
|
+
simplify: options?.simplify,
|
|
759
|
+
textures: options?.textures,
|
|
760
|
+
});
|
|
761
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
762
|
+
}
|
|
763
|
+
case 'export_to_svelte': {
|
|
764
|
+
const { path, output, componentName, assetUrl, mode } = args;
|
|
765
|
+
const result = await exportToSvelte(path, {
|
|
766
|
+
outputPath: output,
|
|
767
|
+
componentName,
|
|
768
|
+
assetUrl,
|
|
769
|
+
mode,
|
|
770
|
+
});
|
|
771
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
772
|
+
}
|
|
773
|
+
case 'load_asset': {
|
|
774
|
+
const { url, name: objName, position, scale } = args;
|
|
775
|
+
await bridge.sendCommand({ action: 'loadAsset', url, name: objName, position, scale });
|
|
776
|
+
return { content: [{ type: 'text', text: `✅ Loaded asset "${objName}" from ${url}` }] };
|
|
777
|
+
}
|
|
778
|
+
case 'apply_material': {
|
|
779
|
+
const { name: objName, type, color, preset } = args;
|
|
780
|
+
await bridge.sendCommand({ action: 'applyMaterial', name: objName, type: type || 'Standard', color, preset });
|
|
781
|
+
return { content: [{ type: 'text', text: `✅ Applied material to "${objName}"` }] };
|
|
782
|
+
}
|
|
783
|
+
case 'get_bridge_status': {
|
|
784
|
+
return { content: [{ type: 'text', text: JSON.stringify(bridge.getStatus(), null, 2) }] };
|
|
785
|
+
}
|
|
786
|
+
default:
|
|
787
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
return {
|
|
792
|
+
content: [{ type: 'text', text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` }],
|
|
793
|
+
isError: true,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
// Start the server
|
|
798
|
+
async function main() {
|
|
799
|
+
const transport = new StdioServerTransport();
|
|
800
|
+
await server.connect(transport);
|
|
801
|
+
console.error('🎮 Threlte MCP Server ready');
|
|
802
|
+
console.error(' Waiting for game client on ws://localhost:8082');
|
|
803
|
+
}
|
|
804
|
+
main().catch((error) => {
|
|
805
|
+
console.error('Failed to start MCP server:', error);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
});
|
|
808
|
+
//# sourceMappingURL=index.js.map
|