@akiojin/unity-mcp-server 2.43.2 → 2.44.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.43.2",
3
+ "version": "2.44.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,183 +1,75 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * Unity MCP Server - Main Server Module
4
+ *
5
+ * This module implements a deferred initialization pattern to ensure
6
+ * npx compatibility. The MCP transport connection is established FIRST,
7
+ * before loading handlers and other heavy dependencies, to avoid
8
+ * Claude Code's 30-second timeout.
9
+ *
10
+ * Initialization order:
11
+ * 1. MCP SDK imports (minimal, fast)
12
+ * 2. Transport connection (must complete before timeout)
13
+ * 3. Handler loading (deferred, after connection)
14
+ * 4. Unity connection (deferred, non-blocking)
15
+ */
2
16
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
17
  import {
4
18
  ListToolsRequestSchema,
5
19
  CallToolRequestSchema,
6
20
  SetLevelRequestSchema
7
21
  } from '@modelcontextprotocol/sdk/types.js';
8
- // Note: filename is lowercase on disk; use exact casing for POSIX filesystems
9
- import { UnityConnection } from './unityConnection.js';
10
- import { createHandlers } from '../handlers/index.js';
11
- import { config, logger } from './config.js';
12
- import { IndexWatcher } from './indexWatcher.js';
13
- import { HybridStdioServerTransport } from './transports/HybridStdioServerTransport.js';
14
-
15
- // Create Unity connection
16
- const unityConnection = new UnityConnection();
17
-
18
- // Create tool handlers
19
- const handlers = createHandlers(unityConnection);
20
-
21
- // Create MCP server
22
- const server = new Server(
23
- {
24
- name: config.server.name,
25
- version: config.server.version
26
- },
27
- {
28
- capabilities: {
29
- // Explicitly advertise tool support; some MCP clients expect a non-empty object
30
- // Setting listChanged enables future push updates if we emit notifications
31
- tools: { listChanged: true },
32
- // Enable MCP logging capability for sendLoggingMessage
33
- logging: {}
34
- }
35
- }
36
- );
37
-
38
- // Register MCP protocol handlers
39
- // Note: Do not log here as it breaks MCP protocol initialization
40
-
41
- // Handle logging/setLevel request (REQ-6)
42
- server.setRequestHandler(SetLevelRequestSchema, async request => {
43
- const { level } = request.params;
44
- logger.setLevel(level);
45
- logger.info(`Log level changed to: ${level}`);
46
- return {};
47
- });
48
-
49
- // Handle tool listing
50
- server.setRequestHandler(ListToolsRequestSchema, async () => {
51
- const tools = Array.from(handlers.values())
52
- .map((handler, index) => {
53
- try {
54
- const definition = handler.getDefinition();
55
- // Validate inputSchema
56
- if (definition.inputSchema && definition.inputSchema.type !== 'object') {
57
- logger.error(
58
- `[MCP] Tool ${handler.name} (index ${index}) has invalid inputSchema type: ${definition.inputSchema.type}`
59
- );
60
- }
61
- return definition;
62
- } catch (error) {
63
- logger.error(`[MCP] Failed to get definition for handler ${handler.name}:`, error);
64
- return null;
65
- }
66
- })
67
- .filter(tool => tool !== null);
68
-
69
- logger.info(`[MCP] Returning ${tools.length} tool definitions`);
70
- return { tools };
71
- });
72
-
73
- // Handle tool execution
74
- server.setRequestHandler(CallToolRequestSchema, async request => {
75
- const { name, arguments: args } = request.params;
76
- const requestTime = Date.now();
77
-
78
- logger.info(
79
- `[MCP] Received tool call request: ${name} at ${new Date(requestTime).toISOString()}`,
80
- { args }
81
- );
82
-
83
- const handler = handlers.get(name);
84
- if (!handler) {
85
- logger.error(`[MCP] Tool not found: ${name}`);
86
- throw new Error(`Tool not found: ${name}`);
87
- }
88
-
89
- try {
90
- logger.info(`[MCP] Starting handler execution for: ${name} at ${new Date().toISOString()}`);
91
- const startTime = Date.now();
92
-
93
- // Handler returns response in our format
94
- const result = await handler.handle(args);
95
-
96
- const duration = Date.now() - startTime;
97
- const totalDuration = Date.now() - requestTime;
98
- logger.info(`[MCP] Handler completed at ${new Date().toISOString()}: ${name}`, {
99
- handlerDuration: `${duration}ms`,
100
- totalDuration: `${totalDuration}ms`,
101
- status: result.status
102
- });
103
-
104
- // Convert to MCP format
105
- if (result.status === 'error') {
106
- logger.error(`[MCP] Handler returned error: ${name}`, {
107
- error: result.error,
108
- code: result.code
109
- });
110
- return {
111
- content: [
112
- {
113
- type: 'text',
114
- text: `Error: ${result.error}\nCode: ${result.code || 'UNKNOWN_ERROR'}${result.details ? '\nDetails: ' + JSON.stringify(result.details, null, 2) : ''}`
115
- }
116
- ]
117
- };
118
- }
119
-
120
- // Success response
121
- logger.info(`[MCP] Returning success response for: ${name} at ${new Date().toISOString()}`);
122
-
123
- // Handle undefined or null results from handlers
124
- let responseText;
125
- if (result.result === undefined || result.result === null) {
126
- responseText = JSON.stringify(
127
- {
128
- status: 'success',
129
- message: 'Operation completed successfully but no details were returned',
130
- tool: name
131
- },
132
- null,
133
- 2
134
- );
135
- } else {
136
- responseText = JSON.stringify(result.result, null, 2);
137
- }
138
-
139
- return {
140
- content: [
141
- {
142
- type: 'text',
143
- text: responseText
144
- }
145
- ]
146
- };
147
- } catch (error) {
148
- const errorTime = Date.now();
149
- logger.error(`[MCP] Handler threw exception at ${new Date(errorTime).toISOString()}: ${name}`, {
150
- error: error.message,
151
- stack: error.stack,
152
- duration: `${errorTime - requestTime}ms`
153
- });
154
- return {
155
- content: [
156
- {
157
- type: 'text',
158
- text: `Error: ${error.message}`
159
- }
160
- ]
161
- };
162
- }
163
- });
164
-
165
- // Handle connection events
166
- unityConnection.on('connected', () => {
167
- logger.info('Unity connection established');
168
- });
22
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
+
24
+ // Deferred state - will be initialized after transport connection
25
+ let unityConnection = null;
26
+ let handlers = null;
27
+ let config = null;
28
+ let logger = null;
29
+
30
+ /**
31
+ * Lazily load handlers and dependencies
32
+ * Called after MCP transport is connected
33
+ */
34
+ async function ensureInitialized() {
35
+ if (handlers !== null) return;
36
+
37
+ // Load config first (needed for logging)
38
+ const configModule = await import('./config.js');
39
+ config = configModule.config;
40
+ logger = configModule.logger;
41
+
42
+ // Load UnityConnection
43
+ const { UnityConnection } = await import('./unityConnection.js');
44
+ unityConnection = new UnityConnection();
45
+
46
+ // Load and create handlers
47
+ const { createHandlers } = await import('../handlers/index.js');
48
+ handlers = createHandlers(unityConnection);
49
+
50
+ // Set up Unity connection event handlers
51
+ unityConnection.on('connected', () => {
52
+ logger.info('Unity connection established');
53
+ });
169
54
 
170
- unityConnection.on('disconnected', () => {
171
- logger.info('Unity connection lost');
172
- });
55
+ unityConnection.on('disconnected', () => {
56
+ logger.info('Unity connection lost');
57
+ });
173
58
 
174
- unityConnection.on('error', error => {
175
- logger.error('Unity connection error:', error.message);
176
- });
59
+ unityConnection.on('error', error => {
60
+ logger.error('Unity connection error:', error.message);
61
+ });
62
+ }
177
63
 
