@akiojin/unity-mcp-server 2.32.0 → 2.37.0

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 (26) hide show
  1. package/README.md +30 -5
  2. package/package.json +9 -4
  3. package/src/core/config.js +241 -242
  4. package/src/core/projectInfo.js +15 -0
  5. package/src/core/transports/HybridStdioServerTransport.js +78 -75
  6. package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +45 -47
  7. package/src/handlers/addressables/AddressablesBuildToolHandler.js +32 -33
  8. package/src/handlers/addressables/AddressablesManageToolHandler.js +74 -75
  9. package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -419
  10. package/src/handlers/index.js +437 -437
  11. package/src/handlers/input/InputGamepadToolHandler.js +162 -0
  12. package/src/handlers/input/InputKeyboardToolHandler.js +127 -0
  13. package/src/handlers/input/InputMouseToolHandler.js +188 -0
  14. package/src/handlers/input/InputSystemControlToolHandler.js +63 -64
  15. package/src/handlers/input/InputTouchToolHandler.js +178 -0
  16. package/src/handlers/playmode/PlaymodePlayToolHandler.js +36 -23
  17. package/src/handlers/playmode/PlaymodeStopToolHandler.js +32 -21
  18. package/src/handlers/test/TestGetStatusToolHandler.js +37 -10
  19. package/src/handlers/test/TestRunToolHandler.js +36 -35
  20. package/src/lsp/LspProcessManager.js +18 -12
  21. package/src/utils/editorState.js +42 -0
  22. package/src/utils/testResultsCache.js +70 -0
  23. package/src/handlers/input/InputGamepadSimulateToolHandler.js +0 -116
  24. package/src/handlers/input/InputKeyboardSimulateToolHandler.js +0 -79
  25. package/src/handlers/input/InputMouseSimulateToolHandler.js +0 -107
  26. package/src/handlers/input/InputTouchSimulateToolHandler.js +0 -142
@@ -1,178 +1,181 @@
1
- import process from 'node:process'
2
- import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js'
1
+ import process from 'node:process';
2
+ import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
3
3
 
4
- const HEADER_END = '\r\n\r\n'
5
- const HEADER_RE = /Content-Length:\s*(\d+)/i
6
- const DEFAULT_BUFFER = Buffer.alloc(0)
4
+ const HEADER_END = '\r\n\r\n';
5
+ const HEADER_RE = /Content-Length:\s*(\d+)/i;
6
+ const DEFAULT_BUFFER = Buffer.alloc(0);
7
7
 
8
8
  function encodeContentLength(message) {
9
- const json = JSON.stringify(message)
10
- const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}${HEADER_END}`
11
- return header + json
9
+ const json = JSON.stringify(message);
10
+ const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}${HEADER_END}`;
11
+ return header + json;
12
12
  }
13
13
 
14
14
  function encodeNdjson(message) {
15
- return `${JSON.stringify(message)}\n`
15
+ return `${JSON.stringify(message)}\n`;
16
16
  }
17
17
 
18
18
  function parseJson(text) {
19
- return JSONRPCMessageSchema.parse(JSON.parse(text))
19
+ return JSONRPCMessageSchema.parse(JSON.parse(text));
20
20
  }
21
21
 
22
22
  export class HybridStdioServerTransport {
23
23
  constructor(stdin = process.stdin, stdout = process.stdout) {
24
- this._stdin = stdin
25
- this._stdout = stdout
26
- this._buffer = DEFAULT_BUFFER
27
- this._started = false
28
- this._mode = null // 'content-length' | 'ndjson'
24
+ this._stdin = stdin;
25
+ this._stdout = stdout;
26
+ this._buffer = DEFAULT_BUFFER;
27
+ this._started = false;
28
+ this._mode = null; // 'content-length' | 'ndjson'
29
29
 
30
30
  this._onData = chunk => {
31
- this._buffer = this._buffer.length ? Buffer.concat([this._buffer, chunk]) : Buffer.from(chunk)
32
- this._processBuffer()
33
- }
31
+ this._buffer = this._buffer.length
32
+ ? Buffer.concat([this._buffer, chunk])
33
+ : Buffer.from(chunk);
34
+ this._processBuffer();
35
+ };
34
36
 
35
37
  this._onError = error => {
36
- this.onerror?.(error)
37
- }
38
+ this.onerror?.(error);
39
+ };
38
40
  }
