@akiojin/unity-mcp-server 2.42.1 → 2.42.3
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/bin/unity-mcp-server +89 -5
- package/package.json +2 -2
- package/src/cli/commands/listInstances.js +10 -0
- package/src/cli/commands/setActive.js +6 -0
- package/src/core/config.js +313 -267
- package/src/core/httpServer.js +147 -0
- package/src/core/instanceRegistry.js +73 -0
- package/src/core/server.js +41 -7
- package/src/core/unityConnection.js +26 -4
package/bin/unity-mcp-server
CHANGED
|
@@ -1,8 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { startServer } from '../src/core/server.js';
|
|
3
|
+
import { listInstances } from '../src/cli/commands/listInstances.js';
|
|
4
|
+
import { setActive } from '../src/cli/commands/setActive.js';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const command = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
8
|
+
const rest = command ? args.slice(1) : args;
|
|
9
|
+
let httpEnabled = false;
|
|
10
|
+
let httpPort;
|
|
11
|
+
let stdioEnabled = true;
|
|
12
|
+
let telemetryEnabled;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < rest.length; i++) {
|
|
15
|
+
const arg = rest[i];
|
|
16
|
+
switch (arg) {
|
|
17
|
+
case '--http':
|
|
18
|
+
httpEnabled = true;
|
|
19
|
+
if (args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
20
|
+
httpPort = parseInt(args[i + 1], 10);
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
case '--no-http':
|
|
25
|
+
httpEnabled = false;
|
|
26
|
+
break;
|
|
27
|
+
case '--stdio':
|
|
28
|
+
stdioEnabled = true;
|
|
29
|
+
break;
|
|
30
|
+
case '--no-stdio':
|
|
31
|
+
stdioEnabled = false;
|
|
32
|
+
break;
|
|
33
|
+
case '--telemetry':
|
|
34
|
+
telemetryEnabled = true;
|
|
35
|
+
break;
|
|
36
|
+
case '--no-telemetry':
|
|
37
|
+
telemetryEnabled = false;
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
if (command === 'list-instances') {
|
|
46
|
+
const portsArg = rest.find(a => a.startsWith('--ports='));
|
|
47
|
+
const ports = portsArg ? portsArg.replace('--ports=', '').split(',').map(p => Number(p)) : [];
|
|
48
|
+
const hostArg = rest.find(a => a.startsWith('--host='));
|
|
49
|
+
const host = hostArg ? hostArg.replace('--host=', '') : 'localhost';
|
|
50
|
+
const json = rest.includes('--json');
|
|
51
|
+
const list = await listInstances({ ports, host });
|
|
52
|
+
if (json) {
|
|
53
|
+
console.log(JSON.stringify(list, null, 2));
|
|
54
|
+
} else {
|
|
55
|
+
for (const e of list) {
|
|
56
|
+
console.log(`${e.active ? '*' : ' '} ${e.id} ${e.status}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command === 'set-active') {
|
|
63
|
+
const id = rest[0];
|
|
64
|
+
if (!id) {
|
|
65
|
+
console.error('Usage: unity-mcp-server set-active <host:port>');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const result = await setActive({ id });
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(e.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
startServer({
|
|
79
|
+
http: {
|
|
80
|
+
enabled: httpEnabled,
|
|
81
|
+
port: httpPort
|
|
82
|
+
},
|
|
83
|
+
telemetry: telemetryEnabled === undefined ? undefined : { enabled: telemetryEnabled },
|
|
84
|
+
stdioEnabled
|
|
85
|
+
}).catch(error => {
|
|
86
|
+
console.error('Fatal error:', error);
|
|
87
|
+
console.error('Stack trace:', error?.stack);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.42.
|
|
3
|
+
"version": "2.42.3",
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"author": "Akio Jinsenji <akio-jinsenji@cloud-creative-studios.com>",
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.24.
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
54
54
|
"better-sqlite3": "^9.4.3",
|
|
55
55
|
"find-up": "^6.3.0"
|
|
56
56
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InstanceRegistry } from '../../core/instanceRegistry.js';
|
|
2
|
+
|
|
3
|
+
export async function listInstances({ ports, host = 'localhost', registry }) {
|
|
4
|
+
const reg = registry || new InstanceRegistry();
|
|
5
|
+
if (Array.isArray(ports)) {
|
|
6
|
+
ports.forEach(p => reg.add({ host, port: Number(p) }));
|
|
7
|
+
}
|
|
8
|
+
await reg.refreshStatus();
|
|
9
|
+
return reg.list();
|
|
10
|
+
}
|
package/src/core/config.js
CHANGED
|
@@ -1,267 +1,313 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as findUpPkg from 'find-up';
|
|
4
|
+
function findUpSyncCompat(matcher, options = {}) {
|
|
5
|
+
if (typeof matcher === 'function') {
|
|
6
|
+
let dir = options.cwd || process.cwd();
|
|
7
|
+
const { root } = path.parse(dir);
|
|
8
|
+
// walk up until root
|
|
9
|
+
while (true) {
|
|
10
|
+
const found = matcher(dir);
|
|
11
|
+
if (found) return found;
|
|
12
|
+
if (dir === root) return undefined;
|
|
13
|
+
dir = path.dirname(dir);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (typeof findUpPkg.sync === 'function') return findUpPkg.sync(matcher, options);
|
|
17
|
+
if (typeof findUpPkg === 'function') return findUpPkg(matcher, options);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Shallow merge utility (simple objects only)
|
|
23
|
+
*/
|
|
24
|
+
function merge(a, b) {
|
|
25
|
+
const out = { ...a };
|
|
26
|
+
for (const [k, v] of Object.entries(b || {})) {
|
|
27
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && a[k] && typeof a[k] === 'object') {
|
|
28
|
+
out[k] = { ...a[k], ...v };
|
|
29
|
+
} else {
|
|
30
|
+
out[k] = v;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolvePackageVersion() {
|
|
37
|
+
const candidates = [];
|
|
38
|
+
|
|
39
|
+
// Resolve relative to this module (always inside mcp-server/src/core)
|
|
40
|
+
try {
|
|
41
|
+
const moduleDir = path.dirname(new URL(import.meta.url).pathname);
|
|
42
|
+
candidates.push(path.resolve(moduleDir, '../../package.json'));
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
// When executed from workspace root (monorepo) or inside mcp-server package
|
|
46
|
+
try {
|
|
47
|
+
const here = findUpSyncCompat('package.json', { cwd: process.cwd() });
|
|
48
|
+
if (here) candidates.push(here);
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
55
|
+
if (pkg?.version) return pkg.version;
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return '0.1.0';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Base configuration for Unity MCP Server Server
|
|
64
|
+
*/
|
|
65
|
+
const envUnityHost = process.env.UNITY_BIND_HOST || process.env.UNITY_HOST || null;
|
|
66
|
+
|
|
67
|
+
const envMcpHost =
|
|
68
|
+
process.env.UNITY_MCP_HOST || process.env.UNITY_CLIENT_HOST || process.env.UNITY_HOST || null;
|
|
69
|
+
|
|
70
|
+
const envBindHost = process.env.UNITY_BIND_HOST || null;
|
|
71
|
+
|
|
72
|
+
const baseConfig = {
|
|
73
|
+
// Unity connection settings
|
|
74
|
+
unity: {
|
|
75
|
+
unityHost: envUnityHost,
|
|
76
|
+
mcpHost: envMcpHost,
|
|
77
|
+
bindHost: envBindHost,
|
|
78
|
+
port: parseInt(process.env.UNITY_PORT || '', 10) || 6400,
|
|
79
|
+
reconnectDelay: 1000,
|
|
80
|
+
maxReconnectDelay: 30000,
|
|
81
|
+
reconnectBackoffMultiplier: 2,
|
|
82
|
+
commandTimeout: 30000
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Server settings
|
|
86
|
+
server: {
|
|
87
|
+
name: 'unity-mcp-server',
|
|
88
|
+
version: resolvePackageVersion(),
|
|
89
|
+
description: 'MCP server for Unity Editor integration'
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Logging settings
|
|
93
|
+
logging: {
|
|
94
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
95
|
+
prefix: '[unity-mcp-server]'
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// HTTP transport (off by default)
|
|
99
|
+
http: {
|
|
100
|
+
enabled: (process.env.UNITY_MCP_HTTP_ENABLED || 'false').toLowerCase() === 'true',
|
|
101
|
+
host: process.env.UNITY_MCP_HTTP_HOST || '0.0.0.0',
|
|
102
|
+
port: parseInt(process.env.UNITY_MCP_HTTP_PORT || '', 10) || 6401,
|
|
103
|
+
healthPath: '/healthz',
|
|
104
|
+
allowedHosts: (process.env.UNITY_MCP_HTTP_ALLOWED_HOSTS || 'localhost,127.0.0.1')
|
|
105
|
+
.split(',')
|
|
106
|
+
.map(h => h.trim())
|
|
107
|
+
.filter(h => h.length > 0)
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
telemetry: {
|
|
111
|
+
enabled: (process.env.UNITY_MCP_TELEMETRY || 'off').toLowerCase() === 'on',
|
|
112
|
+
destinations: [],
|
|
113
|
+
fields: []
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Write queue removed: all edits go through structured Roslyn tools.
|
|
117
|
+
|
|
118
|
+
// Search-related defaults and engine selection
|
|
119
|
+
search: {
|
|
120
|
+
// detail alias: 'compact' maps to returnMode 'snippets'
|
|
121
|
+
defaultDetail: (process.env.SEARCH_DEFAULT_DETAIL || 'compact').toLowerCase(), // compact|metadata|snippets|full
|
|
122
|
+
engine: (process.env.SEARCH_ENGINE || 'naive').toLowerCase() // naive|treesitter (future)
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// LSP client defaults
|
|
126
|
+
lsp: {
|
|
127
|
+
requestTimeoutMs: Number(process.env.LSP_REQUEST_TIMEOUT_MS || 60000)
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Indexing (code index) settings
|
|
131
|
+
indexing: {
|
|
132
|
+
// Enable periodic incremental index updates (polling watcher)
|
|
133
|
+
watch: true,
|
|
134
|
+
// Polling interval (ms)
|
|
135
|
+
intervalMs: Number(process.env.INDEX_WATCH_INTERVAL_MS || 15000),
|
|
136
|
+
// Build options
|
|
137
|
+
concurrency: Number(process.env.INDEX_CONCURRENCY || 8),
|
|
138
|
+
retry: Number(process.env.INDEX_RETRY || 2),
|
|
139
|
+
reportEvery: Number(process.env.INDEX_REPORT_EVERY || 500)
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* External config resolution (no legacy compatibility):
|
|
145
|
+
* Priority:
|
|
146
|
+
* 1) UNITY_MCP_CONFIG (explicit file path)
|
|
147
|
+
* 2) ./.unity/config.json (project-local)
|
|
148
|
+
* 3) ~/.unity/config.json (user-global)
|
|
149
|
+
* If none found, create ./.unity/config.json with defaults.
|
|
150
|
+
*/
|
|
151
|
+
function ensureDefaultProjectConfig(baseDir) {
|
|
152
|
+
const dir = path.resolve(baseDir, '.unity');
|
|
153
|
+
const file = path.join(dir, 'config.json');
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.existsSync(dir)) {
|
|
157
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(file)) {
|
|
161
|
+
const inferredRoot = fs.existsSync(path.join(baseDir, 'Assets')) ? baseDir : '';
|
|
162
|
+
const defaultConfig = {
|
|
163
|
+
unity: {
|
|
164
|
+
unityHost: 'localhost',
|
|
165
|
+
mcpHost: 'localhost',
|
|
166
|
+
port: 6400
|
|
167
|
+
},
|
|
168
|
+
project: {
|
|
169
|
+
root: inferredRoot ? inferredRoot.replace(/\\/g, '/') : ''
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
fs.writeFileSync(file, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
return file;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loadExternalConfig() {
|
|
181
|
+
if (typeof findUpSyncCompat !== 'function') {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
const explicitPath = process.env.UNITY_MCP_CONFIG;
|
|
185
|
+
|
|
186
|
+
const projectPath = findUpSyncCompat(
|
|
187
|
+
directory => {
|
|
188
|
+
const candidate = path.resolve(directory, '.unity', 'config.json');
|
|
189
|
+
return fs.existsSync(candidate) ? candidate : undefined;
|
|
190
|
+
},
|
|
191
|
+
{ cwd: process.cwd() }
|
|
192
|
+
);
|
|
193
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
194
|
+
const userPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
195
|
+
|
|
196
|
+
const candidates = [explicitPath, projectPath, userPath].filter(Boolean);
|
|
197
|
+
for (const p of candidates) {
|
|
198
|
+
try {
|
|
199
|
+
if (p && fs.existsSync(p)) {
|
|
200
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
201
|
+
const json = JSON.parse(raw);
|
|
202
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
203
|
+
out.__configPath = p;
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return { __configLoadError: `${p}: ${e.message}` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const fallbackPath = ensureDefaultProjectConfig(process.cwd());
|
|
211
|
+
if (fallbackPath && fs.existsSync(fallbackPath)) {
|
|
212
|
+
try {
|
|
213
|
+
const raw = fs.readFileSync(fallbackPath, 'utf8');
|
|
214
|
+
const json = JSON.parse(raw);
|
|
215
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
216
|
+
out.__configPath = fallbackPath;
|
|
217
|
+
out.__configGenerated = true;
|
|
218
|
+
return out;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { __configLoadError: `${fallbackPath}: ${e.message}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const external = loadExternalConfig();
|
|
227
|
+
export const config = merge(baseConfig, external);
|
|
228
|
+
|
|
229
|
+
const normalizeUnityConfig = () => {
|
|
230
|
+
const unityConfig = config.unity || (config.unity = {});
|
|
231
|
+
|
|
232
|
+
// Legacy aliases coming from config files or env vars
|
|
233
|
+
const legacyHost = unityConfig.host;
|
|
234
|
+
const legacyClientHost = unityConfig.clientHost;
|
|
235
|
+
const legacyBindHost = unityConfig.bindHost;
|
|
236
|
+
|
|
237
|
+
if (!unityConfig.unityHost) {
|
|
238
|
+
unityConfig.unityHost = legacyBindHost || legacyHost || envUnityHost || 'localhost';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!unityConfig.mcpHost) {
|
|
242
|
+
unityConfig.mcpHost = legacyClientHost || envMcpHost || legacyHost || unityConfig.unityHost;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Keep bindHost for backwards compatibility with legacy code paths
|
|
246
|
+
if (!unityConfig.bindHost) {
|
|
247
|
+
unityConfig.bindHost = legacyBindHost || envBindHost || unityConfig.unityHost;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Maintain legacy properties so older handlers keep working
|
|
251
|
+
unityConfig.host = unityConfig.unityHost;
|
|
252
|
+
unityConfig.clientHost = unityConfig.mcpHost;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
normalizeUnityConfig();
|
|
256
|
+
|
|
257
|
+
// Workspace root detection: directory that contains .unity/config.json used
|
|
258
|
+
const initialCwd = process.cwd();
|
|
259
|
+
let workspaceRoot = initialCwd;
|
|
260
|
+
try {
|
|
261
|
+
if (config.__configPath) {
|
|
262
|
+
const cfgDir = path.dirname(config.__configPath); // <workspace>/.unity
|
|
263
|
+
workspaceRoot = path.dirname(cfgDir); // <workspace>
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
export const WORKSPACE_ROOT = workspaceRoot;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Logger utility
|
|
270
|
+
* IMPORTANT: In MCP servers, all stdout output must be JSON-RPC protocol messages.
|
|
271
|
+
* Logging must go to stderr to avoid breaking the protocol.
|
|
272
|
+
*/
|
|
273
|
+
export const logger = {
|
|
274
|
+
info: (message, ...args) => {
|
|
275
|
+
if (['info', 'debug'].includes(config.logging.level)) {
|
|
276
|
+
console.error(`${config.logging.prefix} ${message}`, ...args);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
warn: (message, ...args) => {
|
|
281
|
+
if (['info', 'debug', 'warn'].includes(config.logging.level)) {
|
|
282
|
+
console.error(`${config.logging.prefix} WARN: ${message}`, ...args);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
error: (message, ...args) => {
|
|
287
|
+
console.error(`${config.logging.prefix} ERROR: ${message}`, ...args);
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
debug: (message, ...args) => {
|
|
291
|
+
if (config.logging.level === 'debug') {
|
|
292
|
+
console.error(`${config.logging.prefix} DEBUG: ${message}`, ...args);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Late log if external config failed to load
|
|
298
|
+
if (config.__configLoadError) {
|
|
299
|
+
console.error(
|
|
300
|
+
`${baseConfig.logging.prefix} WARN: Failed to load external config: ${config.__configLoadError}`
|
|
301
|
+
);
|
|
302
|
+
delete config.__configLoadError;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Startup debug log: output config info to stderr for troubleshooting
|
|
306
|
+
// This helps diagnose connection issues (especially in WSL2/Docker environments)
|
|
307
|
+
console.error(`[unity-mcp-server] Startup config:`);
|
|
308
|
+
console.error(`[unity-mcp-server] Config file: ${config.__configPath || '(defaults)'}`);
|
|
309
|
+
console.error(
|
|
310
|
+
`[unity-mcp-server] Unity host: ${config.unity.mcpHost || config.unity.unityHost || 'localhost'}`
|
|
311
|
+
);
|
|
312
|
+
console.error(`[unity-mcp-server] Unity port: ${config.unity.port}`);
|
|
313
|
+
console.error(`[unity-mcp-server] Workspace root: ${WORKSPACE_ROOT}`);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { logger } from './config.js';
|
|
3
|
+
|
|
4
|
+
function buildHealthResponse({ startedAt, mode, port, telemetryEnabled }) {
|
|
5
|
+
return {
|
|
6
|
+
status: 'ok',
|
|
7
|
+
mode,
|
|
8
|
+
port,
|
|
9
|
+
telemetryEnabled,
|
|
10
|
+
uptimeMs: Date.now() - startedAt
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function suggestPorts(port) {
|
|
15
|
+
if (!port || typeof port !== 'number') return [];
|
|
16
|
+
return [port + 1, port + 2, port + 11];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createHttpServer({
|
|
20
|
+
handlers,
|
|
21
|
+
host = '0.0.0.0',
|
|
22
|
+
port = 6401,
|
|
23
|
+
telemetryEnabled = false,
|
|
24
|
+
healthPath = '/healthz',
|
|
25
|
+
allowedHosts = ['localhost', '127.0.0.1']
|
|
26
|
+
} = {}) {
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
let server;
|
|
29
|
+
|
|
30
|
+
const listener = async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const hostHeader = req.headers.host?.split(':')[0];
|
|
33
|
+
if (allowedHosts && allowedHosts.length && hostHeader && !allowedHosts.includes(hostHeader)) {
|
|
34
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
35
|
+
res.end(JSON.stringify({ error: 'forbidden host' }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
40
|
+
|
|
41
|
+
if (req.method === 'GET' && url.pathname === healthPath) {
|
|
42
|
+
const body = buildHealthResponse({
|
|
43
|
+
startedAt,
|
|
44
|
+
mode: 'http',
|
|
45
|
+
port: server?.address()?.port,
|
|
46
|
+
telemetryEnabled
|
|
47
|
+
});
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
49
|
+
res.end(JSON.stringify(body));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (req.method === 'POST' && url.pathname === '/rpc') {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
56
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = JSON.parse(raw || '{}');
|
|
60
|
+
} catch (e) {
|
|
61
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
62
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Invalid JSON' } }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { method, params, id } = payload || {};
|
|
67
|
+
if (method === 'tools/list' || method === 'listTools') {
|
|
68
|
+
const tools = Array.from(handlers.values()).map(h => h.getDefinition());
|
|
69
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
70
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, result: { tools } }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (method === 'tools/call' || method === 'callTool') {
|
|
75
|
+
const name = params?.name;
|
|
76
|
+
const args = params?.arguments || {};
|
|
77
|
+
const handler = handlers.get(name);
|
|
78
|
+
if (!handler) {
|
|
79
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
80
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32004, message: `Tool not found: ${name}` } }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const result = await handler.handle(args);
|
|
85
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
86
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, result }));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
logger.error(`[http] tool error ${name}: ${e.message}`);
|
|
89
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
90
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: e.message } }));
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
96
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
101
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
logger.error(`[http] unexpected error: ${e.message}`);
|
|
104
|
+
try {
|
|
105
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
106
|
+
res.end(JSON.stringify({ error: 'internal error' }));
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
server = http.createServer(listener);
|
|
112
|
+
|
|
113
|
+
const start = () =>
|
|
114
|
+
new Promise((resolve, reject) => {
|
|
115
|
+
const onError = err => {
|
|
116
|
+
server.off('error', onError);
|
|
117
|
+
if (err.code === 'EADDRINUSE') {
|
|
118
|
+
const suggestions = suggestPorts(port);
|
|
119
|
+
const msg = `Port ${port} is already in use. Try: ${suggestions.join(', ')}`;
|
|
120
|
+
logger.error(msg);
|
|
121
|
+
}
|
|
122
|
+
reject(err);
|
|
123
|
+
};
|
|
124
|
+
server.once('error', onError);
|
|
125
|
+
server.listen(port, host, () => {
|
|
126
|
+
server.off('error', onError);
|
|
127
|
+
const address = server.address();
|
|
128
|
+
logger.info(`HTTP listening on http://${host}:${address.port}, telemetry: ${telemetryEnabled ? 'on' : 'off'}`);
|
|
129
|
+
resolve(address.port);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const close = () =>
|
|
134
|
+
new Promise((resolve, reject) => {
|
|
135
|
+
server.close(err => {
|
|
136
|
+
if (err) reject(err);
|
|
137
|
+
else resolve();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
start,
|
|
143
|
+
close,
|
|
144
|
+
getPort: () => server.address()?.port,
|
|
145
|
+
health: () => buildHealthResponse({ startedAt, mode: 'http', port: server.address()?.port, telemetryEnabled })
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
async function ping(host, port, timeoutMs = 1000) {
|
|
4
|
+
return new Promise(resolve => {
|
|
5
|
+
const socket = net.connect({ host, port });
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
socket.destroy();
|
|
8
|
+
resolve(false);
|
|
9
|
+
}, timeoutMs);
|
|
10
|
+
|
|
11
|
+
socket.on('connect', () => {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
socket.end();
|
|
14
|
+
resolve(true);
|
|
15
|
+
});
|
|
16
|
+
socket.on('error', () => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
resolve(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class InstanceRegistry {
|
|
24
|
+
constructor(entries = []) {
|
|
25
|
+
this.entries = [];
|
|
26
|
+
this.activeId = null;
|
|
27
|
+
entries.forEach(e => this.add(e));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
add(entry) {
|
|
31
|
+
const id = entry.id || `${entry.host}:${entry.port}`;
|
|
32
|
+
if (this.entries.find(e => e.id === id)) return;
|
|
33
|
+
this.entries.push({
|
|
34
|
+
id,
|
|
35
|
+
host: entry.host,
|
|
36
|
+
port: entry.port,
|
|
37
|
+
status: entry.status || 'unknown',
|
|
38
|
+
lastCheckedAt: entry.lastCheckedAt,
|
|
39
|
+
active: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
remove(id) {
|
|
44
|
+
this.entries = this.entries.filter(e => e.id !== id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
list() {
|
|
48
|
+
return this.entries.map(e => ({ ...e, active: e.id === this.activeId }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async refreshStatus(timeoutMs = 1000) {
|
|
52
|
+
for (const entry of this.entries) {
|
|
53
|
+
const ok = await ping(entry.host, entry.port, timeoutMs);
|
|
54
|
+
entry.status = ok ? 'up' : 'down';
|
|
55
|
+
entry.lastCheckedAt = new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
return this.list();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async setActive(id, { timeoutMs = 1000 } = {}) {
|
|
61
|
+
const entry = this.entries.find(e => e.id === id);
|
|
62
|
+
if (!entry) throw new Error(`Instance not found: ${id}`);
|
|
63
|
+
|
|
64
|
+
const ok = await ping(entry.host, entry.port, timeoutMs);
|
|
65
|
+
if (!ok) throw new Error(`Instance unreachable: ${id}`);
|
|
66
|
+
|
|
67
|
+
const previousId = this.activeId;
|
|
68
|
+
this.activeId = id;
|
|
69
|
+
entry.status = 'up';
|
|
70
|
+
entry.lastCheckedAt = new Date().toISOString();
|
|
71
|
+
return { activeId: id, previousId };
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/core/server.js
CHANGED
|
@@ -162,21 +162,53 @@ unityConnection.on('error', error => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
// Initialize server
|
|
165
|
-
export async function startServer() {
|
|
165
|
+
export async function startServer(options = {}) {
|
|
166
166
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
const runtimeConfig = {
|
|
168
|
+
...config,
|
|
169
|
+
http: { ...config.http, ...(options.http || {}) },
|
|
170
|
+
telemetry: { ...config.telemetry, ...(options.telemetry || {}) },
|
|
171
|
+
stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
|
|
172
|
+
};
|
|
169
173
|
|
|
170
|
-
//
|
|
171
|
-
|
|
174
|
+
// Create transport - no logging before connection
|
|
175
|
+
let transport;
|
|
176
|
+
if (runtimeConfig.stdioEnabled !== false) {
|
|
177
|
+
console.error(`[unity-mcp-server] MCP transport connecting...`);
|
|
178
|
+
transport = new HybridStdioServerTransport();
|
|
179
|
+
await server.connect(transport);
|
|
180
|
+
console.error(`[unity-mcp-server] MCP transport connected`);
|
|
181
|
+
}
|
|
172
182
|
|
|
173
183
|
// Now safe to log after connection established
|
|
174
184
|
logger.info('MCP server started successfully');
|
|
175
185
|
|
|
186
|
+
// Optional HTTP transport
|
|
187
|
+
let httpServerInstance;
|
|
188
|
+
if (runtimeConfig.http?.enabled) {
|
|
189
|
+
const { createHttpServer } = await import('./httpServer.js');
|
|
190
|
+
httpServerInstance = createHttpServer({
|
|
191
|
+
handlers,
|
|
192
|
+
host: runtimeConfig.http.host,
|
|
193
|
+
port: runtimeConfig.http.port,
|
|
194
|
+
telemetryEnabled: runtimeConfig.telemetry.enabled,
|
|
195
|
+
healthPath: runtimeConfig.http.healthPath
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
await httpServerInstance.start();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logger.error(`HTTP server failed to start: ${err.message}`);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
// Attempt to connect to Unity
|
|
206
|
+
console.error(`[unity-mcp-server] Unity connection starting...`);
|
|
177
207
|
try {
|
|
178
208
|
await unityConnection.connect();
|
|
209
|
+
console.error(`[unity-mcp-server] Unity connection established`);
|
|
179
210
|
} catch (error) {
|
|
211
|
+
console.error(`[unity-mcp-server] Unity connection failed: ${error.message}`);
|
|
180
212
|
logger.error('Initial Unity connection failed:', error.message);
|
|
181
213
|
logger.info('Unity connection will retry automatically');
|
|
182
214
|
}
|
|
@@ -251,14 +283,16 @@ export async function startServer() {
|
|
|
251
283
|
process.on('SIGINT', async () => {
|
|
252
284
|
logger.info('Shutting down...');
|
|
253
285
|
unityConnection.disconnect();
|
|
254
|
-
await server.close();
|
|
286
|
+
if (transport) await server.close();
|
|
287
|
+
if (httpServerInstance) await httpServerInstance.close();
|
|
255
288
|
process.exit(0);
|
|
256
289
|
});
|
|
257
290
|
|
|
258
291
|
process.on('SIGTERM', async () => {
|
|
259
292
|
logger.info('Shutting down...');
|
|
260
293
|
unityConnection.disconnect();
|
|
261
|
-
await server.close();
|
|
294
|
+
if (transport) await server.close();
|
|
295
|
+
if (httpServerInstance) await httpServerInstance.close();
|
|
262
296
|
process.exit(0);
|
|
263
297
|
});
|
|
264
298
|
} catch (error) {
|
|
@@ -21,6 +21,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
21
21
|
this.sendQueue = [];
|
|
22
22
|
this.inFlight = 0;
|
|
23
23
|
this.maxInFlight = 1; // process one command at a time by default
|
|
24
|
+
this.connectedAt = null; // Timestamp when connection was established
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -48,6 +49,9 @@ export class UnityConnection extends EventEmitter {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const targetHost = config.unity.mcpHost || config.unity.unityHost;
|
|
52
|
+
console.error(
|
|
53
|
+
`[unity-mcp-server] Unity TCP connecting to ${targetHost}:${config.unity.port}...`
|
|
54
|
+
);
|
|
51
55
|
logger.info(`Connecting to Unity at ${targetHost}:${config.unity.port}...`);
|
|
52
56
|
|
|
53
57
|
this.socket = new net.Socket();
|
|
@@ -74,7 +78,11 @@ export class UnityConnection extends EventEmitter {
|
|
|
74
78
|
|
|
75
79
|
// Set up event handlers
|
|
76
80
|
this.socket.on('connect', () => {
|
|
77
|
-
|
|
81
|
+
this.connectedAt = Date.now();
|
|
82
|
+
console.error(
|
|
83
|
+
`[unity-mcp-server] Unity TCP connected to ${targetHost}:${config.unity.port}`
|
|
84
|
+
);
|
|
85
|
+
logger.info(`Connected to Unity Editor at ${targetHost}:${config.unity.port}`);
|
|
78
86
|
this.connected = true;
|
|
79
87
|
this.reconnectAttempts = 0;
|
|
80
88
|
this.connectPromise = null;
|
|
@@ -84,6 +92,12 @@ export class UnityConnection extends EventEmitter {
|
|
|
84
92
|
});
|
|
85
93
|
|
|
86
94
|
this.socket.on('end', () => {
|
|
95
|
+
// Unity closed the connection (FIN received)
|
|
96
|
+
const duration = this.connectedAt ? Date.now() - this.connectedAt : 0;
|
|
97
|
+
console.error(
|
|
98
|
+
`[unity-mcp-server] Unity TCP connection ended by remote (FIN received, duration: ${duration}ms)`
|
|
99
|
+
);
|
|
100
|
+
logger.info(`Unity closed connection (FIN received) after ${duration}ms`);
|
|
87
101
|
// Treat end as close to trigger reconnection
|
|
88
102
|
this.socket.destroy();
|
|
89
103
|
});
|
|
@@ -93,6 +107,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
93
107
|
});
|
|
94
108
|
|
|
95
109
|
this.socket.on('error', error => {
|
|
110
|
+
console.error(`[unity-mcp-server] Unity TCP error: ${error.message}`);
|
|
96
111
|
logger.error('Socket error:', error.message);
|
|
97
112
|
if (this.listenerCount('error') > 0) {
|
|
98
113
|
this.emit('error', error);
|
|
@@ -121,11 +136,15 @@ export class UnityConnection extends EventEmitter {
|
|
|
121
136
|
|
|
122
137
|
// Check if we're already handling disconnection
|
|
123
138
|
if (this.isDisconnecting || !this.socket) {
|
|
139
|
+
console.error(`[unity-mcp-server] Unity TCP close event (already disconnecting)`);
|
|
124
140
|
return;
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
const duration = this.connectedAt ? Date.now() - this.connectedAt : 0;
|
|
144
|
+
console.error(`[unity-mcp-server] Unity TCP disconnected (duration: ${duration}ms)`);
|
|
145
|
+
logger.info(`Disconnected from Unity Editor after ${duration}ms`);
|
|
128
146
|
this.connected = false;
|
|
147
|
+
this.connectedAt = null;
|
|
129
148
|
this.socket = null;
|
|
130
149
|
|
|
131
150
|
// Clear message buffer
|
|
@@ -224,6 +243,9 @@ export class UnityConnection extends EventEmitter {
|
|
|
224
243
|
* @param {Buffer} data
|
|
225
244
|
*/
|
|
226
245
|
handleData(data) {
|
|
246
|
+
// Log received data size for debugging connection issues
|
|
247
|
+
console.error(`[unity-mcp-server] Received ${data.length} bytes from Unity`);
|
|
248
|
+
|
|
227
249
|
// Fast-path: accept single unframed JSON message (NDJSON style) from tests/clients
|
|
228
250
|
if (!this.messageBuffer.length) {
|
|
229
251
|
const asString = data.toString('utf8').trim();
|
|
@@ -240,7 +262,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
240
262
|
// Check if this is an unframed Unity debug log
|
|
241
263
|
if (data.length > 0 && !this.messageBuffer.length) {
|
|
242
264
|
const dataStr = data.toString('utf8');
|
|
243
|
-
if (dataStr.startsWith('[
|
|
265
|
+
if (dataStr.startsWith('[unity-mcp-server]') || dataStr.startsWith('[Unity]')) {
|
|
244
266
|
logger.debug(`[Unity] Received unframed debug log: ${dataStr.trim()}`);
|
|
245
267
|
// Don't process unframed logs as messages
|
|
246
268
|
return;
|
|
@@ -315,7 +337,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
315
337
|
|
|
316
338
|
// Check if this looks like a Unity log message
|
|
317
339
|
const messageStr = messageData.toString();
|
|
318
|
-
if (messageStr.includes('[
|
|
340
|
+
if (messageStr.includes('[unity-mcp-server]')) {
|
|
319
341
|
logger.debug('[Unity] Received Unity log message instead of JSON response');
|
|
320
342
|
// Don't treat this as a critical error
|
|
321
343
|
}
|