@akiojin/unity-mcp-server 2.26.0 → 2.27.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": "2.26.0",
3
+ "version": "2.27.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",
@@ -1,8 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import Database from 'better-sqlite3';
4
- import { logger } from './config.js';
5
-
6
4
  let dbCache = new Map();
7
5
 
8
6
  function getDbPath(projectRoot) {
@@ -47,16 +45,29 @@ export function openDb(projectRoot) {
47
45
  }
48
46
 
49
47
  export function upsertFile(db, filePath, mtimeMs) {
50
- const stmt = db.prepare('INSERT INTO files(path, mtime) VALUES(?, ?) ON CONFLICT(path) DO UPDATE SET mtime=excluded.mtime');
48
+ const stmt = db.prepare(
49
+ 'INSERT INTO files(path, mtime) VALUES(?, ?) ON CONFLICT(path) DO UPDATE SET mtime=excluded.mtime'
50
+ );
51
51
  stmt.run(filePath, Math.floor(mtimeMs));
52
52
  }
53
53
 
54
54
  export function replaceSymbols(db, filePath, symbols) {
55
55
  const del = db.prepare('DELETE FROM symbols WHERE path = ?');
56
56
  del.run(filePath);
57
- const ins = db.prepare('INSERT INTO symbols(path, name, kind, container, ns, line, column) VALUES(?,?,?,?,?,?,?)');
58
- const tr = db.transaction((rows) => {
59
- for (const s of rows) ins.run(filePath, s.name || '', s.kind || '', s.container || null, s.ns || null, s.line || 0, s.column || 0);
57
+ const ins = db.prepare(
58
+ 'INSERT INTO symbols(path, name, kind, container, ns, line, column) VALUES(?,?,?,?,?,?,?)'
59
+ );
60
+ const tr = db.transaction(rows => {
61
+ for (const s of rows)
62
+ ins.run(
63
+ filePath,
64
+ s.name || '',
65
+ s.kind || '',
66
+ s.container || null,
67
+ s.ns || null,
68
+ s.line || 0,
69
+ s.column || 0
70
+ );
60
71
  });
61
72
  tr(symbols || []);
62
73
  }
@@ -65,7 +76,7 @@ export function replaceReferences(db, filePath, refs) {
65
76
  const del = db.prepare('DELETE FROM refs WHERE path = ?');
66
77
  del.run(filePath);
67
78
  const ins = db.prepare('INSERT INTO refs(path, name, line, snippet) VALUES(?,?,?,?)');
68
- const tr = db.transaction((rows) => {
79
+ const tr = db.transaction(rows => {
69
80
  for (const r of rows) ins.run(filePath, r.name || '', r.line || 0, r.snippet || null);
70
81
  });
71
82
  tr(refs || []);
@@ -73,9 +84,15 @@ export function replaceReferences(db, filePath, refs) {
73
84
 
74
85
  export function querySymbolsByName(db, name, kind = null) {
75
86
  if (kind) {
76
- return db.prepare('SELECT path,name,kind,container,ns,line,column FROM symbols WHERE name = ? AND kind = ? LIMIT 500').all(name, kind);
87
+ return db
88
+ .prepare(
89
+ 'SELECT path,name,kind,container,ns,line,column FROM symbols WHERE name = ? AND kind = ? LIMIT 500'
90
+ )
91
+ .all(name, kind);
77
92
  }
78
- return db.prepare('SELECT path,name,kind,container,ns,line,column FROM symbols WHERE name = ? LIMIT 500').all(name);
93
+ return db
94
+ .prepare('SELECT path,name,kind,container,ns,line,column FROM symbols WHERE name = ? LIMIT 500')
95
+ .all(name);
79
96
  }
80
97
 
81
98
  export function queryRefsByName(db, name) {
@@ -93,4 +110,3 @@ export function isFresh(projectRoot, filePath, db) {
93
110
  return false;
94
111
  }
95
112
  }
96
-
@@ -1,65 +1,74 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { logger, config, WORKSPACE_ROOT } from './config.js';
4
-
5
- const normalize = (p) => p.replace(/\\/g, '/');
6
-
7
- const resolveDefaultCodeIndexRoot = (projectRoot) => {
8
- const base = WORKSPACE_ROOT || projectRoot || process.cwd();
9
- return normalize(path.join(base, '.unity', 'cache', 'code-index'));
10
- };
11
-
12
- // Lazy project info resolver. Prefers Unity via get_editor_info, otherwise infers by walking up for Assets/Packages.
13
- export class ProjectInfoProvider {
14
- constructor(unityConnection) {
15
- this.unityConnection = unityConnection;
16
- this.cached = null;
17
- this.lastTried = 0;
18
- }
19
-
20
- async get() {
21
- if (this.cached) return this.cached;
22
- // Config-driven project root (no env fallback)
23
- const cfgRootRaw = config?.project?.root;
24
- if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
25
- const cfgRoot = cfgRootRaw.trim();
26
- // Resolve relative paths against WORKSPACE_ROOT
27
- const projectRoot = normalize(path.isAbsolute(cfgRoot) ? cfgRoot : path.resolve(WORKSPACE_ROOT, cfgRoot));
28
- const codeIndexRoot = normalize(config?.project?.codeIndexRoot || resolveDefaultCodeIndexRoot(projectRoot));
29
- this.cached = {
30
- projectRoot,
31
- assetsPath: normalize(path.join(projectRoot, 'Assets')),
32
- packagesPath: normalize(path.join(projectRoot, 'Packages')),
33
- codeIndexRoot,
34
- };
35
- return this.cached;
36
- }
37
- // Try Unity if connected (rate-limit attempts)
38
- const now = Date.now();
39
- if (this.unityConnection && this.unityConnection.isConnected() && (now - this.lastTried > 1000)) {
40
- this.lastTried = now;
41
- try {
42
- const info = await this.unityConnection.sendCommand('get_editor_info', {});
43
- if (info && info.projectRoot && info.assetsPath) {
44
- this.cached = {
45
- projectRoot: info.projectRoot,
46
- assetsPath: info.assetsPath,
47
- packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
48
- codeIndexRoot: normalize(info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)),
49
- };
50
- return this.cached;
51
- }
52
- } catch (e) {
53
- logger.warn(`get_editor_info failed: ${e.message}`);
54
- }
55
- }
56
- if (typeof cfgRootRaw === 'string') {
57
- throw new Error('project.root is configured but empty. Set a valid path in .unity/config.json or UNITY_MCP_CONFIG.');
58
- }
59
- throw new Error('Unable to resolve Unity project root. Configure project.root in .unity/config.json or provide UNITY_MCP_CONFIG.');
60
- }
61
-
62
- inferFromCwd() {
63
- return null;
64
- }
65
- }
1
+ import path from 'path';
2
+ import { logger, config, WORKSPACE_ROOT } from './config.js';
3
+
4
+ const normalize = p => p.replace(/\\/g, '/');
5
+
6
+ const resolveDefaultCodeIndexRoot = projectRoot => {
7
+ const base = WORKSPACE_ROOT || projectRoot || process.cwd();
8
+ return normalize(path.join(base, '.unity', 'cache', 'code-index'));
9
+ };
10
+
11
+ // Lazy project info resolver. Prefers Unity via get_editor_info, otherwise infers by walking up for Assets/Packages.
12
+ export class ProjectInfoProvider {
13
+ constructor(unityConnection) {
14
+ this.unityConnection = unityConnection;
15
+ this.cached = null;
16
+ this.lastTried = 0;
17
+ }
18
+
19
+ async get() {
20
+ if (this.cached) return this.cached;
21
+ // Config-driven project root (no env fallback)
22
+ const cfgRootRaw = config?.project?.root;
23
+ if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
24
+ const cfgRoot = cfgRootRaw.trim();
25
+ // Resolve relative paths against WORKSPACE_ROOT
26
+ const projectRoot = normalize(
27
+ path.isAbsolute(cfgRoot) ? cfgRoot : path.resolve(WORKSPACE_ROOT, cfgRoot)
28
+ );
29
+ const codeIndexRoot = normalize(
30
+ config?.project?.codeIndexRoot || resolveDefaultCodeIndexRoot(projectRoot)
31
+ );
32
+ this.cached = {
33
+ projectRoot,
34
+ assetsPath: normalize(path.join(projectRoot, 'Assets')),
35
+ packagesPath: normalize(path.join(projectRoot, 'Packages')),
36
+ codeIndexRoot
37
+ };
38
+ return this.cached;
39
+ }
40
+ // Try Unity if connected (rate-limit attempts)
41
+ const now = Date.now();
42
+ if (this.unityConnection && this.unityConnection.isConnected() && now - this.lastTried > 1000) {
43
+ this.lastTried = now;
44
+ try {
45
+ const info = await this.unityConnection.sendCommand('get_editor_info', {});
46
+ if (info && info.projectRoot && info.assetsPath) {
47
+ this.cached = {
48
+ projectRoot: info.projectRoot,
49
+ assetsPath: info.assetsPath,
50
+ packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
51
+ codeIndexRoot: normalize(
52
+ info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)
53
+ )
54
+ };
55
+ return this.cached;
56
+ }
57
+ } catch (e) {
58
+ logger.warn(`get_editor_info failed: ${e.message}`);
59
+ }
60
+ }
61
+ if (typeof cfgRootRaw === 'string') {
62
+ throw new Error(
63
+ 'project.root is configured but empty. Set a valid path in .unity/config.json or UNITY_MCP_CONFIG.'
64
+ );
65
+ }
66
+ throw new Error(
67
+ 'Unable to resolve Unity project root. Configure project.root in .unity/config.json or provide UNITY_MCP_CONFIG.'
68
+ );
69
+ }
70
+
71
+ inferFromCwd() {
72
+ return null;
73
+ }
74
+ }
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import {
5
- ListToolsRequestSchema,
3
+ import {
4
+ ListToolsRequestSchema,
6
5
  CallToolRequestSchema,
7
6
  ListResourcesRequestSchema,
8
7
  ListPromptsRequestSchema
@@ -12,6 +11,7 @@ import { UnityConnection } from './unityConnection.js';
12
11
  import { createHandlers } from '../handlers/index.js';
13
12
  import { config, logger } from './config.js';
14
13
  import { IndexWatcher } from './indexWatcher.js';
14
+ import { HybridStdioServerTransport } from './transports/HybridStdioServerTransport.js';
15
15
 
16
16
  // Create Unity connection
17
17
  const unityConnection = new UnityConnection();
@@ -23,7 +23,7 @@ const handlers = createHandlers(unityConnection);
23
23
  const server = new Server(
24
24
  {
25
25
  name: config.server.name,
26
- version: config.server.version,
26
+ version: config.server.version
27
27
  },
28
28
  {
29
29
  capabilities: {
@@ -41,20 +41,24 @@ const server = new Server(
41
41
 
42
42
  // Handle tool listing
43
43
  server.setRequestHandler(ListToolsRequestSchema, async () => {
44
- const tools = Array.from(handlers.values()).map((handler, index) => {
45
- try {
46
- const definition = handler.getDefinition();
47
- // Validate inputSchema
48
- if (definition.inputSchema && definition.inputSchema.type !== 'object') {
49
- logger.error(`[MCP] Tool ${handler.name} (index ${index}) has invalid inputSchema type: ${definition.inputSchema.type}`);
44
+ const tools = Array.from(handlers.values())
45
+ .map((handler, index) => {
46
+ try {
47
+ const definition = handler.getDefinition();
48
+ // Validate inputSchema
49
+ if (definition.inputSchema && definition.inputSchema.type !== 'object') {
50
+ logger.error(
51
+ `[MCP] Tool ${handler.name} (index ${index}) has invalid inputSchema type: ${definition.inputSchema.type}`
52
+ );
53
+ }
54
+ return definition;
55
+ } catch (error) {
56
+ logger.error(`[MCP] Failed to get definition for handler ${handler.name}:`, error);
57
+ return null;
50
58
  }
51
- return definition;
52
- } catch (error) {
53
- logger.error(`[MCP] Failed to get definition for handler ${handler.name}:`, error);
54
- return null;
55
- }
56
- }).filter(tool => tool !== null);
57
-
59
+ })
60
+ .filter(tool => tool !== null);
61
+
58
62
  logger.info(`[MCP] Returning ${tools.length} tool definitions`);
59
63
  return { tools };
60
64
  });
@@ -74,36 +78,42 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
74
78
  });
75
79
 
76
80
  // Handle tool execution
77
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
81
+ server.setRequestHandler(CallToolRequestSchema, async request => {
78
82
  const { name, arguments: args } = request.params;
79
83
  const requestTime = Date.now();
80
-
81
- logger.info(`[MCP] Received tool call request: ${name} at ${new Date(requestTime).toISOString()}`, { args });
82
-
84
+
85
+ logger.info(
86
+ `[MCP] Received tool call request: ${name} at ${new Date(requestTime).toISOString()}`,
87
+ { args }
88
+ );
89
+
83
90
  const handler = handlers.get(name);
84
91
  if (!handler) {
85
92
  logger.error(`[MCP] Tool not found: ${name}`);
86
93
  throw new Error(`Tool not found: ${name}`);
87
94
  }
88
-
95
+
89
96
  try {
90
97
  logger.info(`[MCP] Starting handler execution for: ${name} at ${new Date().toISOString()}`);
91
98
  const startTime = Date.now();
92
-
99
+
93
100
  // Handler returns response in our format
94
101
  const result = await handler.handle(args);
95
-
102
+
96
103
  const duration = Date.now() - startTime;
97
104
  const totalDuration = Date.now() - requestTime;
98
- logger.info(`[MCP] Handler completed at ${new Date().toISOString()}: ${name}`, {
105
+ logger.info(`[MCP] Handler completed at ${new Date().toISOString()}: ${name}`, {
99
106
  handlerDuration: `${duration}ms`,
100
107
  totalDuration: `${totalDuration}ms`,
101
- status: result.status
108
+ status: result.status
102
109
  });
103
-
110
+
104
111
  // Convert to MCP format
105
112
  if (result.status === 'error') {
106
- logger.error(`[MCP] Handler returned error: ${name}`, { error: result.error, code: result.code });
113
+ logger.error(`[MCP] Handler returned error: ${name}`, {
114
+ error: result.error,
115
+ code: result.code
116
+ });
107
117
  return {
108
118
  content: [
109
119
  {
@@ -113,22 +123,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
113
123
  ]
114
124
  };
115
125
  }
116
-
126
+
117
127
  // Success response
118
128
  logger.info(`[MCP] Returning success response for: ${name} at ${new Date().toISOString()}`);
119
-
129
+
120
130
  // Handle undefined or null results from handlers
121
131
  let responseText;
122
132
  if (result.result === undefined || result.result === null) {
123
- responseText = JSON.stringify({
124
- status: 'success',
125
- message: 'Operation completed successfully but no details were returned',
126
- tool: name
127
- }, null, 2);
133
+ responseText = JSON.stringify(
134
+ {
135
+ status: 'success',
136
+ message: 'Operation completed successfully but no details were returned',
137
+ tool: name
138
+ },
139
+ null,
140
+ 2
141
+ );
128
142
  } else {
129
143
  responseText = JSON.stringify(result.result, null, 2);
130
144
  }
131
-
145
+
132
146
  return {
133
147
  content: [
134
148
  {
@@ -139,8 +153,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
139
153
  };
140
154
  } catch (error) {
141
155
  const errorTime = Date.now();
142
- logger.error(`[MCP] Handler threw exception at ${new Date(errorTime).toISOString()}: ${name}`, {
143
- error: error.message,
156
+ logger.error(`[MCP] Handler threw exception at ${new Date(errorTime).toISOString()}: ${name}`, {
157
+ error: error.message,
144
158
  stack: error.stack,
145
159
  duration: `${errorTime - requestTime}ms`
146
160
  });
@@ -164,7 +178,7 @@ unityConnection.on('disconnected', () => {
164
178
  logger.info('Unity connection lost');
165
179
  });
166
180
 
167
- unityConnection.on('error', (error) => {
181
+ unityConnection.on('error', error => {
168
182
  logger.error('Unity connection error:', error.message);
169
183
  });
170
184
 
@@ -172,14 +186,14 @@ unityConnection.on('error', (error) => {
172
186
  export async function startServer() {
173
187
  try {
174
188
  // Create transport - no logging before connection
175
- const transport = new StdioServerTransport();
176
-
189
+ const transport = new HybridStdioServerTransport();
190
+
177
191
  // Connect to transport
178
192
  await server.connect(transport);
179
-
193
+
180
194
  // Now safe to log after connection established
181
195
  logger.info('MCP server started successfully');
182
-
196
+
183
197
  // Attempt to connect to Unity
184
198
  try {
185
199
  await unityConnection.connect();
@@ -189,13 +203,17 @@ export async function startServer() {
189
203
  }
190
204
 
191
205
  // Best-effort: prepare and start persistent C# LSP process (non-blocking)
192
- ;(async () => {
206
+ (async () => {
193
207
  try {
194
208
  const { LspProcessManager } = await import('../lsp/LspProcessManager.js');
195
209
  const mgr = new LspProcessManager();
196
210
  await mgr.ensureStarted();
197
211
  // Attach graceful shutdown
198
- const shutdown = async () => { try { await mgr.stop(3000); } catch {} };
212
+ const shutdown = async () => {
213
+ try {
214
+ await mgr.stop(3000);
215
+ } catch {}
216
+ };
199
217
  process.on('SIGINT', shutdown);
200
218
  process.on('SIGTERM', shutdown);
201
219
  } catch (e) {
@@ -206,10 +224,14 @@ export async function startServer() {
206
224
  // Start periodic index watcher (incremental)
207
225
  const watcher = new IndexWatcher(unityConnection);
208
226
  watcher.start();
209
- const stopWatch = () => { try { watcher.stop(); } catch {} };
227
+ const stopWatch = () => {
228
+ try {
229
+ watcher.stop();
230
+ } catch {}
231
+ };
210
232
  process.on('SIGINT', stopWatch);
211
233
  process.on('SIGTERM', stopWatch);
212
-
234
+
213
235
  // Handle shutdown
214
236
  process.on('SIGINT', async () => {
215
237
  logger.info('Shutting down...');
@@ -217,14 +239,13 @@ export async function startServer() {
217
239
  await server.close();
218
240
  process.exit(0);
219
241
  });
220
-
242
+
221
243
  process.on('SIGTERM', async () => {
222
244
  logger.info('Shutting down...');
223
245
  unityConnection.disconnect();
224
246
  await server.close();
225
247
  process.exit(0);
226
248
  });
227
-
228
249
  } catch (error) {
229
250
  console.error('Failed to start server:', error);
230
251
  console.error('Stack trace:', error.stack);
@@ -233,17 +254,17 @@ export async function startServer() {
233
254
  }
234
255
 
235
256
  // Maintain backwards compatibility for older callers that expect main()
236
- const main = startServer;
257
+ export const main = startServer;
237
258
 
238
259
  // Export for testing
239
260
  export async function createServer(customConfig = config) {
240
261
  const testUnityConnection = new UnityConnection();
241
262
  const testHandlers = createHandlers(testUnityConnection);
242
-
263
+
243
264
  const testServer = new Server(
244
265
  {
245
266
  name: customConfig.server.name,
246
- version: customConfig.server.version,
267
+ version: customConfig.server.version
247
268
  },
248
269
  {
249
270
  capabilities: {
@@ -253,24 +274,24 @@ export async function createServer(customConfig = config) {
253
274
  }
254
275
  }
255
276
  );
256
-
277
+
257
278
  // Register handlers for test server
258
279
  testServer.setRequestHandler(ListToolsRequestSchema, async () => {
259
280
  const tools = Array.from(testHandlers.values()).map(handler => handler.getDefinition());
260
281
  return { tools };
261
282
  });
262
-
283
+
263
284
  testServer.setRequestHandler(ListResourcesRequestSchema, async () => {
264
285
  return { resources: [] };
265
286
  });
266
-
287
+
267
288
  testServer.setRequestHandler(ListPromptsRequestSchema, async () => {
268
289
  return { prompts: [] };
269
290
  });
270
-
271
- testServer.setRequestHandler(CallToolRequestSchema, async (request) => {
291
+
292
+ testServer.setRequestHandler(CallToolRequestSchema, async request => {
272
293
  const { name, arguments: args } = request.params;
273
-
294
+
274
295
  const handler = testHandlers.get(name);
275
296
  if (!handler) {
276
297
  return {
@@ -279,10 +300,10 @@ export async function createServer(customConfig = config) {
279
300
  code: 'TOOL_NOT_FOUND'
280
301
  };
281
302
  }
282
-
303
+
283
304
  return await handler.handle(args);
284
305
  });
285
-
306
+
286
307
  return {
287
308
  server: testServer,
288
309
  unityConnection: testUnityConnection
@@ -300,8 +321,10 @@ const isDirectExecution = (() => {
300
321
  }
301
322
  })();
302
323
 
303
- if (isDirectExecution) startServer().catch((error) => {
304
- console.error('Fatal error:', error);
305
- console.error('Stack trace:', error.stack);
306
- process.exit(1);
307
- });
324
+ if (isDirectExecution) {
325
+ startServer().catch(error => {
326
+ console.error('Fatal error:', error);
327
+ console.error('Stack trace:', error.stack);
328
+ process.exit(1);
329
+ });
330
+ }