@akiojin/unity-mcp-server 2.25.0 → 2.26.1

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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/core/codeIndexDb.js +26 -10
  3. package/src/core/config.js +242 -242
  4. package/src/core/projectInfo.js +19 -10
  5. package/src/core/server.js +88 -65
  6. package/src/core/transports/HybridStdioServerTransport.js +179 -0
  7. package/src/core/unityConnection.js +52 -45
  8. package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +59 -49
  9. package/src/handlers/addressables/AddressablesBuildToolHandler.js +63 -62
  10. package/src/handlers/addressables/AddressablesManageToolHandler.js +84 -78
  11. package/src/handlers/base/BaseToolHandler.js +5 -5
  12. package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -419
  13. package/src/handlers/console/ConsoleReadToolHandler.js +56 -66
  14. package/src/handlers/editor/EditorSelectionManageToolHandler.js +10 -9
  15. package/src/handlers/gameobject/GameObjectModifyToolHandler.js +22 -11
  16. package/src/handlers/index.js +437 -437
  17. package/src/handlers/menu/MenuItemExecuteToolHandler.js +75 -37
  18. package/src/handlers/screenshot/ScreenshotAnalyzeToolHandler.js +12 -10
  19. package/src/handlers/script/ScriptEditStructuredToolHandler.js +162 -154
  20. package/src/handlers/script/ScriptReadToolHandler.js +80 -85
  21. package/src/handlers/script/ScriptRefsFindToolHandler.js +123 -123
  22. package/src/handlers/script/ScriptSymbolFindToolHandler.js +125 -112
  23. package/src/handlers/system/SystemGetCommandStatsToolHandler.js +1 -1
  24. package/src/handlers/system/SystemRefreshAssetsToolHandler.js +10 -14
  25. package/src/handlers/video/VideoCaptureStartToolHandler.js +15 -5
  26. package/src/handlers/video/VideoCaptureStatusToolHandler.js +5 -9
  27. package/src/handlers/video/VideoCaptureStopToolHandler.js +8 -9
  28. package/src/lsp/LspProcessManager.js +26 -9
  29. package/src/tools/video/recordFor.js +13 -7
  30. package/src/tools/video/recordPlayMode.js +7 -6
  31. package/src/utils/csharpParse.js +14 -8
@@ -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
+ }
@@ -0,0 +1,179 @@
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 encodeNdjson(message) {
15
+ return `${JSON.stringify(message)}\n`
16
+ }
17
+
18
+ function parseJson(text) {
19
+ return JSONRPCMessageSchema.parse(JSON.parse(text))
20
+ }
21
+
22
+ export class HybridStdioServerTransport {
23
+ constructor(stdin = process.stdin, stdout = process.stdout) {
24
+ this._stdin = stdin
25
+ this._stdout = stdout
26
+ this._buffer = DEFAULT_BUFFER
27
+ this._started = false
28
+ this._mode = null // 'content-length' | 'ndjson'
29
+
30
+ this._onData = chunk => {
31
+ this._buffer = this._buffer.length ? Buffer.concat([this._buffer, chunk]) : Buffer.from(chunk)
32
+ this._processBuffer()
33
+ }
34
+
35
+ this._onError = error => {
36
+ this.onerror?.(error)
37
+ }
38
+ }
39
+
40
+ get framingMode() {
41
+ return this._mode
42
+ }
43
+
44
+ async start() {
45
+ if (this._started) {
46
+ throw new Error('HybridStdioServerTransport already started')
47
+ }
48
+ this._started = true
49
+ this._stdin.on('data', this._onData)
50
+ this._stdin.on('error', this._onError)
51
+ }
52
+
53
+ async close() {
54
+ if (!this._started) return
55
+ this._stdin.off('data', this._onData)
56
+ this._stdin.off('error', this._onError)
57
+ this._buffer = DEFAULT_BUFFER
58
+ this._started = false
59
+ this.onclose?.()
60
+ }
61
+
62
+ send(message) {
63
+ return new Promise(resolve => {
64
+ const payload = this._mode === 'ndjson' ? encodeNdjson(message) : 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 = this._buffer.indexOf(HEADER_END)
125
+ if (headerEndIndex === -1) {
126
+ return null
127
+ }
128
+
129
+ const header = this._buffer.toString('utf8', 0, headerEndIndex)
130
+ const match = header.match(HEADER_RE)
131
+ if (!match) {
132
+ this._buffer = this._buffer.subarray(headerEndIndex + HEADER_END.length)
133
+ this.onerror?.(new Error('Invalid Content-Length header'))
134
+ return null
135
+ }
136
+
137
+ const length = Number(match[1])
138
+ const totalMessageLength = headerEndIndex + HEADER_END.length + length
139
+ if (this._buffer.length < totalMessageLength) {
140
+ return null
141
+ }
142
+
143
+ const json = this._buffer.toString(
144
+ 'utf8',
145
+ headerEndIndex + HEADER_END.length,
146
+ totalMessageLength
147
+ )
148
+ this._buffer = this._buffer.subarray(totalMessageLength)
149
+
150
+ try {
151
+ return parseJson(json)
152
+ } catch (error) {
153
+ this.onerror?.(error)
154
+ return null
155
+ }
156
+ }
157
+
158
+ _readNdjsonMessage() {
159
+ while (true) {
160
+ const newlineIndex = this._buffer.indexOf(0x0a)
161
+ if (newlineIndex === -1) {
162
+ return null
163
+ }
164
+
165
+ let line = this._buffer.toString('utf8', 0, newlineIndex)
166
+ this._buffer = this._buffer.subarray(newlineIndex + 1)
167
+ line = line.trim()
168
+ if (!line) {
169
+ continue
170
+ }
171
+
172
+ try {
173
+ return parseJson(line)
174
+ } catch (error) {
175
+ this.onerror?.(error)
176
+ }
177
+ }
178
+ }
179
+ }