178
64
  // Initialize server
179
65
  export async function startServer(options = {}) {
180
66
  try {
67
+ // Step 1: Load minimal config for server metadata
68
+ // We need server name/version before creating the Server instance
69
+ const { config: serverConfig, logger: serverLogger } = await import('./config.js');
70
+ config = serverConfig;
71
+ logger = serverLogger;
72
+
181
73
  const runtimeConfig = {
182
74
  ...config,
183
75
  http: { ...config.http, ...(options.http || {}) },
@@ -185,11 +77,28 @@ export async function startServer(options = {}) {
185
77
  stdioEnabled: options.stdioEnabled !== undefined ? options.stdioEnabled : true
186
78
  };
187
79
 
188
- // Create transport - no logging before connection
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
+ }
94
+ }
95
+ );
96
+
97
+ // Step 3: Connect transport FIRST (critical for npx timeout avoidance)
189
98
  let transport;
190
99
  if (runtimeConfig.stdioEnabled !== false) {
191
100
  console.error(`[unity-mcp-server] MCP transport connecting...`);
192
- transport = new HybridStdioServerTransport();
101
+ transport = new StdioServerTransport();
193
102
  await server.connect(transport);
194
103
  console.error(`[unity-mcp-server] MCP transport connected`);
195
104
 
@@ -197,9 +106,148 @@ export async function startServer(options = {}) {
197
106
  logger.setServer(server);
198
107
  }
199
108
 
109
+ // Step 4: Register request handlers (they will lazily load dependencies)
110
+ // Handle logging/setLevel request (REQ-6)
111
+ server.setRequestHandler(SetLevelRequestSchema, async request => {
112
+ await ensureInitialized();
113
+ const { level } = request.params;
114
+ logger.setLevel(level);
115
+ logger.info(`Log level changed to: ${level}`);
116
+ return {};
117
+ });
118
+
119
+ // Handle tool listing
120
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
121
+ await ensureInitialized();
122
+
123
+ const tools = Array.from(handlers.values())
124
+ .map((handler, index) => {
125
+ try {
126
+ const definition = handler.getDefinition();
127
+ // Validate inputSchema
128
+ if (definition.inputSchema && definition.inputSchema.type !== 'object') {
129
+ logger.error(
130
+ `[MCP] Tool ${handler.name} (index ${index}) has invalid inputSchema type: ${definition.inputSchema.type}`
131
+ );
132
+ }
133
+ return definition;
134
+ } catch (error) {
135
+ logger.error(`[MCP] Failed to get definition for handler ${handler.name}:`, error);
136
+ return null;
137
+ }
138
+ })
139
+ .filter(tool => tool !== null);
140
+
141
+ logger.info(`[MCP] Returning ${tools.length} tool definitions`);
142
+ return { tools };
143
+ });
144
+
145
+ // Handle tool execution
146
+ server.setRequestHandler(CallToolRequestSchema, async request => {
147
+ await ensureInitialized();
148
+
149
+ const { name, arguments: args } = request.params;
150
+ const requestTime = Date.now();
151
+
152
+ logger.info(
153
+ `[MCP] Received tool call request: ${name} at ${new Date(requestTime).toISOString()}`,
154
+ { args }
155
+ );
156
+
157
+ const handler = handlers.get(name);
158
+ if (!handler) {
159
+ logger.error(`[MCP] Tool not found: ${name}`);
160
+ throw new Error(`Tool not found: ${name}`);
161
+ }
162
+
163
+ try {
164
+ logger.info(`[MCP] Starting handler execution for: ${name} at ${new Date().toISOString()}`);
165
+ const startTime = Date.now();
166
+
167
+ // Handler returns response in our format
168
+ const result = await handler.handle(args);
169
+
170
+ const duration = Date.now() - startTime;
171
+ const totalDuration = Date.now() - requestTime;
172
+ logger.info(`[MCP] Handler completed at ${new Date().toISOString()}: ${name}`, {
173
+ handlerDuration: `${duration}ms`,
174
+ totalDuration: `${totalDuration}ms`,
175
+ status: result.status
176
+ });
177
+
178
+ // Convert to MCP format
179
+ if (result.status === 'error') {
180
+ logger.error(`[MCP] Handler returned error: ${name}`, {
181
+ error: result.error,
182
+ code: result.code
183
+ });
184
+ return {
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: `Error: ${result.error}\nCode: ${result.code || 'UNKNOWN_ERROR'}${result.details ? '\nDetails: ' + JSON.stringify(result.details, null, 2) : ''}`
189
+ }
190
+ ]
191
+ };
192
+ }
193
+
194
+ // Success response
195
+ logger.info(`[MCP] Returning success response for: ${name} at ${new Date().toISOString()}`);
196
+
197
+ // Handle undefined or null results from handlers
198
+ let responseText;
199
+ if (result.result === undefined || result.result === null) {
200
+ responseText = JSON.stringify(
201
+ {
202
+ status: 'success',
203
+ message: 'Operation completed successfully but no details were returned',
204
+ tool: name
205
+ },
206
+ null,
207
+ 2
208
+ );
209
+ } else {
210
+ responseText = JSON.stringify(result.result, null, 2);
211
+ }
212
+
213
+ return {
214
+ content: [
215
+ {
216
+ type: 'text',
217
+ text: responseText
218
+ }
219
+ ]
220
+ };
221
+ } catch (error) {
222
+ const errorTime = Date.now();
223
+ logger.error(
224
+ `[MCP] Handler threw exception at ${new Date(errorTime).toISOString()}: ${name}`,
225
+ {
226
+ error: error.message,
227
+ stack: error.stack,
228
+ duration: `${errorTime - requestTime}ms`
229
+ }
230
+ );
231
+ return {
232
+ content: [
233
+ {
234
+ type: 'text',
235
+ text: `Error: ${error.message}`
236
+ }
237
+ ]
238
+ };
239
+ }
240
+ });
241
+
200
242
  // Now safe to log after connection established