39
41
 
40
42
  get framingMode() {
41
- return this._mode
43
+ return this._mode;
42
44
  }
43
45
 
44
46
  async start() {
45
47
  if (this._started) {
46
- throw new Error('HybridStdioServerTransport already started')
48
+ throw new Error('HybridStdioServerTransport already started');
47
49
  }
48
- this._started = true
49
- this._stdin.on('data', this._onData)
50
- this._stdin.on('error', this._onError)
50
+ this._started = true;
51
+ this._stdin.on('data', this._onData);
52
+ this._stdin.on('error', this._onError);
51
53
  }
52
54
 
53
55
  async close() {
54
- if (!this._started) return
55
- this._stdin.off('data', this._onData)
56
- this._stdin.off('error', this._onError)
57
- this._buffer = DEFAULT_BUFFER
58
- this._started = false
59
- this.onclose?.()
56
+ if (!this._started) return;
57
+ this._stdin.off('data', this._onData);
58
+ this._stdin.off('error', this._onError);
59
+ this._buffer = DEFAULT_BUFFER;
60
+ this._started = false;
61
+ this.onclose?.();
60
62
  }
61
63
 
62
64
  send(message) {
63
65
  return new Promise(resolve => {
64
- const payload = this._mode === 'ndjson' ? encodeNdjson(message) : encodeContentLength(message)
66
+ const payload =
67
+ this._mode === 'ndjson' ? encodeNdjson(message) : encodeContentLength(message);
65
68
  if (this._stdout.write(payload)) {
66
- resolve()
69
+ resolve();
67
70
  } else {
68
- this._stdout.once('drain', resolve)
71
+ this._stdout.once('drain', resolve);
69
72
  }
70
- })
73
+ });
71
74
  }
72
75
 
73
76
  _processBuffer() {
74
77
  while (true) {
75
- const message = this._readMessage()
78
+ const message = this._readMessage();
76
79
  if (message === null) {
77
- break
80
+ break;
78
81
  }
79
- this.onmessage?.(message)
82
+ this.onmessage?.(message);
80
83
  }
81
84
  }
82
85
 
83
86
  _readMessage() {
84
87
  if (!this._buffer || this._buffer.length === 0) {
85
- return null
88
+ return null;
86
89
  }
87
90
 
88
91
  if (this._mode === 'content-length') {
89
- return this._readContentLengthMessage()
92
+ return this._readContentLengthMessage();
90
93
  }
91
94
  if (this._mode === 'ndjson') {
92
- return this._readNdjsonMessage()
95
+ return this._readNdjsonMessage();
93
96
  }
94
97
 
95
- const prefix = this._peekPrefix()
98
+ const prefix = this._peekPrefix();
96
99
  if (!prefix.length) {
97
- return null
100
+ return null;
98
101
  }
99
102
 
100
103
  if ('content-length:'.startsWith(prefix.toLowerCase())) {
101
- return null // Wait for full header keyword before deciding
104
+ return null; // Wait for full header keyword before deciding
102
105
  }
103
106
 
104
107
  if (prefix.toLowerCase().startsWith('content-length:')) {
105
- this._mode = 'content-length'
106
- return this._readContentLengthMessage()
108
+ this._mode = 'content-length';
109
+ return this._readContentLengthMessage();
107
110
  }
108
111
 
109
- const newlineIndex = this._buffer.indexOf(0x0a) // '\n'
112
+ const newlineIndex = this._buffer.indexOf(0x0a); // '\n'
110
113
  if (newlineIndex === -1) {
111
- return null
114
+ return null;
112
115
  }
113
116
 
114
- this._mode = 'ndjson'
115
- return this._readNdjsonMessage()
117
+ this._mode = 'ndjson';
118
+ return this._readNdjsonMessage();
116
119
  }
117
120
 
118
121
  _peekPrefix() {
119
- const length = Math.min(this._buffer.length, 32)
120
- return this._buffer.toString('utf8', 0, length).trimStart()
122
+ const length = Math.min(this._buffer.length, 32);
123
+ return this._buffer.toString('utf8', 0, length).trimStart();
121
124
  }
122
125
 
