@directivegames/genesys.sdk 3.2.2 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -60
- package/dist/src/core/cli.js +22 -22
- package/dist/src/templates/scripts/genesys/genesys-mcp.js +25 -25
- package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +4 -4
- package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +30 -30
- package/package.json +176 -176
- package/scripts/post-install.ts +143 -143
- package/src/asset-pack/.gitattributes +88 -88
- package/src/asset-pack/eslint.config.js +45 -45
- package/src/asset-pack/gitignore +11 -11
- package/src/asset-pack/scripts/postinstall.ts +81 -81
- package/src/asset-pack/tsconfig.json +33 -33
- package/src/templates/.cursor/mcp.json +20 -20
- package/src/templates/.cursorignore +2 -2
- package/src/templates/.gitattributes +88 -88
- package/src/templates/.vscode/settings.json +6 -6
- package/src/templates/AGENTS.md +86 -86
- package/src/templates/README.md +24 -24
- package/src/templates/eslint.config.js +45 -45
- package/src/templates/gitignore +11 -11
- package/src/templates/index.html +34 -34
- package/src/templates/pnpm-lock.yaml +3676 -3676
- package/src/templates/scripts/genesys/build-project.ts +51 -51
- package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -272
- package/src/templates/scripts/genesys/common.ts +46 -46
- package/src/templates/scripts/genesys/const.ts +9 -9
- package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -11
- package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -146
- package/src/templates/scripts/genesys/dev/launcher.ts +46 -46
- package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -229
- package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -84
- package/src/templates/scripts/genesys/doc-server.ts +16 -16
- package/src/templates/scripts/genesys/genesys-mcp.ts +526 -526
- package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -86
- package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -151
- package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -73
- package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -35
- package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -30
- package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -858
- package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -380
- package/src/templates/scripts/genesys/mcp/utils.ts +281 -281
- package/src/templates/scripts/genesys/misc.ts +42 -42
- package/src/templates/scripts/genesys/mock.ts +6 -6
- package/src/templates/scripts/genesys/place-actors.ts +179 -179
- package/src/templates/scripts/genesys/post-install.ts +30 -30
- package/src/templates/scripts/genesys/prefab.schema.json +84 -84
- package/src/templates/scripts/genesys/remove-engine-comments.ts +134 -134
- package/src/templates/scripts/genesys/run-mcp-inspector.bat +4 -4
- package/src/templates/scripts/genesys/storageProvider.ts +182 -182
- package/src/templates/scripts/genesys/validate-prefabs.ts +138 -138
- package/src/templates/src/index.ts +22 -22
- package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/firstPerson/src/game.ts +39 -39
- package/src/templates/src/templates/firstPerson/src/player.ts +63 -63
- package/src/templates/src/templates/fps/assets/default.genesys-scene +9459 -9459
- package/src/templates/src/templates/fps/src/game.ts +39 -39
- package/src/templates/src/templates/fps/src/player.ts +69 -69
- package/src/templates/src/templates/fps/src/weapon.ts +54 -54
- package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/freeCamera/src/game.ts +39 -39
- package/src/templates/src/templates/freeCamera/src/player.ts +45 -45
- package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +121 -121
- package/src/templates/src/templates/sideScroller/src/const.ts +45 -45
- package/src/templates/src/templates/sideScroller/src/game.ts +122 -122
- package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -361
- package/src/templates/src/templates/sideScroller/src/player.ts +125 -125
- package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/thirdPerson/src/game.ts +39 -39
- package/src/templates/src/templates/thirdPerson/src/player.ts +61 -61
- package/src/templates/src/templates/vehicle/assets/default.genesys-scene +225 -225
- package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -145
- package/src/templates/src/templates/vehicle/src/game.ts +43 -43
- package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -191
- package/src/templates/src/templates/vehicle/src/player.ts +109 -109
- package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -266
- package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -101
- package/src/templates/src/templates/vr-game/assets/default.genesys-scene +246 -246
- package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -1
- package/src/templates/src/templates/vr-game/src/game.ts +66 -66
- package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -26
- package/src/templates/tsconfig.json +34 -34
- package/src/templates/vite.config.ts +52 -52
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import { getProjectRoot } from '../common.js';
|
|
5
|
-
|
|
6
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const enginePathName = 'node_modules/genesys.js';
|
|
10
|
-
const docPathName = 'node_modules/genesys.js/docs';
|
|
11
|
-
const enginePath = path.join(getProjectRoot(), enginePathName);
|
|
12
|
-
const docsPath = path.join(getProjectRoot(), docPathName);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function listDocs(): string[] {
|
|
16
|
-
const docs: string[] = [];
|
|
17
|
-
for (const filePath of fs.readdirSync(docsPath, { recursive: true })) {
|
|
18
|
-
if (typeof filePath !== 'string') {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (fs.statSync(path.join(docsPath, filePath)).isDirectory()) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
docs.push(filePath.replace(/\\/g, '/'));
|
|
25
|
-
}
|
|
26
|
-
return docs.filter(doc => !doc.includes('deprecated'));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function inlineFileReferences(content: string): string {
|
|
30
|
-
// Regular expression to match <file path={relative/path/to/file}>
|
|
31
|
-
const fileRefRegex = /<file path=\{([^}]+)\}>/g;
|
|
32
|
-
|
|
33
|
-
return content.replace(fileRefRegex, (match, filePath) => {
|
|
34
|
-
try {
|
|
35
|
-
const fullPath = path.join(enginePath, filePath);
|
|
36
|
-
|
|
37
|
-
// Check if file exists
|
|
38
|
-
if (!fs.existsSync(fullPath)) {
|
|
39
|
-
return `<!-- File not found: ${filePath} -->`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Read the file content
|
|
43
|
-
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
44
|
-
let inlinedContent = '<file_contents>\n';
|
|
45
|
-
inlinedContent += `\`\`\`path={${filePath}}\n`;
|
|
46
|
-
inlinedContent += fileContent;
|
|
47
|
-
inlinedContent += '\n\`\`\`\n';
|
|
48
|
-
inlinedContent += '</file_contents>\n';
|
|
49
|
-
// Return the inlined content with proper markdown formatting
|
|
50
|
-
return inlinedContent;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return `<!-- Error reading file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'} -->`;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function extractInstruction(content: string): string | undefined {
|
|
58
|
-
// Look for "## Instruction" section and extract the next line
|
|
59
|
-
const instructionRegex = /^## Instruction\s*\n(.+)$/m;
|
|
60
|
-
const match = content.match(instructionRegex);
|
|
61
|
-
return match ? match[1].trim() : undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function addDocTools(server: McpServer) {
|
|
65
|
-
server.registerTool(
|
|
66
|
-
'listDocs',
|
|
67
|
-
{
|
|
68
|
-
description: 'Lists genesys.js engine development documentations.',
|
|
69
|
-
},
|
|
70
|
-
async () => {
|
|
71
|
-
const docPaths = listDocs();
|
|
72
|
-
const docs = [];
|
|
73
|
-
for (const docPath of docPaths) {
|
|
74
|
-
const docContent = fs.readFileSync(path.join(docsPath, docPath), 'utf8');
|
|
75
|
-
const instruction = extractInstruction(docContent);
|
|
76
|
-
docs.push({
|
|
77
|
-
path: path.join(docPathName, docPath),
|
|
78
|
-
instruction,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
content: [{ type: 'text' as const, text: JSON.stringify(docs, null, 2) }]
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { getProjectRoot } from '../common.js';
|
|
5
|
+
|
|
6
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const enginePathName = 'node_modules/genesys.js';
|
|
10
|
+
const docPathName = 'node_modules/genesys.js/docs';
|
|
11
|
+
const enginePath = path.join(getProjectRoot(), enginePathName);
|
|
12
|
+
const docsPath = path.join(getProjectRoot(), docPathName);
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function listDocs(): string[] {
|
|
16
|
+
const docs: string[] = [];
|
|
17
|
+
for (const filePath of fs.readdirSync(docsPath, { recursive: true })) {
|
|
18
|
+
if (typeof filePath !== 'string') {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (fs.statSync(path.join(docsPath, filePath)).isDirectory()) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
docs.push(filePath.replace(/\\/g, '/'));
|
|
25
|
+
}
|
|
26
|
+
return docs.filter(doc => !doc.includes('deprecated'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inlineFileReferences(content: string): string {
|
|
30
|
+
// Regular expression to match <file path={relative/path/to/file}>
|
|
31
|
+
const fileRefRegex = /<file path=\{([^}]+)\}>/g;
|
|
32
|
+
|
|
33
|
+
return content.replace(fileRefRegex, (match, filePath) => {
|
|
34
|
+
try {
|
|
35
|
+
const fullPath = path.join(enginePath, filePath);
|
|
36
|
+
|
|
37
|
+
// Check if file exists
|
|
38
|
+
if (!fs.existsSync(fullPath)) {
|
|
39
|
+
return `<!-- File not found: ${filePath} -->`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Read the file content
|
|
43
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
44
|
+
let inlinedContent = '<file_contents>\n';
|
|
45
|
+
inlinedContent += `\`\`\`path={${filePath}}\n`;
|
|
46
|
+
inlinedContent += fileContent;
|
|
47
|
+
inlinedContent += '\n\`\`\`\n';
|
|
48
|
+
inlinedContent += '</file_contents>\n';
|
|
49
|
+
// Return the inlined content with proper markdown formatting
|
|
50
|
+
return inlinedContent;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return `<!-- Error reading file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'} -->`;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractInstruction(content: string): string | undefined {
|
|
58
|
+
// Look for "## Instruction" section and extract the next line
|
|
59
|
+
const instructionRegex = /^## Instruction\s*\n(.+)$/m;
|
|
60
|
+
const match = content.match(instructionRegex);
|
|
61
|
+
return match ? match[1].trim() : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function addDocTools(server: McpServer) {
|
|
65
|
+
server.registerTool(
|
|
66
|
+
'listDocs',
|
|
67
|
+
{
|
|
68
|
+
description: 'Lists genesys.js engine development documentations.',
|
|
69
|
+
},
|
|
70
|
+
async () => {
|
|
71
|
+
const docPaths = listDocs();
|
|
72
|
+
const docs = [];
|
|
73
|
+
for (const docPath of docPaths) {
|
|
74
|
+
const docContent = fs.readFileSync(path.join(docsPath, docPath), 'utf8');
|
|
75
|
+
const instruction = extractInstruction(docContent);
|
|
76
|
+
docs.push({
|
|
77
|
+
path: path.join(docPathName, docPath),
|
|
78
|
+
instruction,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: 'text' as const, text: JSON.stringify(docs, null, 2) }]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import * as ENGINE from 'genesys.js';
|
|
5
|
-
import getPort from 'get-port';
|
|
6
|
-
import { nanoid } from 'nanoid';
|
|
7
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
|
|
10
|
-
import { StorageProvider } from '../storageProvider.js';
|
|
11
|
-
|
|
12
|
-
const clients = new Map<string, WebSocket>();
|
|
13
|
-
|
|
14
|
-
// prefer 8765 to 8770
|
|
15
|
-
const port = await getPort({port: [8765, 8766, 8767, 8768, 8769, 8770]});
|
|
16
|
-
const wss = new WebSocketServer({ port });
|
|
17
|
-
const portFile = path.join(ENGINE.PROJECT_PATH_PREFIX, `${process.argv[2] ?? ''}.mcp-port`);
|
|
18
|
-
|
|
19
|
-
wss.on('error', error => {
|
|
20
|
-
console.error('WebSocket server error:', error);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
wss.on('listening', () => {
|
|
24
|
-
new StorageProvider().uploadFile(ENGINE.AssetPath.fromString(portFile), port.toString());
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
interface ClientResponse {
|
|
28
|
-
requestId: string;
|
|
29
|
-
data: any;
|
|
30
|
-
}
|
|
31
|
-
const pendingRequests = new Map<string, (data: any) => void>();
|
|
32
|
-
|
|
33
|
-
wss.on('connection', (ws) => {
|
|
34
|
-
let clientId: string | null = null;
|
|
35
|
-
|
|
36
|
-
ws.on('message', (message) => {
|
|
37
|
-
try {
|
|
38
|
-
const data = JSON.parse(message.toString());
|
|
39
|
-
|
|
40
|
-
// Client registers itself
|
|
41
|
-
if (data.type === 'register' && typeof data.clientId === 'string') {
|
|
42
|
-
clientId = data.clientId;
|
|
43
|
-
clients.set(clientId!, ws);
|
|
44
|
-
// console.log(`Client registered: ${clientId}`);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
const resolver = pendingRequests.get(data.requestId);
|
|
48
|
-
if (resolver) {
|
|
49
|
-
resolver(data);
|
|
50
|
-
pendingRequests.delete(data.requestId);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch (err) {
|
|
54
|
-
// console.error('Invalid message:', message.toString());
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
ws.on('close', () => {
|
|
59
|
-
if (clientId) {
|
|
60
|
-
clients.delete(clientId);
|
|
61
|
-
// console.log(`Client disconnected: ${clientId}`);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// when cursor exits
|
|
67
|
-
process.stdin.on('end', shutdown);
|
|
68
|
-
process.stdin.on('close', shutdown);
|
|
69
|
-
|
|
70
|
-
// normally when process exits
|
|
71
|
-
process.on('SIGTERM', shutdown);
|
|
72
|
-
process.on('SIGINT', shutdown); // Ctrl+C
|
|
73
|
-
process.on('SIGQUIT', shutdown);
|
|
74
|
-
|
|
75
|
-
function shutdown() {
|
|
76
|
-
wss.close(() => {
|
|
77
|
-
new StorageProvider().deleteFile(ENGINE.AssetPath.fromString(portFile));
|
|
78
|
-
process.exit(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
wss.clients.forEach((client) => {
|
|
82
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
83
|
-
client.terminate(); // force close
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
clients.clear();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const EditorIdSchema = z.string().optional().describe(`
|
|
90
|
-
ID of the editor that should respond.
|
|
91
|
-
If not provided, will proceed if there is only one editor connected, will return error if there are multiple editors connected.
|
|
92
|
-
Can always try without providing this, and only ask the user to provide it if there are multiple editors connected.
|
|
93
|
-
`);
|
|
94
|
-
|
|
95
|
-
export function getClientWebSocket(editorId?: string | null) {
|
|
96
|
-
if (clients.size === 0) {
|
|
97
|
-
throw new Error('No editors connected. Please make sure the Genesys editor is running and connected to the MCP.');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const editorIds = Array.from(clients.keys());
|
|
101
|
-
if (!editorId) {
|
|
102
|
-
if (clients.size > 1) {
|
|
103
|
-
throw new Error(`Multiple editors connected, please specify editorId. Available editor IDs: ${editorIds}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// only one editor connected, use its ID
|
|
107
|
-
editorId = editorIds[0];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const ws = clients.get(editorId);
|
|
111
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
112
|
-
return ws;
|
|
113
|
-
} else {
|
|
114
|
-
throw new Error(`Editor with ID ${editorId} is not connected or not ready. Please specify a valid editor ID, available editor IDs: ${editorIds}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
export async function getCurrentScene(editorId?: string | null): Promise<string> {
|
|
120
|
-
let ws: WebSocket;
|
|
121
|
-
try {
|
|
122
|
-
ws = getClientWebSocket(editorId);
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
return Promise.reject(error);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const requestId = nanoid();
|
|
129
|
-
const message = {
|
|
130
|
-
command: 'getCurrentScene',
|
|
131
|
-
requestId,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
ws.send(JSON.stringify(message));
|
|
135
|
-
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
const timeout = setTimeout(() => {
|
|
138
|
-
pendingRequests.delete(requestId);
|
|
139
|
-
reject(new Error('Timeout waiting for client to response with current scene'));
|
|
140
|
-
}, 500); // 500 ms timeout
|
|
141
|
-
|
|
142
|
-
pendingRequests.set(requestId, (data: any) => {
|
|
143
|
-
clearTimeout(timeout);
|
|
144
|
-
if ('currentScene' in data) {
|
|
145
|
-
resolve(data.currentScene);
|
|
146
|
-
} else {
|
|
147
|
-
reject(new Error('Invalid response from editor'));
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
}
|
|
1
|
+
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import * as ENGINE from 'genesys.js';
|
|
5
|
+
import getPort from 'get-port';
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
import { StorageProvider } from '../storageProvider.js';
|
|
11
|
+
|
|
12
|
+
const clients = new Map<string, WebSocket>();
|
|
13
|
+
|
|
14
|
+
// prefer 8765 to 8770
|
|
15
|
+
const port = await getPort({port: [8765, 8766, 8767, 8768, 8769, 8770]});
|
|
16
|
+
const wss = new WebSocketServer({ port });
|
|
17
|
+
const portFile = path.join(ENGINE.PROJECT_PATH_PREFIX, `${process.argv[2] ?? ''}.mcp-port`);
|
|
18
|
+
|
|
19
|
+
wss.on('error', error => {
|
|
20
|
+
console.error('WebSocket server error:', error);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
wss.on('listening', () => {
|
|
24
|
+
new StorageProvider().uploadFile(ENGINE.AssetPath.fromString(portFile), port.toString());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface ClientResponse {
|
|
28
|
+
requestId: string;
|
|
29
|
+
data: any;
|
|
30
|
+
}
|
|
31
|
+
const pendingRequests = new Map<string, (data: any) => void>();
|
|
32
|
+
|
|
33
|
+
wss.on('connection', (ws) => {
|
|
34
|
+
let clientId: string | null = null;
|
|
35
|
+
|
|
36
|
+
ws.on('message', (message) => {
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(message.toString());
|
|
39
|
+
|
|
40
|
+
// Client registers itself
|
|
41
|
+
if (data.type === 'register' && typeof data.clientId === 'string') {
|
|
42
|
+
clientId = data.clientId;
|
|
43
|
+
clients.set(clientId!, ws);
|
|
44
|
+
// console.log(`Client registered: ${clientId}`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const resolver = pendingRequests.get(data.requestId);
|
|
48
|
+
if (resolver) {
|
|
49
|
+
resolver(data);
|
|
50
|
+
pendingRequests.delete(data.requestId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// console.error('Invalid message:', message.toString());
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ws.on('close', () => {
|
|
59
|
+
if (clientId) {
|
|
60
|
+
clients.delete(clientId);
|
|
61
|
+
// console.log(`Client disconnected: ${clientId}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// when cursor exits
|
|
67
|
+
process.stdin.on('end', shutdown);
|
|
68
|
+
process.stdin.on('close', shutdown);
|
|
69
|
+
|
|
70
|
+
// normally when process exits
|
|
71
|
+
process.on('SIGTERM', shutdown);
|
|
72
|
+
process.on('SIGINT', shutdown); // Ctrl+C
|
|
73
|
+
process.on('SIGQUIT', shutdown);
|
|
74
|
+
|
|
75
|
+
function shutdown() {
|
|
76
|
+
wss.close(() => {
|
|
77
|
+
new StorageProvider().deleteFile(ENGINE.AssetPath.fromString(portFile));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
wss.clients.forEach((client) => {
|
|
82
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
83
|
+
client.terminate(); // force close
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
clients.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const EditorIdSchema = z.string().optional().describe(`
|
|
90
|
+
ID of the editor that should respond.
|
|
91
|
+
If not provided, will proceed if there is only one editor connected, will return error if there are multiple editors connected.
|
|
92
|
+
Can always try without providing this, and only ask the user to provide it if there are multiple editors connected.
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
export function getClientWebSocket(editorId?: string | null) {
|
|
96
|
+
if (clients.size === 0) {
|
|
97
|
+
throw new Error('No editors connected. Please make sure the Genesys editor is running and connected to the MCP.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const editorIds = Array.from(clients.keys());
|
|
101
|
+
if (!editorId) {
|
|
102
|
+
if (clients.size > 1) {
|
|
103
|
+
throw new Error(`Multiple editors connected, please specify editorId. Available editor IDs: ${editorIds}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// only one editor connected, use its ID
|
|
107
|
+
editorId = editorIds[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ws = clients.get(editorId);
|
|
111
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
112
|
+
return ws;
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error(`Editor with ID ${editorId} is not connected or not ready. Please specify a valid editor ID, available editor IDs: ${editorIds}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
export async function getCurrentScene(editorId?: string | null): Promise<string> {
|
|
120
|
+
let ws: WebSocket;
|
|
121
|
+
try {
|
|
122
|
+
ws = getClientWebSocket(editorId);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return Promise.reject(error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const requestId = nanoid();
|
|
129
|
+
const message = {
|
|
130
|
+
command: 'getCurrentScene',
|
|
131
|
+
requestId,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
ws.send(JSON.stringify(message));
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const timeout = setTimeout(() => {
|
|
138
|
+
pendingRequests.delete(requestId);
|
|
139
|
+
reject(new Error('Timeout waiting for client to response with current scene'));
|
|
140
|
+
}, 500); // 500 ms timeout
|
|
141
|
+
|
|
142
|
+
pendingRequests.set(requestId, (data: any) => {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
if ('currentScene' in data) {
|
|
145
|
+
resolve(data.currentScene);
|
|
146
|
+
} else {
|
|
147
|
+
reject(new Error('Invalid response from editor'));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
import { EditorIdSchema, getClientWebSocket, getCurrentScene } from './editor-functions.js';
|
|
5
|
-
import { mcpLogger } from './utils.js';
|
|
6
|
-
|
|
7
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
-
import type { WebSocket } from 'ws';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export function addEditorTools(server: McpServer) {
|
|
12
|
-
server.registerTool(
|
|
13
|
-
'selectActors',
|
|
14
|
-
{
|
|
15
|
-
description: 'Make the editor to select an actor by its UUID.',
|
|
16
|
-
inputSchema: {
|
|
17
|
-
actorIds: z.array(z.string()).describe('UUIDs of the actors to select'),
|
|
18
|
-
editorId: EditorIdSchema,
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
async ({actorIds, editorId}) => {
|
|
22
|
-
using loggerContext = mcpLogger(server);
|
|
23
|
-
|
|
24
|
-
let ws: WebSocket;
|
|
25
|
-
try {
|
|
26
|
-
ws = getClientWebSocket(editorId);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
return {
|
|
29
|
-
isError: true,
|
|
30
|
-
content: [{ type: 'text', text: `Error connecting to client: ${error}` }]
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
ws.send(JSON.stringify({
|
|
35
|
-
command: 'selectActorsByIds',
|
|
36
|
-
actorIds
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: 'text', text: 'Success' }]
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
server.registerTool(
|
|
47
|
-
'getCurrentScene',
|
|
48
|
-
{
|
|
49
|
-
description: 'Get the current scene opened in the editor.',
|
|
50
|
-
inputSchema: {
|
|
51
|
-
editorId: EditorIdSchema
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
async ({editorId}) => {
|
|
55
|
-
using loggerContext = mcpLogger(server);
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const currentScene = await getCurrentScene(editorId);
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: 'text', text: `Current scene: ${currentScene}` }]
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
return {
|
|
65
|
-
isError: true,
|
|
66
|
-
content: [{ type: 'text', text: `Failed to get the current scene: ${error}` }]
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { EditorIdSchema, getClientWebSocket, getCurrentScene } from './editor-functions.js';
|
|
5
|
+
import { mcpLogger } from './utils.js';
|
|
6
|
+
|
|
7
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import type { WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export function addEditorTools(server: McpServer) {
|
|
12
|
+
server.registerTool(
|
|
13
|
+
'selectActors',
|
|
14
|
+
{
|
|
15
|
+
description: 'Make the editor to select an actor by its UUID.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
actorIds: z.array(z.string()).describe('UUIDs of the actors to select'),
|
|
18
|
+
editorId: EditorIdSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
async ({actorIds, editorId}) => {
|
|
22
|
+
using loggerContext = mcpLogger(server);
|
|
23
|
+
|
|
24
|
+
let ws: WebSocket;
|
|
25
|
+
try {
|
|
26
|
+
ws = getClientWebSocket(editorId);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
isError: true,
|
|
30
|
+
content: [{ type: 'text', text: `Error connecting to client: ${error}` }]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ws.send(JSON.stringify({
|
|
35
|
+
command: 'selectActorsByIds',
|
|
36
|
+
actorIds
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: 'Success' }]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
server.registerTool(
|
|
47
|
+
'getCurrentScene',
|
|
48
|
+
{
|
|
49
|
+
description: 'Get the current scene opened in the editor.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
editorId: EditorIdSchema
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async ({editorId}) => {
|
|
55
|
+
using loggerContext = mcpLogger(server);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const currentScene = await getCurrentScene(editorId);
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: `Current scene: ${currentScene}` }]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
isError: true,
|
|
66
|
+
content: [{ type: 'text', text: `Failed to get the current scene: ${error}` }]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|