201
243
  logger.info('MCP server started successfully');
202
244
 
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
+
203
251
  // Optional HTTP transport
204
252
  let httpServerInstance;
205
253
  if (runtimeConfig.http?.enabled) {
@@ -219,16 +267,19 @@ export async function startServer(options = {}) {
219
267
  }
220
268
  }
221
269
 
222
- // Attempt to connect to Unity
223
- console.error(`[unity-mcp-server] Unity connection starting...`);
224
- try {
225
- await unityConnection.connect();
226
- console.error(`[unity-mcp-server] Unity connection established`);
227
- } catch (error) {
228
- console.error(`[unity-mcp-server] Unity connection failed: ${error.message}`);
229
- logger.error('Initial Unity connection failed:', error.message);
230
- logger.info('Unity connection will retry automatically');
231
- }
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
+ })();
232
283
 
233
284
  // Best-effort: prepare and start persistent C# LSP process (non-blocking)
234
285
  (async () => {
@@ -250,57 +301,61 @@ export async function startServer(options = {}) {
250
301
  })();
251
302
 
252
303
  // Start periodic index watcher (incremental)
253
- const watcher = new IndexWatcher(unityConnection);
254
- watcher.start();
255
- const stopWatch = () => {
256
- try {
257
- watcher.stop();
258
- } catch {}
259
- };
260
- process.on('SIGINT', stopWatch);
261
- process.on('SIGTERM', stopWatch);
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
+ })();
262
317
 
