@akiojin/unity-mcp-server 2.42.4 → 2.43.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/bin/unity-mcp-server.js +132 -0
- package/package.json +3 -3
- package/src/core/config.js +5 -23
- package/src/core/indexBuildWorkerPool.js +2 -2
- package/src/core/indexWatcher.js +4 -4
- package/src/core/mcpLogger.js +182 -0
- package/src/core/projectInfo.js +1 -1
- package/src/core/server.js +23 -6
- package/src/core/unityConnection.js +5 -5
- package/src/handlers/script/CodeIndexBuildToolHandler.js +3 -1
- package/src/lsp/CSharpLspUtils.js +3 -3
- package/src/lsp/LspProcessManager.js +1 -1
- package/src/lsp/LspRpcClient.js +1 -1
- package/src/lsp/LspRpcClientSingleton.js +3 -3
- package/src/utils/testResultsCache.js +5 -3
- package/src/utils/testRunState.js +2 -2
- package/bin/bootstrap.js +0 -38
- package/bin/unity-mcp-server +0 -92
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unity MCP Server - Single Entry Point
|
|
4
|
+
*
|
|
5
|
+
* This file uses ONLY dynamic imports to ensure:
|
|
6
|
+
* 1. Early stderr output before any module loading
|
|
7
|
+
* 2. Global exception handlers are registered first
|
|
8
|
+
* 3. Module load failures are caught and reported
|
|
9
|
+
*
|
|
10
|
+
* ESM static imports are hoisted, so we use dynamic import() to
|
|
11
|
+
* ensure our error handlers are set up before loading any modules.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Synchronous stderr write - this MUST appear before any module loading
|
|
15
|
+
process.stderr.write('[unity-mcp-server] Starting...\n');
|
|
16
|
+
|
|
17
|
+
// Global exception handlers - catch any unhandled errors
|
|
18
|
+
process.on('uncaughtException', err => {
|
|
19
|
+
process.stderr.write(`[unity-mcp-server] FATAL: Uncaught exception: ${err.message}\n`);
|
|
20
|
+
process.stderr.write(`${err.stack}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on('unhandledRejection', (reason, _promise) => {
|
|
25
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
26
|
+
const stack = reason instanceof Error ? reason.stack : '';
|
|
27
|
+
process.stderr.write(`[unity-mcp-server] FATAL: Unhandled rejection: ${msg}\n`);
|
|
28
|
+
if (stack) process.stderr.write(`${stack}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Parse command line arguments (no static imports required)
|
|
33
|
+
const args = process.argv.slice(2);
|
|
34
|
+
const command = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
|
35
|
+
const rest = command ? args.slice(1) : args;
|
|
36
|
+
|
|
37
|
+
let httpEnabled = false;
|
|
38
|
+
let httpPort;
|
|
39
|
+
let stdioEnabled = true;
|
|
40
|
+
let telemetryEnabled;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
const arg = rest[i];
|
|
44
|
+
switch (arg) {
|
|
45
|
+
case '--http':
|
|
46
|
+
httpEnabled = true;
|
|
47
|
+
if (rest[i + 1] && !rest[i + 1].startsWith('--')) {
|
|
48
|
+
httpPort = parseInt(rest[i + 1], 10);
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case '--no-http':
|
|
53
|
+
httpEnabled = false;
|
|
54
|
+
break;
|
|
55
|
+
case '--stdio':
|
|
56
|
+
stdioEnabled = true;
|
|
57
|
+
break;
|
|
58
|
+
case '--no-stdio':
|
|
59
|
+
stdioEnabled = false;
|
|
60
|
+
break;
|
|
61
|
+
case '--telemetry':
|
|
62
|
+
telemetryEnabled = true;
|
|
63
|
+
break;
|
|
64
|
+
case '--no-telemetry':
|
|
65
|
+
telemetryEnabled = false;
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Main entry point - all imports are dynamic
|
|
73
|
+
async function main() {
|
|
74
|
+
try {
|
|
75
|
+
if (command === 'list-instances') {
|
|
76
|
+
const { listInstances } = await import('../src/cli/commands/listInstances.js');
|
|
77
|
+
const portsArg = rest.find(a => a.startsWith('--ports='));
|
|
78
|
+
const ports = portsArg
|
|
79
|
+
? portsArg
|
|
80
|
+
.replace('--ports=', '')
|
|
81
|
+
.split(',')
|
|
82
|
+
.map(p => Number(p))
|
|
83
|
+
: [];
|
|
84
|
+
const hostArg = rest.find(a => a.startsWith('--host='));
|
|
85
|
+
const host = hostArg ? hostArg.replace('--host=', '') : 'localhost';
|
|
86
|
+
const json = rest.includes('--json');
|
|
87
|
+
const list = await listInstances({ ports, host });
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify(list, null, 2));
|
|
90
|
+
} else {
|
|
91
|
+
for (const e of list) {
|
|
92
|
+
console.log(`${e.active ? '*' : ' '} ${e.id} ${e.status}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === 'set-active') {
|
|
99
|
+
const { setActive } = await import('../src/cli/commands/setActive.js');
|
|
100
|
+
const id = rest[0];
|
|
101
|
+
if (!id) {
|
|
102
|
+
console.error('Usage: unity-mcp-server set-active <host:port>');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const result = await setActive({ id });
|
|
107
|
+
console.log(JSON.stringify(result, null, 2));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(e.message);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Start MCP server (dynamic import)
|
|
116
|
+
const { startServer } = await import('../src/core/server.js');
|
|
117
|
+
await startServer({
|
|
118
|
+
http: {
|
|
119
|
+
enabled: httpEnabled,
|
|
120
|
+
port: httpPort
|
|
121
|
+
},
|
|
122
|
+
telemetry: telemetryEnabled === undefined ? undefined : { enabled: telemetryEnabled },
|
|
123
|
+
stdioEnabled
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
process.stderr.write(`[unity-mcp-server] Startup failed: ${err.message}\n`);
|
|
127
|
+
process.stderr.write(`${err.stack}\n`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.43.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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"unity-mcp-server": "./bin/
|
|
8
|
+
"unity-mcp-server": "./bin/unity-mcp-server.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/core/server.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prebuild:better-sqlite3": "node scripts/prebuild-better-sqlite3.mjs",
|
|
30
30
|
"prebuilt:manifest": "node scripts/generate-prebuilt-manifest.mjs",
|
|
31
31
|
"prepublishOnly": "npm run test:ci && npm run prebuilt:manifest",
|
|
32
|
-
"postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/
|
|
32
|
+
"postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server.js || true",
|
|
33
33
|
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
|
|
34
34
|
"test:unity": "node tests/run-unity-integration.mjs",
|
|
35
35
|
"test:nounity": "npm run test:integration",
|
package/src/core/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import * as findUpPkg from 'find-up';
|
|
4
|
+
import { MCPLogger } from './mcpLogger.js';
|
|
4
5
|
|
|
5
6
|
// Diagnostic log: confirm module loading reached this point
|
|
6
7
|
process.stderr.write('[unity-mcp-server] Config module loading...\n');
|
|
@@ -273,30 +274,11 @@ export const WORKSPACE_ROOT = workspaceRoot;
|
|
|
273
274
|
* Logger utility
|
|
274
275
|
* IMPORTANT: In MCP servers, all stdout output must be JSON-RPC protocol messages.
|
|
275
276
|
* Logging must go to stderr to avoid breaking the protocol.
|
|
277
|
+
*
|
|
278
|
+
* MCP SDK-compliant logger with RFC 5424 log levels.
|
|
279
|
+
* Supports dual output: stderr (developer) + MCP notification (client)
|
|
276
280
|
*/
|
|
277
|
-
export const logger =
|
|
278
|
-
info: (message, ...args) => {
|
|
279
|
-
if (['info', 'debug'].includes(config.logging.level)) {
|
|
280
|
-
console.error(`${config.logging.prefix} ${message}`, ...args);
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
warn: (message, ...args) => {
|
|
285
|
-
if (['info', 'debug', 'warn'].includes(config.logging.level)) {
|
|
286
|
-
console.error(`${config.logging.prefix} WARN: ${message}`, ...args);
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
error: (message, ...args) => {
|
|
291
|
-
console.error(`${config.logging.prefix} ERROR: ${message}`, ...args);
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
debug: (message, ...args) => {
|
|
295
|
-
if (config.logging.level === 'debug') {
|
|
296
|
-
console.error(`${config.logging.prefix} DEBUG: ${message}`, ...args);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
};
|
|
281
|
+
export const logger = new MCPLogger(config);
|
|
300
282
|
|
|
301
283
|
// Late log if external config failed to load
|
|
302
284
|
if (config.__configLoadError) {
|
|
@@ -68,7 +68,7 @@ export class IndexBuildWorkerPool {
|
|
|
68
68
|
try {
|
|
69
69
|
cb(message.data);
|
|
70
70
|
} catch (e) {
|
|
71
|
-
logger.
|
|
71
|
+
logger.warning(`[worker-pool] Progress callback error: ${e.message}`);
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -102,7 +102,7 @@ export class IndexBuildWorkerPool {
|
|
|
102
102
|
|
|
103
103
|
this.worker.on('exit', code => {
|
|
104
104
|
if (code !== 0 && this.worker) {
|
|
105
|
-
logger.
|
|
105
|
+
logger.warning(`[worker-pool] Worker exited with code ${code}`);
|
|
106
106
|
this._cleanup();
|
|
107
107
|
reject(new Error(`Worker exited with code ${code}`));
|
|
108
108
|
}
|
package/src/core/indexWatcher.js
CHANGED
|
@@ -54,7 +54,7 @@ export class IndexWatcher {
|
|
|
54
54
|
const driverOk = await probe._ensureDriver();
|
|
55
55
|
if (!driverOk || probe.disabled) {
|
|
56
56
|
const reason = probe.disableReason || 'SQLite native binding not available';
|
|
57
|
-
logger.
|
|
57
|
+
logger.warning(`[index] watcher: code index disabled (${reason}); stopping watcher`);
|
|
58
58
|
this.stop();
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -69,7 +69,7 @@ export class IndexWatcher {
|
|
|
69
69
|
const dbExists = fs.default.existsSync(dbPath);
|
|
70
70
|
|
|
71
71
|
if (!dbExists) {
|
|
72
|
-
logger.
|
|
72
|
+
logger.warning('[index] watcher: code index DB file not found, triggering full rebuild');
|
|
73
73
|
// Force full rebuild when DB file is missing
|
|
74
74
|
const jobId = `watcher-rebuild-${Date.now()}`;
|
|
75
75
|
this.currentWatcherJobId = jobId;
|
|
@@ -117,7 +117,7 @@ export class IndexWatcher {
|
|
|
117
117
|
// (Job result will be logged when it completes/fails)
|
|
118
118
|
this._monitorJob(jobId);
|
|
119
119
|
} catch (e) {
|
|
120
|
-
logger.
|
|
120
|
+
logger.warning(`[index] watcher exception: ${e.message}`);
|
|
121
121
|
} finally {
|
|
122
122
|
this.running = false;
|
|
123
123
|
}
|
|
@@ -174,7 +174,7 @@ export class IndexWatcher {
|
|
|
174
174
|
);
|
|
175
175
|
clearInterval(checkInterval);
|
|
176
176
|
} else if (job.status === 'failed') {
|
|
177
|
-
logger.
|
|
177
|
+
logger.warning(`[index] watcher: auto-build failed - ${job.error}`);
|
|
178
178
|
clearInterval(checkInterval);
|
|
179
179
|
}
|
|
180
180
|
}, 1000);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SDK-compliant Logger
|
|
3
|
+
*
|
|
4
|
+
* RFC 5424 compliant logging with MCP notification support.
|
|
5
|
+
* Provides 8 log levels: emergency, alert, critical, error, warning, notice, info, debug
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Dual output: stderr (developer) + MCP notification (client)
|
|
9
|
+
* - Dynamic log level control via setLevel()
|
|
10
|
+
* - Fire-and-forget MCP notifications (non-blocking)
|
|
11
|
+
* - Error resilience: continues logging even if MCP notification fails
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* RFC 5424 log levels (syslog severity)
|
|
16
|
+
* Lower numbers = higher severity
|
|
17
|
+
*/
|
|
18
|
+
export const LOG_LEVELS = {
|
|
19
|
+
emergency: 0, // System is unusable
|
|
20
|
+
alert: 1, // Action must be taken immediately
|
|
21
|
+
critical: 2, // Critical conditions
|
|
22
|
+
error: 3, // Error conditions
|
|
23
|
+
warning: 4, // Warning conditions
|
|
24
|
+
notice: 5, // Normal but significant condition
|
|
25
|
+
info: 6, // Informational messages
|
|
26
|
+
debug: 7 // Debug-level messages
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* MCP-compliant Logger class
|
|
31
|
+
*/
|
|
32
|
+
export class MCPLogger {
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} config - Configuration object
|
|
35
|
+
* @param {Object} [config.logging] - Logging configuration
|
|
36
|
+
* @param {string} [config.logging.prefix='[unity-mcp-server]'] - Log prefix
|
|
37
|
+
* @param {string} [config.logging.level='info'] - Minimum log level
|
|
38
|
+
*/
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.prefix = config?.logging?.prefix || '[unity-mcp-server]';
|
|
41
|
+
this.minLevel = LOG_LEVELS[config?.logging?.level || 'info'];
|
|
42
|
+
this.server = null;
|
|
43
|
+
this.transportConnected = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set MCP Server instance for sending notifications
|
|
48
|
+
* Should be called after transport is connected
|
|
49
|
+
* @param {Object} server - MCP Server instance with sendLoggingMessage method
|
|
50
|
+
*/
|
|
51
|
+
setServer(server) {
|
|
52
|
+
this.server = server;
|
|
53
|
+
this.transportConnected = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dynamically change log level
|
|
58
|
+
* @param {string} level - New log level (RFC 5424 level name)
|
|
59
|
+
*/
|
|
60
|
+
setLevel(level) {
|
|
61
|
+
if (LOG_LEVELS[level] !== undefined) {
|
|
62
|
+
this.minLevel = LOG_LEVELS[level];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Internal log method
|
|
68
|
+
* @param {string} level - Log level
|
|
69
|
+
* @param {string} message - Log message
|
|
70
|
+
* @param {...any} args - Additional arguments
|
|
71
|
+
*/
|
|
72
|
+
_log(level, message, ...args) {
|
|
73
|
+
const levelNum = LOG_LEVELS[level];
|
|
74
|
+
|
|
75
|
+
// Filter by minimum level
|
|
76
|
+
if (levelNum > this.minLevel) return;
|
|
77
|
+
|
|
78
|
+
// 1. Always output to stderr (developer-facing)
|
|
79
|
+
const formatted = this._formatMessage(level, message);
|
|
80
|
+
console.error(formatted, ...args);
|
|
81
|
+
|
|
82
|
+
// 2. Send MCP notification (client-facing, only when connected)
|
|
83
|
+
if (this.transportConnected && this.server) {
|
|
84
|
+
this._sendMcpLog(level, message, args);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Send log message via MCP notification
|
|
90
|
+
* @param {string} level - Log level
|
|
91
|
+
* @param {string} message - Log message
|
|
92
|
+
* @param {any[]} args - Additional arguments
|
|
93
|
+
*/
|
|
94
|
+
_sendMcpLog(level, message, args) {
|
|
95
|
+
try {
|
|
96
|
+
// Concatenate additional arguments into a single string
|
|
97
|
+
const fullMessage =
|
|
98
|
+
args.length > 0
|
|
99
|
+
? `${message} ${args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}`
|
|
100
|
+
: message;
|
|
101
|
+
|
|
102
|
+
this.server.sendLoggingMessage({
|
|
103
|
+
level,
|
|
104
|
+
logger: 'unity-mcp-server',
|
|
105
|
+
data: fullMessage // String only (no structured data)
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Log failure but continue (error resilience)
|
|
109
|
+
console.error(`${this.prefix} [MCP notification failed] ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format message for stderr output
|
|
115
|
+
* @param {string} level - Log level
|
|
116
|
+
* @param {string} message - Log message
|
|
117
|
+
* @returns {string} Formatted message
|
|
118
|
+
*/
|
|
119
|
+
_formatMessage(level, message) {
|
|
120
|
+
// info level doesn't include level label (matches existing behavior)
|
|
121
|
+
const label = level === 'info' ? '' : ` ${level.toUpperCase()}:`;
|
|
122
|
+
return `${this.prefix}${label} ${message}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Public log methods (RFC 5424 levels)
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Emergency: System is unusable
|
|
129
|
+
*/
|
|
130
|
+
emergency(message, ...args) {
|
|
131
|
+
this._log('emergency', message, ...args);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Alert: Action must be taken immediately
|
|
136
|
+
*/
|
|
137
|
+
alert(message, ...args) {
|
|
138
|
+
this._log('alert', message, ...args);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Critical: Critical conditions
|
|
143
|
+
*/
|
|
144
|
+
critical(message, ...args) {
|
|
145
|
+
this._log('critical', message, ...args);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Error: Error conditions
|
|
150
|
+
*/
|
|
151
|
+
error(message, ...args) {
|
|
152
|
+
this._log('error', message, ...args);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Warning: Warning conditions
|
|
157
|
+
*/
|
|
158
|
+
warning(message, ...args) {
|
|
159
|
+
this._log('warning', message, ...args);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Notice: Normal but significant condition
|
|
164
|
+
*/
|
|
165
|
+
notice(message, ...args) {
|
|
166
|
+
this._log('notice', message, ...args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Info: Informational messages
|
|
171
|
+
*/
|
|
172
|
+
info(message, ...args) {
|
|
173
|
+
this._log('info', message, ...args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Debug: Debug-level messages
|
|
178
|
+
*/
|
|
179
|
+
debug(message, ...args) {
|
|
180
|
+
this._log('debug', message, ...args);
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/core/projectInfo.js
CHANGED
package/src/core/server.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
SetLevelRequestSchema
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
4
8
|
// Note: filename is lowercase on disk; use exact casing for POSIX filesystems
|
|
5
9
|
import { UnityConnection } from './unityConnection.js';
|
|
6
10
|
import { createHandlers } from '../handlers/index.js';
|
|
@@ -24,7 +28,9 @@ const server = new Server(
|
|
|
24
28
|
capabilities: {
|
|
25
29
|
// Explicitly advertise tool support; some MCP clients expect a non-empty object
|
|
26
30
|
// Setting listChanged enables future push updates if we emit notifications
|
|
27
|
-
tools: { listChanged: true }
|
|
31
|
+
tools: { listChanged: true },
|
|
32
|
+
// Enable MCP logging capability for sendLoggingMessage
|
|
33
|
+
logging: {}
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
);
|
|
@@ -32,6 +38,14 @@ const server = new Server(
|
|
|
32
38
|
// Register MCP protocol handlers
|
|
33
39
|
// Note: Do not log here as it breaks MCP protocol initialization
|
|
34
40
|
|
|
41
|
+
// Handle logging/setLevel request (REQ-6)
|
|
42
|
+
server.setRequestHandler(SetLevelRequestSchema, async request => {
|
|
43
|
+
const { level } = request.params;
|
|
44
|
+
logger.setLevel(level);
|
|
45
|
+
logger.info(`Log level changed to: ${level}`);
|
|
46
|
+
return {};
|
|
47
|
+
});
|
|
48
|
+
|
|
35
49
|
// Handle tool listing
|
|
36
50
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
37
51
|
const tools = Array.from(handlers.values())
|
|
@@ -178,6 +192,9 @@ export async function startServer(options = {}) {
|
|
|
178
192
|
transport = new HybridStdioServerTransport();
|
|
179
193
|
await server.connect(transport);
|
|
180
194
|
console.error(`[unity-mcp-server] MCP transport connected`);
|
|
195
|
+
|
|
196
|
+
// Enable MCP logging notifications (REQ-4)
|
|
197
|
+
logger.setServer(server);
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
// Now safe to log after connection established
|
|
@@ -228,7 +245,7 @@ export async function startServer(options = {}) {
|
|
|
228
245
|
process.on('SIGINT', shutdown);
|
|
229
246
|
process.on('SIGTERM', shutdown);
|
|
230
247
|
} catch (e) {
|
|
231
|
-
logger.
|
|
248
|
+
logger.warning(`[startup] csharp-lsp start failed: ${e.message}`);
|
|
232
249
|
}
|
|
233
250
|
})();
|
|
234
251
|
|
|
@@ -252,7 +269,7 @@ export async function startServer(options = {}) {
|
|
|
252
269
|
|
|
253
270
|
if (!ready) {
|
|
254
271
|
if (index.disabled) {
|
|
255
|
-
logger.
|
|
272
|
+
logger.warning(
|
|
256
273
|
`[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
|
|
257
274
|
);
|
|
258
275
|
return;
|
|
@@ -269,13 +286,13 @@ export async function startServer(options = {}) {
|
|
|
269
286
|
`[startup] Code index auto-build started: jobId=${result.jobId}. Use code_index_status to check progress.`
|
|
270
287
|
);
|
|
271
288
|
} else {
|
|
272
|
-
logger.
|
|
289
|
+
logger.warning(`[startup] Code index auto-build failed: ${result.message}`);
|
|
273
290
|
}
|
|
274
291
|
} else {
|
|
275
292
|
logger.info('[startup] Code index DB already exists. Skipping auto-build.');
|
|
276
293
|
}
|
|
277
294
|
} catch (e) {
|
|
278
|
-
logger.
|
|
295
|
+
logger.warning(`[startup] Code index auto-init failed: ${e.message}`);
|
|
279
296
|
}
|
|
280
297
|
})();
|
|
281
298
|
|
|
@@ -300,7 +300,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
if (recoveryIndex > 0) {
|
|
303
|
-
logger.
|
|
303
|
+
logger.warning(`[Unity] Discarding ${recoveryIndex} bytes of invalid data`);
|
|
304
304
|
this.messageBuffer = this.messageBuffer.slice(recoveryIndex);
|
|
305
305
|
continue;
|
|
306
306
|
} else {
|
|
@@ -323,7 +323,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
323
323
|
|
|
324
324
|
// Skip non-JSON messages (like debug logs)
|
|
325
325
|
if (!message.trim().startsWith('{')) {
|
|
326
|
-
logger.
|
|
326
|
+
logger.warning(`[Unity] Skipping non-JSON message: ${message.substring(0, 50)}...`);
|
|
327
327
|
continue;
|
|
328
328
|
}
|
|
329
329
|
|
|
@@ -359,7 +359,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
359
359
|
logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
|
|
360
360
|
|
|
361
361
|
if (!this.connected) {
|
|
362
|
-
logger.
|
|
362
|
+
logger.warning('[Unity] Not connected; waiting for reconnection before sending command');
|
|
363
363
|
await this.ensureConnected({
|
|
364
364
|
timeoutMs: config.unity.commandTimeout
|
|
365
365
|
});
|
|
@@ -463,7 +463,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
463
463
|
result = JSON.parse(result);
|
|
464
464
|
logger.info(`[Unity] Parsed string result as JSON:`, result);
|
|
465
465
|
} catch (parseError) {
|
|
466
|
-
logger.
|
|
466
|
+
logger.warning(`[Unity] Failed to parse result as JSON: ${parseError.message}`);
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
if (response.version) result._version = response.version;
|
|
@@ -476,7 +476,7 @@ export class UnityConnection extends EventEmitter {
|
|
|
476
476
|
err.code = response.code;
|
|
477
477
|
pending.reject(err);
|
|
478
478
|
} else {
|
|
479
|
-
logger.
|
|
479
|
+
logger.warning(`[Unity] Command ${targetId} has unknown response format`);
|
|
480
480
|
pending.resolve(response);
|
|
481
481
|
}
|
|
482
482
|
return;
|
|
@@ -210,7 +210,9 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
210
210
|
// This allows build to continue even if some files fail
|
|
211
211
|
if (processed % 50 === 0) {
|
|
212
212
|
// Log occasionally to avoid spam
|
|
213
|
-
logger.
|
|
213
|
+
logger.warning(
|
|
214
|
+
`[index][${job.id}] Skipped file due to error: ${rel} - ${err.message}`
|
|
215
|
+
);
|
|
214
216
|
}
|
|
215
217
|
} finally {
|
|
216
218
|
processed += 1;
|
|
@@ -92,7 +92,7 @@ export class CSharpLspUtils {
|
|
|
92
92
|
}
|
|
93
93
|
logger.info(`[csharp-lsp] migrated legacy binary to ${path.dirname(primary)}`);
|
|
94
94
|
} catch (e) {
|
|
95
|
-
logger.
|
|
95
|
+
logger.warning(`[csharp-lsp] legacy migration failed: ${e.message}`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -135,7 +135,7 @@ export class CSharpLspUtils {
|
|
|
135
135
|
// バージョン取得失敗時もバイナリが存在すれば使用
|
|
136
136
|
if (!desired) {
|
|
137
137
|
if (fs.existsSync(p)) {
|
|
138
|
-
logger.
|
|
138
|
+
logger.warning('[csharp-lsp] version not found, using existing binary');
|
|
139
139
|
return p;
|
|
140
140
|
}
|
|
141
141
|
throw new Error('mcp-server version not found; cannot resolve LSP tag');
|
|
@@ -152,7 +152,7 @@ export class CSharpLspUtils {
|
|
|
152
152
|
return p;
|
|
153
153
|
} catch (e) {
|
|
154
154
|
if (fs.existsSync(p)) {
|
|
155
|
-
logger.
|
|
155
|
+
logger.warning(`[csharp-lsp] download failed, using existing binary: ${e.message}`);
|
|
156
156
|
return p;
|
|
157
157
|
}
|
|
158
158
|
throw e;
|
|
@@ -23,7 +23,7 @@ export class LspProcessManager {
|
|
|
23
23
|
const proc = spawn(bin, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
24
24
|
proc.on('error', e => logger.error(`[csharp-lsp] process error: ${e.message}`));
|
|
25
25
|
proc.on('close', (code, sig) => {
|
|
26
|
-
logger.
|
|
26
|
+
logger.warning(`[csharp-lsp] exited code=${code} signal=${sig || ''}`);
|
|
27
27
|
if (this.state.proc === proc) {
|
|
28
28
|
this.state.proc = null;
|
|
29
29
|
}
|
package/src/lsp/LspRpcClient.js
CHANGED
|
@@ -161,7 +161,7 @@ export class LspRpcClient {
|
|
|
161
161
|
this.proc = null;
|
|
162
162
|
this.initialized = false;
|
|
163
163
|
this.buf = Buffer.alloc(0);
|
|
164
|
-
logger.
|
|
164
|
+
logger.warning(`[csharp-lsp] recoverable error on ${method}: ${msg}. Retrying once...`);
|
|
165
165
|
return await this.#requestWithRetry(method, params, attempt + 1);
|
|
166
166
|
}
|
|
167
167
|
// Standardize error message
|
|
@@ -42,7 +42,7 @@ export class LspRpcClientSingleton {
|
|
|
42
42
|
try {
|
|
43
43
|
await instance.mgr.stop();
|
|
44
44
|
} catch (e) {
|
|
45
|
-
logger.
|
|
45
|
+
logger.warning(`[LspRpcClientSingleton] error stopping: ${e.message}`);
|
|
46
46
|
}
|
|
47
47
|
instance = null;
|
|
48
48
|
currentProjectRoot = null;
|
|
@@ -59,7 +59,7 @@ export class LspRpcClientSingleton {
|
|
|
59
59
|
if (!instance) return;
|
|
60
60
|
// Check if process is still alive before attempting heartbeat
|
|
61
61
|
if (!instance.proc || instance.proc.killed) {
|
|
62
|
-
logger.
|
|
62
|
+
logger.warning('[LspRpcClientSingleton] process dead, resetting...');
|
|
63
63
|
instance = null;
|
|
64
64
|
currentProjectRoot = null;
|
|
65
65
|
LspRpcClientSingleton.#stopHeartbeat();
|
|
@@ -69,7 +69,7 @@ export class LspRpcClientSingleton {
|
|
|
69
69
|
// Use workspace/symbol with empty query as a lightweight ping
|
|
70
70
|
await instance.request('workspace/symbol', { query: '' });
|
|
71
71
|
} catch (e) {
|
|
72
|
-
logger.
|
|
72
|
+
logger.warning(`[LspRpcClientSingleton] heartbeat failed: ${e.message}, resetting...`);
|
|
73
73
|
// Process is dead, reset instance for next request
|
|
74
74
|
instance = null;
|
|
75
75
|
currentProjectRoot = null;
|
|
@@ -30,7 +30,9 @@ export async function persistTestResults(result) {
|
|
|
30
30
|
await fs.writeFile(filePath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
31
31
|
return filePath;
|
|
32
32
|
} catch (error) {
|
|
33
|
-
logger.
|
|
33
|
+
logger.warning(
|
|
34
|
+
`[TestResultsCache] Failed to write test results to ${filePath}: ${error.message}`
|
|
35
|
+
);
|
|
34
36
|
return null;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -46,7 +48,7 @@ export async function loadCachedTestResults(targetPath) {
|
|
|
46
48
|
return JSON.parse(data);
|
|
47
49
|
} catch (error) {
|
|
48
50
|
if (error.code !== 'ENOENT') {
|
|
49
|
-
logger.
|
|
51
|
+
logger.warning(
|
|
50
52
|
`[TestResultsCache] Failed to read test results from ${filePath}: ${error.message}`
|
|
51
53
|
);
|
|
52
54
|
}
|
|
@@ -63,7 +65,7 @@ export async function resetTestResultsCache() {
|
|
|
63
65
|
try {
|
|
64
66
|
await fs.rm(filePath, { force: true });
|
|
65
67
|
} catch (error) {
|
|
66
|
-
logger.
|
|
68
|
+
logger.warning(
|
|
67
69
|
`[TestResultsCache] Failed to reset test results cache ${filePath}: ${error.message}`
|
|
68
70
|
);
|
|
69
71
|
}
|
|
@@ -27,7 +27,7 @@ await (async () => {
|
|
|
27
27
|
logger.info(`[testRunState] Loaded persisted state from ${runStatePath}`);
|
|
28
28
|
} catch (error) {
|
|
29
29
|
if (error.code !== 'ENOENT') {
|
|
30
|
-
logger.
|
|
30
|
+
logger.warning(`[testRunState] Failed to load state from ${runStatePath}: ${error.message}`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
})();
|
|
@@ -85,6 +85,6 @@ async function persist() {
|
|
|
85
85
|
await fs.mkdir(dir, { recursive: true });
|
|
86
86
|
await fs.writeFile(runStatePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
87
87
|
} catch (error) {
|
|
88
|
-
logger.
|
|
88
|
+
logger.warning(`[testRunState] Failed to persist state to ${runStatePath}: ${error.message}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
package/bin/bootstrap.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Bootstrap wrapper for unity-mcp-server
|
|
4
|
-
*
|
|
5
|
-
* This wrapper ensures:
|
|
6
|
-
* 1. Early stderr output before any module loading
|
|
7
|
-
* 2. Global exception handlers are registered first
|
|
8
|
-
* 3. Module load failures are caught and reported
|
|
9
|
-
*
|
|
10
|
-
* ESM static imports are hoisted, so we use dynamic import() to
|
|
11
|
-
* ensure our error handlers are set up before loading the main module.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// Synchronous stderr write - this MUST appear before any module loading
|
|
15
|
-
process.stderr.write('[unity-mcp-server] Bootstrap starting...\n');
|
|
16
|
-
|
|
17
|
-
// Global exception handlers - catch any unhandled errors
|
|
18
|
-
process.on('uncaughtException', err => {
|
|
19
|
-
process.stderr.write(`[unity-mcp-server] FATAL: Uncaught exception: ${err.message}\n`);
|
|
20
|
-
process.stderr.write(`${err.stack}\n`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
process.on('unhandledRejection', (reason, _promise) => {
|
|
25
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
26
|
-
const stack = reason instanceof Error ? reason.stack : '';
|
|
27
|
-
process.stderr.write(`[unity-mcp-server] FATAL: Unhandled rejection: ${msg}\n`);
|
|
28
|
-
if (stack) process.stderr.write(`${stack}\n`);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Dynamic import to load the main module AFTER error handlers are set up
|
|
33
|
-
// This allows us to catch module load failures
|
|
34
|
-
import('./unity-mcp-server').catch(err => {
|
|
35
|
-
process.stderr.write(`[unity-mcp-server] FATAL: Module load failed: ${err.message}\n`);
|
|
36
|
-
process.stderr.write(`${err.stack}\n`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
});
|
package/bin/unity-mcp-server
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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';
|
|
5
|
-
|
|
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();
|