@directivegames/genesys.sdk 3.2.2 → 3.2.5

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 (125) hide show
  1. package/README.md +60 -60
  2. package/dist/src/asset-pack/eslint.config.js +4 -4
  3. package/dist/src/core/cli.js +22 -22
  4. package/dist/src/core/common.js +2 -2
  5. package/dist/src/core/tools/build-project.js +1 -1
  6. package/dist/src/core/tools/new-asset-pack.js +1 -1
  7. package/dist/src/core/tools/new-project.js +1 -1
  8. package/dist/src/dependencies.js +1 -1
  9. package/dist/src/electron/backend/tools/const.js +1 -1
  10. package/dist/src/electron/backend/tools/open-project.js +1 -1
  11. package/dist/src/templates/eslint.config.js +4 -4
  12. package/dist/src/templates/scripts/genesys/calc-bounding-box.js +2 -2
  13. package/dist/src/templates/scripts/genesys/dev/dump-default-scene.js +1 -1
  14. package/dist/src/templates/scripts/genesys/dev/generate-manifest.js +1 -1
  15. package/dist/src/templates/scripts/genesys/dev/launcher.js +1 -1
  16. package/dist/src/templates/scripts/genesys/dev/storage-provider.js +4 -4
  17. package/dist/src/templates/scripts/genesys/genesys-mcp.js +26 -26
  18. package/dist/src/templates/scripts/genesys/mcp/doc-tools.js +2 -2
  19. package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +5 -5
  20. package/dist/src/templates/scripts/genesys/mcp/search-actors.js +1 -1
  21. package/dist/src/templates/scripts/genesys/mcp/search-assets.js +1 -1
  22. package/dist/src/templates/scripts/genesys/mcp/utils.js +1 -1
  23. package/dist/src/templates/scripts/genesys/misc.js +1 -1
  24. package/dist/src/templates/scripts/genesys/mock.js +1 -1
  25. package/dist/src/templates/scripts/genesys/place-actors.js +1 -1
  26. package/dist/src/templates/scripts/genesys/post-install.js +1 -1
  27. package/dist/src/templates/scripts/genesys/storageProvider.js +2 -2
  28. package/dist/src/templates/src/index.js +1 -1
  29. package/dist/src/templates/src/templates/firstPerson/src/game.js +1 -1
  30. package/dist/src/templates/src/templates/firstPerson/src/player.js +1 -1
  31. package/dist/src/templates/src/templates/fps/src/game.js +1 -1
  32. package/dist/src/templates/src/templates/fps/src/player.js +1 -1
  33. package/dist/src/templates/src/templates/fps/src/weapon.js +1 -1
  34. package/dist/src/templates/src/templates/freeCamera/src/game.js +1 -1
  35. package/dist/src/templates/src/templates/freeCamera/src/player.js +1 -1
  36. package/dist/src/templates/src/templates/sideScroller/src/game.js +1 -1
  37. package/dist/src/templates/src/templates/sideScroller/src/level-generator.js +1 -1
  38. package/dist/src/templates/src/templates/sideScroller/src/player.js +1 -1
  39. package/dist/src/templates/src/templates/thirdPerson/src/game.js +1 -1
  40. package/dist/src/templates/src/templates/thirdPerson/src/player.js +1 -1
  41. package/dist/src/templates/src/templates/vehicle/src/base-vehicle.js +1 -1
  42. package/dist/src/templates/src/templates/vehicle/src/game.js +1 -1
  43. package/dist/src/templates/src/templates/vehicle/src/mesh-vehicle.js +1 -1
  44. package/dist/src/templates/src/templates/vehicle/src/player.js +1 -1
  45. package/dist/src/templates/src/templates/vehicle/src/primitive-vehicle.js +1 -1
  46. package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +31 -31
  47. package/dist/src/templates/src/templates/vr-game/src/game.js +1 -1
  48. package/dist/src/templates/src/templates/vr-game/src/sample-vr-actor.js +1 -1
  49. package/package.json +176 -176
  50. package/scripts/post-install.ts +143 -143
  51. package/src/asset-pack/.gitattributes +88 -88
  52. package/src/asset-pack/eslint.config.js +45 -45
  53. package/src/asset-pack/gitignore +11 -11
  54. package/src/asset-pack/scripts/postinstall.ts +81 -81
  55. package/src/asset-pack/tsconfig.json +33 -33
  56. package/src/templates/.cursor/mcp.json +20 -20
  57. package/src/templates/.cursorignore +2 -2
  58. package/src/templates/.gitattributes +88 -88
  59. package/src/templates/.vscode/settings.json +6 -6
  60. package/src/templates/AGENTS.md +86 -86
  61. package/src/templates/README.md +24 -24
  62. package/src/templates/eslint.config.js +45 -45
  63. package/src/templates/gitignore +11 -11
  64. package/src/templates/index.html +34 -34
  65. package/src/templates/pnpm-lock.yaml +3676 -3676
  66. package/src/templates/scripts/genesys/build-project.ts +51 -51
  67. package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -272
  68. package/src/templates/scripts/genesys/common.ts +46 -46
  69. package/src/templates/scripts/genesys/const.ts +9 -9
  70. package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -11
  71. package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -146
  72. package/src/templates/scripts/genesys/dev/launcher.ts +46 -46
  73. package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -229
  74. package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -84
  75. package/src/templates/scripts/genesys/doc-server.ts +16 -16
  76. package/src/templates/scripts/genesys/genesys-mcp.ts +526 -526
  77. package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -86
  78. package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -151
  79. package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -73
  80. package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -35
  81. package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -30
  82. package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -858
  83. package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -380
  84. package/src/templates/scripts/genesys/mcp/utils.ts +281 -281
  85. package/src/templates/scripts/genesys/misc.ts +42 -42
  86. package/src/templates/scripts/genesys/mock.ts +6 -6
  87. package/src/templates/scripts/genesys/place-actors.ts +179 -179
  88. package/src/templates/scripts/genesys/post-install.ts +30 -30
  89. package/src/templates/scripts/genesys/prefab.schema.json +84 -84
  90. package/src/templates/scripts/genesys/remove-engine-comments.ts +134 -134
  91. package/src/templates/scripts/genesys/run-mcp-inspector.bat +4 -4
  92. package/src/templates/scripts/genesys/storageProvider.ts +182 -182
  93. package/src/templates/scripts/genesys/validate-prefabs.ts +138 -138
  94. package/src/templates/src/index.ts +22 -22
  95. package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +165 -165
  96. package/src/templates/src/templates/firstPerson/src/game.ts +39 -39
  97. package/src/templates/src/templates/firstPerson/src/player.ts +63 -63
  98. package/src/templates/src/templates/fps/assets/default.genesys-scene +9459 -9459
  99. package/src/templates/src/templates/fps/src/game.ts +39 -39
  100. package/src/templates/src/templates/fps/src/player.ts +69 -69
  101. package/src/templates/src/templates/fps/src/weapon.ts +54 -54
  102. package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +165 -165
  103. package/src/templates/src/templates/freeCamera/src/game.ts +39 -39
  104. package/src/templates/src/templates/freeCamera/src/player.ts +45 -45
  105. package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +121 -121
  106. package/src/templates/src/templates/sideScroller/src/const.ts +45 -45
  107. package/src/templates/src/templates/sideScroller/src/game.ts +122 -122
  108. package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -361
  109. package/src/templates/src/templates/sideScroller/src/player.ts +125 -125
  110. package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +165 -165
  111. package/src/templates/src/templates/thirdPerson/src/game.ts +39 -39
  112. package/src/templates/src/templates/thirdPerson/src/player.ts +61 -61
  113. package/src/templates/src/templates/vehicle/assets/default.genesys-scene +225 -225
  114. package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -145
  115. package/src/templates/src/templates/vehicle/src/game.ts +43 -43
  116. package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -191
  117. package/src/templates/src/templates/vehicle/src/player.ts +109 -109
  118. package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -266
  119. package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -101
  120. package/src/templates/src/templates/vr-game/assets/default.genesys-scene +246 -246
  121. package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -1
  122. package/src/templates/src/templates/vr-game/src/game.ts +66 -66
  123. package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -26
  124. package/src/templates/tsconfig.json +34 -34
  125. package/src/templates/vite.config.ts +52 -52