123
126
  _readContentLengthMessage() {
124
- const headerEndIndex = this._buffer.indexOf(HEADER_END)
127
+ const headerEndIndex = this._buffer.indexOf(HEADER_END);
125
128
  if (headerEndIndex === -1) {
126
- return null
129
+ return null;
127
130
  }
128
131
 
129
- const header = this._buffer.toString('utf8', 0, headerEndIndex)
130
- const match = header.match(HEADER_RE)
132
+ const header = this._buffer.toString('utf8', 0, headerEndIndex);
133
+ const match = header.match(HEADER_RE);
131
134
  if (!match) {
132
- this._buffer = this._buffer.subarray(headerEndIndex + HEADER_END.length)
133
- this.onerror?.(new Error('Invalid Content-Length header'))
134
- return null
135
+ this._buffer = this._buffer.subarray(headerEndIndex + HEADER_END.length);
136
+ this.onerror?.(new Error('Invalid Content-Length header'));
137
+ return null;
135
138
  }
136
139
 
137
- const length = Number(match[1])
138
- const totalMessageLength = headerEndIndex + HEADER_END.length + length
140
+ const length = Number(match[1]);
141
+ const totalMessageLength = headerEndIndex + HEADER_END.length + length;
139
142
  if (this._buffer.length < totalMessageLength) {
140
- return null
143
+ return null;
141
144
  }
142
145
 
143
146
  const json = this._buffer.toString(
144
147
  'utf8',
145
148
  headerEndIndex + HEADER_END.length,
146
149
  totalMessageLength
147
- )
148
- this._buffer = this._buffer.subarray(totalMessageLength)
150
+ );
151
+ this._buffer = this._buffer.subarray(totalMessageLength);
149
152
 
150
153
  try {
151
- return parseJson(json)
154
+ return parseJson(json);
152
155
  } catch (error) {
153
- this.onerror?.(error)
154
- return null
156
+ this.onerror?.(error);
157
+ return null;
155
158
  }
156
159
  }
157
160
 
158
161
  _readNdjsonMessage() {
159
162
  while (true) {
160
- const newlineIndex = this._buffer.indexOf(0x0a)
163
+ const newlineIndex = this._buffer.indexOf(0x0a);
161
164
  if (newlineIndex === -1) {
162
- return null
165
+ return null;
163
166
  }
164
167
 
165
- let line = this._buffer.toString('utf8', 0, newlineIndex)
166
- this._buffer = this._buffer.subarray(newlineIndex + 1)
167
- line = line.trim()
168
+ let line = this._buffer.toString('utf8', 0, newlineIndex);
169
+ this._buffer = this._buffer.subarray(newlineIndex + 1);
170
+ line = line.trim();
168
171
  if (!line) {
169
- continue
172
+ continue;
170
173
  }
171
174
 
172
175
  try {
173
- return parseJson(line)
176
+ return parseJson(line);
174
177
  } catch (error) {
175
- this.onerror?.(error)
178
+ this.onerror?.(error);
176
179
  }
177
180
  }
178
181
  }
@@ -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,51 +38,51 @@ 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
@@ -93,88 +93,86 @@ export default class AddressablesAnalyzeToolHandler extends BaseToolHandler {
93
93
  text: this.formatResultText(action, result)
94
94
  }
95
95
  ]
96
- }
96
+ };
97
97
  }
98
98
 
99
99
  formatResultText(action, result) {
100
- const lines = []
100
+ const lines = [];
101
101
 
102
102
  switch (action) {
103
103
  case 'analyze_duplicates':
104
- lines.push('🔍 重複アセット分析結果')
104
+ lines.push('🔍 重複アセット分析結果');
105
105
  if (result.data && result.data.duplicates) {
106
106
  if (result.data.duplicates.length === 0) {
107
- lines.push(' ✅ 重複アセットは見つかりませんでした')
107
+ lines.push(' ✅ 重複アセットは見つかりませんでした');
108
108
  } else {
109
- lines.push(` ⚠️ 重複アセット: ${result.pagination.total}件`)
109
+ lines.push(` ⚠️ 重複アセット: ${result.pagination.total}件`);
110
110
  result.data.duplicates.forEach(dup => {
111
- lines.push(`\n 📁 ${dup.assetPath}`)
112
- lines.push(` 使用グループ: ${dup.groups.join(', ')}`)
113
- })
111
+ lines.push(`\n 📁 ${dup.assetPath}`);
112
+ lines.push(` 使用グループ: ${dup.groups.join(', ')}`);
113
+ });
114
114
  if (result.pagination.hasMore) {
115
115
  lines.push(
116
116
  `\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`
117
- )
117
+ );
118
118
  }
119
119
  }
