@akiojin/unity-mcp-server 4.1.5 → 4.2.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.
- package/package.json +1 -1
- package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +2 -2
- package/src/handlers/addressables/AddressablesBuildToolHandler.js +2 -2
- package/src/handlers/addressables/AddressablesManageToolHandler.js +2 -2
- package/src/handlers/input/InputSystemControlToolHandler.js +1 -1
- package/src/handlers/input/InputTouchToolHandler.js +2 -2
- package/src/handlers/package/PackageManagerToolHandler.js +2 -2
- package/src/handlers/script/ScriptEditSnippetToolHandler.js +16 -0
- package/src/handlers/script/ScriptEditStructuredToolHandler.js +19 -0
- package/src/handlers/script/csharpSyntaxCheck.js +177 -0
- package/src/lsp/LspRpcClient.js +11 -4
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
7
7
|
export default class AddressablesAnalyzeToolHandler extends BaseToolHandler {
|
|
8
8
|
constructor(unityConnection) {
|
|
9
9
|
super(
|
|
10
|
-
'
|
|
10
|
+
'analyze_addressables',
|
|
11
11
|
'Analyze Unity Addressables for duplicates, dependencies, and unused assets',
|
|
12
12
|
{
|
|
13
13
|
type: 'object',
|
|
@@ -68,7 +68,7 @@ export default class AddressablesAnalyzeToolHandler extends BaseToolHandler {
|
|
|
68
68
|
await this.unityConnection.connect();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const result = await this.unityConnection.sendCommand('
|
|
71
|
+
const result = await this.unityConnection.sendCommand('analyze_addressables', {
|
|
72
72
|
action,
|
|
73
73
|
...parameters
|
|
74
74
|
});
|
|
@@ -6,7 +6,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export default class AddressablesBuildToolHandler extends BaseToolHandler {
|
|
8
8
|
constructor(unityConnection) {
|
|
9
|
-
super('
|
|
9
|
+
super('build_addressables', 'Build Unity Addressables content or clean build cache', {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
12
12
|
action: {
|
|
@@ -75,7 +75,7 @@ export default class AddressablesBuildToolHandler extends BaseToolHandler {
|
|
|
75
75
|
const timeout = 300000; // 5 minutes
|
|
76
76
|
|
|
77
77
|
const result = await this.unityConnection.sendCommand(
|
|
78
|
-
'
|
|
78
|
+
'build_addressables',
|
|
79
79
|
{
|
|
80
80
|
action,
|
|
81
81
|
...parameters
|
|
@@ -7,7 +7,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
7
7
|
export default class AddressablesManageToolHandler extends BaseToolHandler {
|
|
8
8
|
constructor(unityConnection) {
|
|
9
9
|
super(
|
|
10
|
-
'
|
|
10
|
+
'manage_addressables',
|
|
11
11
|
'Manage Unity Addressables assets and groups - add, remove, organize entries and groups',
|
|
12
12
|
{
|
|
13
13
|
type: 'object',
|
|
@@ -141,7 +141,7 @@ export default class AddressablesManageToolHandler extends BaseToolHandler {
|
|
|
141
141
|
await this.unityConnection.connect();
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const result = await this.unityConnection.sendCommand('
|
|
144
|
+
const result = await this.unityConnection.sendCommand('manage_addressables', {
|
|
145
145
|
action,
|
|
146
146
|
...parameters
|
|
147
147
|
});
|
|
@@ -5,7 +5,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export class InputSystemControlToolHandler extends BaseToolHandler {
|
|
7
7
|
constructor(unityConnection) {
|
|
8
|
-
super('
|
|
8
|
+
super('control_input_system', 'Main handler for Input System operations', {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
11
|
operation: {
|
|
@@ -127,7 +127,7 @@ function validateTouchAction(params, context = 'action') {
|
|
|
127
127
|
*/
|
|
128
128
|
export class InputTouchToolHandler extends BaseToolHandler {
|
|
129
129
|
constructor(unityConnection) {
|
|
130
|
-
super('
|
|
130
|
+
super('simulate_touch', 'Touch input (tap/swipe/pinch/multi) with batching.', {
|
|
131
131
|
type: 'object',
|
|
132
132
|
properties: {
|
|
133
133
|
...actionProperties,
|
|
@@ -181,7 +181,7 @@ export class InputTouchToolHandler extends BaseToolHandler {
|
|
|
181
181
|
const hasBatch = Array.isArray(params.actions) && params.actions.length > 0;
|
|
182
182
|
const payload = hasBatch ? { actions: params.actions } : params;
|
|
183
183
|
|
|
184
|
-
const result = await this.unityConnection.sendCommand('
|
|
184
|
+
const result = await this.unityConnection.sendCommand('simulate_touch', payload);
|
|
185
185
|
return result;
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -7,7 +7,7 @@ import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
|
7
7
|
|
|
8
8
|
export default class PackageManagerToolHandler extends BaseToolHandler {
|
|
9
9
|
constructor(unityConnection) {
|
|
10
|
-
super('
|
|
10
|
+
super('manage_packages', 'Manage Unity packages - search, install, remove, and list packages', {
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {
|
|
13
13
|
action: {
|
|
@@ -84,7 +84,7 @@ export default class PackageManagerToolHandler extends BaseToolHandler {
|
|
|
84
84
|
await this.unityConnection.connect();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const result = await this.unityConnection.sendCommand('
|
|
87
|
+
const result = await this.unityConnection.sendCommand('manage_packages', {
|
|
88
88
|
action,
|
|
89
89
|
...parameters
|
|
90
90
|
});
|
|
@@ -4,6 +4,7 @@ import crypto from 'crypto';
|
|
|
4
4
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
5
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
6
6
|
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
7
|
+
import { preSyntaxCheck } from './csharpSyntaxCheck.js';
|
|
7
8
|
|
|
8
9
|
const MAX_INSTRUCTIONS = 10;
|
|
9
10
|
const MAX_DIFF_CHARS = 80;
|
|
@@ -125,6 +126,12 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
125
126
|
throw e;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// Pre-syntax check: fail fast if file has obvious syntax errors
|
|
130
|
+
const syntaxCheck = preSyntaxCheck(original, relative);
|
|
131
|
+
if (!syntaxCheck.valid) {
|
|
132
|
+
throw new Error(`${syntaxCheck.error}. ${syntaxCheck.recoveryHint || ''}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
let working = original;
|
|
129
136
|
const results = [];
|
|
130
137
|
for (let i = 0; i < instructions.length; i++) {
|
|
@@ -138,6 +145,15 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
138
145
|
return this.#buildResponse({ preview, results, original, updated: working });
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
// Pre-syntax check on edited content before LSP validation
|
|
149
|
+
const editedSyntaxCheck = preSyntaxCheck(working, relative);
|
|
150
|
+
if (!editedSyntaxCheck.valid) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Edit would introduce syntax error: ${editedSyntaxCheck.error}. ` +
|
|
153
|
+
'Check your edit instructions for balanced braces/brackets.'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
const diagnostics = await this.#validateWithLsp(info, relative, working);
|
|
142
158
|
const hasErrors = diagnostics.some(d => this.#severityIsError(d.severity));
|
|
143
159
|
if (hasErrors) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
4
|
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
3
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
6
|
+
import { preSyntaxCheck } from './csharpSyntaxCheck.js';
|
|
4
7
|
|
|
5
8
|
export class ScriptEditStructuredToolHandler extends BaseToolHandler {
|
|
6
9
|
constructor(unityConnection) {
|
|
@@ -91,6 +94,22 @@ export class ScriptEditStructuredToolHandler extends BaseToolHandler {
|
|
|
91
94
|
|
|
92
95
|
// Map operations to LSP extensions
|
|
93
96
|
const info = await this.projectInfo.get();
|
|
97
|
+
|
|
98
|
+
// Pre-syntax check: fail fast if file has obvious syntax errors
|
|
99
|
+
const absolutePath = path.join(info.projectRoot, relative.replace(/\//g, path.sep));
|
|
100
|
+
try {
|
|
101
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
102
|
+
const syntaxCheck = preSyntaxCheck(content, relative);
|
|
103
|
+
if (!syntaxCheck.valid) {
|
|
104
|
+
throw new Error(`${syntaxCheck.error}. ${syntaxCheck.recoveryHint || ''}`);
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// Re-throw syntax errors, let LSP handle file-not-found
|
|
108
|
+
if (e && e.code !== 'ENOENT') {
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
95
114
|
|
|
96
115
|
if (operation === 'replace_body') {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight C# syntax pre-check to detect obvious errors before LSP processing.
|
|
3
|
+
* This helps fail fast instead of waiting for 60s LSP timeout on malformed files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check for balanced braces, brackets, and parentheses.
|
|
8
|
+
* @param {string} content - C# source code
|
|
9
|
+
* @returns {{ valid: boolean, error?: string, details?: object }}
|
|
10
|
+
*/
|
|
11
|
+
export function checkBraceBalance(content) {
|
|
12
|
+
const stack = [];
|
|
13
|
+
const pairs = { '{': '}', '[': ']', '(': ')' };
|
|
14
|
+
const openings = new Set(Object.keys(pairs));
|
|
15
|
+
const closings = new Set(Object.values(pairs));
|
|
16
|
+
|
|
17
|
+
let inString = false;
|
|
18
|
+
let inChar = false;
|
|
19
|
+
let inSingleLineComment = false;
|
|
20
|
+
let inMultiLineComment = false;
|
|
21
|
+
let lineNumber = 1;
|
|
22
|
+
let columnNumber = 1;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < content.length; i++) {
|
|
25
|
+
const ch = content[i];
|
|
26
|
+
const next = content[i + 1];
|
|
27
|
+
const prev = content[i - 1];
|
|
28
|
+
|
|
29
|
+
// Track line numbers
|
|
30
|
+
if (ch === '\n') {
|
|
31
|
+
lineNumber++;
|
|
32
|
+
columnNumber = 1;
|
|
33
|
+
inSingleLineComment = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
columnNumber++;
|
|
37
|
+
|
|
38
|
+
// Skip comments
|
|
39
|
+
if (!inString && !inChar) {
|
|
40
|
+
if (inMultiLineComment) {
|
|
41
|
+
if (ch === '*' && next === '/') {
|
|
42
|
+
inMultiLineComment = false;
|
|
43
|
+
i++;
|
|
44
|
+
columnNumber++;
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (inSingleLineComment) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === '/' && next === '/') {
|
|
52
|
+
inSingleLineComment = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '/' && next === '*') {
|
|
56
|
+
inMultiLineComment = true;
|
|
57
|
+
i++;
|
|
58
|
+
columnNumber++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle strings (skip escaped quotes)
|
|
64
|
+
if (!inChar && !inSingleLineComment && !inMultiLineComment) {
|
|
65
|
+
if (ch === '"' && prev !== '\\') {
|
|
66
|
+
// Check for verbatim string @""
|
|
67
|
+
if (!inString && prev === '@') {
|
|
68
|
+
// Verbatim string - skip until unescaped closing "
|
|
69
|
+
i++;
|
|
70
|
+
while (i < content.length) {
|
|
71
|
+
if (content[i] === '"') {
|
|
72
|
+
if (content[i + 1] === '"') {
|
|
73
|
+
i++; // escaped quote in verbatim
|
|
74
|
+
} else {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (content[i] === '\n') {
|
|
79
|
+
lineNumber++;
|
|
80
|
+
columnNumber = 0;
|
|
81
|
+
}
|
|
82
|
+
i++;
|
|
83
|
+
columnNumber++;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
inString = !inString;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle char literals
|
|
93
|
+
if (!inString && !inSingleLineComment && !inMultiLineComment) {
|
|
94
|
+
if (ch === "'" && prev !== '\\') {
|
|
95
|
+
inChar = !inChar;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Skip if inside string or char
|
|
101
|
+
if (inString || inChar) continue;
|
|
102
|
+
|
|
103
|
+
// Track braces
|
|
104
|
+
if (openings.has(ch)) {
|
|
105
|
+
stack.push({ char: ch, line: lineNumber, column: columnNumber });
|
|
106
|
+
} else if (closings.has(ch)) {
|
|
107
|
+
if (stack.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
error: `Unexpected closing '${ch}' at line ${lineNumber}, column ${columnNumber}`,
|
|
111
|
+
details: { line: lineNumber, column: columnNumber, char: ch }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const top = stack.pop();
|
|
115
|
+
if (pairs[top.char] !== ch) {
|
|
116
|
+
return {
|
|
117
|
+
valid: false,
|
|
118
|
+
error: `Mismatched '${top.char}' (line ${top.line}) and '${ch}' (line ${lineNumber})`,
|
|
119
|
+
details: {
|
|
120
|
+
opening: { char: top.char, line: top.line, column: top.column },
|
|
121
|
+
closing: { char: ch, line: lineNumber, column: columnNumber }
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (stack.length > 0) {
|
|
129
|
+
const unclosed = stack.map(s => `'${s.char}' at line ${s.line}`).join(', ');
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
error: `Unclosed brackets: ${unclosed}`,
|
|
133
|
+
details: { unclosed: stack }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Perform lightweight pre-validation on C# source code.
|
|
142
|
+
* Returns early with actionable error if basic syntax is broken.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} content - C# source code
|
|
145
|
+
* @param {string} [filePath] - Optional file path for error messages
|
|
146
|
+
* @returns {{ valid: boolean, error?: string, recoveryHint?: string }}
|
|
147
|
+
*/
|
|
148
|
+
export function preSyntaxCheck(content, filePath = 'file') {
|
|
149
|
+
// Check 1: Brace balance
|
|
150
|
+
const braceResult = checkBraceBalance(content);
|
|
151
|
+
if (!braceResult.valid) {
|
|
152
|
+
return {
|
|
153
|
+
valid: false,
|
|
154
|
+
error: `Syntax error in ${filePath}: ${braceResult.error}`,
|
|
155
|
+
recoveryHint:
|
|
156
|
+
'The file has unbalanced braces/brackets. ' +
|
|
157
|
+
'Use Bash tool with "cat > file << EOF" to rewrite the file, ' +
|
|
158
|
+
'or fix manually in Unity Editor.'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check 2: Basic structure validation (namespace/class/method patterns)
|
|
163
|
+
// Lightweight check - just ensure there's at least one class/struct/interface
|
|
164
|
+
const hasTypeDeclaration = /\b(class|struct|interface|enum|record)\s+\w+/.test(content);
|
|
165
|
+
if (!hasTypeDeclaration && content.trim().length > 100) {
|
|
166
|
+
// File has content but no type declaration - likely corrupted
|
|
167
|
+
return {
|
|
168
|
+
valid: false,
|
|
169
|
+
error: `No type declaration found in ${filePath}`,
|
|
170
|
+
recoveryHint:
|
|
171
|
+
'The file appears to be missing class/struct/interface declarations. ' +
|
|
172
|
+
'This may indicate file corruption. Check the file content.'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { valid: true };
|
|
177
|
+
}
|
package/src/lsp/LspRpcClient.js
CHANGED
|
@@ -166,10 +166,17 @@ export class LspRpcClient {
|
|
|
166
166
|
);
|
|
167
167
|
return await this.#requestWithRetry(method, params, attempt + 1);
|
|
168
168
|
}
|
|
169
|
-
// Standardize error message
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
// Standardize error message with actionable recovery instructions
|
|
170
|
+
let hint;
|
|
171
|
+
if (recoverable) {
|
|
172
|
+
hint =
|
|
173
|
+
'LSP failed to parse the file (likely syntax errors). ' +
|
|
174
|
+
'Recovery options: (1) Use Bash tool with "cat > file << EOF" to rewrite the file, ' +
|
|
175
|
+
'(2) Fix syntax errors manually in Unity Editor, ' +
|
|
176
|
+
'(3) Check for unbalanced braces/brackets in the target file.';
|
|
177
|
+
} else {
|
|
178
|
+
hint = 'Check request parameters or increase lsp.requestTimeoutMs.';
|
|
179
|
+
}
|
|
173
180
|
throw new Error(`[${method}] failed: ${msg}. ${hint}`);
|
|
174
181
|
}
|
|
175
182
|
}
|