@akiojin/unity-mcp-server 2.45.4 → 2.46.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.
@@ -5,27 +5,42 @@
5
5
  * This module implements a deferred initialization pattern to ensure
6
6
  * npx compatibility. The MCP transport connection is established FIRST,
7
7
  * before loading handlers and other heavy dependencies, to avoid
8
- * Claude Code's 30-second timeout.
8
+ * MCP client startup timeouts (NFR: startup <= 10s).
9
9
  *
10
10
  * Initialization order:
11
- * 1. MCP SDK imports (minimal, fast)
12
- * 2. Transport connection (must complete before timeout)
11
+ * 1. Stdio JSON-RPC listener (fast, no heavy deps)
12
+ * 2. Initialization handshake (must complete before timeout)
13
13
  * 3. Handler loading (deferred, after connection)
14
14
  * 4. Unity connection (deferred, non-blocking)
15
15
  */
16
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
- import {
18
- ListToolsRequestSchema,
19
- CallToolRequestSchema,
20
- SetLevelRequestSchema
21
- } from '@modelcontextprotocol/sdk/types.js';
22
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import fs from 'node:fs';
17
+ import { StdioRpcServer } from './stdioRpcServer.js';
23
18
 
24
19
  // Deferred state - will be initialized after transport connection
25
20
  let unityConnection = null;
26
21
  let handlers = null;
27
22
  let config = null;
28
23
  let logger = null;
24
+ let initializationPromise = null;
25
+
26
+ let cachedToolManifest = null;
27
+ function readToolManifest() {
28
+ if (cachedToolManifest) return cachedToolManifest;
29
+ try {
30
+ const raw = fs.readFileSync(new URL('./toolManifest.json', import.meta.url), 'utf8');
31
+ const parsed = JSON.parse(raw);
32
+ const tools = Array.isArray(parsed)
33
+ ? parsed
34
+ : Array.isArray(parsed?.tools)
35
+ ? parsed.tools
36
+ : null;
37
+ if (tools && tools.every(t => t && typeof t === 'object' && typeof t.name === 'string')) {
38
+ cachedToolManifest = tools;
39
+ return tools;
40
+ }
41
+ } catch {}
42
+ return null;
43
+ }
29
44
 
30
45
  /**
31
46
  * Lazily load handlers and dependencies
@@ -33,39 +48,51 @@ let logger = null;
33
48
  */
34
49
  async function ensureInitialized() {
35
50
  if (handlers !== null) return;
51
+ if (initializationPromise) {
52
+ await initializationPromise;
53
+ return;
54
+ }
36
55
 
37
- // Load config first (needed for logging)
38
- const configModule = await import('./config.js');
39
- config = configModule.config;
40
- logger = configModule.logger;
56
+ initializationPromise = (async () => {
57
+ // Load config first (needed for logging)
58
+ const configModule = await import('./config.js');
59
+ config = configModule.config;
60
+ logger = configModule.logger;
41
61
 
42
- // Load UnityConnection
43
- const { UnityConnection } = await import('./unityConnection.js');
44
- unityConnection = new UnityConnection();
62
+ // Load UnityConnection
63
+ const { UnityConnection } = await import('./unityConnection.js');
64
+ unityConnection = new UnityConnection();
45
65
 
46
- // Load and create handlers
47
- const { createHandlers } = await import('../handlers/index.js');
48
- handlers = createHandlers(unityConnection);
66
+ // Load and create handlers
67
+ const { createHandlers } = await import('../handlers/index.js');
68
+ handlers = createHandlers(unityConnection);
49
69
 
50
- // Set up Unity connection event handlers
51
- unityConnection.on('connected', () => {
52
- logger.info('Unity connection established');
53
- });
70
+ // Set up Unity connection event handlers
71
+ unityConnection.on('connected', () => {
72
+ logger.info('Unity connection established');
73
+ });
54
74
 
55
- unityConnection.on('disconnected', () => {
56
- logger.info('Unity connection lost');
57
- });
75
+ unityConnection.on('disconnected', () => {
76
+ logger.info('Unity connection lost');
77
+ });
58
78
 
59
- unityConnection.on('error', error => {
60
- logger.error('Unity connection error:', error.message);
61
- });
79
+ unityConnection.on('error', error => {
80
+ logger.error('Unity connection error:', error.message);
81
+ });
82
+ })();
83
+
84
+ try {
85
+ await initializationPromise;
86
+ } finally {
87
+ initializationPromise = null;
88
+ }
62
89
  }