@@ -1,526 +1,526 @@
1
- // How to use:
2
- // it must be built first with: pnpm build
3
- // open cursor settings - MCP servers, refresh and make sure "genesys" is connected
4
- // and you're good to go with asking cursor to place primitives in a specified project
5
- //
6
- // for another MCP client (e.g. cline) that wants to use this, the command is: `pnpm exec tsc ./scripts/genesys/genesys-mcp.ts`
7
- import pathlib from 'path';
8
-
9
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import * as ENGINE from 'genesys.js';
12
- import * as THREE from 'three';
13
- import { z } from 'zod';
14
-
15
-
16
- import { ActorInfoSchema, TransformSchema } from './common.js';
17
- import { EditorIdSchema, getClientWebSocket, getCurrentScene } from './mcp/editor-functions.js';
18
- import { addEditorTools } from './mcp/editor-tools.js';
19
- import { getSceneState } from './mcp/get-scene-state.js';
20
- import { assetDescriptions, AssetType, populateAssets, searchForAssets } from './mcp/search-assets.js';
21
- import { mcpLogger } from './mcp/utils.js';
22
- import { generateCode } from './misc.js';
23
- import { addGltf, placeJsClassActor, placePrefab, placePrimitive, removeActors, updateActors } from './place-actors.js';
24
-
25
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
26
-
27
-
28
- // Create an MCP server
29
- const server = new McpServer({
30
- name: 'Genesys-MCP',
31
- version: '0.0.1'
32
- }, {
33
- capabilities: {
34
- logging: {} // to support sendLoggingMessage
35
- }
36
- });
37
-
38
-
39
- const classRule = `
40
- The class name should start with '${ENGINE.Prefix.GAME}' or '${ENGINE.Prefix.ENGINE}' to indicate if it is a class from game project or engine.
41
- If it starts with neither, it will be matched against all registered classes. If none is found, an error will be returned.
42
- `;
43
-
44
-
45
- function requestEditorReload(editorId?: string) {
46
- const ws = getClientWebSocket(editorId);
47
- ws.send(JSON.stringify({
48
- command: 'reload'
49
- }));
50
- }
51
-
52
- addEditorTools(server);
53
-
54
- const primitiveTypes = Object.values(ENGINE.WorldCommands.PrimitiveType);
55
-
56
- server.registerTool(
57
- 'placePrimitive',
58
- {
59
- description: 'Place 3D primitives in the specified scene.',
60
- inputSchema: {
61
- editorId: EditorIdSchema,
62
- primitiveActors: z.array(
63
- z.object({
64
- primitives: z.array(
65
- z.object({
66
- type: z.enum(primitiveTypes as [string, ...string[]])
67
- .describe(`Type of primitive to place, one of: ${primitiveTypes.join(', ')}`),
68
- transform: TransformSchema.optional().describe('Transform of the primitive relative to the actor'),
69
- color: z.union([z.array(z.number()).length(3), z.string(), z.number()]).optional().describe('Color as [r, g, b], hex string, hex number, or X11 color name'),
70
- }).strict().describe('A primitive to add to the actor.')
71
- ).describe('A list of primitives to add to the actor.'),
72
- info: ActorInfoSchema.describe('Information about the primitive actor.'),
73
- transform: TransformSchema.optional().describe('Transform of the actor in the scene'),
74
- }).strict().describe('An actor containing primitives to place.')
75
- ).describe('A list of lists of primitives to add. Each inner list represents a single actor to place.'),
76
- },
77
- },
78
- async ({ editorId, primitiveActors }) => {
79
- let actorIds: string[] = [];
80
- let sceneName: string;
81
-
82
- {
83
- using loggerContext = mcpLogger(server);
84
- try {
85
- sceneName = await getCurrentScene(editorId);
86
- actorIds = await placePrimitive({
87
- sceneName,
88
- primitiveActors: primitiveActors.map(primitiveActor => ({
89
- miscInfo: {
90
- displayName: primitiveActor.info.displayName,
91
- description: primitiveActor.info.description ?? '',
92
- },
93
- transform: {
94
- position: new THREE.Vector3(...(primitiveActor.transform?.position ?? [0, 0, 0])),
95
- rotation: new THREE.Euler(...(primitiveActor.transform?.rotation ?? [0, 0, 0])),
96
- scale: new THREE.Vector3(...(primitiveActor.transform?.scale ?? [1, 1, 1])),
97
- },
98
- primitives: primitiveActor.primitives.map(primitive => ({
99
- type: primitive.type,
100
- transform: {
101
- position: new THREE.Vector3(...(primitive.transform?.position ?? [0, 0, 0])),
102
- rotation: new THREE.Euler(...(primitive.transform?.rotation ?? [0, 0, 0])),
103
- scale: new THREE.Vector3(...(primitive.transform?.scale ?? [1, 1, 1])),
104
- },
105
- color: Array.isArray(primitive.color) ? new THREE.Color(...primitive.color) : new THREE.Color(primitive.color as (string | number)),
106
- }))
107
- }))
108
- });
109
- } catch (error) {
110
- return {
111
- isError: true,
112
- content: [{ type: 'text', text: `${error}` }]
113
- };
114
- }
115
-
116
- if (actorIds.length > 0) {
117
- requestEditorReload(editorId);
118
- }
119
- }
120
-
121
- const uuids = actorIds.join(', ');
122
- const text = `Primitives placed successfully in ${sceneName}, placed actor UUIDs are [${uuids}]`;
123
- return {
124
- content: [{ type: 'text', text }]
125
- };
126
- }
127
- );
128
-
129
- server.registerTool(
130
- 'removeActors',
131
- {
132
- description: 'Remove one or more actors from the scene by their UUIDs.',
133
- inputSchema: {
134
- editorId: EditorIdSchema,
135
- actorIds: z.array(z.string()).describe('List of actor UUIDs to remove'),
136
- },
137
- },
138
- async ({ editorId, actorIds }) => {
139
- {
140
- using loggerContext = mcpLogger(server);
141
- const sceneName = await getCurrentScene(editorId);
142
- await removeActors({ sceneName, actorIds });
143
-
144
- if (actorIds.length > 0) {
145
- requestEditorReload(editorId);
146
- }
147
- }
148
- const text = `${actorIds.length} actors removed successfully`;
149
- return {
150
- content: [{ type: 'text', text }]
151
- };
152
- }
153
- );
154
-
155
-
156
- server.registerTool(
157
- 'addGltf',
158
- {
159
- description: 'Add GLTF models to the scene.',
160
- inputSchema: {
161
- editorId: EditorIdSchema,
162
- gltfs: z.array(
163
- z.object({
164
- path: z.string().describe(`Path or URL to the GLTF model to add.
165
- Unless explicitly told by user, otherwise it should always be one of these
166
- - fetched from the result of searchAssets tool
167
- - copied from an existing path of an existing placed GLTF actor.
168
- - use a verified URL to a GLTF model
169
- Never make up a relative path to a file, because that won't be resolved by the editor.
170
- `),
171
- transform: TransformSchema.optional().describe('Transform of the GLTF model'),
172
- info: ActorInfoSchema.describe('Information about the GLTF model actor.'),
173
- }).strict().describe('A GLTF model to add.')
174
- ).describe('A list of GLTF models to add.'),
175
- },
176
- },
177
- async ({ editorId, gltfs }) => {
178
- let actorIds: string[] = [];
179
-
180
- {
181
- using loggerContext = mcpLogger(server);
182
- const sceneName = await getCurrentScene(editorId);
183
- actorIds = await addGltf({
184
- sceneName,
185
- gltfs: gltfs.map(gltf => ({
186
- path: gltf.path,
187
- transform: {
188
- position: new THREE.Vector3(...(gltf.transform?.position ?? [0, 0, 0])),
189
- rotation: new THREE.Euler(...(gltf.transform?.rotation ?? [0, 0, 0])),
190
- scale: new THREE.Vector3(...(gltf.transform?.scale ?? [1, 1, 1])),
191
- },
192
- miscInfo: {
193
- displayName: gltf.info.displayName,
194
- description: gltf.info.description ?? '',
195
- },
196
- }))
197
- });
198
-
199
- if (actorIds.length > 0) {
200
- requestEditorReload(editorId);
201
- }
202
- }
203
-
204
- const uuids = actorIds.join(', ');
205
- const text = `GLTFs added successfully, placed actor UUIDs are [${uuids}]`;
206
- return {
207
- content: [{ type: 'text', text }]
208
- };
209
- }
210
- );
211
-
212
- server.registerTool(
213
- 'placePrefab',
214
- {
215
- description: 'Place prefabs in the scene.',
216
- inputSchema: {
217
- editorId: EditorIdSchema,
218
- prefabs: z.array(
219
- z.object({
220
- path: z.string().describe('Path to the prefab to place'),
221
- transform: TransformSchema.optional().describe('Transform of the prefab'),
222
- info: ActorInfoSchema.describe('Information about the prefab actor.'),
223
- }).strict().describe('A prefab to place.')
224
- ).describe('A list of prefabs to place. Each prefab can have a transform.'),
225
- },
226
- },
227
- async ({ editorId, prefabs }) => {
228
- let actorIds: string[] = [];
229
-
230
- {
231
- using loggerContext = mcpLogger(server);
232
- const sceneName = await getCurrentScene(editorId);
233
- actorIds = await placePrefab({
234
- sceneName,
235
- prefabs: prefabs.map(prefab => ({
236
- path: prefab.path,
237
- transform: {
238
- position: new THREE.Vector3(...(prefab.transform?.position ?? [0, 0, 0])),
239
- rotation: new THREE.Euler(...(prefab.transform?.rotation ?? [0, 0, 0])),
240
- scale: new THREE.Vector3(...(prefab.transform?.scale ?? [1, 1, 1])),
241
- },
242
- miscInfo: {
243
- displayName: prefab.info.displayName,
244
- description: prefab.info.description ?? '',
245
- },
246
- }))
247
- });
248
-
249
- if (actorIds.length > 0) {
250
- requestEditorReload(editorId);
251
- }
252
- }
253
- const uuids = actorIds.join(', ');
254
- const text = `Prefabs placed successfully, placed actor UUIDs are [${uuids}]`;
255
- return {
256
- content: [{ type: 'text', text }]
257
- };
258
- }
259
- );
260
-
261
-
262
- server.registerTool(
263
- 'searchAssets',
264
- {
265
- description: `Search for assets in the project and engine directories.
266
- When the user requests to place something in the scene, unless it is clearly a primitive, always try calling this first to see if any assets are fitting the request.
267
- Before using the assets found, **always** call the \`getAssetsMetadata\` tool to get the metadata about the assets, unless the given metadata is already fetched.
268
- Here is the list of assets types, including:
269
- ${Object.entries(assetDescriptions).map(([type, description]) => `- ${type}: ${description}`).join('\n')}
270
- `,
271
- inputSchema: {
272
- acceptedTypes: z.array(z.enum(Object.values(AssetType) as [string, ...string[]])).describe('List of types of assets to search for. An empty array means all types.'),
273
- searchKeywords: z.array(z.string()).describe('List of keywords to search for in the assets. Wildcards are not supported, use multiple keywords to search for multiple terms. Use an empty array to search for all assets.'),
274
- },
275
- },
276
- async ({ acceptedTypes, searchKeywords }) => {
277
- using loggerContext = mcpLogger(server);
278
-
279
- const dirs = [`${ENGINE.PROJECT_PATH_PREFIX}/assets`, `${ENGINE.ENGINE_PATH_PREFIX}/assets`];
280
- const {assets, metadataDescription} = await searchForAssets(dirs, acceptedTypes as AssetType[], searchKeywords);
281
-
282
- const result: CallToolResult = {content: []};
283
- if (metadataDescription && Object.keys(metadataDescription).length > 0) {
284
- result.content.push(
285
- { type: 'text', text: `Here is the description of the metadata fields of the assets, per asset type:\n${JSON.stringify(metadataDescription, null, 2)}` }
286
- );
287
- }
288
-
289
- result.content.push({ type: 'text', text: `Assets:\n${JSON.stringify(assets, null, 2)}` });
290
- return result;
291
- }
292
- );
293
-
294
-
295
- server.registerTool(
296
- 'getAssetsMetadata',
297
- {
298
- description: 'Get metadata for the assets found by the `searchAssets` tool.',
299
- inputSchema: {
300
- assetPaths: z.array(z.string()).describe('List of asset paths to get metadata for. The asset paths are the keys retrieved from the result of \`searchAssets\` tool.'),
301
- },
302
- },
303
- async ({ assetPaths }) => {
304
- using loggerContext = mcpLogger(server);
305
-
306
- try {
307
- const assetsInfo = await populateAssets(assetPaths, { peek: false });
308
- const metadataDescription = assetsInfo.metadataDescription;
309
- const assets = assetsInfo.assets;
310
-
311
- const result: CallToolResult = {content: []};
312
- if (metadataDescription && Object.keys(metadataDescription).length > 0) {
313
- result.content.push(
314
- { type: 'text', text: `Here is the description of the metadata fields of the assets, per asset type:\n${JSON.stringify(metadataDescription, null, 2)}` }
315
- );
316
- }
317
-
318
- result.content.push({ type: 'text', text: `Assets:\n${JSON.stringify(assets, null, 2)}` });
319
- return result;
320
-
321
- } catch (error) {
322
- return {
323
- isError: true,
324
- content: [{ type: 'text', text: `Error getting assets metadata: ${error}` }]
325
- };
326
- }
327
- }
328
- );
329
-
330
-
331
- server.registerTool(
332
- 'placeJsClassActor',
333
- {
334
- description: 'Place a JavaScript game or engine class actor in the scene.',
335
- inputSchema: {
336
- editorId: EditorIdSchema,
337
- actors: z.array(
338
- z.object({
339
- className: z.string().describe(`The name of the JavaScript class to place. ${classRule}`),
340
- constructorParams: z.array(z.record(z.any())).optional()
341
- .describe(`Optional constructor parameters to pass to the actor class.
342
- Use the getAssetsMetadata tool to discover what parameters each actor class requires,
343
- and the parameters should be an array that corresponds to the "jsClassConstructorParams" from the result of the getAssetsMetadata tool.
344
- If the actor class has no constructor parameters, the array should be empty.
345
- `),
346
- info: ActorInfoSchema.describe('Information about the actor.'),
347
- }).strict().describe('A JavaScript class actor to place.')
348
- ).describe('A list of JavaScript class actors to place. Each actor can have a transform and constructor parameters.'),
349
- },
350
- },
351
- async ({ editorId, actors }) => {
352
- using loggerContext = mcpLogger(server);
353
-
354
- const sceneName = await getCurrentScene(editorId);
355
-
356
- const result = await placeJsClassActor({sceneName, jsClasses: actors.map(actor => ({
357
- className: actor.className,
358
- constructorParams: actor.constructorParams,
359
- actorInfo: actor.info,
360
- }))});
361
-
362
- if (result.length > 0) {
363
- requestEditorReload(editorId);
364
- }
365
-
366
- const uuids = result.join(', ');
367
- const text = `JavaScript class actors placed successfully, placed actor UUIDs are [${uuids}]`;
368
- return {
369
- content: [{ type: 'text', text }]
370
- };
371
- }
372
- );
373
-
374
-
375
- server.registerTool(
376
- 'generateTemplateCode',
377
- {
378
- description: 'Generate template code for a class of actor or component. When the user asks to create a new actor or component, always try calling this first to see if any template code is available, and iterate on the code it generates until it is correct.',
379
- inputSchema: {
380
- className: z.string().describe('The name of the class to generate template code for.'),
381
- filePath: z.string().describe(`The path to the file to generate the template code for.
382
- If it is a relative path, it will be relative to the 'src/' directory, so don't include the extra 'src/' in the path.
383
- The file name should be lower camel case, such as 'myActor.ts', or 'fancyMovementComponent.ts'.`
384
- ),
385
- baseClassName: z.string().describe(`The name of the base class to generate the template code for. ${classRule}`),
386
- },
387
- },
388
- async ({ className, filePath, baseClassName }) => {
389
- using loggerContext = mcpLogger(server);
390
-
391
- const fullFilePath = pathlib.isAbsolute(filePath) ? filePath : pathlib.join('src', filePath);
392
- let success = false;
393
- try {
394
- success = await generateCode(className, fullFilePath, baseClassName);
395
- }
396
- catch (error) {
397
- return {
398
- isError: true,
399
- content: [{ type: 'text', text: `Failed to generate code: ${error}` }]
400
- };
401
- }
402
-
403
- if (success) {
404
- return {
405
- content: [
406
- {
407
- type: 'text',
408
- text: `template code for class ${className} has been generated at ${fullFilePath}`
409
- }
410
- ]
411
- };
412
- }
413
-
414
- return {
415
- isError: true,
416
- content: [{ type: 'text', text: `Error: Template code for class ${className} has not been generated.` }]
417
- };
418
- }
419
- );
420
-
421
-
422
- server.registerTool(
423
- 'getSceneState',
424
- {
425
- description: `
426
- Get the current state of the scene to understand the state of the current scene and its actors.
427
- **DO NOT** try reading the scene file directly unless explicitly asked by the user.
428
- The first time calling this tool, always call without specifiedActors and getDetailedComponentsInfo to have an overview of the scene.
429
- And then call this tool with \`specifiedActors\`, and set \`getDetailedComponentsInfo\` to true, to get more detailed information about specific actors, including their bounding boxes info, etc.
430
- `,
431
- inputSchema: {
432
- editorId: EditorIdSchema,
433
- specifiedActors: z.array(z.string()).optional().describe('A list of actor UUIDs. If specified, actors not in this list will contain only very basic information, namely their UUID and name.'),
434
- getDetailedComponentsInfo: z.boolean().optional().describe('If true, will contain detailed information about their properties, otherwise they will contain only their UUID and name. If specifiedActors is present, only actors in this list will have detailed information.'),
435
- },
436
- },
437
- async ({editorId, specifiedActors, getDetailedComponentsInfo}) => {
438
- using loggerContext = mcpLogger(server);
439
-
440
- try {
441
- const sceneName = await getCurrentScene(editorId);
442
- const state = await getSceneState(sceneName, specifiedActors, getDetailedComponentsInfo);
443
- function roundNumbersReplacer(key: string, value: any) {
444
- if (typeof value === 'number') {
445
- return Number(value.toFixed(5));
446
- }
447
- return value;
448
- }
449
- return {
450
- content: [{ type: 'text', text: `The following is the state of the scene currently opened in the editor:\n\`\`\`json\n${JSON.stringify(state, roundNumbersReplacer)}\n\`\`\`` }]
451
- };
452
- }
453
- catch (error) {
454
- return {
455
- isError: true,
456
- content: [{ type: 'text', text: `Error getting scene state: ${error}` }]
457
- };
458
- }
459
- }
460
- );
461
-
462
- server.registerTool(
463
- 'updateActors',
464
- {
465
- description: 'Update the properties of existing actors in the scene.',
466
- inputSchema: {
467
- editorId: EditorIdSchema,
468
- actors: z.array(
469
- z.object({
470
- uuid: z.string().describe('UUID of the actor to update'),
471
- transform: TransformSchema.optional().describe('New transform of the actor'),
472
- info: ActorInfoSchema.optional().describe('New information about the actor.'),
473
- }).strict().describe('An actor to update with its new properties.')
474
- ).describe('A list of actors to update with their new properties.'),
475
- },
476
- },
477
- async ({editorId, actors}) => {
478
- using loggerContext = mcpLogger(server);
479
-
480
- const makeActorUpdateArg = (actor: typeof actors[number]) => {
481
- const args: any = {};
482
- args.uuid = actor.uuid;
483
- if (actor.transform) {
484
- args.transform = {};
485
- if (actor.transform.position) {
486
- args.transform.position = new THREE.Vector3(...actor.transform.position);
487
- }
488
- if (actor.transform.rotation) {
489
- args.transform.rotation = new THREE.Euler(...actor.transform.rotation);
490
- }
491
- if (actor.transform.scale) {
492
- args.transform.scale = new THREE.Vector3(...actor.transform.scale);
493
- }
494
- }
495
- if (actor.info) {
496
- args.actorInfo = {};
497
- if (actor.info.displayName) {
498
- args.actorInfo.displayName = actor.info.displayName;
499
- }
500
- if (actor.info.description) {
501
- args.actorInfo.description = actor.info.description;
502
- }
503
- }
504
- return args;
505
- };
506
-
507
- const sceneName = await getCurrentScene(editorId);
508
- const updatedActorsNum = await updateActors({
509
- sceneName,
510
- actorsToUpdate: actors.map(actorToUpdate => makeActorUpdateArg(actorToUpdate))
511
- });
512
-
513
- if (updatedActorsNum > 0) {
514
- requestEditorReload(editorId);
515
- }
516
-
517
- return {
518
- content: [{ type: 'text', text: `Completed, ${updatedActorsNum} actors updated` }]
519
- };
520
- }
521
- );
522
-
523
- // Start receiving messages on stdin and sending messages on stdout
524
- const transport = new StdioServerTransport();
525
- await server.connect(transport);
526
- server.server.sendLoggingMessage({level: 'info', data: 'starting up MCP server for Genesys'});
1
+ // How to use:
2
+ // it must be built first with: pnpm build
3
+ // open cursor settings - MCP servers, refresh and make sure "genesys" is connected
4
+ // and you're good to go with asking cursor to place primitives in a specified project
5
+ //
6
+ // for another MCP client (e.g. cline) that wants to use this, the command is: `pnpm exec tsc ./scripts/genesys/genesys-mcp.ts`
7
+ import pathlib from 'path';
8
+
9
+ import * as ENGINE from '@directivegames/genesys.js';
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import * as THREE from 'three';
13
+ import { z } from 'zod';
14
+
15
+
16
+ import { ActorInfoSchema, TransformSchema } from './common.js';
17
+ import { EditorIdSchema, getClientWebSocket, getCurrentScene } from './mcp/editor-functions.js';
18
+ import { addEditorTools } from './mcp/editor-tools.js';
19
+ import { getSceneState } from './mcp/get-scene-state.js';
20
+ import { assetDescriptions, AssetType, populateAssets, searchForAssets } from './mcp/search-assets.js';
21
+ import { mcpLogger } from './mcp/utils.js';
22
+ import { generateCode } from './misc.js';
23
+ import { addGltf, placeJsClassActor, placePrefab, placePrimitive, removeActors, updateActors } from './place-actors.js';
24
+
25
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
26
+
27
+
28
+ // Create an MCP server
29
+ const server = new McpServer({
30
+ name: 'Genesys-MCP',
31
+ version: '0.0.1'
32
+ }, {
33
+ capabilities: {
34
+ logging: {} // to support sendLoggingMessage
35
+ }
36
+ });
37
+
38
+
39
+ const classRule = `
40
+ The class name should start with '${ENGINE.Prefix.GAME}' or '${ENGINE.Prefix.ENGINE}' to indicate if it is a class from game project or engine.
41
+ If it starts with neither, it will be matched against all registered classes. If none is found, an error will be returned.
42
+ `;
43
+
44
+
45
+ function requestEditorReload(editorId?: string) {
46
+ const ws = getClientWebSocket(editorId);
47
+ ws.send(JSON.stringify({
48
+ command: 'reload'
49
+ }));
50
+ }
51
+
52
+ addEditorTools(server);
53
+
54
+ const primitiveTypes = Object.values(ENGINE.WorldCommands.PrimitiveType);
55
+
56
+ server.registerTool(
57
+ 'placePrimitive',
58
+ {
59
+ description: 'Place 3D primitives in the specified scene.',
60
+ inputSchema: {
61
+ editorId: EditorIdSchema,
62
+ primitiveActors: z.array(
63
+ z.object({
64
+ primitives: z.array(
65
+ z.object({
66
+ type: z.enum(primitiveTypes as [string, ...string[]])
67
+ .describe(`Type of primitive to place, one of: ${primitiveTypes.join(', ')}`),
68
+ transform: TransformSchema.optional().describe('Transform of the primitive relative to the actor'),
69
+ color: z.union([z.array(z.number()).length(3), z.string(), z.number()]).optional().describe('Color as [r, g, b], hex string, hex number, or X11 color name'),
70
+ }).strict().describe('A primitive to add to the actor.')
71
+ ).describe('A list of primitives to add to the actor.'),
72
+ info: ActorInfoSchema.describe('Information about the primitive actor.'),
73
+ transform: TransformSchema.optional().describe('Transform of the actor in the scene'),
74
+ }).strict().describe('An actor containing primitives to place.')
75
+ ).describe('A list of lists of primitives to add. Each inner list represents a single actor to place.'),
76
+ },
77
+ },
78
+ async ({ editorId, primitiveActors }) => {
79
+ let actorIds: string[] = [];
80
+ let sceneName: string;
81
+
82
+ {
83
+ using loggerContext = mcpLogger(server);
84
+ try {
85
+ sceneName = await getCurrentScene(editorId);
86
+ actorIds = await placePrimitive({
87
+ sceneName,
88
+ primitiveActors: primitiveActors.map(primitiveActor => ({
89
+ miscInfo: {
90
+ displayName: primitiveActor.info.displayName,
91
+ description: primitiveActor.info.description ?? '',
92
+ },
93
+ transform: {
94
+ position: new THREE.Vector3(...(primitiveActor.transform?.position ?? [0, 0, 0])),
95
+ rotation: new THREE.Euler(...(primitiveActor.transform?.rotation ?? [0, 0, 0])),
96
+ scale: new THREE.Vector3(...(primitiveActor.transform?.scale ?? [1, 1, 1])),
97
+ },
98
+ primitives: primitiveActor.primitives.map(primitive => ({
99
+ type: primitive.type,
100
+ transform: {
101
+ position: new THREE.Vector3(...(primitive.transform?.position ?? [0, 0, 0])),
102
+ rotation: new THREE.Euler(...(primitive.transform?.rotation ?? [0, 0, 0])),
103
+ scale: new THREE.Vector3(...(primitive.transform?.scale ?? [1, 1, 1])),
104
+ },
105
+ color: Array.isArray(primitive.color) ? new THREE.Color(...primitive.color) : new THREE.Color(primitive.color as (string | number)),
106
+ }))
107
+ }))
108
+ });
109
+ } catch (error) {
110
+ return {
111
+ isError: true,
112
+ content: [{ type: 'text', text: `${error}` }]
113
+ };
114
+ }
115
+
116
+ if (actorIds.length > 0) {
117
+ requestEditorReload(editorId);
118
+ }
119
+ }
120
+
121
+ const uuids = actorIds.join(', ');
122
+ const text = `Primitives placed successfully in ${sceneName}, placed actor UUIDs are [${uuids}]`;
123
+ return {
124
+ content: [{ type: 'text', text }]
125
+ };
126
+ }
127
+ );
128
+
129
+ server.registerTool(
130
+ 'removeActors',
131
+ {
132
+ description: 'Remove one or more actors from the scene by their UUIDs.',
133
+ inputSchema: {
134
+ editorId: EditorIdSchema,
135
+ actorIds: z.array(z.string()).describe('List of actor UUIDs to remove'),
136
+ },
137
+ },
138
+ async ({ editorId, actorIds }) => {
139
+ {
140
+ using loggerContext = mcpLogger(server);
141
+ const sceneName = await getCurrentScene(editorId);
142
+ await removeActors({ sceneName, actorIds });
143
+
144
+ if (actorIds.length > 0) {
145
+ requestEditorReload(editorId);
146
+ }
147
+ }
148
+ const text = `${actorIds.length} actors removed successfully`;
149
+ return {
150
+ content: [{ type: 'text', text }]
151
+ };
152
+ }
153
+ );
154
+
155
+
156
+ server.registerTool(
157
+ 'addGltf',
158
+ {
159
+ description: 'Add GLTF models to the scene.',
160
+ inputSchema: {
161
+ editorId: EditorIdSchema,
162
+ gltfs: z.array(
163
+ z.object({
164
+ path: z.string().describe(`Path or URL to the GLTF model to add.
165
+ Unless explicitly told by user, otherwise it should always be one of these
166
+ - fetched from the result of searchAssets tool
167
+ - copied from an existing path of an existing placed GLTF actor.
168
+ - use a verified URL to a GLTF model
169
+ Never make up a relative path to a file, because that won't be resolved by the editor.
170
+ `),
171
+ transform: TransformSchema.optional().describe('Transform of the GLTF model'),
172
+ info: ActorInfoSchema.describe('Information about the GLTF model actor.'),
173
+ }).strict().describe('A GLTF model to add.')
174
+ ).describe('A list of GLTF models to add.'),
175
+ },
176
+ },
177
+ async ({ editorId, gltfs }) => {
178
+ let actorIds: string[] = [];
179
+
180
+ {
181
+ using loggerContext = mcpLogger(server);
182
+ const sceneName = await getCurrentScene(editorId);
183
+ actorIds = await addGltf({
184
+ sceneName,
185
+ gltfs: gltfs.map(gltf => ({
186
+ path: gltf.path,
187
+ transform: {
188
+ position: new THREE.Vector3(...(gltf.transform?.position ?? [0, 0, 0])),
189
+ rotation: new THREE.Euler(...(gltf.transform?.rotation ?? [0, 0, 0])),
190
+ scale: new THREE.Vector3(...(gltf.transform?.scale ?? [1, 1, 1])),
191
+ },
192
+ miscInfo: {
193
+ displayName: gltf.info.displayName,
194
+ description: gltf.info.description ?? '',
195
+ },
196
+ }))
197
+ });
198
+
199
+ if (actorIds.length > 0) {
200
+ requestEditorReload(editorId);
201
+ }
202
+ }
203
+
204
+ const uuids = actorIds.join(', ');
205
+ const text = `GLTFs added successfully, placed actor UUIDs are [${uuids}]`;
206
+ return {
207
+ content: [{ type: 'text', text }]
208
+ };
209
+ }
210
+ );
211
+
212
+ server.registerTool(
213
+ 'placePrefab',
214
+ {
215
+ description: 'Place prefabs in the scene.',
216
+ inputSchema: {
217
+ editorId: EditorIdSchema,
218
+ prefabs: z.array(
219
+ z.object({
220
+ path: z.string().describe('Path to the prefab to place'),
221
+ transform: TransformSchema.optional().describe('Transform of the prefab'),
222
+ info: ActorInfoSchema.describe('Information about the prefab actor.'),
223
+ }).strict().describe('A prefab to place.')
224
+ ).describe('A list of prefabs to place. Each prefab can have a transform.'),
225
+ },
226
+ },
227
+ async ({ editorId, prefabs }) => {
228
+ let actorIds: string[] = [];
229
+
230
+ {
231
+ using loggerContext = mcpLogger(server);
232
+ const sceneName = await getCurrentScene(editorId);
233
+ actorIds = await placePrefab({
234
+ sceneName,
235
+ prefabs: prefabs.map(prefab => ({
236
+ path: prefab.path,
237
+ transform: {
238
+ position: new THREE.Vector3(...(prefab.transform?.position ?? [0, 0, 0])),
239
+ rotation: new THREE.Euler(...(prefab.transform?.rotation ?? [0, 0, 0])),
240
+ scale: new THREE.Vector3(...(prefab.transform?.scale ?? [1, 1, 1])),
241
+ },
242
+ miscInfo: {
243
+ displayName: prefab.info.displayName,
244
+ description: prefab.info.description ?? '',
245
+ },
246
+ }))
247
+ });
248
+
249
+ if (actorIds.length > 0) {
250
+ requestEditorReload(editorId);
251
+ }
252
+ }
253
+ const uuids = actorIds.join(', ');
254
+ const text = `Prefabs placed successfully, placed actor UUIDs are [${uuids}]`;
255
+ return {
256
+ content: [{ type: 'text', text }]
257
+ };
258
+ }
259
+ );
260
+
261
+
262
+ server.registerTool(
263
+ 'searchAssets',
264
+ {
265
+ description: `Search for assets in the project and engine directories.
266
+ When the user requests to place something in the scene, unless it is clearly a primitive, always try calling this first to see if any assets are fitting the request.
267
+ Before using the assets found, **always** call the \`getAssetsMetadata\` tool to get the metadata about the assets, unless the given metadata is already fetched.
268
+ Here is the list of assets types, including:
269
+ ${Object.entries(assetDescriptions).map(([type, description]) => `- ${type}: ${description}`).join('\n')}
270
+ `,
271
+ inputSchema: {
272
+ acceptedTypes: z.array(z.enum(Object.values(AssetType) as [string, ...string[]])).describe('List of types of assets to search for. An empty array means all types.'),
273
+ searchKeywords: z.array(z.string()).describe('List of keywords to search for in the assets. Wildcards are not supported, use multiple keywords to search for multiple terms. Use an empty array to search for all assets.'),
274
+ },
275
+ },
276
+ async ({ acceptedTypes, searchKeywords }) => {
277
+ using loggerContext = mcpLogger(server);
278
+
279
+ const dirs = [`${ENGINE.PROJECT_PATH_PREFIX}/assets`, `${ENGINE.ENGINE_PATH_PREFIX}/assets`];
280
+ const {assets, metadataDescription} = await searchForAssets(dirs, acceptedTypes as AssetType[], searchKeywords);
281
+
282
+ const result: CallToolResult = {content: []};
283
+ if (metadataDescription && Object.keys(metadataDescription).length > 0) {
284
+ result.content.push(
285
+ { type: 'text', text: `Here is the description of the metadata fields of the assets, per asset type:\n${JSON.stringify(metadataDescription, null, 2)}` }
286
+ );
287
+ }
288
+
289
+ result.content.push({ type: 'text', text: `Assets:\n${JSON.stringify(assets, null, 2)}` });
290
+ return result;
291
+ }
292
+ );
293
+
294
+
295
+ server.registerTool(
296
+ 'getAssetsMetadata',
297
+ {
298
+ description: 'Get metadata for the assets found by the `searchAssets` tool.',
299
+ inputSchema: {
300
+ assetPaths: z.array(z.string()).describe('List of asset paths to get metadata for. The asset paths are the keys retrieved from the result of \`searchAssets\` tool.'),
301
+ },
302
+ },
303
+ async ({ assetPaths }) => {
304
+ using loggerContext = mcpLogger(server);
305
+
306
+ try {
307
+ const assetsInfo = await populateAssets(assetPaths, { peek: false });
308
+ const metadataDescription = assetsInfo.metadataDescription;
309
+ const assets = assetsInfo.assets;
310
+
311
+ const result: CallToolResult = {content: []};
312
+ if (metadataDescription && Object.keys(metadataDescription).length > 0) {
313
+ result.content.push(
314
+ { type: 'text', text: `Here is the description of the metadata fields of the assets, per asset type:\n${JSON.stringify(metadataDescription, null, 2)}` }
315
+ );
316
+ }
317
+
318
+ result.content.push({ type: 'text', text: `Assets:\n${JSON.stringify(assets, null, 2)}` });
319
+ return result;
320
+
321
+ } catch (error) {
322
+ return {
323
+ isError: true,
324
+ content: [{ type: 'text', text: `Error getting assets metadata: ${error}` }]
325
+ };
326
+ }
327
+ }
328
+ );
329
+
330
+
331
+ server.registerTool(
332
+ 'placeJsClassActor',
333
+ {
334
+ description: 'Place a JavaScript game or engine class actor in the scene.',
335
+ inputSchema: {
336
+ editorId: EditorIdSchema,
337
+ actors: z.array(
338
+ z.object({
339
+ className: z.string().describe(`The name of the JavaScript class to place. ${classRule}`),
340
+ constructorParams: z.array(z.record(z.any())).optional()
341
+ .describe(`Optional constructor parameters to pass to the actor class.
342
+ Use the getAssetsMetadata tool to discover what parameters each actor class requires,
343
+ and the parameters should be an array that corresponds to the "jsClassConstructorParams" from the result of the getAssetsMetadata tool.
344
+ If the actor class has no constructor parameters, the array should be empty.
345
+ `),
346
+ info: ActorInfoSchema.describe('Information about the actor.'),
347
+ }).strict().describe('A JavaScript class actor to place.')
348
+ ).describe('A list of JavaScript class actors to place. Each actor can have a transform and constructor parameters.'),
349
+ },
350
+ },
351
+ async ({ editorId, actors }) => {
352
+ using loggerContext = mcpLogger(server);
353
+
354
+ const sceneName = await getCurrentScene(editorId);
355
+
356
+ const result = await placeJsClassActor({sceneName, jsClasses: actors.map(actor => ({
357
+ className: actor.className,
358
+ constructorParams: actor.constructorParams,
359
+ actorInfo: actor.info,
360
+ }))});
361
+
362
+ if (result.length > 0) {
363
+ requestEditorReload(editorId);
364
+ }
365
+
366
+ const uuids = result.join(', ');
367
+ const text = `JavaScript class actors placed successfully, placed actor UUIDs are [${uuids}]`;
368
+ return {
369
+ content: [{ type: 'text', text }]
370
+ };
371
+ }
372
+ );
373
+
374
+
375
+ server.registerTool(
376
+ 'generateTemplateCode',
377
+ {
378
+ description: 'Generate template code for a class of actor or component. When the user asks to create a new actor or component, always try calling this first to see if any template code is available, and iterate on the code it generates until it is correct.',
379
+ inputSchema: {
380
+ className: z.string().describe('The name of the class to generate template code for.'),
381
+ filePath: z.string().describe(`The path to the file to generate the template code for.
382
+ If it is a relative path, it will be relative to the 'src/' directory, so don't include the extra 'src/' in the path.
383
+ The file name should be lower camel case, such as 'myActor.ts', or 'fancyMovementComponent.ts'.`
384
+ ),
385
+ baseClassName: z.string().describe(`The name of the base class to generate the template code for. ${classRule}`),
386
+ },
387
+ },
388
+ async ({ className, filePath, baseClassName }) => {
389
+ using loggerContext = mcpLogger(server);
390
+
391
+ const fullFilePath = pathlib.isAbsolute(filePath) ? filePath : pathlib.join('src', filePath);
392
+ let success = false;
393
+ try {
394
+ success = await generateCode(className, fullFilePath, baseClassName);
395
+ }
396
+ catch (error) {
397
+ return {
398
+ isError: true,
399
+ content: [{ type: 'text', text: `Failed to generate code: ${error}` }]
400
+ };
401
+ }
402
+
403
+ if (success) {
404
+ return {
405
+ content: [
406
+ {
407
+ type: 'text',
408
+ text: `template code for class ${className} has been generated at ${fullFilePath}`
409
+ }
410
+ ]
411
+ };
412
+ }
413
+
414
+ return {
415
+ isError: true,
416
+ content: [{ type: 'text', text: `Error: Template code for class ${className} has not been generated.` }]
417
+ };
418
+ }
419
+ );
420
+
421
+
422
+ server.registerTool(
423
+ 'getSceneState',
424
+ {
425
+ description: `
426
+ Get the current state of the scene to understand the state of the current scene and its actors.
427
+ **DO NOT** try reading the scene file directly unless explicitly asked by the user.
428
+ The first time calling this tool, always call without specifiedActors and getDetailedComponentsInfo to have an overview of the scene.
429
+ And then call this tool with \`specifiedActors\`, and set \`getDetailedComponentsInfo\` to true, to get more detailed information about specific actors, including their bounding boxes info, etc.
430
+ `,
431
+ inputSchema: {
432
+ editorId: EditorIdSchema,
433
+ specifiedActors: z.array(z.string()).optional().describe('A list of actor UUIDs. If specified, actors not in this list will contain only very basic information, namely their UUID and name.'),
434
+ getDetailedComponentsInfo: z.boolean().optional().describe('If true, will contain detailed information about their properties, otherwise they will contain only their UUID and name. If specifiedActors is present, only actors in this list will have detailed information.'),
435
+ },
436
+ },
437
+ async ({editorId, specifiedActors, getDetailedComponentsInfo}) => {
438
+ using loggerContext = mcpLogger(server);
439
+
440
+ try {
441
+ const sceneName = await getCurrentScene(editorId);
442
+ const state = await getSceneState(sceneName, specifiedActors, getDetailedComponentsInfo);
443
+ function roundNumbersReplacer(key: string, value: any) {
444
+ if (typeof value === 'number') {
445
+ return Number(value.toFixed(5));
446
+ }
447
+ return value;
448
+ }
449
+ return {
450
+ content: [{ type: 'text', text: `The following is the state of the scene currently opened in the editor:\n\`\`\`json\n${JSON.stringify(state, roundNumbersReplacer)}\n\`\`\`` }]
451
+ };
452
+ }
453
+ catch (error) {
454
+ return {
455
+ isError: true,
456
+ content: [{ type: 'text', text: `Error getting scene state: ${error}` }]
457
+ };
458
+ }
459
+ }
460
+ );
461
+
462
+ server.registerTool(
463
+ 'updateActors',
464
+ {
465
+ description: 'Update the properties of existing actors in the scene.',
466
+ inputSchema: {
467
+ editorId: EditorIdSchema,
468
+ actors: z.array(
469
+ z.object({
470
+ uuid: z.string().describe('UUID of the actor to update'),
471
+ transform: TransformSchema.optional().describe('New transform of the actor'),
472
+ info: ActorInfoSchema.optional().describe('New information about the actor.'),
473
+ }).strict().describe('An actor to update with its new properties.')
474
+ ).describe('A list of actors to update with their new properties.'),
475
+ },
476
+ },
477
+ async ({editorId, actors}) => {
478
+ using loggerContext = mcpLogger(server);
479
+
480
+ const makeActorUpdateArg = (actor: typeof actors[number]) => {
481
+ const args: any = {};
482
+ args.uuid = actor.uuid;
483
+ if (actor.transform) {
484
+ args.transform = {};
485
+ if (actor.transform.position) {
486
+ args.transform.position = new THREE.Vector3(...actor.transform.position);
487
+ }
488
+ if (actor.transform.rotation) {
489
+ args.transform.rotation = new THREE.Euler(...actor.transform.rotation);
490
+ }
491
+ if (actor.transform.scale) {
492
+ args.transform.scale = new THREE.Vector3(...actor.transform.scale);
493
+ }
494
+ }
495
+ if (actor.info) {
496
+ args.actorInfo = {};
497
+ if (actor.info.displayName) {
498
+ args.actorInfo.displayName = actor.info.displayName;
499
+ }
500
+ if (actor.info.description) {
501
+ args.actorInfo.description = actor.info.description;
502
+ }
503
+ }
504
+ return args;
505
+ };
506
+
507
+ const sceneName = await getCurrentScene(editorId);
508
+ const updatedActorsNum = await updateActors({
509
+ sceneName,
510
+ actorsToUpdate: actors.map(actorToUpdate => makeActorUpdateArg(actorToUpdate))
511
+ });
512
+
513
+ if (updatedActorsNum > 0) {
514
+ requestEditorReload(editorId);
515
+ }
516
+
517
+ return {
518
+ content: [{ type: 'text', text: `Completed, ${updatedActorsNum} actors updated` }]
519
+ };
520
+ }
521
+ );
522
+
523
+ // Start receiving messages on stdin and sending messages on stdout
524
+ const transport = new StdioServerTransport();
525
+ await server.connect(transport);
526
+ server.server.sendLoggingMessage({level: 'info', data: 'starting up MCP server for Genesys'});