@akiojin/unity-mcp-server 4.1.4 → 4.2.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
|
@@ -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
|
}
|