63
90
 
64
91
  // Initialize server
65
92
  export async function startServer(options = {}) {
66
93
  try {
67
94
  // Step 1: Load minimal config for server metadata
68
- // We need server name/version before creating the Server instance
95
+ // (config import is lightweight; avoid importing the MCP TS SDK on startup)
69
96
  const { config: serverConfig, logger: serverLogger } = await import('./config.js');
70
97
  config = serverConfig;
71
98
  logger = serverLogger;
@@ -77,47 +104,190 @@ export async function startServer(options = {}) {
77
104
  stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
78
105
  };
79
106
 
80
- // Step 2: Create MCP server with minimal configuration
81
- const server = new Server(
82
- {
83
- name: config.server.name,
84
- version: config.server.version
85
- },
86
- {
87
- capabilities: {
88
- // Explicitly advertise tool support; some MCP clients expect a non-empty object
89
- // Setting listChanged enables future push updates if we emit notifications
90
- tools: { listChanged: true },
91
- // Enable MCP logging capability for sendLoggingMessage
92
- logging: {}
93
- }
107
+ // Step 2: Create a lightweight stdio MCP server (no TS SDK import)
108
+ const server =
109
+ runtimeConfig.stdioEnabled === false
110
+ ? null
111
+ : new StdioRpcServer({
112
+ serverInfo: {
113
+ name: config.server.name,
114
+ version: config.server.version
115
+ },
116
+ capabilities: {
117
+ // Advertise tool support with listChanged enabled for future push updates
118
+ tools: { listChanged: true },
119
+ // Enable MCP logging capability for notifications/message
120
+ logging: {}
121
+ }
122
+ });
123
+
124
+ let httpServerInstance;
125
+
126
+ // Defer expensive background work until after the client has completed
127
+ // initialization and received the initial tool list.
128
+ let mcpInitComplete = runtimeConfig.stdioEnabled === false;
129
+ let postInitRequested = runtimeConfig.stdioEnabled === false;
130
+ let postInitStarted = false;
131
+
132
+ const startPostInit = () => {
133
+ if (postInitStarted) return;
134
+ postInitStarted = true;
135
+
136
+ // Enable MCP logging notifications (REQ-4) after initialization handshake
137
+ if (runtimeConfig.stdioEnabled !== false) {
138
+ logger.setServer(server);
94
139
  }
95
- );
96
140
 
97
- // Step 3: Connect transport FIRST (critical for npx timeout avoidance)
98
- let transport;
99
- if (runtimeConfig.stdioEnabled !== false) {
100
- console.error(`[unity-mcp-server] MCP transport connecting...`);
101
- transport = new StdioServerTransport();
102
- await server.connect(transport);
103
- console.error(`[unity-mcp-server] MCP transport connected`);
141
+ // Start loading handlers in background so first request is faster
142
+ ensureInitialized().catch(err => {
143
+ console.error(`[unity-mcp-server] Background initialization failed: ${err.message}`);
144
+ });
104
145
 
105
- // Enable MCP logging notifications (REQ-4)
106
- logger.setServer(server);
146
+ // Optional HTTP transport (requires handlers)
147
+ if (runtimeConfig.http?.enabled) {
148
+ (async () => {
149
+ await ensureInitialized();
150
+ const { createHttpServer } = await import('./httpServer.js');
151
+ httpServerInstance = createHttpServer({
152
+ handlers,
153
+ host: runtimeConfig.http.host,
154
+ port: runtimeConfig.http.port,
155
+ telemetryEnabled: runtimeConfig.telemetry.enabled,
156
+ healthPath: runtimeConfig.http.healthPath,
157
+ allowedHosts: runtimeConfig.http.allowedHosts
158
+ });
159
+ try {
160
+ await httpServerInstance.start();
161
+ } catch (err) {
162
+ logger.error(`HTTP server failed to start: ${err.message}`);
163
+ if (runtimeConfig.stdioEnabled === false) {
164
+ process.exit(1);
165
+ }
166
+ }
167
+ })();
168
+ }
169
+
170
+ // Attempt to connect to Unity (deferred, non-blocking)
171
+ (async () => {
172
+ await ensureInitialized();
173
+ console.error(`[unity-mcp-server] Unity connection starting...`);
174
+ try {
175
+ await unityConnection.connect();
176
+ console.error(`[unity-mcp-server] Unity connection established`);
177
+ } catch (error) {
178
+ console.error(`[unity-mcp-server] Unity connection failed: ${error.message}`);
179
+ logger.error('Initial Unity connection failed:', error.message);
180
+ logger.info('Unity connection will retry automatically');
181
+ }
182
+ })();
183
+
184
+ // Best-effort: prepare and start persistent C# LSP process (non-blocking)
185
+ (async () => {
186
+ try {
187
+ const { LspProcessManager } = await import('../lsp/LspProcessManager.js');
188
+ const mgr = new LspProcessManager();
189
+ await mgr.ensureStarted();
190
+ // Attach graceful shutdown
191
+ const shutdown = async () => {
192
+ try {
193
+ await mgr.stop(3000);
194
+ } catch {}
195
+ };
196
+ process.on('SIGINT', shutdown);
197
+ process.on('SIGTERM', shutdown);
198
+ } catch (e) {
199
+ logger.warning(`[startup] csharp-lsp start failed: ${e.message}`);
200
+ }
201
+ })();
202
+
203
+ // Start periodic index watcher (incremental)
204
+ (async () => {
205
+ await ensureInitialized();
206
+ const { IndexWatcher } = await import('./indexWatcher.js');
207
+ const watcher = new IndexWatcher(unityConnection);
208
+ watcher.start();
209
+ const stopWatch = () => {
210
+ try {
211
+ watcher.stop();
212
+ } catch {}
213
+ };
214
+ process.on('SIGINT', stopWatch);
215
+ process.on('SIGTERM', stopWatch);
216
+ })();
217
+
218
+ // Auto-initialize code index if DB doesn't exist
219
+ (async () => {
220
+ await ensureInitialized();
221
+ try {
222
+ const { CodeIndex } = await import('./codeIndex.js');
223
+ const index = new CodeIndex(unityConnection);
224
+ const ready = await index.isReady();
225
+
226
+ if (!ready) {
227
+ if (index.disabled) {
228
+ logger.warning(
229
+ `[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
230
+ );
231
+ return;
232
+ }
233
+ logger.info('[startup] Code index DB not ready. Starting auto-build...');
234
+ const { CodeIndexBuildToolHandler } =
235
+ await import('../handlers/script/CodeIndexBuildToolHandler.js');
236
+ const builder = new CodeIndexBuildToolHandler(unityConnection);
237
+ const result = await builder.execute({});
238
+
239
+ if (result.success) {
240
+ logger.info(
241
+ `[startup] Code index auto-build started: jobId=${result.jobId}. Use code_index_status to check progress.`
242
+ );
243
+ } else {
244
+ logger.warning(`[startup] Code index auto-build failed: ${result.message}`);
245
+ }
246
+ } else {
247
+ logger.info('[startup] Code index DB already exists. Skipping auto-build.');
248
+ }
249
+ } catch (e) {
250
+ logger.warning(`[startup] Code index auto-init failed: ${e.message}`);
251
+ }
252
+ })();
253
+ };
254
+
255
+ const requestPostInit = () => {
256
+ postInitRequested = true;
257
+ if (mcpInitComplete) {
258
+ setImmediate(startPostInit);
259
+ }
260
+ };
261
+
262
+ // For stdio MCP, wait for notifications/initialized AND the initial tools/list
263
+ // to avoid blocking startup tool discovery in clients with short timeouts.
264
+ if (server) {
265
+ server.oninitialized = () => {
266
+ mcpInitComplete = true;
267
+ if (postInitRequested) {
268
+ setImmediate(startPostInit);
269
+ }
270
+ };
107
271
  }
108
272
 
109
- // Step 4: Register request handlers (they will lazily load dependencies)
273
+ // Step 3: Register request handlers (they will lazily load dependencies)
110
274
  // Handle logging/setLevel request (REQ-6)
111
- server.setRequestHandler(SetLevelRequestSchema, async request => {
112
- await ensureInitialized();
113
- const { level } = request.params;
275
+ server?.setRequestHandler('logging/setLevel', async request => {
276
+ const { level } = request.params || {};
114
277
  logger.setLevel(level);
115
278
  logger.info(`Log level changed to: ${level}`);
116
279
  return {};
117
280
  });
118
281
 
119
282
  // Handle tool listing
120
- server.setRequestHandler(ListToolsRequestSchema, async () => {
283
+ server?.setRequestHandler('tools/list', async () => {
284
+ const manifestTools = readToolManifest();
285
+ if (manifestTools) {
286
+ logger.info(`[MCP] Returning ${manifestTools.length} tool definitions`);
287
+ requestPostInit();
288
+ return { tools: manifestTools };
289
+ }
290
+
121
291
  await ensureInitialized();
122
292
 
123
293
  const tools = Array.from(handlers.values())
@@ -139,14 +309,15 @@ export async function startServer(options = {}) {
139
309
  .filter(tool => tool !== null);
140
310
 
141
311
  logger.info(`[MCP] Returning ${tools.length} tool definitions`);
312
+ requestPostInit();
142
313
  return { tools };
143
314
  });
144
315
 
145
316
  // Handle tool execution
146
- server.setRequestHandler(CallToolRequestSchema, async request => {
317
+ server?.setRequestHandler('tools/call', async request => {
147
318
  await ensureInitialized();
148
319
 
149
- const { name, arguments: args } = request.params;
320
+ const { name, arguments: args } = request.params || {};
150
321
  const requestTime = Date.now();
151
322
 
152
323
  logger.info(
@@ -239,124 +410,27 @@ export async function startServer(options = {}) {
239
410
  }
240
411
  });
241
412
 
413
+ // Step 4: Start stdio listener (critical for short client timeouts)
414
+ if (server) {
415
+ console.error(`[unity-mcp-server] MCP transport connecting...`);
416
+ await server.start();
417
+ console.error(`[unity-mcp-server] MCP transport connected`);
418
+ }
419
+
242
420
  // Now safe to log after connection established
243
421
  logger.info('MCP server started successfully');
244
422
 
245
- // Step 5: Background initialization (non-blocking)
246
- // Start loading handlers in background so first request is faster
247
- ensureInitialized().catch(err => {
248
- console.error(`[unity-mcp-server] Background initialization failed: ${err.message}`);
249
- });
250
-
251
- // Optional HTTP transport
252
- let httpServerInstance;
253
- if (runtimeConfig.http?.enabled) {
254
- const { createHttpServer } = await import('./httpServer.js');
255
- httpServerInstance = createHttpServer({
256
- handlers,
257
- host: runtimeConfig.http.host,
258
- port: runtimeConfig.http.port,
259
- telemetryEnabled: runtimeConfig.telemetry.enabled,
260
- healthPath: runtimeConfig.http.healthPath
261
- });
262
- try {
263
- await httpServerInstance.start();
264
- } catch (err) {
265
- logger.error(`HTTP server failed to start: ${err.message}`);
266
- throw err;
267
- }
423
+ // In HTTP-only mode (--no-stdio), there is no MCP tools/list handshake.
424
+ // Start background work immediately so the HTTP server can come up.
425
+ if (runtimeConfig.stdioEnabled === false) {
426
+ startPostInit();
268
427
  }
269
428
 
270
- // Attempt to connect to Unity (deferred, non-blocking)
271
- (async () => {
272
- await ensureInitialized();
273
- console.error(`[unity-mcp-server] Unity connection starting...`);
274
- try {
275
- await unityConnection.connect();
276
- console.error(`[unity-mcp-server] Unity connection established`);
277
- } catch (error) {
278
- console.error(`[unity-mcp-server] Unity connection failed: ${error.message}`);
279
- logger.error('Initial Unity connection failed:', error.message);
280
- logger.info('Unity connection will retry automatically');
281
- }
282
- })();
283
-
284
- // Best-effort: prepare and start persistent C# LSP process (non-blocking)
285
- (async () => {
286
- try {
287
- const { LspProcessManager } = await import('../lsp/LspProcessManager.js');
288
- const mgr = new LspProcessManager();
289
- await mgr.ensureStarted();
290
- // Attach graceful shutdown
291
- const shutdown = async () => {
292
- try {
293
- await mgr.stop(3000);
294
- } catch {}
295
- };
296
- process.on('SIGINT', shutdown);
297
- process.on('SIGTERM', shutdown);
298
- } catch (e) {
299
- logger.warning(`[startup] csharp-lsp start failed: ${e.message}`);
300
- }
301
- })();
302
-
303
- // Start periodic index watcher (incremental)
304
- (async () => {
305
- await ensureInitialized();
306
- const { IndexWatcher } = await import('./indexWatcher.js');
307
- const watcher = new IndexWatcher(unityConnection);
308
- watcher.start();
309
- const stopWatch = () => {
310
- try {
311
- watcher.stop();
312
- } catch {}
313
- };
314
- process.on('SIGINT', stopWatch);
315
- process.on('SIGTERM', stopWatch);
316
- })();
317
-
318
- // Auto-initialize code index if DB doesn't exist
319
- (async () => {
320
- await ensureInitialized();
321
- try {
322
- const { CodeIndex } = await import('./codeIndex.js');
323
- const index = new CodeIndex(unityConnection);
324
- const ready = await index.isReady();
325
-
326
- if (!ready) {
327
- if (index.disabled) {
328
- logger.warning(
329
- `[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
330
- );
331
- return;
332
- }
333
- logger.info('[startup] Code index DB not ready. Starting auto-build...');
334
- const { CodeIndexBuildToolHandler } = await import(
335
- '../handlers/script/CodeIndexBuildToolHandler.js'
336
- );
337
- const builder = new CodeIndexBuildToolHandler(unityConnection);
338
- const result = await builder.execute({});
339
-
340
- if (result.success) {
341
- logger.info(
342
- `[startup] Code index auto-build started: jobId=${result.jobId}. Use code_index_status to check progress.`
343
- );
344
- } else {
345
- logger.warning(`[startup] Code index auto-build failed: ${result.message}`);
346
- }
347
- } else {
348
- logger.info('[startup] Code index DB already exists. Skipping auto-build.');
349
- }
350
- } catch (e) {
351
- logger.warning(`[startup] Code index auto-init failed: ${e.message}`);
352
- }
353
- })();
354
-
355
429
  // Handle shutdown
356
430
  process.on('SIGINT', async () => {
357
431
  logger.info('Shutting down...');
358
432
  if (unityConnection) unityConnection.disconnect();
359
- if (transport) await server.close();
433
+ if (server) await server.close();
360
434
  if (httpServerInstance) await httpServerInstance.close();
361
435
  process.exit(0);
362
436
  });
@@ -364,7 +438,7 @@ export async function startServer(options = {}) {
364
438
  process.on('SIGTERM', async () => {
365
439
  logger.info('Shutting down...');
366
440
  if (unityConnection) unityConnection.disconnect();
367
- if (transport) await server.close();
441
+ if (server) await server.close();
368
442
  if (httpServerInstance) await httpServerInstance.close();
369
443
  process.exit(0);
370
444
  });
@@ -380,9 +454,16 @@ export const main = startServer;
380
454
 
381
455
  // Export for testing
382
456
  export async function createServer(customConfig) {
383
- // For testing, we need to load dependencies synchronously
384
- const { config: defaultConfig } = await import('./config.js');
457
+ // For testing, keep using the official MCP SDK to validate compatibility.
458
+ const [{ config: defaultConfig }, sdkServerModule, sdkTypesModule] = await Promise.all([
459
+ import('./config.js'),
460
+ import('@modelcontextprotocol/sdk/server/index.js'),
461
+ import('@modelcontextprotocol/sdk/types.js')
462
+ ]);
463
+
385
464
  const actualConfig = customConfig || defaultConfig;
465
+ const { Server } = sdkServerModule;
466
+ const { ListToolsRequestSchema, CallToolRequestSchema } = sdkTypesModule;
386
467
 
387
468
  const { UnityConnection } = await import('./unityConnection.js');
388
469
  const { createHandlers } = await import('../handlers/index.js');