@akiojin/unity-mcp-server 2.41.2 → 2.41.4
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/README.md +27 -1
- package/package.json +1 -1
- package/src/core/config.js +267 -267
- package/src/core/sqliteFallback.js +6 -7
package/README.md
CHANGED
|
@@ -332,6 +332,30 @@ npm uninstall -g @akiojin/unity-mcp-server
|
|
|
332
332
|
npm install -g @akiojin/unity-mcp-server
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
### Native Module (better-sqlite3) Issues
|
|
336
|
+
|
|
337
|
+
If you encounter errors related to `better-sqlite3` during installation or startup:
|
|
338
|
+
|
|
339
|
+
**Symptom**: Installation fails with `node-gyp` errors, or startup shows "Could not locate the bindings file."
|
|
340
|
+
|
|
341
|
+
**Cause**: The package includes prebuilt native binaries for supported platforms (Linux/macOS/Windows × x64/arm64 × Node 18/20/22). If your platform isn't supported or the prebuilt fails to load, the system falls back to WASM.
|
|
342
|
+
|
|
343
|
+
**Solution 1 - Use WASM fallback (recommended for unsupported platforms)**:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Skip native build and use sql.js WASM fallback
|
|
347
|
+
UNITY_MCP_SKIP_NATIVE_BUILD=1 npm install @akiojin/unity-mcp-server
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Solution 2 - Force native rebuild**:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Force rebuild from source (requires build tools)
|
|
354
|
+
UNITY_MCP_FORCE_NATIVE=1 npm install @akiojin/unity-mcp-server
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Note**: WASM fallback is fully functional but may have slightly slower performance for large codebases. Code index features work normally in either mode.
|
|
358
|
+
|
|
335
359
|
### MCP Client Shows "Capabilities: none"
|
|
336
360
|
|
|
337
361
|
If your MCP client (Claude Code, Cursor, etc.) shows "Capabilities: none" despite successful connection:
|
|
@@ -359,7 +383,9 @@ Full source code and documentation: <https://github.com/akiojin/unity-mcp-server
|
|
|
359
383
|
|
|
360
384
|
## Release Automation
|
|
361
385
|
|
|
362
|
-
- Releases are generated by release-please on `main
|
|
386
|
+
- Releases are generated by release-please on `main`.
|
|
387
|
+
- If changes are in develop, run `scripts/prepare-release-pr.sh` (creates/auto-merges develop→main PR).
|
|
388
|
+
- release-please runs on main and publishes via tags; no automatic back-merge to develop.
|
|
363
389
|
- Required checks: Markdown, ESLint & Formatting / Commit Message Lint / Test & Coverage / Package.
|
|
364
390
|
- Tags trigger `publish.yml` to push npm, OpenUPM (via tag), and csharp-lsp artifacts; Unity package version is kept in sync via release-please extra-files.
|
|
365
391
|
- If OpenUPM lags, create a new release so the tag and Unity package version match.
|
package/package.json
CHANGED
package/src/core/config.js
CHANGED
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { findUpSync } from 'find-up';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Shallow merge utility (simple objects only)
|
|
7
|
-
*/
|
|
8
|
-
function merge(a, b) {
|
|
9
|
-
const out = { ...a };
|
|
10
|
-
for (const [k, v] of Object.entries(b || {})) {
|
|
11
|
-
if (v && typeof v === 'object' && !Array.isArray(v) && a[k] && typeof a[k] === 'object') {
|
|
12
|
-
out[k] = { ...a[k], ...v };
|
|
13
|
-
} else {
|
|
14
|
-
out[k] = v;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function resolvePackageVersion() {
|
|
21
|
-
const candidates = [];
|
|
22
|
-
|
|
23
|
-
// Resolve relative to this module (always inside mcp-server/src/core)
|
|
24
|
-
try {
|
|
25
|
-
const moduleDir = path.dirname(new URL(import.meta.url).pathname);
|
|
26
|
-
candidates.push(path.resolve(moduleDir, '../../package.json'));
|
|
27
|
-
} catch {}
|
|
28
|
-
|
|
29
|
-
// When executed from workspace root (monorepo) or inside mcp-server package
|
|
30
|
-
try {
|
|
31
|
-
const here = findUpSync('package.json', { cwd: process.cwd() });
|
|
32
|
-
if (here) candidates.push(here);
|
|
33
|
-
} catch {}
|
|
34
|
-
|
|
35
|
-
for (const candidate of candidates) {
|
|
36
|
-
try {
|
|
37
|
-
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
38
|
-
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
39
|
-
if (pkg?.version) return pkg.version;
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return '0.1.0';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Base configuration for Unity MCP Server Server
|
|
48
|
-
*/
|
|
49
|
-
const envUnityHost =
|
|
50
|
-
process.env.
|
|
51
|
-
|
|
52
|
-
const envMcpHost =
|
|
53
|
-
process.env.UNITY_MCP_HOST || process.env.UNITY_CLIENT_HOST || process.env.UNITY_HOST || null;
|
|
54
|
-
|
|
55
|
-
const envBindHost = process.env.UNITY_BIND_HOST || null;
|
|
56
|
-
|
|
57
|
-
const baseConfig = {
|
|
58
|
-
// Unity connection settings
|
|
59
|
-
unity: {
|
|
60
|
-
unityHost: envUnityHost,
|
|
61
|
-
mcpHost: envMcpHost,
|
|
62
|
-
bindHost: envBindHost,
|
|
63
|
-
port: parseInt(process.env.UNITY_PORT || '', 10) || 6400,
|
|
64
|
-
reconnectDelay: 1000,
|
|
65
|
-
maxReconnectDelay: 30000,
|
|
66
|
-
reconnectBackoffMultiplier: 2,
|
|
67
|
-
commandTimeout: 30000
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
// Server settings
|
|
71
|
-
server: {
|
|
72
|
-
name: 'unity-mcp-server',
|
|
73
|
-
version: resolvePackageVersion(),
|
|
74
|
-
description: 'MCP server for Unity Editor integration'
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
// Logging settings
|
|
78
|
-
logging: {
|
|
79
|
-
level: process.env.LOG_LEVEL || 'info',
|
|
80
|
-
prefix: '[Unity MCP Server]'
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
// Write queue removed: all edits go through structured Roslyn tools.
|
|
84
|
-
|
|
85
|
-
// Search-related defaults and engine selection
|
|
86
|
-
search: {
|
|
87
|
-
// detail alias: 'compact' maps to returnMode 'snippets'
|
|
88
|
-
defaultDetail: (process.env.SEARCH_DEFAULT_DETAIL || 'compact').toLowerCase(), // compact|metadata|snippets|full
|
|
89
|
-
engine: (process.env.SEARCH_ENGINE || 'naive').toLowerCase() // naive|treesitter (future)
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
// LSP client defaults
|
|
93
|
-
lsp: {
|
|
94
|
-
requestTimeoutMs: Number(process.env.LSP_REQUEST_TIMEOUT_MS || 60000)
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
// Indexing (code index) settings
|
|
98
|
-
indexing: {
|
|
99
|
-
// Enable periodic incremental index updates (polling watcher)
|
|
100
|
-
watch: true,
|
|
101
|
-
// Polling interval (ms)
|
|
102
|
-
intervalMs: Number(process.env.INDEX_WATCH_INTERVAL_MS || 15000),
|
|
103
|
-
// Build options
|
|
104
|
-
concurrency: Number(process.env.INDEX_CONCURRENCY || 8),
|
|
105
|
-
retry: Number(process.env.INDEX_RETRY || 2),
|
|
106
|
-
reportEvery: Number(process.env.INDEX_REPORT_EVERY || 500)
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* External config resolution (no legacy compatibility):
|
|
112
|
-
* Priority:
|
|
113
|
-
* 1) UNITY_MCP_CONFIG (explicit file path)
|
|
114
|
-
* 2) ./.unity/config.json (project-local)
|
|
115
|
-
* 3) ~/.unity/config.json (user-global)
|
|
116
|
-
* If none found, create ./.unity/config.json with defaults.
|
|
117
|
-
*/
|
|
118
|
-
function ensureDefaultProjectConfig(baseDir) {
|
|
119
|
-
const dir = path.resolve(baseDir, '.unity');
|
|
120
|
-
const file = path.join(dir, 'config.json');
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
if (!fs.existsSync(dir)) {
|
|
124
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!fs.existsSync(file)) {
|
|
128
|
-
const inferredRoot = fs.existsSync(path.join(baseDir, 'Assets')) ? baseDir : '';
|
|
129
|
-
const defaultConfig = {
|
|
130
|
-
unity: {
|
|
131
|
-
unityHost: 'localhost',
|
|
132
|
-
mcpHost: 'localhost',
|
|
133
|
-
port: 6400
|
|
134
|
-
},
|
|
135
|
-
project: {
|
|
136
|
-
root: inferredRoot ? inferredRoot.replace(/\\/g, '/') : ''
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
fs.writeFileSync(file, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8');
|
|
140
|
-
}
|
|
141
|
-
return file;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function loadExternalConfig() {
|
|
148
|
-
const explicitPath = process.env.UNITY_MCP_CONFIG;
|
|
149
|
-
|
|
150
|
-
const projectPath = findUpSync(
|
|
151
|
-
directory => {
|
|
152
|
-
const candidate = path.resolve(directory, '.unity', 'config.json');
|
|
153
|
-
return fs.existsSync(candidate) ? candidate : undefined;
|
|
154
|
-
},
|
|
155
|
-
{ cwd: process.cwd() }
|
|
156
|
-
);
|
|
157
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
158
|
-
const userPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
159
|
-
|
|
160
|
-
const candidates = [explicitPath, projectPath, userPath].filter(Boolean);
|
|
161
|
-
for (const p of candidates) {
|
|
162
|
-
try {
|
|
163
|
-
if (p && fs.existsSync(p)) {
|
|
164
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
165
|
-
const json = JSON.parse(raw);
|
|
166
|
-
const out = json && typeof json === 'object' ? json : {};
|
|
167
|
-
out.__configPath = p;
|
|
168
|
-
return out;
|
|
169
|
-
}
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return { __configLoadError: `${p}: ${e.message}` };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const fallbackPath = ensureDefaultProjectConfig(process.cwd());
|
|
175
|
-
if (fallbackPath && fs.existsSync(fallbackPath)) {
|
|
176
|
-
try {
|
|
177
|
-
const raw = fs.readFileSync(fallbackPath, 'utf8');
|
|
178
|
-
const json = JSON.parse(raw);
|
|
179
|
-
const out = json && typeof json === 'object' ? json : {};
|
|
180
|
-
out.__configPath = fallbackPath;
|
|
181
|
-
out.__configGenerated = true;
|
|
182
|
-
return out;
|
|
183
|
-
} catch (e) {
|
|
184
|
-
return { __configLoadError: `${fallbackPath}: ${e.message}` };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return {};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const external = loadExternalConfig();
|
|
191
|
-
export const config = merge(baseConfig, external);
|
|
192
|
-
|
|
193
|
-
const normalizeUnityConfig = () => {
|
|
194
|
-
const unityConfig = config.unity || (config.unity = {});
|
|
195
|
-
|
|
196
|
-
// Legacy aliases coming from config files or env vars
|
|
197
|
-
const legacyHost = unityConfig.host;
|
|
198
|
-
const legacyClientHost = unityConfig.clientHost;
|
|
199
|
-
const legacyBindHost = unityConfig.bindHost;
|
|
200
|
-
|
|
201
|
-
if (!unityConfig.unityHost) {
|
|
202
|
-
unityConfig.unityHost = legacyBindHost || legacyHost || envUnityHost || 'localhost';
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!unityConfig.mcpHost) {
|
|
206
|
-
unityConfig.mcpHost = legacyClientHost || envMcpHost || legacyHost || unityConfig.unityHost;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Keep bindHost for backwards compatibility with legacy code paths
|
|
210
|
-
if (!unityConfig.bindHost) {
|
|
211
|
-
unityConfig.bindHost = legacyBindHost || envBindHost || unityConfig.unityHost;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Maintain legacy properties so older handlers keep working
|
|
215
|
-
unityConfig.host = unityConfig.unityHost;
|
|
216
|
-
unityConfig.clientHost = unityConfig.mcpHost;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
normalizeUnityConfig();
|
|
220
|
-
|
|
221
|
-
// Workspace root detection: directory that contains .unity/config.json used
|
|
222
|
-
const initialCwd = process.cwd();
|
|
223
|
-
let workspaceRoot = initialCwd;
|
|
224
|
-
try {
|
|
225
|
-
if (config.__configPath) {
|
|
226
|
-
const cfgDir = path.dirname(config.__configPath); // <workspace>/.unity
|
|
227
|
-
workspaceRoot = path.dirname(cfgDir); // <workspace>
|
|
228
|
-
}
|
|
229
|
-
} catch {}
|
|
230
|
-
export const WORKSPACE_ROOT = workspaceRoot;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Logger utility
|
|
234
|
-
* IMPORTANT: In MCP servers, all stdout output must be JSON-RPC protocol messages.
|
|
235
|
-
* Logging must go to stderr to avoid breaking the protocol.
|
|
236
|
-
*/
|
|
237
|
-
export const logger = {
|
|
238
|
-
info: (message, ...args) => {
|
|
239
|
-
if (['info', 'debug'].includes(config.logging.level)) {
|
|
240
|
-
console.error(`${config.logging.prefix} ${message}`, ...args);
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
warn: (message, ...args) => {
|
|
245
|
-
if (['info', 'debug', 'warn'].includes(config.logging.level)) {
|
|
246
|
-
console.error(`${config.logging.prefix} WARN: ${message}`, ...args);
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
error: (message, ...args) => {
|
|
251
|
-
console.error(`${config.logging.prefix} ERROR: ${message}`, ...args);
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
debug: (message, ...args) => {
|
|
255
|
-
if (config.logging.level === 'debug') {
|
|
256
|
-
console.error(`${config.logging.prefix} DEBUG: ${message}`, ...args);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Late log if external config failed to load
|
|
262
|
-
if (config.__configLoadError) {
|
|
263
|
-
console.error(
|
|
264
|
-
`${baseConfig.logging.prefix} WARN: Failed to load external config: ${config.__configLoadError}`
|
|
265
|
-
);
|
|
266
|
-
delete config.__configLoadError;
|
|
267
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { findUpSync } from 'find-up';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shallow merge utility (simple objects only)
|
|
7
|
+
*/
|
|
8
|
+
function merge(a, b) {
|
|
9
|
+
const out = { ...a };
|
|
10
|
+
for (const [k, v] of Object.entries(b || {})) {
|
|
11
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && a[k] && typeof a[k] === 'object') {
|
|
12
|
+
out[k] = { ...a[k], ...v };
|
|
13
|
+
} else {
|
|
14
|
+
out[k] = v;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolvePackageVersion() {
|
|
21
|
+
const candidates = [];
|
|
22
|
+
|
|
23
|
+
// Resolve relative to this module (always inside mcp-server/src/core)
|
|
24
|
+
try {
|
|
25
|
+
const moduleDir = path.dirname(new URL(import.meta.url).pathname);
|
|
26
|
+
candidates.push(path.resolve(moduleDir, '../../package.json'));
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
// When executed from workspace root (monorepo) or inside mcp-server package
|
|
30
|
+
try {
|
|
31
|
+
const here = findUpSync('package.json', { cwd: process.cwd() });
|
|
32
|
+
if (here) candidates.push(here);
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
try {
|
|
37
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
39
|
+
if (pkg?.version) return pkg.version;
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return '0.1.0';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Base configuration for Unity MCP Server Server
|
|
48
|
+
*/
|
|
49
|
+
const envUnityHost =
|
|
50
|
+
process.env.UNITY_BIND_HOST || process.env.UNITY_HOST || null;
|
|
51
|
+
|
|
52
|
+
const envMcpHost =
|
|
53
|
+
process.env.UNITY_MCP_HOST || process.env.UNITY_CLIENT_HOST || process.env.UNITY_HOST || null;
|
|
54
|
+
|
|
55
|
+
const envBindHost = process.env.UNITY_BIND_HOST || null;
|
|
56
|
+
|
|
57
|
+
const baseConfig = {
|
|
58
|
+
// Unity connection settings
|
|
59
|
+
unity: {
|
|
60
|
+
unityHost: envUnityHost,
|
|
61
|
+
mcpHost: envMcpHost,
|
|
62
|
+
bindHost: envBindHost,
|
|
63
|
+
port: parseInt(process.env.UNITY_PORT || '', 10) || 6400,
|
|
64
|
+
reconnectDelay: 1000,
|
|
65
|
+
maxReconnectDelay: 30000,
|
|
66
|
+
reconnectBackoffMultiplier: 2,
|
|
67
|
+
commandTimeout: 30000
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Server settings
|
|
71
|
+
server: {
|
|
72
|
+
name: 'unity-mcp-server',
|
|
73
|
+
version: resolvePackageVersion(),
|
|
74
|
+
description: 'MCP server for Unity Editor integration'
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Logging settings
|
|
78
|
+
logging: {
|
|
79
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
80
|
+
prefix: '[Unity MCP Server]'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Write queue removed: all edits go through structured Roslyn tools.
|
|
84
|
+
|
|
85
|
+
// Search-related defaults and engine selection
|
|
86
|
+
search: {
|
|
87
|
+
// detail alias: 'compact' maps to returnMode 'snippets'
|
|
88
|
+
defaultDetail: (process.env.SEARCH_DEFAULT_DETAIL || 'compact').toLowerCase(), // compact|metadata|snippets|full
|
|
89
|
+
engine: (process.env.SEARCH_ENGINE || 'naive').toLowerCase() // naive|treesitter (future)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// LSP client defaults
|
|
93
|
+
lsp: {
|
|
94
|
+
requestTimeoutMs: Number(process.env.LSP_REQUEST_TIMEOUT_MS || 60000)
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Indexing (code index) settings
|
|
98
|
+
indexing: {
|
|
99
|
+
// Enable periodic incremental index updates (polling watcher)
|
|
100
|
+
watch: true,
|
|
101
|
+
// Polling interval (ms)
|
|
102
|
+
intervalMs: Number(process.env.INDEX_WATCH_INTERVAL_MS || 15000),
|
|
103
|
+
// Build options
|
|
104
|
+
concurrency: Number(process.env.INDEX_CONCURRENCY || 8),
|
|
105
|
+
retry: Number(process.env.INDEX_RETRY || 2),
|
|
106
|
+
reportEvery: Number(process.env.INDEX_REPORT_EVERY || 500)
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* External config resolution (no legacy compatibility):
|
|
112
|
+
* Priority:
|
|
113
|
+
* 1) UNITY_MCP_CONFIG (explicit file path)
|
|
114
|
+
* 2) ./.unity/config.json (project-local)
|
|
115
|
+
* 3) ~/.unity/config.json (user-global)
|
|
116
|
+
* If none found, create ./.unity/config.json with defaults.
|
|
117
|
+
*/
|
|
118
|
+
function ensureDefaultProjectConfig(baseDir) {
|
|
119
|
+
const dir = path.resolve(baseDir, '.unity');
|
|
120
|
+
const file = path.join(dir, 'config.json');
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(dir)) {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(file)) {
|
|
128
|
+
const inferredRoot = fs.existsSync(path.join(baseDir, 'Assets')) ? baseDir : '';
|
|
129
|
+
const defaultConfig = {
|
|
130
|
+
unity: {
|
|
131
|
+
unityHost: 'localhost',
|
|
132
|
+
mcpHost: 'localhost',
|
|
133
|
+
port: 6400
|
|
134
|
+
},
|
|
135
|
+
project: {
|
|
136
|
+
root: inferredRoot ? inferredRoot.replace(/\\/g, '/') : ''
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
fs.writeFileSync(file, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8');
|
|
140
|
+
}
|
|
141
|
+
return file;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadExternalConfig() {
|
|
148
|
+
const explicitPath = process.env.UNITY_MCP_CONFIG;
|
|
149
|
+
|
|
150
|
+
const projectPath = findUpSync(
|
|
151
|
+
directory => {
|
|
152
|
+
const candidate = path.resolve(directory, '.unity', 'config.json');
|
|
153
|
+
return fs.existsSync(candidate) ? candidate : undefined;
|
|
154
|
+
},
|
|
155
|
+
{ cwd: process.cwd() }
|
|
156
|
+
);
|
|
157
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
158
|
+
const userPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
159
|
+
|
|
160
|
+
const candidates = [explicitPath, projectPath, userPath].filter(Boolean);
|
|
161
|
+
for (const p of candidates) {
|
|
162
|
+
try {
|
|
163
|
+
if (p && fs.existsSync(p)) {
|
|
164
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
165
|
+
const json = JSON.parse(raw);
|
|
166
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
167
|
+
out.__configPath = p;
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { __configLoadError: `${p}: ${e.message}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const fallbackPath = ensureDefaultProjectConfig(process.cwd());
|
|
175
|
+
if (fallbackPath && fs.existsSync(fallbackPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const raw = fs.readFileSync(fallbackPath, 'utf8');
|
|
178
|
+
const json = JSON.parse(raw);
|
|
179
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
180
|
+
out.__configPath = fallbackPath;
|
|
181
|
+
out.__configGenerated = true;
|
|
182
|
+
return out;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { __configLoadError: `${fallbackPath}: ${e.message}` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const external = loadExternalConfig();
|
|
191
|
+
export const config = merge(baseConfig, external);
|
|
192
|
+
|
|
193
|
+
const normalizeUnityConfig = () => {
|
|
194
|
+
const unityConfig = config.unity || (config.unity = {});
|
|
195
|
+
|
|
196
|
+
// Legacy aliases coming from config files or env vars
|
|
197
|
+
const legacyHost = unityConfig.host;
|
|
198
|
+
const legacyClientHost = unityConfig.clientHost;
|
|
199
|
+
const legacyBindHost = unityConfig.bindHost;
|
|
200
|
+
|
|
201
|
+
if (!unityConfig.unityHost) {
|
|
202
|
+
unityConfig.unityHost = legacyBindHost || legacyHost || envUnityHost || 'localhost';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!unityConfig.mcpHost) {
|
|
206
|
+
unityConfig.mcpHost = legacyClientHost || envMcpHost || legacyHost || unityConfig.unityHost;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Keep bindHost for backwards compatibility with legacy code paths
|
|
210
|
+
if (!unityConfig.bindHost) {
|
|
211
|
+
unityConfig.bindHost = legacyBindHost || envBindHost || unityConfig.unityHost;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Maintain legacy properties so older handlers keep working
|
|
215
|
+
unityConfig.host = unityConfig.unityHost;
|
|
216
|
+
unityConfig.clientHost = unityConfig.mcpHost;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
normalizeUnityConfig();
|
|
220
|
+
|
|
221
|
+
// Workspace root detection: directory that contains .unity/config.json used
|
|
222
|
+
const initialCwd = process.cwd();
|
|
223
|
+
let workspaceRoot = initialCwd;
|
|
224
|
+
try {
|
|
225
|
+
if (config.__configPath) {
|
|
226
|
+
const cfgDir = path.dirname(config.__configPath); // <workspace>/.unity
|
|
227
|
+
workspaceRoot = path.dirname(cfgDir); // <workspace>
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
export const WORKSPACE_ROOT = workspaceRoot;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Logger utility
|
|
234
|
+
* IMPORTANT: In MCP servers, all stdout output must be JSON-RPC protocol messages.
|
|
235
|
+
* Logging must go to stderr to avoid breaking the protocol.
|
|
236
|
+
*/
|
|
237
|
+
export const logger = {
|
|
238
|
+
info: (message, ...args) => {
|
|
239
|
+
if (['info', 'debug'].includes(config.logging.level)) {
|
|
240
|
+
console.error(`${config.logging.prefix} ${message}`, ...args);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
warn: (message, ...args) => {
|
|
245
|
+
if (['info', 'debug', 'warn'].includes(config.logging.level)) {
|
|
246
|
+
console.error(`${config.logging.prefix} WARN: ${message}`, ...args);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
error: (message, ...args) => {
|
|
251
|
+
console.error(`${config.logging.prefix} ERROR: ${message}`, ...args);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
debug: (message, ...args) => {
|
|
255
|
+
if (config.logging.level === 'debug') {
|
|
256
|
+
console.error(`${config.logging.prefix} DEBUG: ${message}`, ...args);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Late log if external config failed to load
|
|
262
|
+
if (config.__configLoadError) {
|
|
263
|
+
console.error(
|
|
264
|
+
`${baseConfig.logging.prefix} WARN: Failed to load external config: ${config.__configLoadError}`
|
|
265
|
+
);
|
|
266
|
+
delete config.__configLoadError;
|
|
267
|
+
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
3
|
import { createRequire } from 'module';
|
|
5
4
|
import initSqlJs from 'sql.js';
|
|
6
5
|
|
|
7
6
|
// Create a lightweight better-sqlite3 compatible surface using sql.js (WASM)
|
|
8
7
|
export async function createSqliteFallback(dbPath) {
|
|
8
|
+
// Use Node's module resolution to find sql.js regardless of package manager (npm, pnpm, yarn)
|
|
9
|
+
// require.resolve('sql.js') returns the main entry point (dist/sql-wasm.js)
|
|
10
|
+
// so we just need sql-wasm.wasm in the same directory
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const wasmPath =
|
|
13
|
-
// Resolve from package-local node_modules first, then workspace root
|
|
14
|
-
paths: [path.join(pkgRoot, 'node_modules'), path.join(pkgRoot, '..', 'node_modules')]
|
|
15
|
-
});
|
|
12
|
+
const sqlJsPath = require.resolve('sql.js');
|
|
13
|
+
const sqlJsDir = path.dirname(sqlJsPath);
|
|
14
|
+
const wasmPath = path.resolve(sqlJsDir, 'sql-wasm.wasm');
|
|
16
15
|
const SQL = await initSqlJs({ locateFile: () => wasmPath });
|
|
17
16
|
|
|
18
17
|
const loadDb = () => {
|