@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
@@ -42,11 +42,11 @@ export class UnityConnection extends EventEmitter {
42
42
 
43
43
  const targetHost = config.unity.mcpHost || config.unity.unityHost;
44
44
  logger.info(`Connecting to Unity at ${targetHost}:${config.unity.port}...`);
45
-
45
+
46
46
  this.socket = new net.Socket();
47
47
  let connectionTimeout = null;
48
48
  let resolved = false;
49
-
49
+
50
50
  // Helper to clean up the connection timeout
51
51
  const clearConnectionTimeout = () => {
52
52
  if (connectionTimeout) {
@@ -54,7 +54,7 @@ export class UnityConnection extends EventEmitter {
54
54
  connectionTimeout = null;
55
55
  }
56
56
  };
57
-
57
+
58
58
  // Set up event handlers
59
59
  this.socket.on('connect', () => {
60
60
  logger.info('Connected to Unity Editor');
@@ -66,14 +66,14 @@ export class UnityConnection extends EventEmitter {
66
66
  resolve();
67
67
  });
68
68
 
69
- this.socket.on('data', (data) => {
69
+ this.socket.on('data', data => {
70
70
  this.handleData(data);
71
71
  });
72
72
 
73
- this.socket.on('error', (error) => {
73
+ this.socket.on('error', error => {
74
74
  logger.error('Socket error:', error.message);
75
75
  this.emit('error', error);
76
-
76
+
77
77
  if (!this.connected && !resolved) {
78
78
  resolved = true;
79
79
  clearConnectionTimeout();
@@ -89,29 +89,28 @@ export class UnityConnection extends EventEmitter {
89
89
  this.socket.on('close', () => {
90
90
  // Clear the connection timeout when socket closes
91
91
  clearConnectionTimeout();
92
-
92
+
93
93
  // Check if we're already handling disconnection
94
94
  if (this.isDisconnecting || !this.socket) {
95
95
  return;
96
96
  }
97
-
97
+
98
98
  logger.info('Disconnected from Unity Editor');
99
99
  this.connected = false;
100
- const wasSocket = this.socket;
101
100
  this.socket = null;
102
-
101
+
103
102
  // Clear message buffer
104
103
  this.messageBuffer = Buffer.alloc(0);
105
-
104
+
106
105
  // Clear pending commands
107
- for (const [id, pending] of this.pendingCommands) {
106
+ for (const [, pending] of this.pendingCommands) {
108
107
  pending.reject(new Error('Connection closed'));
109
108
  }
110
109
  this.pendingCommands.clear();
111
-
110
+
112
111
  // Emit disconnected event
113
112
  this.emit('disconnected');
114
-
113
+
115
114
  // Attempt reconnection only if not intentionally disconnecting
116
115
  if (!this.isDisconnecting && process.env.DISABLE_AUTO_RECONNECT !== 'true') {
117
116
  this.scheduleReconnect();
@@ -120,7 +119,7 @@ export class UnityConnection extends EventEmitter {
120
119
 
121
120
  // Attempt connection
122
121
  this.socket.connect(config.unity.port, targetHost);
123
-
122
+
124
123
  // Set timeout for initial connection
125
124
  connectionTimeout = setTimeout(() => {
126
125
  if (!this.connected && !resolved && this.socket) {
@@ -139,12 +138,12 @@ export class UnityConnection extends EventEmitter {
139
138
  */
140
139
  disconnect() {
141
140
  this.isDisconnecting = true;
142
-
141
+
143
142
  if (this.reconnectTimer) {
144
143
  clearTimeout(this.reconnectTimer);
145
144
  this.reconnectTimer = null;
146
145
  }
147
-
146
+
148
147
  if (this.socket) {
149
148
  try {
150
149
  // Remove all listeners before destroying to prevent async callbacks
@@ -155,7 +154,7 @@ export class UnityConnection extends EventEmitter {
155
154
  }
156
155
  this.socket = null;
157
156
  }
158
-
157
+
159
158
  this.connected = false;
160
159
  this.isDisconnecting = false;
161
160
  }
@@ -169,7 +168,8 @@ export class UnityConnection extends EventEmitter {
169
168
  }
170
169
 
171
170
  const delay = Math.min(
172
- config.unity.reconnectDelay * Math.pow(config.unity.reconnectBackoffMultiplier, this.reconnectAttempts),
171
+ config.unity.reconnectDelay *
172
+ Math.pow(config.unity.reconnectBackoffMultiplier, this.reconnectAttempts),
173
173
  config.unity.maxReconnectDelay
174
174
  );
175
175
 
@@ -178,7 +178,7 @@ export class UnityConnection extends EventEmitter {
178
178
  this.reconnectTimer = setTimeout(() => {
179
179
  this.reconnectTimer = null;
180
180
  this.reconnectAttempts++;
181
- this.connect().catch((error) => {
181
+ this.connect().catch(error => {
182
182
  logger.error('Reconnection failed:', error.message);
183
183
  });
184
184
  }, delay);
@@ -198,19 +198,20 @@ export class UnityConnection extends EventEmitter {
198
198
  return;
199
199
  }
200
200
  }
201
-
201
+
202
202
  // Append new data to buffer
203
203
  this.messageBuffer = Buffer.concat([this.messageBuffer, data]);
204
-
204
+
205
205
  // Process complete messages
206
206
  while (this.messageBuffer.length >= 4) {
207
207
  // Read message length (first 4 bytes, big-endian)
208
208
  const messageLength = this.messageBuffer.readInt32BE(0);
209
-
209
+
210
210
  // Validate message length
211
- if (messageLength < 0 || messageLength > 1024 * 1024) { // Max 1MB messages
211
+ if (messageLength < 0 || messageLength > 1024 * 1024) {
212
+ // Max 1MB messages
212
213
  logger.error(`[Unity] Invalid message length: ${messageLength}`);
213
-
214
+
214
215
  // Try to recover by looking for valid framed message
215
216
  // Look for a reasonable length value (positive, less than 10KB for typical responses)
216
217
  let recoveryIndex = -1;
@@ -227,7 +228,7 @@ export class UnityConnection extends EventEmitter {
227
228
  }
228
229
  }
229
230
  }
230
-
231
+
231
232
  if (recoveryIndex > 0) {
232
233
  logger.warn(`[Unity] Discarding ${recoveryIndex} bytes of invalid data`);
233
234
  this.messageBuffer = this.messageBuffer.slice(recoveryIndex);
@@ -239,40 +240,42 @@ export class UnityConnection extends EventEmitter {
239
240
  break;
240
241
  }
241
242
  }
242
-
243
+
243
244
  // Check if we have the complete message
244
245
  if (this.messageBuffer.length >= 4 + messageLength) {
245
246
  // Extract message
246
247
  const messageData = this.messageBuffer.slice(4, 4 + messageLength);
247
248
  this.messageBuffer = this.messageBuffer.slice(4 + messageLength);
248
-
249
+
249
250
  // Process the message
250
251
  try {
251
252
  const message = messageData.toString('utf8');
252
-
253
+
253
254
  // Skip non-JSON messages (like debug logs)
254
255
  if (!message.trim().startsWith('{')) {
255
256
  logger.warn(`[Unity] Skipping non-JSON message: ${message.substring(0, 50)}...`);
256
257
  continue;
257
258
  }
258
-
259
+
259
260
  logger.debug(`[Unity] Received framed message (length=${message.length})`);
260
-
261
+
261
262
  const response = JSON.parse(message);
262
- logger.debug(`[Unity] Parsed response id=${response.id || 'n/a'} status=${response.status || (response.success === false ? 'error' : 'success')}`);
263
-
263
+ logger.debug(
264
+ `[Unity] Parsed response id=${response.id || 'n/a'} status=${response.status || (response.success === false ? 'error' : 'success')}`
265
+ );
266
+
264
267
  // Check if this is a response to a pending command
265
268
  if (response.id && this.pendingCommands.has(response.id)) {
266
269
  logger.info(`[Unity] Found pending command for ID ${response.id}`);
267
270
  const pending = this.pendingCommands.get(response.id);
268
271
  this.pendingCommands.delete(response.id);
269
-
272
+
270
273
  // Handle both old and new response formats
271
274
  if (response.status === 'success' || response.success === true) {
272
275
  logger.info(`[Unity] Command ${response.id} succeeded`);
273
-
276
+
274
277
  let result = response.result || response.data || {};
275
-
278
+
276
279
  // If result is a string, try to parse it as JSON
277
280
  if (typeof result === 'string') {
278
281
  try {
@@ -283,7 +286,7 @@ export class UnityConnection extends EventEmitter {
283
286
  // Keep the original string value
284
287
  }
285
288
  }
286
-
289
+
287
290
  // Include version and editorState information if available
288
291
  if (response.version) {
289
292
  result._version = response.version;
@@ -291,7 +294,7 @@ export class UnityConnection extends EventEmitter {
291
294
  if (response.editorState) {
292
295
  result._editorState = response.editorState;
293
296
  }
294
-
297
+
295
298
  logger.info(`[Unity] Command ${response.id} resolved successfully`);
296
299
  pending.resolve(result);
297
300
  } else if (response.status === 'error' || response.success === false) {
@@ -310,7 +313,7 @@ export class UnityConnection extends EventEmitter {
310
313
  } catch (error) {
311
314
  logger.error('[Unity] Failed to parse response:', error.message);
312
315
  logger.debug(`[Unity] Raw message: ${messageData.toString().substring(0, 200)}...`);
313
-
316
+
314
317
  // Check if this looks like a Unity log message
315
318
  const messageStr = messageData.toString();
316
319
  if (messageStr.includes('[Unity Editor MCP]')) {
@@ -333,7 +336,7 @@ export class UnityConnection extends EventEmitter {
333
336
  */
334
337
  async sendCommand(type, params = {}) {
335
338
  logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
336
-
339
+
337
340
  if (!this.connected) {
338
341
  logger.error('[Unity] Cannot send command - not connected');
339
342
  throw new Error('Not connected to Unity');
@@ -375,18 +378,22 @@ export class UnityConnection extends EventEmitter {
375
378
 
376
379
  // Store pending with wrappers to manage queue progression
377
380
  this.pendingCommands.set(id, {
378
- resolve: (data) => {
381
+ resolve: data => {
379
382
  logger.info(`[Unity] Command ${id} resolved`);
380
383
  clearTimeout(timeout);
381
- try { task.outerResolve(data); } finally {
384
+ try {
385
+ task.outerResolve(data);
386
+ } finally {
382
387
  this.inFlight = Math.max(0, this.inFlight - 1);
383
388
  this._pumpQueue();
384
389
  }
385
390
  },
386
- reject: (error) => {
391
+ reject: error => {
387
392
  logger.error(`[Unity] Command ${id} rejected: ${error.message}`);
388
393
  clearTimeout(timeout);
389
- try { task.outerReject(error); } finally {
394
+ try {
395
+ task.outerReject(error);
396
+ } finally {
390
397
  this.inFlight = Math.max(0, this.inFlight - 1);
391
398
  this._pumpQueue();
392
399
  }
@@ -394,7 +401,7 @@ export class UnityConnection extends EventEmitter {
394
401
  });
395
402
 
396
403
  // Send framed message
397
- this.socket.write(framedMessage, (error) => {
404
+ this.socket.write(framedMessage, error => {
398
405
  if (error) {
399
406
  logger.error(`[Unity] Failed to write command ${id}: ${error.message}`);
400
407
  clearTimeout(timeout);
@@ -1,4 +1,4 @@
1
- import { BaseToolHandler } from '../base/BaseToolHandler.js';
1
+ import { BaseToolHandler } from '../base/BaseToolHandler.js'
2
2
 
3
3
  /**
4
4
  * Addressables Analysis Tool Handler for Unity MCP
@@ -38,133 +38,143 @@ export default class AddressablesAnalyzeToolHandler extends BaseToolHandler {
38
38
  },
39
39
  required: ['action']
40
40
  }
41
- );
42
- this.unityConnection = unityConnection;
41
+ )
42
+ this.unityConnection = unityConnection
43
43
  }
44
44
 
45
45
  validate(params) {
46
- const { action, assetPath } = params || {};
46
+ const { action, assetPath } = params || {}
47
47
 
48
48
  if (!action) {
49
- throw new Error('action is required');
49
+ throw new Error('action is required')
50
50
  }
51
51
 
52
- const validActions = ['analyze_duplicates', 'analyze_dependencies', 'analyze_unused'];
52
+ const validActions = ['analyze_duplicates', 'analyze_dependencies', 'analyze_unused']
53
53
  if (!validActions.includes(action)) {
54
- throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`);
54
+ throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`)
55
55
  }
56
56
 
57
57
  // Action-specific validation
58
58
  if (action === 'analyze_dependencies' && !assetPath) {
59
- throw new Error('assetPath is required for analyze_dependencies');
59
+ throw new Error('assetPath is required for analyze_dependencies')
60
60
  }
61
61
  }
62
62
 
63
63
  async execute(params) {
64
- const { action, ...parameters } = params;
64
+ const { action, ...parameters } = params
65
65
 
66
66
  // Ensure connected
67
67
  if (!this.unityConnection.isConnected()) {
68
- await this.unityConnection.connect();
68
+ await this.unityConnection.connect()
69
69
  }
70
70
 
71
71
  const result = await this.unityConnection.sendCommand('addressables_analyze', {
72
72
  action,
73
73
  ...parameters
74
- });
74
+ })
75
75
 
76
- return this.formatResponse(action, result);
76
+ return this.formatResponse(action, result)
77
77
  }
78
78
 
79
79
  formatResponse(action, result) {
80
80
  if (result && result.error) {
81
- throw new Error(result.error.message || result.error);
81
+ throw new Error(result.error.message || result.error)
82
82
  }
83
83
 
84
84
  if (!result || typeof result !== 'object') {
85
- throw new Error('Invalid response from Unity');
85
+ throw new Error('Invalid response from Unity')
86
86
  }
87
87
 
88
88
  // Return formatted response
89
89
  return {
90
- content: [{
91
- type: 'text',
92
- text: this.formatResultText(action, result)
93
- }]
94
- };
90
+ content: [
91
+ {
92
+ type: 'text',
93
+ text: this.formatResultText(action, result)
94
+ }
95
+ ]
96
+ }
95
97
  }
96
98
 
97
99
  formatResultText(action, result) {
98
- const lines = [];
100
+ const lines = []
99
101
 
100
102
  switch (action) {
101
103
  case 'analyze_duplicates':
102
- lines.push(`🔍 重複アセット分析結果`);
104
+ lines.push('🔍 重複アセット分析結果')
103
105
  if (result.data && result.data.duplicates) {
104
106
  if (result.data.duplicates.length === 0) {
105
- lines.push(`重複アセットは見つかりませんでした`);
107
+ lines.push('重複アセットは見つかりませんでした')
106
108
  } else {
107
- lines.push(` ⚠️ 重複アセット: ${result.pagination.total}件`);
109
+ lines.push(` ⚠️ 重複アセット: ${result.pagination.total}件`)
108
110
  result.data.duplicates.forEach(dup => {
109
- lines.push(`\n 📁 ${dup.assetPath}`);
110
- lines.push(` 使用グループ: ${dup.groups.join(', ')}`);
111
- });
111
+ lines.push(`\n 📁 ${dup.assetPath}`)
112
+ lines.push(` 使用グループ: ${dup.groups.join(', ')}`)
113
+ })
112
114
  if (result.pagination.hasMore) {
113
- lines.push(`\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`);
115
+ lines.push(
116
+ `\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`
117
+ )
114
118
  }
115
119
  }
116
120
  }
117
- break;
121
+ break
118
122
 
119
123
  case 'analyze_dependencies':
120
- lines.push(`🔍 依存関係分析結果`);
124
+ lines.push('🔍 依存関係分析結果')
121
125
  if (result.data && result.data.dependencies) {
122
- const deps = Object.entries(result.data.dependencies);
126
+ const deps = Object.entries(result.data.dependencies)
123
127
  if (deps.length === 0) {
124
- lines.push(`依存関係がありません`);
128
+ lines.push('依存関係がありません')
125
129
  } else {
126
130
  deps.forEach(([assetPath, dependencies]) => {
127
- lines.push(`\n 📁 ${assetPath}`);
131
+ lines.push(`\n 📁 ${assetPath}`)
128
132
  if (dependencies.length === 0) {
129
- lines.push(` 依存なし`);
133
+ lines.push(' 依存なし')
130
134
  } else {
131
- lines.push(` 依存数: ${dependencies.length}個`);
135
+ lines.push(` 依存数: ${dependencies.length}個`)
132
136
  dependencies.forEach((dep, idx) => {
133
137
  if (idx < 10) {
134
- lines.push(` → ${dep}`);
138
+ lines.push(` → ${dep}`)
135
139
  }
136
- });
140
+ })
137
141
  if (dependencies.length > 10) {
138
- lines.push(` ... 他${dependencies.length - 10}件`);
142
+ lines.push(` ... 他${dependencies.length - 10}件`)
139
143
  }
140
144
  }
141
- });
145
+ })
142
146
  }
143
147
  }
144
- break;
148
+ break
145
149
 
146
150
  case 'analyze_unused':
147
- lines.push(`🔍 未使用アセット分析結果`);
151
+ lines.push('🔍 未使用アセット分析結果')
148
152
  if (result.data && result.data.unused) {
149
153
  if (result.data.unused.length === 0) {
150
- lines.push(`すべてのアセットが使用されています`);
154
+ lines.push('すべてのアセットが使用されています')
151
155
  } else {
152
- lines.push(` ⚠️ 未使用アセット: ${result.pagination.total}件`);
156
+ lines.push(` ⚠️ 未使用アセット: ${result.pagination.total}件`)
153
157
  result.data.unused.forEach(path => {
154
- lines.push(` 📁 ${path}`);
155
- });
158
+ lines.push(` 📁 ${path}`)
159
+ })
156
160
  if (result.pagination.hasMore) {
157
- lines.push(`\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`);
161
+ lines.push(
162
+ `\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`
163
+ )
158
164
  }
159
- lines.push(`\n 💡 これらのアセットはAddressableとして登録されておらず、他のAddressableからも参照されていません`);
165
+ lines.push(
166
+ '\n 💡 これらのアセットはAddressableとして登録されておらず、他のAddressableからも参照されていません'
167
+ )
160
168
  }
161
169
  }
162
- break;
170
+ break
163
171
 
164
172
  default:
165
- lines.push(JSON.stringify(result, null, 2));
173
+ lines.push(JSON.stringify(result, null, 2))
166
174
  }
167
175
 
168
- return lines.join('\n');
176
+ return lines.join('\n')
169
177
  }
170
178
  }
179
+
180
+