@akiojin/unity-mcp-server 2.37.2 → 2.38.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.37.2",
3
+ "version": "2.38.0",
4
4
  "description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
5
5
  "type": "module",
6
6
  "main": "src/core/server.js",
@@ -11,6 +11,11 @@ export class ConsoleReadToolHandler extends BaseToolHandler {
11
11
  {
12
12
  type: 'object',
13
13
  properties: {
14
+ raw: {
15
+ type: 'boolean',
16
+ description:
17
+ 'When true, do not expand/validate logTypes and pass them directly to Unity. Use with caution.'
18
+ },
14
19
  count: {
15
20
  type: 'number',
16
21
  description: 'Number of logs to retrieve (1-1000, default: 100)',
@@ -19,10 +24,11 @@ export class ConsoleReadToolHandler extends BaseToolHandler {
19
24
  },
20
25
  logTypes: {
21
26
  type: 'array',
22
- description: 'Filter by log types (default: ["All"])',
27
+ description:
28
+ 'Filter by log types. Allowed: Log, Warning, Error. Error expands to include Exception/Assert internally. Default: all three.',
23
29
  items: {
24
30
  type: 'string',
25
- enum: ['Info', 'Warning', 'Error', 'All']
31
+ enum: ['Log', 'Warning', 'Error']
26
32
  }
27
33
  },
28
34
  filterText: {
@@ -88,11 +94,10 @@ export class ConsoleReadToolHandler extends BaseToolHandler {
88
94
  throw new Error('logTypes must be an array');
89
95
  }
90
96
 
91
- const validTypes = ['Info', 'Warning', 'Error', 'All'];
97
+ const validTypes = ['Log', 'Warning', 'Error'];
92
98
  for (const type of logTypes) {
93
99
  if (!validTypes.includes(type)) {
94
- // Invalid types are treated as 'All' later, so just log a warning
95
- console.warn(`Invalid log type: ${type}. Will be treated as 'All'.`);
100
+ throw new Error(`logTypes must be one of: ${validTypes.join(', ')}`);
96
101
  }
97
102
  }
98
103
  }
@@ -177,34 +182,31 @@ export class ConsoleReadToolHandler extends BaseToolHandler {
177
182
  groupBy
178
183
  } = params;
179
184
 
180
- // Convert simplified log types to Unity log types
185
+ // Allowed: Log / Warning / Error. Error expands to include Exception/Assert.
181
186
  let expandedLogTypes = [];
182
-
183
- if (!logTypes || logTypes.length === 0 || logTypes.includes('All')) {
184
- // Default to all types
187
+ if (!logTypes || logTypes.length === 0) {
185
188
  expandedLogTypes = ['Log', 'Warning', 'Error', 'Exception', 'Assert'];
186
189
  } else {
187
- // Expand each simplified type to Unity types
188
190
  logTypes.forEach(type => {
189
191
  switch (type) {
190
- case 'Info':
192
+ case 'Log':
191
193
  expandedLogTypes.push('Log');
192
194
  break;
193
195
  case 'Warning':
194
196
  expandedLogTypes.push('Warning');
195
197
  break;
196
198
  case 'Error':
197
- // Error includes all error-related types
198
199
  expandedLogTypes.push('Error', 'Exception', 'Assert');
199
200
  break;
200
- case 'All':
201
- expandedLogTypes.push('Log', 'Warning', 'Error', 'Exception', 'Assert');
201
+ default:
202
+ // ignore (validate already enforces)
202
203
  break;
203
204
  }
204
205
  });
205
-
206
- // Remove duplicates
207
206
  expandedLogTypes = [...new Set(expandedLogTypes)];
207
+ if (expandedLogTypes.length === 0) {
208
+ expandedLogTypes = ['Log', 'Warning', 'Error', 'Exception', 'Assert'];
209
+ }
208
210
  }
209
211
 
210
212
  // Ensure connection to Unity
@@ -0,0 +1,26 @@
1
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
+
3
+ /**
4
+ * Handler to quit Unity Editor safely.
5
+ */
6
+ export class EditorQuitToolHandler extends BaseToolHandler {
7
+ constructor(unityConnection) {
8
+ super('editor_quit', 'Quit Unity Editor', {
9
+ type: 'object',
10
+ properties: {}
11
+ });
12
+
13
+ this.unityConnection = unityConnection;
14
+ }
15
+
16
+ async execute() {
17
+ if (!this.unityConnection.isConnected()) {
18
+ await this.unityConnection.connect();
19
+ }
20
+ const response = await this.unityConnection.sendCommand('quit_editor', {});
21
+ if (response.error) {
22
+ throw new Error(response.error);
23
+ }
24
+ return response;
25
+ }
26
+ }
@@ -94,6 +94,7 @@ import { EditorLayersManageToolHandler } from './editor/EditorLayersManageToolHa
94
94
  import { EditorSelectionManageToolHandler } from './editor/EditorSelectionManageToolHandler.js';
95
95
  import { EditorWindowsManageToolHandler } from './editor/EditorWindowsManageToolHandler.js';
96
96
  import { EditorToolsManageToolHandler } from './editor/EditorToolsManageToolHandler.js';
97
+ import { EditorQuitToolHandler } from './editor/EditorQuitToolHandler.js';
97
98
  import { SettingsGetToolHandler } from './settings/SettingsGetToolHandler.js';
98
99
  import { SettingsUpdateToolHandler } from './settings/SettingsUpdateToolHandler.js';
99
100
  import PackageManagerToolHandler from './package/PackageManagerToolHandler.js';
@@ -392,6 +393,7 @@ const HANDLER_CLASSES = [
392
393
  EditorSelectionManageToolHandler,
393
394
  EditorWindowsManageToolHandler,
394
395
  EditorToolsManageToolHandler,
396
+ EditorQuitToolHandler,
395
397
 
396
398
  // Settings handlers
397
399
  SettingsGetToolHandler,
@@ -1,5 +1,6 @@
1
1
  import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
2
  import * as testResultsCache from '../../utils/testResultsCache.js';
3
+ import * as testRunState from '../../utils/testRunState.js';
3
4
 
4
5
  /**
5
6
  * Handler for getting Unity test execution status
@@ -33,13 +34,32 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
33
34
  * @returns {Promise<Object>} Test execution status and results
34
35
  */
35
36
  async execute(params) {
36
- // Ensure connection to Unity
37
- if (!this.unityConnection.isConnected()) {
38
- await this.unityConnection.connect();
39
- }
37
+ let response;
38
+ try {
39
+ // Ensure connection to Unity
40
+ if (!this.unityConnection.isConnected()) {
41
+ await this.unityConnection.connect();
42
+ }
40
43
 
41
- // Send command to Unity
42
- const response = await this.unityConnection.sendCommand('get_test_status', params || {});
44
+ // Send command to Unity
45
+ response = await this.unityConnection.sendCommand('get_test_status', params || {});
46
+ } catch (error) {
47
+ // 一度リトライしてみる(ドメインリロード直後を考慮)
48
+ try {
49
+ await this.unityConnection.connect();
50
+ response = await this.unityConnection.sendCommand('get_test_status', params || {});
51
+ } catch (retryError) {
52
+ // Connection or transport error persists: fall back to cached run state so callers
53
+ // can still distinguish "テスト起因のPlayMode" かどうか。
54
+ const cached = testRunState.getState();
55
+ return {
56
+ ...cached,
57
+ status: cached.status || 'error',
58
+ error: retryError.message,
59
+ connectionState: 'disconnected'
60
+ };
61
+ }
62
+ }
43
63
 
44
64
  // Handle Unity response
45
65
  if (response.error) {
@@ -48,12 +68,14 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
48
68
 
49
69
  // Return status directly if still running or idle
50
70
  if (response.status === 'running') {
51
- return response;
71
+ const merged = { ...testRunState.updateFromStatus(response), ...response };
72
+ return merged;
52
73
  }
53
74
 
54
75
  if (response.status === 'idle') {
76
+ testRunState.markIdle();
55
77
  const cached = await this.testResultsCache.loadCachedTestResults();
56
- return cached || response;
78
+ return cached || testRunState.getState() || response;
57
79
  }
58
80
 
59
81
  // Format completed results
@@ -68,13 +90,16 @@ export class TestGetStatusToolHandler extends BaseToolHandler {
68
90
  inconclusiveTests: response.inconclusiveTests,
69
91
  summary: `${response.passedTests}/${response.totalTests} tests passed`,
70
92
  failures: response.failures || [],
71
- tests: response.tests || []
93
+ tests: response.tests || [],
94
+ testMode: response.testMode || testRunState.getState().testMode,
95
+ runId: testRunState.getState().runId
72
96
  };
73
97
 
74
98
  if (response.latestResult) {
75
99
  result.latestResult = response.latestResult;
76
100
  }
77
101
 
102
+ testRunState.updateFromStatus({ ...result, status: 'completed' });
78
103
  const artifactPath = await this.testResultsCache.persistTestResults(result);
79
104
  const cachedResult = await this.testResultsCache.loadCachedTestResults(artifactPath);
80
105
 
@@ -1,5 +1,6 @@
1
1
  import { BaseToolHandler } from '../base/BaseToolHandler.js';
2
2
  import { resetTestResultsCache } from '../../utils/testResultsCache.js';
3
+ import * as testRunState from '../../utils/testRunState.js';
3
4
 
4
5
  /**
5
6
  * Handler for running Unity NUnit tests via Test Runner API
@@ -67,10 +68,14 @@ export class TestRunToolHandler extends BaseToolHandler {
67
68
 
68
69
  // Handle new non-blocking response format (status: "running")
69
70
  if (response.status === 'running') {
71
+ const runState = testRunState.startRun(response.testMode || params.testMode, response.runId);
70
72
  return {
71
73
  status: 'running',
72
74
  message:
73
- response.message || 'Test execution started. Use get_test_status to check progress.'
75
+ response.message || 'Test execution started. Use get_test_status to check progress.',
76
+ testMode: runState.testMode || response.testMode,
77
+ runId: runState.runId || response.runId,
78
+ startedAt: runState.startedAt
74
79
  };
75
80
  }
76
81
 
@@ -0,0 +1,90 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { WORKSPACE_ROOT, logger } from '../core/config.js';
5
+
6
+ // Simple in-memory state for the latest test run.
7
+ // Persisting is unnecessary for the current use case because the Node process
8
+ // stays alive across Unity domain reloads, which is where we lose connection.
9
+ const state = {
10
+ status: 'idle', // idle | running | completed | error
11
+ runId: null,
12
+ testMode: null,
13
+ startedAt: null,
14
+ completedAt: null,
15
+ lastUpdate: null
16
+ };
17
+
18
+ const workspaceUnityRoot = path.join(WORKSPACE_ROOT || process.cwd(), '.unity');
19
+ const runStatePath = path.join(workspaceUnityRoot, 'tests', 'test-run-state.json');
20
+
21
+ // Load persisted state once at module load
22
+ await (async () => {
23
+ try {
24
+ const data = await fs.readFile(runStatePath, 'utf8');
25
+ const parsed = JSON.parse(data);
26
+ Object.assign(state, parsed);
27
+ logger.info(`[testRunState] Loaded persisted state from ${runStatePath}`);
28
+ } catch (error) {
29
+ if (error.code !== 'ENOENT') {
30
+ logger.warn(`[testRunState] Failed to load state from ${runStatePath}: ${error.message}`);
31
+ }
32
+ }
33
+ })();
34
+
35
+ export function startRun(testMode, runId) {
36
+ const now = new Date().toISOString();
37
+ state.status = 'running';
38
+ state.runId = runId || crypto.randomUUID();
39
+ state.testMode = testMode || state.testMode;
40
+ state.startedAt = now;
41
+ state.completedAt = null;
42
+ state.lastUpdate = now;
43
+ persist();
44
+ return { ...state };
45
+ }
46
+
47
+ export function updateFromStatus(response) {
48
+ const now = new Date().toISOString();
49
+ if (response?.status) {
50
+ state.status = response.status;
51
+ }
52
+ if (response?.testMode) {
53
+ state.testMode = response.testMode;
54
+ }
55
+ if (response?.runId) {
56
+ state.runId = response.runId;
57
+ }
58
+ state.lastUpdate = now;
59
+
60
+ if (response?.status === 'completed') {
61
+ state.completedAt = now;
62
+ }
63
+ if (response?.status === 'idle') {
64
+ state.runId = null;
65
+ state.testMode = null;
66
+ state.startedAt = null;
67
+ state.completedAt = null;
68
+ }
69
+ persist();
70
+ return { ...state };
71
+ }
72
+
73
+ export function markIdle() {
74
+ const res = updateFromStatus({ status: 'idle' });
75
+ return res;
76
+ }
77
+
78
+ export function getState() {
79
+ return { ...state };
80
+ }
81
+
82
+ async function persist() {
83
+ try {
84
+ const dir = path.dirname(runStatePath);
85
+ await fs.mkdir(dir, { recursive: true });
86
+ await fs.writeFile(runStatePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
87
+ } catch (error) {
88
+ logger.warn(`[testRunState] Failed to persist state to ${runStatePath}: ${error.message}`);
89
+ }
90
+ }