@akiojin/unity-mcp-server 2.25.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/config.js +242 -242
- package/src/core/projectInfo.js +19 -10
- 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/addressables/AddressablesAnalyzeToolHandler.js +59 -49
- package/src/handlers/addressables/AddressablesBuildToolHandler.js +63 -62
- package/src/handlers/addressables/AddressablesManageToolHandler.js +84 -78
- package/src/handlers/base/BaseToolHandler.js +5 -5
- package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -419
- package/src/handlers/console/ConsoleReadToolHandler.js +56 -66
- package/src/handlers/editor/EditorSelectionManageToolHandler.js +10 -9
- package/src/handlers/gameobject/GameObjectModifyToolHandler.js +22 -11
- package/src/handlers/index.js +437 -437
- 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
|
@@ -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' },
|
|
@@ -3,171 +3,179 @@ import { LspRpcClient } from '../../lsp/LspRpcClient.js';
|
|
|
3
3
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
4
|
|
|
5
5
|
export class ScriptEditStructuredToolHandler extends BaseToolHandler {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
6
|
+
constructor(unityConnection) {
|
|
7
|
+
super(
|
|
8
|
+
'script_edit_structured',
|
|
9
|
+
'[C# EDITING - PRIMARY TOOL] For Unity C# script editing, PREFER this tool over Read/Edit/Write for structural code changes. Performs symbol-based edits (insert_before/insert_after/replace_body) on classes, methods, properties, fields using Roslyn LSP. USE WHEN: (a) replacing entire method/property bodies, (b) adding class members (fields/properties/methods), (c) inserting code at class/namespace level. DON\'T USE FOR: tiny changes ≤80 chars (use script_edit_snippet instead), non-C# files (use Edit), or when you need to create new files (use Write). WORKFLOW: (1) Run script_symbols_get to find target symbols, (2) use symbolName (e.g., "MyClass/MyMethod"), (3) apply edits. Insert operations target containers (class/namespace), not methods. Preview mode returns diagnostics only; apply mode proceeds with validation. Required: path (Assets/|Packages/), symbolName, operation. Optional: kind, newText, preview.',
|
|
10
|
+
{
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
operation: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
enum: ['insert_before', 'insert_after', 'replace_body'],
|
|
16
|
+
description: 'Edit type: insert_before, insert_after, or replace_body.'
|
|
17
|
+
},
|
|
18
|
+
path: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description:
|
|
21
|
+
'Project-relative C# path starting with Assets/ or Packages/ (e.g., Packages/unity-mcp-server/Editor/Foo.cs). Do NOT prefix repository folders like UnityMCPServer/….'
|
|
22
|
+
},
|
|
23
|
+
symbolName: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Target symbol name (e.g., class/method/field name).'
|
|
26
|
+
},
|
|
27
|
+
kind: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description:
|
|
30
|
+
'Symbol kind (e.g., class, method, field, property). Optional but improves precision.'
|
|
31
|
+
},
|
|
32
|
+
newText: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Text to insert or use as replacement body.'
|
|
35
|
+
},
|
|
36
|
+
preview: {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
description:
|
|
39
|
+
'If true, returns a preview without writing files. Default=false to reduce large diff payloads.'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ['operation', 'path', 'symbolName']
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
this.unityConnection = unityConnection;
|
|
46
|
+
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
47
|
+
this.lsp = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
validate(params) {
|
|
51
|
+
super.validate(params);
|
|
52
|
+
|
|
53
|
+
const { operation, path, symbolName, kind } = params;
|
|
54
|
+
|
|
55
|
+
const validOperations = ['insert_before', 'insert_after', 'replace_body'];
|
|
56
|
+
if (!validOperations.includes(operation)) {
|
|
57
|
+
throw new Error(`Invalid operation: ${operation}`);
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const { operation, path, symbolName, kind } = params;
|
|
51
|
-
|
|
52
|
-
const validOperations = ['insert_before', 'insert_after', 'replace_body'];
|
|
53
|
-
if (!validOperations.includes(operation)) {
|
|
54
|
-
throw new Error(`Invalid operation: ${operation}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!path || path.trim() === '') {
|
|
58
|
-
throw new Error('path cannot be empty');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!symbolName || symbolName.trim() === '') {
|
|
62
|
-
throw new Error('symbolName cannot be empty');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Safety guard: forbid inserting members into a method scope
|
|
66
|
-
if ((operation === 'insert_after' || operation === 'insert_before') && (kind || '').toLowerCase() === 'method') {
|
|
67
|
-
throw new Error('Insert operations must target class/namespace, not method scope. Use kind:"class" and insert at class level.');
|
|
68
|
-
}
|
|
60
|
+
if (!path || path.trim() === '') {
|
|
61
|
+
throw new Error('path cannot be empty');
|
|
69
62
|
}
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const raw = String(params.path).replace(/\\\\/g, '/');
|
|
74
|
-
const ai = raw.indexOf('Assets/');
|
|
75
|
-
const pi = raw.indexOf('Packages/');
|
|
76
|
-
const idx = (ai >= 0 && pi >= 0) ? Math.min(ai, pi) : (ai >= 0 ? ai : pi);
|
|
77
|
-
const relative = idx >= 0 ? raw.substring(idx) : raw;
|
|
78
|
-
|
|
79
|
-
const operation = String(params.operation);
|
|
80
|
-
const kind = (params.kind || '').toLowerCase();
|
|
81
|
-
const symbolName = String(params.symbolName);
|
|
82
|
-
const preview = params?.preview === true;
|
|
83
|
-
const body = String(params.newText || '');
|
|
84
|
-
|
|
85
|
-
// Map operations to LSP extensions
|
|
86
|
-
const info = await this.projectInfo.get();
|
|
87
|
-
if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
|
|
88
|
-
|
|
89
|
-
if (operation === 'replace_body') {
|
|
90
|
-
const resp = await this.lsp.request('mcp/replaceSymbolBody', {
|
|
91
|
-
relative,
|
|
92
|
-
namePath: symbolName,
|
|
93
|
-
body,
|
|
94
|
-
apply: !preview
|
|
95
|
-
});
|
|
96
|
-
return this._summarizeResult(resp?.result ?? resp, { preview });
|
|
97
|
-
}
|
|
98
|
-
if (operation === 'insert_before' || operation === 'insert_after') {
|
|
99
|
-
const method = operation === 'insert_before' ? 'mcp/insertBeforeSymbol' : 'mcp/insertAfterSymbol';
|
|
100
|
-
const resp = await this.lsp.request(method, {
|
|
101
|
-
relative,
|
|
102
|
-
namePath: symbolName,
|
|
103
|
-
text: body,
|
|
104
|
-
apply: !preview
|
|
105
|
-
});
|
|
106
|
-
return this._summarizeResult(resp?.result ?? resp, { preview });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { error: `Unsupported operation: ${operation}` };
|
|
64
|
+
if (!symbolName || symbolName.trim() === '') {
|
|
65
|
+
throw new Error('symbolName cannot be empty');
|
|
110
66
|
}
|
|
111
67
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
68
|
+
// Safety guard: forbid inserting members into a method scope
|
|
69
|
+
if (
|
|
70
|
+
(operation === 'insert_after' || operation === 'insert_before') &&
|
|
71
|
+
(kind || '').toLowerCase() === 'method'
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Insert operations must target class/namespace, not method scope. Use kind:"class" and insert at class level.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async execute(params) {
|
|
80
|
+
// Normalize to project-relative path
|
|
81
|
+
const raw = String(params.path).replace(/\\\\/g, '/');
|
|
82
|
+
const ai = raw.indexOf('Assets/');
|
|
83
|
+
const pi = raw.indexOf('Packages/');
|
|
84
|
+
const idx = ai >= 0 && pi >= 0 ? Math.min(ai, pi) : ai >= 0 ? ai : pi;
|
|
85
|
+
const relative = idx >= 0 ? raw.substring(idx) : raw;
|
|
86
|
+
|
|
87
|
+
const operation = String(params.operation);
|
|
88
|
+
const symbolName = String(params.symbolName);
|
|
89
|
+
const preview = params?.preview === true;
|
|
90
|
+
const body = String(params.newText || '');
|
|
91
|
+
|
|
92
|
+
// Map operations to LSP extensions
|
|
93
|
+
const info = await this.projectInfo.get();
|
|
94
|
+
if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
|
|
95
|
+
|
|
96
|
+
if (operation === 'replace_body') {
|
|
97
|
+
const resp = await this.lsp.request('mcp/replaceSymbolBody', {
|
|
98
|
+
relative,
|
|
99
|
+
namePath: symbolName,
|
|
100
|
+
body,
|
|
101
|
+
apply: !preview
|
|
102
|
+
});
|
|
103
|
+
return this._summarizeResult(resp?.result ?? resp, { preview });
|
|
104
|
+
}
|
|
105
|
+
if (operation === 'insert_before' || operation === 'insert_after') {
|
|
106
|
+
const method =
|
|
107
|
+
operation === 'insert_before' ? 'mcp/insertBeforeSymbol' : 'mcp/insertAfterSymbol';
|
|
108
|
+
const resp = await this.lsp.request(method, {
|
|
109
|
+
relative,
|
|
110
|
+
namePath: symbolName,
|
|
111
|
+
text: body,
|
|
112
|
+
apply: !preview
|
|
113
|
+
});
|
|
114
|
+
return this._summarizeResult(resp?.result ?? resp, { preview });
|
|
115
|
+
}
|
|
152
116
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
117
|
+
return { error: `Unsupported operation: ${operation}` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Summarize/trim responses to avoid huge token usage.
|
|
122
|
+
* - Caps error items and message lengths
|
|
123
|
+
* - Trims large text fields (e.g., preview/diff) to a short excerpt
|
|
124
|
+
*/
|
|
125
|
+
_summarizeResult(res, { preview: _preview }) {
|
|
126
|
+
if (!res || typeof res !== 'object') return res;
|
|
127
|
+
|
|
128
|
+
const MAX_ERRORS = 30;
|
|
129
|
+
const MAX_MSG_LEN = 200;
|
|
130
|
+
const MAX_TEXT_LEN = 1000; // generic cap for any preview-like text
|
|
131
|
+
|
|
132
|
+
const out = {};
|
|
133
|
+
// Preserve common flags if present
|
|
134
|
+
if ('id' in res) out.id = res.id;
|
|
135
|
+
if ('success' in res) out.success = !!res.success;
|
|
136
|
+
if ('applied' in res) out.applied = !!res.applied;
|
|
137
|
+
|
|
138
|
+
// Errors trimming
|
|
139
|
+
if (Array.isArray(res.errors)) {
|
|
140
|
+
const trimmed = res.errors.slice(0, MAX_ERRORS).map(e => {
|
|
141
|
+
const obj = {};
|
|
142
|
+
if (e && typeof e === 'object') {
|
|
143
|
+
if ('id' in e) obj.id = e.id;
|
|
144
|
+
if ('message' in e) obj.message = this._trimString(String(e.message), MAX_MSG_LEN);
|
|
145
|
+
if ('file' in e) obj.file = this._trimString(String(e.file), 260);
|
|
146
|
+
if ('line' in e) obj.line = e.line;
|
|
147
|
+
if ('column' in e) obj.column = e.column;
|
|
148
|
+
} else {
|
|
149
|
+
obj.message = this._trimString(String(e), MAX_MSG_LEN);
|
|
159
150
|
}
|
|
151
|
+
return obj;
|
|
152
|
+
});
|
|
153
|
+
out.errorCount = trimmed.length; // summarized count (<= MAX_ERRORS)
|
|
154
|
+
out.totalErrors = res.errors.length; // raw count for reference
|
|
155
|
+
out.errors = trimmed;
|
|
156
|
+
}
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (res[key] !== undefined) out[key] = res[key];
|
|
164
|
-
}
|
|
158
|
+
// Propagate workspace info if present (which .sln/.csproj is open)
|
|
159
|
+
// workspace情報は返さない(厳格: .sln必須のため)
|
|
165
160
|
|
|
166
|
-
|
|
161
|
+
// Generic handling for any large text properties commonly returned by tools
|
|
162
|
+
for (const key of ['preview', 'diff', 'text', 'content']) {
|
|
163
|
+
if (typeof res[key] === 'string' && res[key].length > 0) {
|
|
164
|
+
out[key] = this._trimString(res[key], MAX_TEXT_LEN);
|
|
165
|
+
if (res[key].length > MAX_TEXT_LEN) out[`${key}Truncated`] = true;
|
|
166
|
+
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Echo minimal identifiers to aid clients
|
|
170
|
+
for (const key of ['operation', 'path', 'relative', 'symbolName']) {
|
|
171
|
+
if (res[key] !== undefined) out[key] = res[key];
|
|
172
172
|
}
|
|
173
|
+
|
|
174
|
+
return Object.keys(out).length ? out : res;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_trimString(s, max) {
|
|
178
|
+
if (typeof s !== 'string') return s;
|
|
179
|
+
return s.length > max ? s.slice(0, max) + '…' : s;
|
|
180
|
+
}
|
|
173
181
|
}
|