263
318
  // Auto-initialize code index if DB doesn't exist
264
- // DISABLED: Investigating Issue #168 - npx timeout with Claude Code
265
- // (async () => {
266
- // try {
267
- // const { CodeIndex } = await import('./codeIndex.js');
268
- // const index = new CodeIndex(unityConnection);
269
- // const ready = await index.isReady();
270
-
271
- // if (!ready) {
272
- // if (index.disabled) {
273
- // logger.warning(
274
- // `[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
275
- // );
276
- // return;
277
- // }
278
- // logger.info('[startup] Code index DB not ready. Starting auto-build...');
279
- // const { CodeIndexBuildToolHandler } = await import(
280
- // '../handlers/script/CodeIndexBuildToolHandler.js'
281
- // );
282
- // const builder = new CodeIndexBuildToolHandler(unityConnection);
283
- // const result = await builder.execute({});
284
-
285
- // if (result.success) {
286
- // logger.info(
287
- // `[startup] Code index auto-build started: jobId=${result.jobId}. Use code_index_status to check progress.`
288
- // );
289
- // } else {
290
- // logger.warning(`[startup] Code index auto-build failed: ${result.message}`);
291
- // }
292
- // } else {
293
- // logger.info('[startup] Code index DB already exists. Skipping auto-build.');
294
- // }
295
- // } catch (e) {
296
- // logger.warning(`[startup] Code index auto-init failed: ${e.message}`);
297
- // }
298
- // })();
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
+ })();
299
354
 
300
355
  // Handle shutdown