120
120
  }
121
- break
121
+ break;
122
122
 
123
123
  case 'analyze_dependencies':
124
- lines.push('🔍 依存関係分析結果')
124
+ lines.push('🔍 依存関係分析結果');
125
125
  if (result.data && result.data.dependencies) {
126
- const deps = Object.entries(result.data.dependencies)
126
+ const deps = Object.entries(result.data.dependencies);
127
127
  if (deps.length === 0) {
128
- lines.push(' ✅ 依存関係がありません')
128
+ lines.push(' ✅ 依存関係がありません');
129
129
  } else {
130
130
  deps.forEach(([assetPath, dependencies]) => {
131
- lines.push(`\n 📁 ${assetPath}`)
131
+ lines.push(`\n 📁 ${assetPath}`);
132
132
  if (dependencies.length === 0) {
133
- lines.push(' 依存なし')
133
+ lines.push(' 依存なし');
134
134
  } else {
135
- lines.push(` 依存数: ${dependencies.length}個`)
135
+ lines.push(` 依存数: ${dependencies.length}個`);
136
136
  dependencies.forEach((dep, idx) => {
137
137
  if (idx < 10) {
138
- lines.push(` → ${dep}`)
138
+ lines.push(` → ${dep}`);
139
139
  }
140
- })
140
+ });
141
141
  if (dependencies.length > 10) {
142
- lines.push(` ... 他${dependencies.length - 10}件`)
142
+ lines.push(` ... 他${dependencies.length - 10}件`);
143
143
  }
144
144
  }
145
- })
145
+ });
146
146
  }
147
147
  }
148
- break
148
+ break;
149
149
 
150
150
  case 'analyze_unused':
151
- lines.push('🔍 未使用アセット分析結果')
151
+ lines.push('🔍 未使用アセット分析結果');
152
152
  if (result.data && result.data.unused) {
153
153
  if (result.data.unused.length === 0) {
154
- lines.push(' ✅ すべてのアセットが使用されています')
154
+ lines.push(' ✅ すべてのアセットが使用されています');
155
155
  } else {
156
- lines.push(` ⚠️ 未使用アセット: ${result.pagination.total}件`)
156
+ lines.push(` ⚠️ 未使用アセット: ${result.pagination.total}件`);
157
157
  result.data.unused.forEach(path => {
158
- lines.push(` 📁 ${path}`)
159
- })
158
+ lines.push(` 📁 ${path}`);
159
+ });
160
160
  if (result.pagination.hasMore) {
161
161
  lines.push(
162
162
  `\n ... さらに${result.pagination.total - result.pagination.offset - result.pagination.pageSize}件あります`
163
- )
163
+ );
164
164
  }
165
165
  lines.push(
166
166
  '\n 💡 これらのアセットはAddressableとして登録されておらず、他のAddressableからも参照されていません'
167
- )
167
+ );
168
168
  }
169
169
  }
170
- break
170
+ break;
171
171
 
172
172
  default:
173
- lines.push(JSON.stringify(result, null, 2))
173
+ lines.push(JSON.stringify(result, null, 2));
174
174
  }
175
175
 
176
- return lines.join('\n')
176
+ return lines.join('\n');
177
177
  }
178
178
  }
179
-
180
-
@@ -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 Build Tool Handler for Unity MCP
@@ -29,20 +29,20 @@ export default class AddressablesBuildToolHandler extends BaseToolHandler {
29
29
  }
30
30
  },
31
31
  required: ['action']
32
- })
33
- this.unityConnection = unityConnection
32
+ });
33
+ this.unityConnection = unityConnection;
34
34
  }
35
35
 
36
36
  validate(params) {
37
- const { action, buildTarget } = params || {}
37
+ const { action, buildTarget } = params || {};
38
38
 
39
39
  if (!action) {
40
- throw new Error('action is required')
40
+ throw new Error('action is required');
41
41
  }
42
42
 
43
- const validActions = ['build', 'clean_build']
43
+ const validActions = ['build', 'clean_build'];
44
44
  if (!validActions.includes(action)) {
45
- throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`)
45
+ throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`);
46
46
  }
47
47
 
