@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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/core/codeIndexDb.js +26 -10
  3. package/src/core/config.js +242 -242
  4. package/src/core/projectInfo.js +19 -10
  5. package/src/core/server.js +88 -65
  6. package/src/core/transports/HybridStdioServerTransport.js +179 -0
  7. package/src/core/unityConnection.js +52 -45
  8. package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +59 -49
  9. package/src/handlers/addressables/AddressablesBuildToolHandler.js +63 -62
  10. package/src/handlers/addressables/AddressablesManageToolHandler.js +84 -78
  11. package/src/handlers/base/BaseToolHandler.js +5 -5
  12. package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -419
  13. package/src/handlers/console/ConsoleReadToolHandler.js +56 -66
  14. package/src/handlers/editor/EditorSelectionManageToolHandler.js +10 -9
  15. package/src/handlers/gameobject/GameObjectModifyToolHandler.js +22 -11
  16. package/src/handlers/index.js +437 -437
  17. package/src/handlers/menu/MenuItemExecuteToolHandler.js +75 -37
  18. package/src/handlers/screenshot/ScreenshotAnalyzeToolHandler.js +12 -10
  19. package/src/handlers/script/ScriptEditStructuredToolHandler.js +162 -154
  20. package/src/handlers/script/ScriptReadToolHandler.js +80 -85
  21. package/src/handlers/script/ScriptRefsFindToolHandler.js +123 -123
  22. package/src/handlers/script/ScriptSymbolFindToolHandler.js +125 -112
  23. package/src/handlers/system/SystemGetCommandStatsToolHandler.js +1 -1
  24. package/src/handlers/system/SystemRefreshAssetsToolHandler.js +10 -14
  25. package/src/handlers/video/VideoCaptureStartToolHandler.js +15 -5
  26. package/src/handlers/video/VideoCaptureStatusToolHandler.js +5 -9
  27. package/src/handlers/video/VideoCaptureStopToolHandler.js +8 -9
  28. package/src/lsp/LspProcessManager.js +26 -9
  29. package/src/tools/video/recordFor.js +13 -7
  30. package/src/tools/video/recordPlayMode.js +7 -6
  31. 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: 'Action to perform: execute menu item or get available menus (default: execute)'
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: 'Enable safety checks to prevent execution of dangerous menu items (default: true)'
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(`Menu item is blacklisted for safety: ${menuPath}. Use safetyCheck: false to override.`);
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 (!normalizedForValidation.includes('/') || normalizedForValidation.startsWith('/') || normalizedForValidation.endsWith('/')) {
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
- let normalized = menuPath.replace(/[\u200B-\u200D\uFEFF\u00AD\u034F\u061C\u180E\u2060-\u2069]/g, '');
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
- 'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'х': 'x', 'у': 'y',
289
- 'і': 'i', 'ј': 'j', 'ѕ': 's', 'һ': 'h', 'ց': 'q', 'ԁ': 'd', 'ɡ': 'g',
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
- 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h',
292
- 'θ': 'o', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': 'x',
293
- 'ο': 'o', 'π': 'p', 'ρ': 'p', 'σ': 's', 'τ': 't', 'υ': 'u', 'φ': 'f',
294
- 'χ': 'x', 'ψ': 'y', 'ω': 'w'
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: 'Type of analysis: basic (colors, dimensions), ui (UI element detection), content (scene content), full (all)'
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: 'Optional prompt for AI-based analysis (e.g., "Find all buttons in the UI")'
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: fileSize > 100000 ? 'Likely PNG or high-quality JPEG' : 'Likely compressed JPEG'
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
- 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: '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/….'
21
- },
22
- symbolName: {
23
- type: 'string',
24
- description: 'Target symbol name (e.g., class/method/field name).'
25
- },
26
- kind: {
27
- type: 'string',
28
- description: 'Symbol kind (e.g., class, method, field, property). Optional but improves precision.'
29
- },
30
- newText: {
31
- type: 'string',
32
- description: 'Text to insert or use as replacement body.'
33
- },
34
- preview: {
35
- type: 'boolean',
36
- description: 'If true, returns a preview without writing files. Default=false to reduce large diff payloads.'
37
- }
38
- },
39
- required: ['operation', 'path', 'symbolName']
40
- }
41
- );
42
- this.unityConnection = unityConnection;
43
- this.projectInfo = new ProjectInfoProvider(unityConnection);
44
- this.lsp = null;
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
- validate(params) {
48
- super.validate(params);
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
- async execute(params) {
72
- // Normalize to project-relative path
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
- * Summarize/trim responses to avoid huge token usage.
114
- * - Caps error items and message lengths
115
- * - Trims large text fields (e.g., preview/diff) to a short excerpt
116
- */
117
- _summarizeResult(res, { preview }) {
118
- if (!res || typeof res !== 'object') return res;
119
-
120
- const MAX_ERRORS = 30;
121
- const MAX_MSG_LEN = 200;
122
- const MAX_TEXT_LEN = 1000; // generic cap for any preview-like text
123
-
124
- const out = {};
125
- // Preserve common flags if present
126
- if ('id' in res) out.id = res.id;
127
- if ('success' in res) out.success = !!res.success;
128
- if ('applied' in res) out.applied = !!res.applied;
129
-
130
- // Errors trimming
131
- if (Array.isArray(res.errors)) {
132
- const trimmed = res.errors.slice(0, MAX_ERRORS).map(e => {
133
- const obj = {};
134
- if (e && typeof e === 'object') {
135
- if ('id' in e) obj.id = e.id;
136
- if ('message' in e) obj.message = this._trimString(String(e.message), MAX_MSG_LEN);
137
- if ('file' in e) obj.file = this._trimString(String(e.file), 260);
138
- if ('line' in e) obj.line = e.line;
139
- if ('column' in e) obj.column = e.column;
140
- } else {
141
- obj.message = this._trimString(String(e), MAX_MSG_LEN);
142
- }
143
- return obj;
144
- });
145
- out.errorCount = trimmed.length; // summarized count (<= MAX_ERRORS)
146
- out.totalErrors = res.errors.length; // raw count for reference
147
- out.errors = trimmed;
148
- }
149
-
150
- // Propagate workspace info if present (which .sln/.csproj is open)
151
- // workspace情報は返さない(厳格: .sln必須のため)
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
- // Generic handling for any large text properties commonly returned by tools
154
- for (const key of ['preview', 'diff', 'text', 'content']) {
155
- if (typeof res[key] === 'string' && res[key].length > 0) {
156
- out[key] = this._trimString(res[key], MAX_TEXT_LEN);
157
- if (res[key].length > MAX_TEXT_LEN) out[`${key}Truncated`] = true;
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
- // Echo minimal identifiers to aid clients
162
- for (const key of ['operation', 'path', 'relative', 'symbolName']) {
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
- return Object.keys(out).length ? out : res;
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
- _trimString(s, max) {
170
- if (typeof s !== 'string') return s;
171
- return s.length > max ? (s.slice(0, max) + '…') : s;
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
  }