@akiojin/unity-mcp-server 2.37.1 → 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 +1 -1
- package/src/handlers/console/ConsoleReadToolHandler.js +18 -16
- package/src/handlers/editor/EditorQuitToolHandler.js +26 -0
- package/src/handlers/index.js +2 -0
- package/src/handlers/input/InputGamepadToolHandler.js +12 -3
- package/src/handlers/input/InputKeyboardToolHandler.js +12 -3
- package/src/handlers/input/InputMouseToolHandler.js +12 -3
- package/src/handlers/input/InputTouchToolHandler.js +12 -3
- package/src/handlers/test/TestGetStatusToolHandler.js +34 -9
- package/src/handlers/test/TestRunToolHandler.js +6 -1
- package/src/utils/testRunState.js +90 -0
package/package.json
CHANGED
|
@@ -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:
|
|
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: ['
|
|
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 = ['
|
|
97
|
+
const validTypes = ['Log', 'Warning', 'Error'];
|
|
92
98
|
for (const type of logTypes) {
|
|
93
99
|
if (!validTypes.includes(type)) {
|
|
94
|
-
|
|
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
|
-
//
|
|
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 '
|
|
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
|
-
|
|
201
|
-
|
|
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
|
+
}
|
package/src/handlers/index.js
CHANGED
|
@@ -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,
|
|
@@ -123,15 +123,24 @@ export class InputGamepadToolHandler extends BaseToolHandler {
|
|
|
123
123
|
required: ['action']
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
}
|
|
127
|
-
anyOf: [{ required: ['action'] }, { required: ['actions'] }]
|
|
126
|
+
}
|
|
128
127
|
}
|
|
129
128
|
);
|
|
130
129
|
this.unityConnection = unityConnection;
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
validate(params) {
|
|
134
|
-
const { actions } = params;
|
|
133
|
+
const { action, actions } = params;
|
|
134
|
+
|
|
135
|
+
// Either 'action' or 'actions' must be provided
|
|
136
|
+
if (!action && !actions) {
|
|
137
|
+
throw new Error('Either "action" or "actions" parameter is required');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// But not both
|
|
141
|
+
if (action && actions) {
|
|
142
|
+
throw new Error('Cannot specify both "action" and "actions" parameters');
|
|
143
|
+
}
|
|
135
144
|
|
|
136
145
|
if (Array.isArray(actions)) {
|
|
137
146
|
if (actions.length === 0) {
|
|
@@ -88,15 +88,24 @@ export class InputKeyboardToolHandler extends BaseToolHandler {
|
|
|
88
88
|
required: ['action']
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
}
|
|
92
|
-
anyOf: [{ required: ['action'] }, { required: ['actions'] }]
|
|
91
|
+
}
|
|
93
92
|
}
|
|
94
93
|
);
|
|
95
94
|
this.unityConnection = unityConnection;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
validate(params) {
|
|
99
|
-
const { actions } = params;
|
|
98
|
+
const { action, actions } = params;
|
|
99
|
+
|
|
100
|
+
// Either 'action' or 'actions' must be provided
|
|
101
|
+
if (!action && !actions) {
|
|
102
|
+
throw new Error('Either "action" or "actions" parameter is required');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// But not both
|
|
106
|
+
if (action && actions) {
|
|
107
|
+
throw new Error('Cannot specify both "action" and "actions" parameters');
|
|
108
|
+
}
|
|
100
109
|
|
|
101
110
|
if (Array.isArray(actions)) {
|
|
102
111
|
if (actions.length === 0) {
|
|
@@ -149,15 +149,24 @@ export class InputMouseToolHandler extends BaseToolHandler {
|
|
|
149
149
|
required: ['action']
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
}
|
|
153
|
-
anyOf: [{ required: ['action'] }, { required: ['actions'] }]
|
|
152
|
+
}
|
|
154
153
|
}
|
|
155
154
|
);
|
|
156
155
|
this.unityConnection = unityConnection;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
validate(params) {
|
|
160
|
-
const { actions } = params;
|
|
159
|
+
const { action, actions } = params;
|
|
160
|
+
|
|
161
|
+
// Either 'action' or 'actions' must be provided
|
|
162
|
+
if (!action && !actions) {
|
|
163
|
+
throw new Error('Either "action" or "actions" parameter is required');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// But not both
|
|
167
|
+
if (action && actions) {
|
|
168
|
+
throw new Error('Cannot specify both "action" and "actions" parameters');
|
|
169
|
+
}
|
|
161
170
|
|
|
162
171
|
if (Array.isArray(actions)) {
|
|
163
172
|
if (actions.length === 0) {
|
|
@@ -140,14 +140,23 @@ export class InputTouchToolHandler extends BaseToolHandler {
|
|
|
140
140
|
required: ['action']
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
}
|
|
144
|
-
anyOf: [{ required: ['action'] }, { required: ['actions'] }]
|
|
143
|
+
}
|
|
145
144
|
});
|
|
146
145
|
this.unityConnection = unityConnection;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
validate(params) {
|
|
150
|
-
const { actions } = params;
|
|
149
|
+
const { action, actions } = params;
|
|
150
|
+
|
|
151
|
+
// Either 'action' or 'actions' must be provided
|
|
152
|
+
if (!action && !actions) {
|
|
153
|
+
throw new Error('Either "action" or "actions" parameter is required');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// But not both
|
|
157
|
+
if (action && actions) {
|
|
158
|
+
throw new Error('Cannot specify both "action" and "actions" parameters');
|
|
159
|
+
}
|
|
151
160
|
|
|
152
161
|
if (Array.isArray(actions)) {
|
|
153
162
|
if (actions.length === 0) {
|
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
+
}
|