@akiojin/unity-mcp-server 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "5.4.0",
3
+ "version": "5.5.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",
@@ -28,7 +28,7 @@
28
28
  "lru-cache": "^11.0.2"
29
29
  },
30
30
  "engines": {
31
- "node": ">=18 <23"
31
+ "node": ">=18 <25"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
@@ -7,7 +7,8 @@ import { createRequire } from 'node:module';
7
7
  const ABI_BY_NODE_MAJOR = new Map([
8
8
  [18, 115],
9
9
  [20, 120],
10
- [22, 131]
10
+ [22, 131],
11
+ [24, 137]
11
12
  ]);
12
13
 
13
14
  export function parseEnvFlag(value) {
@@ -52,6 +52,15 @@ function parseIntEnv(value) {
52
52
  return Number.isFinite(n) ? n : undefined;
53
53
  }
54
54
 
55
+ function parseCsvEnv(value) {
56
+ if (typeof value !== 'string') return undefined;
57
+ const items = value
58
+ .split(',')
59
+ .map(s => s.trim())
60
+ .filter(Boolean);
61
+ return items;
62
+ }
63
+
55
64
  function envString(key) {
56
65
  const raw = process.env[key];
57
66
  if (typeof raw !== 'string') return undefined;
@@ -143,6 +152,12 @@ const baseConfig = {
143
152
  fields: []
144
153
  },
145
154
 
155
+ // Tool visibility filter
156
+ tools: {
157
+ includeCategories: [],
158
+ excludeCategories: []
159
+ },
160
+
146
161
  // Write queue removed: all edits go through structured Roslyn tools.
147
162
 
148
163
  // Search-related defaults and engine selection
@@ -190,6 +205,8 @@ function loadEnvConfig() {
190
205
  const lspRequestTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_REQUEST_TIMEOUT_MS);
191
206
  const lspSlowRequestWarnMs = parseIntEnv(process.env.UNITY_MCP_LSP_SLOW_REQUEST_WARN_MS);
192
207
  const lspValidationTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_VALIDATION_TIMEOUT_MS);
208
+ const includeCategories = parseCsvEnv(process.env.UNITY_MCP_TOOL_INCLUDE_CATEGORIES);
209
+ const excludeCategories = parseCsvEnv(process.env.UNITY_MCP_TOOL_EXCLUDE_CATEGORIES);
193
210
 
194
211
  const out = {};
195
212
 
@@ -227,6 +244,12 @@ function loadEnvConfig() {
227
244
  out.telemetry = { enabled: telemetryEnabled };
228
245
  }
229
246
 
247
+ if (includeCategories !== undefined || excludeCategories !== undefined) {
248
+ out.tools = {};
249
+ if (includeCategories !== undefined) out.tools.includeCategories = includeCategories;
250
+ if (excludeCategories !== undefined) out.tools.excludeCategories = excludeCategories;
251
+ }
252
+
230
253
  if (lspRequestTimeoutMs !== undefined) {
231
254
  out.lsp = { requestTimeoutMs: lspRequestTimeoutMs };
232
255
  }
@@ -306,6 +329,25 @@ function validateAndNormalizeConfig(cfg) {
306
329
  cfg.project.codeIndexRoot = null;
307
330
  }
308
331
 
332
+ if (!cfg.tools || typeof cfg.tools !== 'object') {
333
+ cfg.tools = {};
334
+ }
335
+ if (!Array.isArray(cfg.tools.includeCategories)) {
336
+ cfg.tools.includeCategories = [];
337
+ }
338
+ if (!Array.isArray(cfg.tools.excludeCategories)) {
339
+ cfg.tools.excludeCategories = [];
340
+ }
341
+
342
+ cfg.tools.includeCategories = cfg.tools.includeCategories
343
+ .filter(v => typeof v === 'string')
344
+ .map(v => v.trim())
345
+ .filter(Boolean);
346
+ cfg.tools.excludeCategories = cfg.tools.excludeCategories
347
+ .filter(v => typeof v === 'string')
348
+ .map(v => v.trim())
349
+ .filter(Boolean);
350
+
309
351
  // lsp timeout sanity
310
352
  if (cfg.lsp?.requestTimeoutMs !== undefined) {
311
353
  const t = Number(cfg.lsp.requestTimeoutMs);
@@ -16,6 +16,7 @@
16
16
  import fs from 'node:fs';
17
17
  import { StdioRpcServer } from './stdioRpcServer.js';
18
18
  import { createProjectRootGuard } from './projectRootGuard.js';
19
+ import { createToolCategoryPolicy, filterToolsByCategory } from './toolCategoryFilter.js';
19
20
 
20
21
  // Deferred state - will be initialized after transport connection
21
22
  let unityConnection = null;
@@ -126,8 +127,41 @@ export async function startServer(options = {}) {
126
127
  ...config,
127
128
  http: { ...config.http, ...(options.http || {}) },
128
129
  telemetry: { ...config.telemetry, ...(options.telemetry || {}) },
130
+ tools: {
131
+ includeCategories: config.tools?.includeCategories || [],
132
+ excludeCategories: config.tools?.excludeCategories || []
133
+ },
129
134
  stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
130
135
  };
136
+ const toolCategoryPolicy = createToolCategoryPolicy(runtimeConfig.tools, logger);
137
+ let publicToolNames = null;
138
+ let toolFilterLogged = false;
139
+
140
+ const applyCategoryFilter = tools => {
141
+ if (!toolCategoryPolicy.isActive) {
142
+ return tools;
143
+ }
144
+
145
+ const filtered = filterToolsByCategory(tools, toolCategoryPolicy);
146
+ publicToolNames = filtered.publicToolNames;
147
+
148
+ if (!toolFilterLogged) {
149
+ const include =
150
+ toolCategoryPolicy.includeList.length > 0
151
+ ? toolCategoryPolicy.includeList.join(', ')
152
+ : '(all)';
153
+ const exclude =
154
+ toolCategoryPolicy.excludeList.length > 0
155
+ ? toolCategoryPolicy.excludeList.join(', ')
156
+ : '(none)';
157
+ logger.info(
158
+ `[MCP] Tool category filter enabled. include=${include}, exclude=${exclude}`
159
+ );
160
+ toolFilterLogged = true;
161
+ }
162
+
163
+ return filtered.tools;
164
+ };
131
165
 
132
166
  const projectInfoProvider =
133
167
  deps.projectInfoProvider ||
@@ -331,14 +365,15 @@ export async function startServer(options = {}) {
331
365
  server?.setRequestHandler('tools/list', async () => {
332
366
  const manifestTools = readToolManifest();
333
367
  if (manifestTools) {
334
- logger.info(`[MCP] Returning ${manifestTools.length} tool definitions`);
368
+ const visibleTools = applyCategoryFilter(manifestTools);
369
+ logger.info(`[MCP] Returning ${visibleTools.length} tool definitions`);
335
370
  requestPostInit();
336
- return { tools: manifestTools };
371
+ return { tools: visibleTools };
337
372
  }
338
373
 
339
374
  await ensureInitialized(deps);
340
375
 
341
- const tools = Array.from(handlers.values())
376
+ const allTools = Array.from(handlers.values())
342
377
  .map((handler, index) => {
343
378
  try {
344
379
  const definition = handler.getDefinition();
@@ -356,6 +391,7 @@ export async function startServer(options = {}) {
356
391
  })
357
392
  .filter(tool => tool !== null);
358
393
 
394
+ const tools = applyCategoryFilter(allTools);
359
395
  logger.info(`[MCP] Returning ${tools.length} tool definitions`);
360
396
  requestPostInit();
361
397
  return { tools };
@@ -373,6 +409,17 @@ export async function startServer(options = {}) {
373
409
  { args }
374
410
  );
375
411
 
412
+ if (toolCategoryPolicy.isActive) {
413
+ if (!publicToolNames) {
414
+ const handlerTools = Array.from(handlers.values()).map(handler => ({ name: handler.name }));
415
+ publicToolNames = filterToolsByCategory(handlerTools, toolCategoryPolicy).publicToolNames;
416
+ }
417
+ if (!publicToolNames.has(name)) {
418
+ logger.error(`[MCP] Tool not found (filtered): ${name}`);
419
+ throw new Error(`Tool not found: ${name}`);
420
+ }
421
+ }
422
+
376
423
  const guardError = await projectRootGuard(args || {});
377
424
  if (guardError) {
378
425
  logger.error(`[MCP] projectRoot guard failed: ${guardError}`);
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Tool category filtering for tools/list and tools/call visibility control.
3
+ */
4
+
5
+ const CATEGORY_NAMES = Object.freeze([
6
+ 'system',
7
+ 'gameobject',
8
+ 'scene',
9
+ 'analysis',
10
+ 'playmode',
11
+ 'ui',
12
+ 'input',
13
+ 'asset',
14
+ 'prefab',
15
+ 'material',
16
+ 'addressables',
17
+ 'menu',
18
+ 'console',
19
+ 'screenshot',
20
+ 'video',
21
+ 'component',
22
+ 'compilation',
23
+ 'test',
24
+ 'editor',
25
+ 'settings',
26
+ 'package',
27
+ 'script',
28
+ 'profiler',
29
+ 'general'
30
+ ]);
31
+
32
+ function toCategoryKey(value) {
33
+ if (typeof value !== 'string') return '';
34
+ return value
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/[\s_-]+/g, '');
38
+ }
39
+
40
+ const CATEGORY_KEY_TO_NAME = new Map(CATEGORY_NAMES.map(name => [toCategoryKey(name), name]));
41
+
42
+ // User-friendly aliases
43
+ CATEGORY_KEY_TO_NAME.set('addressable', 'addressables');
44
+ CATEGORY_KEY_TO_NAME.set('gameobj', 'gameobject');
45
+ CATEGORY_KEY_TO_NAME.set('play', 'playmode');
46
+ CATEGORY_KEY_TO_NAME.set('playmode', 'playmode');
47
+ CATEGORY_KEY_TO_NAME.set('uitoolkit', 'ui');
48
+ CATEGORY_KEY_TO_NAME.set('ugui', 'ui');
49
+ CATEGORY_KEY_TO_NAME.set('imgui', 'ui');
50
+
51
+ const SYSTEM_TOOLS = new Set([
52
+ 'ping',
53
+ 'refresh_assets',
54
+ 'get_command_stats',
55
+ 'get_server_info',
56
+ 'search_tools'
57
+ ]);
58
+
59
+ const GAMEOBJECT_TOOLS = new Set([
60
+ 'create_gameobject',
61
+ 'find_gameobject',
62
+ 'modify_gameobject',
63
+ 'delete_gameobject',
64
+ 'get_hierarchy'
65
+ ]);
66
+
67
+ const SCENE_TOOLS = new Set(['create_scene', 'load_scene', 'save_scene', 'list_scenes', 'get_scene_info']);
68
+
69
+ const ANALYSIS_TOOLS = new Set([
70
+ 'get_gameobject_details',
71
+ 'analyze_scene_contents',
72
+ 'get_component_values',
73
+ 'find_by_component',
74
+ 'get_object_references',
75
+ 'get_animator_state',
76
+ 'get_animator_runtime_info',
77
+ 'get_input_actions_state',
78
+ 'analyze_input_actions_asset'
79
+ ]);
80
+
81
+ const PLAYMODE_TOOLS = new Set(['play_game', 'pause_game', 'stop_game', 'playmode_wait_for_state']);
82
+
83
+ const UI_TOOLS = new Set([
84
+ 'find_ui_elements',
85
+ 'click_ui_element',
86
+ 'get_ui_element_state',
87
+ 'set_ui_element_value',
88
+ 'simulate_ui_input'
89
+ ]);
90
+
91
+ const INPUT_TOOLS = new Set([
92
+ 'input_system_control',
93
+ 'input_keyboard',
94
+ 'input_mouse',
95
+ 'input_gamepad',
96
+ 'input_touch',
97
+ 'create_action_map',
98
+ 'remove_action_map',
99
+ 'add_input_action',
100
+ 'remove_input_action',
101
+ 'add_input_binding',
102
+ 'remove_input_binding',
103
+ 'remove_all_bindings',
104
+ 'create_composite_binding',
105
+ 'manage_control_schemes'
106
+ ]);
107
+
108
+ const ASSET_TOOLS = new Set([
109
+ 'manage_asset_database',
110
+ 'manage_asset_import_settings',
111
+ 'analyze_asset_dependencies'
112
+ ]);
113
+
114
+ const PREFAB_TOOLS = new Set([
115
+ 'create_prefab',
116
+ 'modify_prefab',
117
+ 'instantiate_prefab',
118
+ 'open_prefab',
119
+ 'exit_prefab_mode',
120
+ 'save_prefab'
121
+ ]);
122
+
123
+ const MATERIAL_TOOLS = new Set(['create_material', 'modify_material']);
124
+
125
+ const MENU_TOOLS = new Set(['execute_menu_item']);
126
+ const CONSOLE_TOOLS = new Set(['clear_console', 'read_console']);
127
+ const SCREENSHOT_TOOLS = new Set(['capture_screenshot', 'analyze_screenshot']);
128
+ const VIDEO_TOOLS = new Set([
129
+ 'capture_video_start',
130
+ 'capture_video_stop',
131
+ 'capture_video_status',
132
+ 'video_capture_for'
133
+ ]);
134
+ const COMPONENT_TOOLS = new Set([
135
+ 'add_component',
136
+ 'remove_component',
137
+ 'modify_component',
138
+ 'list_components',
139
+ 'get_component_types',
140
+ 'set_component_field'
141
+ ]);
142
+ const COMPILATION_TOOLS = new Set(['get_compilation_state']);
143
+ const TEST_TOOLS = new Set(['run_tests', 'get_test_status']);
144
+ const EDITOR_TOOLS = new Set([
145
+ 'manage_tags',
146
+ 'manage_layers',
147
+ 'manage_selection',
148
+ 'manage_windows',
149
+ 'manage_tools',
150
+ 'get_editor_state',
151
+ 'quit_editor'
152
+ ]);
153
+ const SETTINGS_TOOLS = new Set(['get_project_settings', 'update_project_settings']);
154
+ const PACKAGE_TOOLS = new Set(['package_manager', 'registry_config', 'list_packages']);
155
+ const SCRIPT_TOOLS = new Set([
156
+ 'read',
157
+ 'search',
158
+ 'edit_structured',
159
+ 'edit_snippet',
160
+ 'get_symbols',
161
+ 'find_symbol',
162
+ 'find_refs',
163
+ 'build_index',
164
+ 'update_index',
165
+ 'get_index_status',
166
+ 'rename_symbol',
167
+ 'create_class',
168
+ 'remove_symbol'
169
+ ]);
170
+
171
+ const TOOL_SETS = [
172
+ ['system', SYSTEM_TOOLS],
173
+ ['gameobject', GAMEOBJECT_TOOLS],
174
+ ['scene', SCENE_TOOLS],
175
+ ['analysis', ANALYSIS_TOOLS],
176
+ ['playmode', PLAYMODE_TOOLS],
177
+ ['ui', UI_TOOLS],
178
+ ['input', INPUT_TOOLS],
179
+ ['asset', ASSET_TOOLS],
180
+ ['prefab', PREFAB_TOOLS],
181
+ ['material', MATERIAL_TOOLS],
182
+ ['menu', MENU_TOOLS],
183
+ ['console', CONSOLE_TOOLS],
184
+ ['screenshot', SCREENSHOT_TOOLS],
185
+ ['video', VIDEO_TOOLS],
186
+ ['component', COMPONENT_TOOLS],
187
+ ['compilation', COMPILATION_TOOLS],
188
+ ['test', TEST_TOOLS],
189
+ ['editor', EDITOR_TOOLS],
190
+ ['settings', SETTINGS_TOOLS],
191
+ ['package', PACKAGE_TOOLS],
192
+ ['script', SCRIPT_TOOLS]
193
+ ];
194
+
195
+ function logWarning(logger, message) {
196
+ if (!logger) return;
197
+ if (typeof logger.warning === 'function') {
198
+ logger.warning(message);
199
+ return;
200
+ }
201
+ if (typeof logger.warn === 'function') {
202
+ logger.warn(message);
203
+ return;
204
+ }
205
+ if (typeof logger.info === 'function') {
206
+ logger.info(message);
207
+ }
208
+ }
209
+
210
+ function normalizeCategoryList(rawValues = []) {
211
+ const values = Array.isArray(rawValues) ? rawValues : [];
212
+ const normalized = [];
213
+ const unknown = [];
214
+ const seen = new Set();
215
+
216
+ for (const value of values) {
217
+ const key = toCategoryKey(value);
218
+ if (!key) continue;
219
+ const category = CATEGORY_KEY_TO_NAME.get(key);
220
+ if (!category) {
221
+ unknown.push(String(value).trim());
222
+ continue;
223
+ }
224
+ if (seen.has(category)) continue;
225
+ seen.add(category);
226
+ normalized.push(category);
227
+ }
228
+
229
+ return { normalized, unknown };
230
+ }
231
+
232
+ export function getToolCategory(toolName) {
233
+ const name = typeof toolName === 'string' ? toolName : '';
234
+ if (!name) return 'general';
235
+ if (name.startsWith('addressables_')) return 'addressables';
236
+ if (name.startsWith('profiler_')) return 'profiler';
237
+
238
+ for (const [category, set] of TOOL_SETS) {
239
+ if (set.has(name)) return category;
240
+ }
241
+
242
+ return 'general';
243
+ }
244
+
245
+ export function createToolCategoryPolicy(config = {}, logger) {
246
+ const includeResult = normalizeCategoryList(config.includeCategories);
247
+ const excludeResult = normalizeCategoryList(config.excludeCategories);
248
+
249
+ if (includeResult.unknown.length > 0) {
250
+ logWarning(
251
+ logger,
252
+ `[tool-filter] Ignoring unknown include categories: ${includeResult.unknown.join(', ')}`
253
+ );
254
+ }
255
+ if (excludeResult.unknown.length > 0) {
256
+ logWarning(
257
+ logger,
258
+ `[tool-filter] Ignoring unknown exclude categories: ${excludeResult.unknown.join(', ')}`
259
+ );
260
+ }
261
+
262
+ return {
263
+ includeCategories: new Set(includeResult.normalized),
264
+ excludeCategories: new Set(excludeResult.normalized),
265
+ includeList: includeResult.normalized,
266
+ excludeList: excludeResult.normalized,
267
+ isActive: includeResult.normalized.length > 0 || excludeResult.normalized.length > 0
268
+ };
269
+ }
270
+
271
+ export function filterToolsByCategory(tools, policy) {
272
+ const list = Array.isArray(tools) ? tools : [];
273
+ const publicTools = [];
274
+ const hiddenToolNames = new Set();
275
+ const publicToolNames = new Set();
276
+ const includeCategories = policy?.includeCategories ?? new Set();
277
+ const excludeCategories = policy?.excludeCategories ?? new Set();
278
+ const includeEnabled = includeCategories.size > 0;
279
+ const excludeEnabled = excludeCategories.size > 0;
280
+
281
+ for (const tool of list) {
282
+ const name = tool?.name;
283
+ if (typeof name !== 'string' || name.length === 0) {
284
+ continue;
285
+ }
286
+
287
+ const category = getToolCategory(name);
288
+ const includeMatch = !includeEnabled || includeCategories.has(category);
289
+ const excludeMatch = excludeEnabled && excludeCategories.has(category);
290
+ const visible = includeMatch && !excludeMatch;
291
+
292
+ if (visible) {
293
+ publicTools.push(tool);
294
+ publicToolNames.add(name);
295
+ } else {
296
+ hiddenToolNames.add(name);
297
+ }
298
+ }
299
+
300
+ return {
301
+ tools: publicTools,
302
+ publicToolNames,
303
+ hiddenToolNames
304
+ };
305
+ }
306
+
307
+ export function getKnownToolCategories() {
308
+ return [...CATEGORY_NAMES];
309
+ }