@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
package/src/core/server.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
172
|
-
});
|
|
55
|
+
unityConnection.on('disconnected', () => {
|
|
56
|
+
logger.info('Unity connection lost');
|
|
57
|
+
});
|
|
173
58
|
|
|
174
|
-
unityConnection.on('error', error => {
|
|
175
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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:
|
|
334
|
-
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(`[
|
|
93
|
+
logger.info(`[unity-mcp-server:lsp] migrated legacy binary to ${path.dirname(primary)}`);
|
|
94
94
|
} catch (e) {
|
|
95
|
-
logger.warning(`[
|
|
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('[
|
|
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(
|
|
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(
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
33
|
+
if (s) logger.debug(`[unity-mcp-server:lsp] ${s}`);
|
|
34
34
|
});
|
|
35
35
|
this.state.proc = proc;
|
|
36
|
-
logger.info(`[
|
|
36
|
+
logger.info(`[unity-mcp-server:lsp] started (pid=${proc.pid})`);
|
|
37
37
|
return proc;
|
|
38
38
|
} catch (e) {
|
|
39
|
-
logger.error(`[
|
|
39
|
+
logger.error(`[unity-mcp-server:lsp] failed to start: ${e.message}`);
|
|
40
40
|
throw e;
|
|
41
41
|
}
|
|
42
42
|
})();
|
package/src/lsp/LspRpcClient.js
CHANGED
|
@@ -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(`[
|
|
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(
|
|
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
|
-
}
|