@akiojin/unity-mcp-server 2.41.2 → 2.41.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/README.md CHANGED
@@ -359,7 +359,9 @@ Full source code and documentation: <https://github.com/akiojin/unity-mcp-server
359
359
 
360
360
  ## Release Automation
361
361
 
362
- - Releases are generated by release-please on `main`; run `scripts/create-release-branch.sh` to open the `chore(release): x.y.z` PR (auto-merge after checks).
362
+ - Releases are generated by release-please on `main`.
363
+ - If changes are in develop, run `scripts/prepare-release-pr.sh` (creates/auto-merges develop→main PR).
364
+ - release-please runs on main and publishes via tags; no automatic back-merge to develop.
363
365
  - Required checks: Markdown, ESLint & Formatting / Commit Message Lint / Test & Coverage / Package.
364
366
  - 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
367
  - If OpenUPM lags, create a new release so the tag and Unity package version match.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.41.2",
3
+ "version": "2.41.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",
@@ -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.UNITY_UNITY_HOST || 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
+ 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
+ }