@akiojin/unity-mcp-server 2.26.0 → 2.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/codeIndexDb.js +26 -10
- package/src/core/projectInfo.js +74 -65
- package/src/core/server.js +88 -65
- package/src/core/transports/HybridStdioServerTransport.js +179 -0
- package/src/core/unityConnection.js +52 -45
- package/src/handlers/base/BaseToolHandler.js +5 -5
- package/src/handlers/console/ConsoleReadToolHandler.js +285 -295
- package/src/handlers/editor/EditorSelectionManageToolHandler.js +10 -9
- package/src/handlers/gameobject/GameObjectModifyToolHandler.js +22 -11
- package/src/handlers/menu/MenuItemExecuteToolHandler.js +75 -37
- package/src/handlers/screenshot/ScreenshotAnalyzeToolHandler.js +12 -10
- package/src/handlers/script/ScriptEditStructuredToolHandler.js +162 -154
- package/src/handlers/script/ScriptReadToolHandler.js +80 -85
- package/src/handlers/script/ScriptRefsFindToolHandler.js +123 -123
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +125 -112
- package/src/handlers/system/SystemGetCommandStatsToolHandler.js +1 -1
- package/src/handlers/system/SystemRefreshAssetsToolHandler.js +10 -14
- package/src/handlers/video/VideoCaptureStartToolHandler.js +15 -5
- package/src/handlers/video/VideoCaptureStatusToolHandler.js +5 -9
- package/src/handlers/video/VideoCaptureStopToolHandler.js +8 -9
- package/src/lsp/LspProcessManager.js +26 -9
- package/src/tools/video/recordFor.js +13 -7
- package/src/tools/video/recordPlayMode.js +7 -6
- package/src/utils/csharpParse.js +14 -8
|
@@ -70,7 +70,7 @@ export class GameObjectModifyToolHandler extends BaseToolHandler {
|
|
|
70
70
|
required: ['path']
|
|
71
71
|
}
|
|
72
72
|
);
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
this.unityConnection = unityConnection;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -81,23 +81,34 @@ export class GameObjectModifyToolHandler extends BaseToolHandler {
|
|
|
81
81
|
*/
|
|
82
82
|
validate(params) {
|
|
83
83
|
super.validate(params);
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
// Path is required
|
|
86
86
|
validateGameObjectPath(params.path);
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// At least one modification must be specified
|
|
89
|
-
const modifiableProps = [
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const modifiableProps = [
|
|
90
|
+
'name',
|
|
91
|
+
'position',
|
|
92
|
+
'rotation',
|
|
93
|
+
'scale',
|
|
94
|
+
'active',
|
|
95
|
+
'parentPath',
|
|
96
|
+
'tag',
|
|
97
|
+
'layer'
|
|
98
|
+
];
|
|
99
|
+
const hasModification = modifiableProps.some(prop =>
|
|
100
|
+
Object.prototype.hasOwnProperty.call(params, prop)
|
|
101
|
+
);
|
|
102
|
+
|
|
92
103
|
if (!hasModification) {
|
|
93
104
|
throw new Error('At least one property to modify must be specified');
|
|
94
105
|
}
|
|
95
|
-
|
|
106
|
+
|
|
96
107
|
// Validate vector3 properties
|
|
97
108
|
if (params.position) validateVector3(params.position, 'position');
|
|
98
109
|
if (params.rotation) validateVector3(params.rotation, 'rotation');
|
|
99
110
|
if (params.scale) validateVector3(params.scale, 'scale');
|
|
100
|
-
|
|
111
|
+
|
|
101
112
|
// Validate layer
|
|
102
113
|
if (params.layer !== undefined) {
|
|
103
114
|
validateLayer(Number(params.layer));
|
|
@@ -114,15 +125,15 @@ export class GameObjectModifyToolHandler extends BaseToolHandler {
|
|
|
114
125
|
if (!this.unityConnection.isConnected()) {
|
|
115
126
|
await this.unityConnection.connect();
|
|
116
127
|
}
|
|
117
|
-
|
|
128
|
+
|
|
118
129
|
// Send modify_gameobject command
|
|
119
130
|
const result = await this.unityConnection.sendCommand('modify_gameobject', params);
|
|
120
|
-
|
|
131
|
+
|
|
121
132
|
// Check for errors from Unity
|
|
122
133
|
if (result.error) {
|
|
123
134
|
throw new Error(result.error);
|
|
124
135
|
}
|
|
125
|
-
|
|
136
|
+
|
|
126
137
|
return result;
|
|
127
138
|
}
|
|
128
139
|
}
|
|
@@ -18,7 +18,8 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
18
18
|
action: {
|
|
19
19
|
type: 'string',
|
|
20
20
|
enum: ['execute', 'get_available_menus'],
|
|
21
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
'Action to perform: execute menu item or get available menus (default: execute)'
|
|
22
23
|
},
|
|
23
24
|
alias: {
|
|
24
25
|
type: 'string',
|
|
@@ -30,47 +31,48 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
30
31
|
},
|
|
31
32
|
safetyCheck: {
|
|
32
33
|
type: 'boolean',
|
|
33
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'Enable safety checks to prevent execution of dangerous menu items (default: true)'
|
|
34
36
|
}
|
|
35
37
|
},
|
|
36
38
|
required: ['menuPath']
|
|
37
39
|
}
|
|
38
40
|
);
|
|
39
|
-
|
|
41
|
+
|
|
40
42
|
this.unityConnection = unityConnection;
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
// Define blacklisted menu items for safety
|
|
43
45
|
// Includes dialog-opening menus that cause MCP hanging
|
|
44
46
|
this.blacklistedMenus = new Set([
|
|
45
47
|
// Application control
|
|
46
48
|
'File/Quit',
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
// Dialog-opening file operations (cause MCP hanging)
|
|
49
51
|
'File/Open Scene',
|
|
50
|
-
'File/New Scene',
|
|
52
|
+
'File/New Scene',
|
|
51
53
|
'File/Save Scene As...',
|
|
52
54
|
'File/Build Settings...',
|
|
53
55
|
'File/Build And Run',
|
|
54
|
-
|
|
56
|
+
|
|
55
57
|
// Dialog-opening asset operations (cause MCP hanging)
|
|
56
58
|
'Assets/Import New Asset...',
|
|
57
59
|
'Assets/Import Package/Custom Package...',
|
|
58
60
|
'Assets/Export Package...',
|
|
59
61
|
'Assets/Delete',
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
// Dialog-opening preferences and settings (cause MCP hanging)
|
|
62
64
|
'Edit/Preferences...',
|
|
63
65
|
'Edit/Project Settings...',
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
// Dialog-opening window operations (may cause issues)
|
|
66
68
|
'Window/Package Manager',
|
|
67
69
|
'Window/Asset Store',
|
|
68
|
-
|
|
70
|
+
|
|
69
71
|
// Scene view operations that may require focus (potential hanging)
|
|
70
72
|
'GameObject/Align With View',
|
|
71
73
|
'GameObject/Align View to Selected'
|
|
72
74
|
]);
|
|
73
|
-
|
|
75
|
+
|
|
74
76
|
// Common menu aliases
|
|
75
77
|
this.menuAliases = new Map([
|
|
76
78
|
['refresh', 'Assets/Refresh'],
|
|
@@ -104,12 +106,18 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
104
106
|
|
|
105
107
|
// Safety check for blacklisted items with security normalization (BEFORE format validation)
|
|
106
108
|
if (safetyCheck && this.isMenuPathBlacklisted(menuPath)) {
|
|
107
|
-
throw new Error(
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Menu item is blacklisted for safety: ${menuPath}. Use safetyCheck: false to override.`
|
|
111
|
+
);
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
// Validate menu path format (should contain at least one slash) - after normalization for security
|
|
111
115
|
const normalizedForValidation = this.normalizeMenuPath(menuPath);
|
|
112
|
-
if (
|
|
116
|
+
if (
|
|
117
|
+
!normalizedForValidation.includes('/') ||
|
|
118
|
+
normalizedForValidation.startsWith('/') ||
|
|
119
|
+
normalizedForValidation.endsWith('/')
|
|
120
|
+
) {
|
|
113
121
|
throw new Error('menuPath must be in format "Category/MenuItem" (e.g., "Assets/Refresh")');
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -125,13 +133,7 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
125
133
|
* @returns {Promise<Object>} The result of the menu operation
|
|
126
134
|
*/
|
|
127
135
|
async execute(params) {
|
|
128
|
-
const {
|
|
129
|
-
menuPath,
|
|
130
|
-
action = 'execute',
|
|
131
|
-
alias,
|
|
132
|
-
parameters,
|
|
133
|
-
safetyCheck = true
|
|
134
|
-
} = params;
|
|
136
|
+
const { menuPath, action = 'execute', alias, parameters, safetyCheck = true } = params;
|
|
135
137
|
|
|
136
138
|
// Ensure connection to Unity
|
|
137
139
|
if (!this.unityConnection.isConnected()) {
|
|
@@ -242,7 +244,7 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
242
244
|
isMenuPathBlacklisted(menuPath) {
|
|
243
245
|
// Normalize the input path to prevent bypass attacks
|
|
244
246
|
const normalizedPath = this.normalizeMenuPath(menuPath);
|
|
245
|
-
|
|
247
|
+
|
|
246
248
|
// Check against normalized blacklist entries
|
|
247
249
|
for (const blacklistedItem of this.blacklistedMenus) {
|
|
248
250
|
const normalizedBlacklistItem = this.normalizeMenuPath(blacklistedItem);
|
|
@@ -250,7 +252,7 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
250
252
|
return true;
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
|
-
|
|
255
|
+
|
|
254
256
|
return false;
|
|
255
257
|
}
|
|
256
258
|
|
|
@@ -265,40 +267,76 @@ export class MenuItemExecuteToolHandler extends BaseToolHandler {
|
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
// Step 1: Remove zero-width and invisible Unicode characters
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
// eslint-disable-next-line no-misleading-character-class
|
|
271
|
+
let normalized = menuPath.replace(
|
|
272
|
+
/[\u200B-\u200D\uFEFF\u00AD\u034F\u061C\u180E\u2060-\u2069]/gu,
|
|
273
|
+
''
|
|
274
|
+
);
|
|
275
|
+
|
|
270
276
|
// Step 2: Normalize Unicode to canonical form (handles homograph attacks)
|
|
271
277
|
normalized = normalized.normalize('NFC');
|
|
272
|
-
|
|
278
|
+
|
|
273
279
|
// Step 3: Convert to lowercase for case-insensitive comparison
|
|
274
280
|
normalized = normalized.toLowerCase();
|
|
275
|
-
|
|
281
|
+
|
|
276
282
|
// Step 4: Trim whitespace and remove all internal whitespace (security bypass prevention)
|
|
277
283
|
normalized = normalized.trim().replace(/\s+/g, '');
|
|
278
|
-
|
|
284
|
+
|
|
279
285
|
// Step 5: Normalize path separators (convert backslashes to forward slashes)
|
|
280
286
|
normalized = normalized.replace(/\\/g, '/');
|
|
281
|
-
|
|
287
|
+
|
|
282
288
|
// Step 6: Remove duplicate path separators
|
|
283
289
|
normalized = normalized.replace(/\/+/g, '/');
|
|
284
|
-
|
|
290
|
+
|
|
285
291
|
// Step 7: Handle common homograph substitutions for ASCII characters
|
|
286
292
|
const homographMap = {
|
|
287
293
|
// Cyrillic lookalikes
|
|
288
|
-
|
|
289
|
-
|
|
294
|
+
а: 'a',
|
|
295
|
+
е: 'e',
|
|
296
|
+
о: 'o',
|
|
297
|
+
р: 'p',
|
|
298
|
+
с: 'c',
|
|
299
|
+
х: 'x',
|
|
300
|
+
у: 'y',
|
|
301
|
+
і: 'i',
|
|
302
|
+
ј: 'j',
|
|
303
|
+
ѕ: 's',
|
|
304
|
+
һ: 'h',
|
|
305
|
+
ց: 'q',
|
|
306
|
+
ԁ: 'd',
|
|
307
|
+
ɡ: 'g',
|
|
290
308
|
// Greek lookalikes
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
α: 'a',
|
|
310
|
+
β: 'b',
|
|
311
|
+
γ: 'g',
|
|
312
|
+
δ: 'd',
|
|
313
|
+
ε: 'e',
|
|
314
|
+
ζ: 'z',
|
|
315
|
+
η: 'h',
|
|
316
|
+
θ: 'o',
|
|
317
|
+
ι: 'i',
|
|
318
|
+
κ: 'k',
|
|
319
|
+
λ: 'l',
|
|
320
|
+
μ: 'm',
|
|
321
|
+
ν: 'n',
|
|
322
|
+
ξ: 'x',
|
|
323
|
+
ο: 'o',
|
|
324
|
+
π: 'p',
|
|
325
|
+
ρ: 'p',
|
|
326
|
+
σ: 's',
|
|
327
|
+
τ: 't',
|
|
328
|
+
υ: 'u',
|
|
329
|
+
φ: 'f',
|
|
330
|
+
χ: 'x',
|
|
331
|
+
ψ: 'y',
|
|
332
|
+
ω: 'w'
|
|
295
333
|
};
|
|
296
|
-
|
|
334
|
+
|
|
297
335
|
// Replace homographs
|
|
298
336
|
for (const [homograph, ascii] of Object.entries(homographMap)) {
|
|
299
337
|
normalized = normalized.replace(new RegExp(homograph, 'g'), ascii);
|
|
300
338
|
}
|
|
301
|
-
|
|
339
|
+
|
|
302
340
|
return normalized;
|
|
303
341
|
}
|
|
304
342
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -24,18 +23,20 @@ export class ScreenshotAnalyzeToolHandler extends BaseToolHandler {
|
|
|
24
23
|
analysisType: {
|
|
25
24
|
type: 'string',
|
|
26
25
|
enum: ['basic', 'ui', 'content', 'full'],
|
|
27
|
-
|
|
28
|
-
description:
|
|
26
|
+
|
|
27
|
+
description:
|
|
28
|
+
'Type of analysis: basic (colors, dimensions), ui (UI element detection), content (scene content), full (all)'
|
|
29
29
|
},
|
|
30
30
|
prompt: {
|
|
31
31
|
type: 'string',
|
|
32
|
-
description:
|
|
32
|
+
description:
|
|
33
|
+
'Optional prompt for AI-based analysis (e.g., "Find all buttons in the UI")'
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
required: []
|
|
36
37
|
}
|
|
37
38
|
);
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
this.unityConnection = unityConnection;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -62,7 +63,7 @@ export class ScreenshotAnalyzeToolHandler extends BaseToolHandler {
|
|
|
62
63
|
if (!imagePath.startsWith('Assets/')) {
|
|
63
64
|
throw new Error('imagePath must be within the Assets folder');
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
const ext = path.extname(imagePath).toLowerCase();
|
|
67
68
|
if (!['.png', '.jpg', '.jpeg'].includes(ext)) {
|
|
68
69
|
throw new Error('imagePath must be a PNG or JPEG file');
|
|
@@ -159,24 +160,25 @@ export class ScreenshotAnalyzeToolHandler extends BaseToolHandler {
|
|
|
159
160
|
result.analysis = {
|
|
160
161
|
note: 'Basic analysis of base64 images requires image processing library integration',
|
|
161
162
|
fileSize: fileSize,
|
|
162
|
-
estimatedFormat:
|
|
163
|
+
estimatedFormat:
|
|
164
|
+
fileSize > 100000 ? 'Likely PNG or high-quality JPEG' : 'Likely compressed JPEG'
|
|
163
165
|
};
|
|
164
166
|
break;
|
|
165
|
-
|
|
167
|
+
|
|
166
168
|
case 'ui':
|
|
167
169
|
result.uiAnalysis = {
|
|
168
170
|
note: 'UI element detection requires computer vision integration',
|
|
169
171
|
placeholder: 'This would detect buttons, text fields, panels, etc.'
|
|
170
172
|
};
|
|
171
173
|
break;
|
|
172
|
-
|
|
174
|
+
|
|
173
175
|
case 'content':
|
|
174
176
|
result.contentAnalysis = {
|
|
175
177
|
note: 'Content analysis requires scene understanding models',
|
|
176
178
|
placeholder: 'This would identify GameObjects, lighting, materials, etc.'
|
|
177
179
|
};
|
|
178
180
|
break;
|
|
179
|
-
|
|
181
|
+
|
|
180
182
|
case 'full':
|
|
181
183
|
result.fullAnalysis = {
|
|
182
184
|
basic: { note: 'Requires image processing library' },
|