301
356
  process.on('SIGINT', async () => {
302
357
  logger.info('Shutting down...');
303
- unityConnection.disconnect();
358
+ if (unityConnection) unityConnection.disconnect();
304
359
  if (transport) await server.close();
305
360
  if (httpServerInstance) await httpServerInstance.close();
306
361
  process.exit(0);
@@ -308,7 +363,7 @@ export async function startServer(options = {}) {
308
363
 
309
364
  process.on('SIGTERM', async () => {
310
365
  logger.info('Shutting down...');
311
- unityConnection.disconnect();
366
+ if (unityConnection) unityConnection.disconnect();
312
367
  if (transport) await server.close();
313
368
  if (httpServerInstance) await httpServerInstance.close();
314
369
  process.exit(0);
@@ -324,14 +379,21 @@ export async function startServer(options = {}) {
324
379
  export const main = startServer;
325
380
 
326
381
  // Export for testing
327
- export async function createServer(customConfig = config) {
382
+ export async function createServer(customConfig) {
383
+ // For testing, we need to load dependencies synchronously
384
+ const { config: defaultConfig } = await import('./config.js');
385
+ const actualConfig = customConfig || defaultConfig;
386
+
387
+ const { UnityConnection } = await import('./unityConnection.js');
388
+ const { createHandlers } = await import('../handlers/index.js');
389
+
328
390
  const testUnityConnection = new UnityConnection();
329
391
  const testHandlers = createHandlers(testUnityConnection);
330
392
 
331
393
  const testServer = new Server(
332
394
  {
333
- name: customConfig.server.name,
334
- version: customConfig.server.version
395
+ name: actualConfig.server.name,
396
+ version: actualConfig.server.version
335
397
  },
336
398
  {
337
399
  capabilities: {
@@ -90,9 +90,9 @@ export class CSharpLspUtils {
90
90
  fs.copyFileSync(legacyVersion, primaryVersion);
91
91
  } catch {}
92
92
  }
93
- logger.info(`[csharp-lsp] migrated legacy binary to ${path.dirname(primary)}`);
93
+ logger.info(`[unity-mcp-server:lsp] migrated legacy binary to ${path.dirname(primary)}`);
94
94
  } catch (e) {
95
- logger.warning(`[csharp-lsp] legacy migration failed: ${e.message}`);
95
+ logger.warning(`[unity-mcp-server:lsp] legacy migration failed: ${e.message}`);
96
96
  }
97
97
  }
98
98
 
@@ -135,7 +135,7 @@ export class CSharpLspUtils {
135
135
  // バージョン取得失敗時もバイナリが存在すれば使用
136
136
  if (!desired) {
137
137
  if (fs.existsSync(p)) {
138
- logger.warning('[csharp-lsp] version not found, using existing binary');
138
+ logger.warning('[unity-mcp-server:lsp] version not found, using existing binary');
139
139
  return p;
140
140
  }
141
141
  throw new Error('mcp-server version not found; cannot resolve LSP tag');
@@ -152,7 +152,9 @@ export class CSharpLspUtils {
152
152
  return p;
153
153
  } catch (e) {
154
154
  if (fs.existsSync(p)) {
155
- logger.warning(`[csharp-lsp] download failed, using existing binary: ${e.message}`);
155
+ logger.warning(
156
+ `[unity-mcp-server:lsp] download failed, using existing binary: ${e.message}`
157
+ );
156
158
  return p;
157
159
  }
158
160
  throw e;
@@ -212,7 +214,9 @@ export class CSharpLspUtils {
212
214
  try {
213
215
  if (process.platform !== 'win32') fs.chmodSync(dest, 0o755);
214
216
  } catch {}
215
- logger.info(`[csharp-lsp] downloaded: ${path.basename(dest)} @ ${path.dirname(dest)}`);
217
+ logger.info(
218
+ `[unity-mcp-server:lsp] downloaded: ${path.basename(dest)} @ ${path.dirname(dest)}`
219
+ );
216
220
  // manifestから実際のバージョンを取得(信頼性の高いソースとして使用)
217
221
  const actualVersion = manifest.version || targetVersion;
218
222
  return actualVersion;
@@ -21,22 +21,22 @@ export class LspProcessManager {
21
21
  const rid = this.utils.detectRid();
22
22
  const bin = await this.utils.ensureLocal(rid);
23
23
  const proc = spawn(bin, { stdio: ['pipe', 'pipe', 'pipe'] });
24
- proc.on('error', e => logger.error(`[csharp-lsp] process error: ${e.message}`));
24
+ proc.on('error', e => logger.error(`[unity-mcp-server:lsp] process error: ${e.message}`));
25
25
  proc.on('close', (code, sig) => {
26
- logger.warning(`[csharp-lsp] exited code=${code} signal=${sig || ''}`);
26
+ logger.warning(`[unity-mcp-server:lsp] exited code=${code} signal=${sig || ''}`);
27
27
  if (this.state.proc === proc) {
28
28
  this.state.proc = null;
29
29
  }
30
30
  });
31
31
  proc.stderr.on('data', d => {
32
32
  const s = String(d || '').trim();
33
- if (s) logger.debug(`[csharp-lsp] ${s}`);
33
+ if (s) logger.debug(`[unity-mcp-server:lsp] ${s}`);
34
34
  });
35
35
  this.state.proc = proc;
36
- logger.info(`[csharp-lsp] started (pid=${proc.pid})`);
36
+ logger.info(`[unity-mcp-server:lsp] started (pid=${proc.pid})`);
37
37
  return proc;
38
38
  } catch (e) {
39
- logger.error(`[csharp-lsp] failed to start: ${e.message}`);
39
+ logger.error(`[unity-mcp-server:lsp] failed to start: ${e.message}`);
40
40
  throw e;
41
41
  }
42
42
  })();
@@ -76,7 +76,7 @@ export class LspRpcClient {
76
76
  try {
77
77
  this.proc.stdin.write(payload, 'utf8');
78
78
  } catch (e) {
79
- logger.error(`[csharp-lsp] writeMessage failed: ${e.message}`);
79
+ logger.error(`[unity-mcp-server:lsp] writeMessage failed: ${e.message}`);
80
80
  // Mark process as unavailable to prevent further writes
81
81
  this.proc = null;
82
82
  this.initialized = false;
@@ -161,7 +161,9 @@ export class LspRpcClient {
161
161
  this.proc = null;
162
162
  this.initialized = false;
163
163
  this.buf = Buffer.alloc(0);
164
- logger.warning(`[csharp-lsp] recoverable error on ${method}: ${msg}. Retrying once...`);
164
+ logger.warning(
165
+ `[unity-mcp-server:lsp] recoverable error on ${method}: ${msg}. Retrying once...`
166
+ );
165
167
  return await this.#requestWithRetry(method, params, attempt + 1);
166
168
  }
167
169
  // Standardize error message
@@ -1,191 +0,0 @@
1
- import process from 'node:process';
2
- import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
3
-
4
- const HEADER_END = '\r\n\r\n';
5
- const HEADER_RE = /Content-Length:\s*(\d+)/i;
6
- const DEFAULT_BUFFER = Buffer.alloc(0);
7
-
8
- function encodeContentLength(message) {
9
- const json = JSON.stringify(message);
10
- const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}${HEADER_END}`;
11
- return header + json;
12
- }
13
-
14
- function parseJson(text) {
15
- return JSONRPCMessageSchema.parse(JSON.parse(text));
16
- }
17
-
18
- export class HybridStdioServerTransport {
19
- constructor(stdin = process.stdin, stdout = process.stdout) {
20
- this._stdin = stdin;
21
- this._stdout = stdout;
22
- this._buffer = DEFAULT_BUFFER;
23
- this._started = false;
24
- this._mode = null; // 'content-length' | 'ndjson'
25
-
26
- this._onData = chunk => {
27
- this._buffer = this._buffer.length
28
- ? Buffer.concat([this._buffer, chunk])
29
- : Buffer.from(chunk);
30
- this._processBuffer();
31
- };
32
-
33
- this._onError = error => {
34
- this.onerror?.(error);
35
- };
36
- }
37
-
38
- get framingMode() {
39
- return this._mode;
40
- }
41
-
42
- async start() {
43
- if (this._started) {
44
- throw new Error('HybridStdioServerTransport already started');
45
- }
46
- this._started = true;
47
- this._stdin.on('data', this._onData);
48
- this._stdin.on('error', this._onError);
49
- }
50
-
51
- async close() {
52
- if (!this._started) return;
53
- this._stdin.off('data', this._onData);
54
- this._stdin.off('error', this._onError);
55
- this._buffer = DEFAULT_BUFFER;
56
- this._started = false;
57
- this.onclose?.();
58
- }
59
-
60
- send(message) {
61
- return new Promise(resolve => {
62
- // Always use Content-Length framing for output (MCP protocol standard)
63
- // Input remains hybrid (Content-Length or NDJSON) for client compatibility
64
- const payload = encodeContentLength(message);
65
- if (this._stdout.write(payload)) {
66
- resolve();
67
- } else {
68
- this._stdout.once('drain', resolve);
69
- }
70
- });
71
- }
72
-
73
- _processBuffer() {
74
- while (true) {
75
- const message = this._readMessage();
76
- if (message === null) {
77
- break;
78
- }
79
- this.onmessage?.(message);
80
- }
81
- }
82
-
83
- _readMessage() {
84
- if (!this._buffer || this._buffer.length === 0) {
85
- return null;
86
- }
87
-
88
- if (this._mode === 'content-length') {
89
- return this._readContentLengthMessage();
90
- }
91
- if (this._mode === 'ndjson') {
92
- return this._readNdjsonMessage();
93
- }
94
-
95
- const prefix = this._peekPrefix();
96
- if (!prefix.length) {
97
- return null;
98
- }
99
-
100
- if ('content-length:'.startsWith(prefix.toLowerCase())) {
101
- return null; // Wait for full header keyword before deciding
102
- }
103
-
104
- if (prefix.toLowerCase().startsWith('content-length:')) {
105
- this._mode = 'content-length';
106
- return this._readContentLengthMessage();
107
- }
108
-
109
- const newlineIndex = this._buffer.indexOf(0x0a); // '\n'
110
- if (newlineIndex === -1) {
111
- return null;
112
- }
113
-
114
- this._mode = 'ndjson';
115
- return this._readNdjsonMessage();
116
- }
117
-
118
- _peekPrefix() {
119
- const length = Math.min(this._buffer.length, 32);
120
- return this._buffer.toString('utf8', 0, length).trimStart();
121
- }
122
-
123
- _readContentLengthMessage() {
124
- const { headerEndIndex, separatorLength } = (() => {
125
- const crlfIndex = this._buffer.indexOf('\r\n\r\n');
126
- const lfIndex = this._buffer.indexOf('\n\n');
127
-
128
- if (crlfIndex === -1 && lfIndex === -1) return { headerEndIndex: -1, separatorLength: 0 };
129
- if (crlfIndex === -1) return { headerEndIndex: lfIndex, separatorLength: 2 };
130
- if (lfIndex === -1) return { headerEndIndex: crlfIndex, separatorLength: 4 };
131
-
132
- return crlfIndex < lfIndex
133
- ? { headerEndIndex: crlfIndex, separatorLength: 4 }
134
- : { headerEndIndex: lfIndex, separatorLength: 2 };
135
- })();
136
-
137
- if (headerEndIndex === -1) {
138
- return null;
139
- }
140
-
141
- const header = this._buffer.toString('utf8', 0, headerEndIndex);
142
- const match = header.match(HEADER_RE);
143
- if (!match) {
144
- this._buffer = this._buffer.subarray(headerEndIndex + separatorLength);
145
- this.onerror?.(new Error('Invalid Content-Length header'));
146
- return null;
147
- }
148
-
149
- const length = Number(match[1]);
150
- const totalMessageLength = headerEndIndex + separatorLength + length;
151
- if (this._buffer.length < totalMessageLength) {
152
- return null;
153
- }
154
-
155
- const json = this._buffer.toString(
156
- 'utf8',
157
- headerEndIndex + separatorLength,
158
- totalMessageLength
159
- );
160
- this._buffer = this._buffer.subarray(totalMessageLength);
161
-
162
- try {
163
- return parseJson(json);
164
- } catch (error) {
165
- this.onerror?.(error);
166
- return null;
167
- }
168
- }
169
-
170
- _readNdjsonMessage() {
171
- while (true) {
172
- const newlineIndex = this._buffer.indexOf(0x0a);
173
- if (newlineIndex === -1) {
174
- return null;
175
- }
176
-
177
- let line = this._buffer.toString('utf8', 0, newlineIndex);
178
- this._buffer = this._buffer.subarray(newlineIndex + 1);
179
- line = line.trim();
180
- if (!line) {
181
- continue;
182
- }
183
-
184
- try {
185
- return parseJson(line);
186
- } catch (error) {
187
- this.onerror?.(error);
188
- }
189
- }
190
- }
191
- }