48
48
  if (buildTarget) {
@@ -54,25 +54,25 @@ export default class AddressablesBuildToolHandler extends BaseToolHandler {
54
54
  'iOS',
55
55
  'Android',
56
56
  'WebGL'
57
- ]
57
+ ];
58
58
  if (!validTargets.includes(buildTarget)) {
59
59
  throw new Error(
60
60
  `Invalid buildTarget: ${buildTarget}. Must be one of: ${validTargets.join(', ')}`
61
- )
61
+ );
62
62
  }
63
63
  }
64
64
  }
65
65
 
66
66
  async execute(params) {
67
- const { action, ...parameters } = params
67
+ const { action, ...parameters } = params;
68
68
 
69
69
  // Ensure connected
70
70
  if (!this.unityConnection.isConnected()) {
71
- await this.unityConnection.connect()
71
+ await this.unityConnection.connect();
72
72
  }
73
73
 
74
74
  // Build operations can take several minutes
75
- const timeout = 300000 // 5 minutes
75
+ const timeout = 300000; // 5 minutes
76
76
 
77
77
  const result = await this.unityConnection.sendCommand(
78
78
  'addressables_build',
@@ -81,18 +81,18 @@ export default class AddressablesBuildToolHandler extends BaseToolHandler {
81
81
  ...parameters
82
82
  },
83
83
  timeout
84
- )
84
+ );
85
85
 
86
- return this.formatResponse(action, result)
86
+ return this.formatResponse(action, result);
87
87
  }
88
88
 
89
89
  formatResponse(action, result) {
90
90
  if (result && result.error) {
91
- throw new Error(result.error.message || result.error)
91
+ throw new Error(result.error.message || result.error);
92
92
  }
93
93
 
94
94
  if (!result || typeof result !== 'object') {
95
- throw new Error('Invalid response from Unity')
95
+ throw new Error('Invalid response from Unity');
96
96
  }
97
97
 
98
98
  // Return formatted response
@@ -103,44 +103,43 @@ export default class AddressablesBuildToolHandler extends BaseToolHandler {
103
103
  text: this.formatResultText(action, result)
104
104
  }
105
105
  ]
106
- }
106
+ };
107
107
  }
108
108
 
109
109
  formatResultText(action, result) {
110
- const lines = []
110
+ const lines = [];
111
111
 
112
112
  switch (action) {
113
113
  case 'build':
114
114
  if (result.data) {
115
115
  if (result.data.success) {
116
- lines.push('✅ Addressablesビルドが成功しました')
117
- lines.push(` ⏱️ 所要時間: ${result.data.duration.toFixed(2)}秒`)
116
+ lines.push('✅ Addressablesビルドが成功しました');
117
+ lines.push(` ⏱️ 所要時間: ${result.data.duration.toFixed(2)}秒`);
118
118
  if (result.data.outputPath) {
119
- lines.push(` 📁 出力先: ${result.data.outputPath}`)
119
+ lines.push(` 📁 出力先: ${result.data.outputPath}`);
120
120
  }
121
121
  } else {
122
- lines.push('❌ Addressablesビルドが失敗しました')
123
- lines.push(` ⏱️ 所要時間: ${result.data.duration.toFixed(2)}秒`)
122
+ lines.push('❌ Addressablesビルドが失敗しました');
123
+ lines.push(` ⏱️ 所要時間: ${result.data.duration.toFixed(2)}秒`);
124
124
  if (result.data.errors && result.data.errors.length > 0) {
125
- lines.push('\n エラー:')
125
+ lines.push('\n エラー:');
126
126
  result.data.errors.forEach(error => {
127
- lines.push(` ❌ ${error}`)
128
- })
127
+ lines.push(` ❌ ${error}`);
128
+ });
129
129
  }
130
130
  }
131
131
  }
132
- break
132
+ break;
133
133
 
134
134
  case 'clean_build':
135
- lines.push(`✅ ${result.message}`)
136
- lines.push(' ビルドキャッシュをクリアしました。次回ビルドは完全ビルドになります。')
137
- break
135
+ lines.push(`✅ ${result.message}`);
136
+ lines.push(' ビルドキャッシュをクリアしました。次回ビルドは完全ビルドになります。');
137
+ break;
138
138
 
139
139
  default:
140
- lines.push(JSON.stringify(result, null, 2))
140
+ lines.push(JSON.stringify(result, null, 2));
141
141
  }
142
142
 
143
- return lines.join('\n')
143
+ return lines.join('\n');
144
144
  }
145
145
  }
146
-