forked from getsentry/XcodeBuildMCP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiagnostic.ts
More file actions
381 lines (330 loc) · 12.5 KB
/
diagnostic.ts
File metadata and controls
381 lines (330 loc) · 12.5 KB
1
2
3
4
5
6
7
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/**
* Diagnostic Tool - Provides comprehensive information about the MCP server environment
*
* This module provides a diagnostic tool that reports on the server environment,
* available dependencies, and configuration status. It's only registered when
* the XCODEBUILDMCP_DEBUG environment variable is set.
*
* Responsibilities:
* - Reporting on Node.js and system environment
* - Checking for required dependencies (xcodebuild, idb, etc.)
* - Reporting on environment variables that affect server behavior
* - Providing detailed information for debugging and troubleshooting
*/
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ToolResponse } from '../types/common.js';
import { log } from '../utils/logger.js';
import { execSync } from 'child_process';
import { version } from '../version.js';
import { areIdbToolsAvailable } from '../utils/idb-setup.js';
import * as os from 'os';
import { ToolGroup, isSelectiveToolsEnabled, listEnabledGroups } from '../utils/tool-groups.js';
// Constants
const LOG_PREFIX = '[Diagnostic]';
/**
* Check if a binary is available in the PATH and attempt to get its version
* @param binary The binary name to check
* @returns Object with availability status and optional version string
*/
export function checkBinaryAvailability(binary: string): { available: boolean; version?: string } {
// First check if the binary exists at all
try {
execSync(`which ${binary}`, { stdio: 'ignore' });
} catch {
// Binary not found in PATH
return { available: false };
}
// Binary exists, now try to get version info if possible
let version: string | undefined;
// Define version commands for specific binaries
const versionCommands: Record<string, string> = {
idb_companion: 'idb_companion --version',
python3: 'python3 --version',
python: 'python --version',
pip3: 'pip3 --version',
pip: 'pip --version',
mise: 'mise --version',
};
// Try to get version using binary-specific commands
if (binary in versionCommands) {
try {
const output = execSync(versionCommands[binary], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
if (output) {
// For xcodebuild, include both version and build info
if (binary === 'xcodebuild') {
const lines = output.split('\n').slice(0, 2);
version = lines.join(' - ');
} else {
version = output;
}
}
} catch {
// Command failed, continue to generic attempts
}
}
// We only care about the specific binaries we've defined
return {
available: true,
version: version || 'Available (version info not available)',
};
}
/**
* Get information about the Xcode installation
*/
export function getXcodeInfo():
| { version: string; path: string; selectedXcode: string; xcrunVersion: string }
| { error: string } {
try {
// Get Xcode version info
const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim();
const version = xcodebuildOutput.split('\n').slice(0, 2).join(' - ');
// Get Xcode selection info
const path = execSync('xcode-select -p', { encoding: 'utf8' }).trim();
const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8' }).trim();
// Get xcrun version info
const xcrunVersion = execSync('xcrun --version', { encoding: 'utf8' }).trim();
return { version, path, selectedXcode, xcrunVersion };
} catch (error) {
return { error: error instanceof Error ? error.message : String(error) };
}
}
/**
* Get information about the environment variables
*/
export function getEnvironmentVariables(): Record<string, string | undefined> {
const relevantVars = [
'XCODEBUILDMCP_DEBUG',
'XCODEMAKE_ENABLED',
'XCODEBUILDMCP_RUNNING_UNDER_MISE',
'PATH',
'DEVELOPER_DIR',
'HOME',
'USER',
'TMPDIR',
'PYTHONPATH',
'NODE_ENV',
'SENTRY_DISABLED',
];
const envVars: Record<string, string | undefined> = {};
// Add standard environment variables
for (const varName of relevantVars) {
envVars[varName] = process.env[varName];
}
// Add all tool and group environment variables for debugging
Object.keys(process.env).forEach((key) => {
if (
key.startsWith('XCODEBUILDMCP_TOOL_') ||
key.startsWith('XCODEBUILDMCP_GROUP_') ||
key.startsWith('XCODEBUILDMCP_')
) {
envVars[key] = process.env[key];
}
});
return envVars;
}
/**
* Get system information
*/
function getSystemInfo(): Record<string, string> {
return {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
cpus: `${os.cpus().length} x ${os.cpus()[0]?.model || 'Unknown'}`,
memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
hostname: os.hostname(),
username: os.userInfo().username,
homedir: os.homedir(),
tmpdir: os.tmpdir(),
};
}
/**
* Get Node.js information
*/
function getNodeInfo(): Record<string, string> {
return {
version: process.version,
execPath: process.execPath,
pid: process.pid.toString(),
ppid: process.ppid.toString(),
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
argv: process.argv.join(' '),
};
}
/**
* Get information about tool groups and their status
*/
export function getToolGroupsInfo(): Record<string, unknown> {
const selectiveMode = isSelectiveToolsEnabled();
const enabledGroups = listEnabledGroups();
const toolGroups: Record<string, { enabled: boolean; envVar: string }> = {};
// Add information about each tool group
for (const group of Object.values(ToolGroup)) {
const isEnabled = process.env[group] === 'true';
toolGroups[group] = {
enabled: isEnabled,
envVar: group,
};
}
return {
selectiveMode,
enabledGroups,
groups: toolGroups,
};
}
/**
* Get a list of individually enabled tools via environment variables
*/
function getIndividuallyEnabledTools(): string[] {
return Object.keys(process.env)
.filter((key) => key.startsWith('XCODEBUILDMCP_TOOL_') && process.env[key] === 'true')
.map((key) => key.replace('XCODEBUILDMCP_TOOL_', ''));
}
/**
* Run the diagnostic tool and return the results
* @returns Promise resolving to ToolResponse with diagnostic information
*/
export async function runDiagnosticTool(): Promise<ToolResponse> {
log('info', `${LOG_PREFIX}: Running diagnostic tool`);
// Check for required binaries
const requiredBinaries = ['idb', 'idb_companion', 'python3', 'pip3', 'xcodemake', 'mise'];
const binaryStatus: Record<string, { available: boolean; version?: string }> = {};
for (const binary of requiredBinaries) {
binaryStatus[binary] = checkBinaryAvailability(binary);
}
// Get Xcode information
const xcodeInfo = getXcodeInfo();
// Get environment variables
const envVars = getEnvironmentVariables();
// Get system information
const systemInfo = getSystemInfo();
// Get Node.js information
const nodeInfo = getNodeInfo();
// Check for idb tools availability
const idbAvailable = areIdbToolsAvailable();
// Get tool groups information
const toolGroupsInfo = getToolGroupsInfo();
// Get individually enabled tools
const individuallyEnabledTools = getIndividuallyEnabledTools();
// Compile the diagnostic information
const diagnosticInfo = {
serverVersion: version,
timestamp: new Date().toISOString(),
system: systemInfo,
node: nodeInfo,
xcode: xcodeInfo,
dependencies: binaryStatus,
environmentVariables: envVars,
features: {
idb: {
available: idbAvailable,
uiAutomationSupported:
idbAvailable && binaryStatus['idb'].available && binaryStatus['idb_companion'].available,
},
mise: {
running_under_mise: Boolean(process.env.XCODEBUILDMCP_RUNNING_UNDER_MISE),
available: binaryStatus['mise'].available,
},
},
toolGroups: toolGroupsInfo as {
selectiveMode: boolean;
enabledGroups: string[];
groups: Record<string, { enabled: boolean; envVar: string }>;
},
individuallyEnabledTools,
};
// Format the diagnostic information as a nicely formatted text response
const formattedOutput = [
`# XcodeBuildMCP Diagnostic Report`,
`\nGenerated: ${diagnosticInfo.timestamp}`,
`Server Version: ${diagnosticInfo.serverVersion}`,
`\n## System Information`,
...Object.entries(diagnosticInfo.system).map(([key, value]) => `- ${key}: ${value}`),
`\n## Node.js Information`,
...Object.entries(diagnosticInfo.node).map(([key, value]) => `- ${key}: ${value}`),
`\n## Xcode Information`,
...('error' in diagnosticInfo.xcode
? [`- Error: ${diagnosticInfo.xcode.error}`]
: Object.entries(diagnosticInfo.xcode).map(([key, value]) => `- ${key}: ${value}`)),
`\n## Dependencies`,
...Object.entries(diagnosticInfo.dependencies).map(
([binary, status]) =>
`- ${binary}: ${status.available ? `✅ ${status.version || 'Available'}` : '❌ Not found'}`,
),
`\n## Environment Variables`,
...Object.entries(diagnosticInfo.environmentVariables)
.filter(([key]) => key !== 'PATH' && key !== 'PYTHONPATH') // These are too long, handle separately
.map(([key, value]) => `- ${key}: ${value || '(not set)'}`),
`\n### PATH`,
`\`\`\``,
`${diagnosticInfo.environmentVariables.PATH || '(not set)'}`.split(':').join('\n'),
`\`\`\``,
`\n## Feature Status`,
`\n### UI Automation (idb)`,
`- Available: ${diagnosticInfo.features.idb.available ? '✅ Yes' : '❌ No'}`,
`- UI Automation Supported: ${diagnosticInfo.features.idb.uiAutomationSupported ? '✅ Yes' : '❌ No'}`,
`\n### Mise Integration`,
`- Running under mise: ${diagnosticInfo.features.mise.running_under_mise ? '✅ Yes' : '❌ No'}`,
`- Mise available: ${diagnosticInfo.features.mise.available ? '✅ Yes' : '❌ No'}`,
`\n### Tool Groups Status`,
...(diagnosticInfo.toolGroups.selectiveMode
? Object.entries(diagnosticInfo.toolGroups.groups).map(([group, info]) => {
// Extract the group name without the prefix for display purposes
const displayName = group.replace('XCODEBUILDMCP_GROUP_', '');
return `- ${displayName}: ${info.enabled ? '✅ Enabled' : '❌ Disabled'} (Set with ${info.envVar}=true)`;
})
: ['- All tool groups are enabled (selective mode is disabled).']),
`\n### Individually Enabled Tools`,
...(diagnosticInfo.toolGroups.selectiveMode
? diagnosticInfo.individuallyEnabledTools.length > 0
? diagnosticInfo.individuallyEnabledTools.map(
(tool) => `- ${tool}: ✅ Enabled (via XCODEBUILDMCP_TOOL_${tool}=true)`,
)
: ['- No tools are individually enabled via environment variables.']
: ['- All tools are enabled (selective mode is disabled).']),
`\n## Tool Availability Summary`,
`- Build Tools: ${!('error' in diagnosticInfo.xcode) ? '\u2705 Available' : '\u274c Not available'}`,
`- UI Automation Tools: ${diagnosticInfo.features.idb.uiAutomationSupported ? '\u2705 Available' : '\u274c Not available'}`,
`\n## Sentry`,
`- Sentry enabled: ${diagnosticInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
`\n## Troubleshooting Tips`,
`- If UI automation tools are not available, install idb: \`pip3 install fb-idb\``,
`- For mise integration, follow instructions in the README.md file`,
`- To enable specific tool groups, set the appropriate environment variables (e.g., \`export XCODEBUILDMCP_GROUP_DISCOVERY=true\`)`,
`- If you're having issues with environment variables, make sure to use the correct prefix:`,
` - Use \`XCODEBUILDMCP_GROUP_NAME=true\` to enable a tool group`,
` - Use \`XCODEBUILDMCP_TOOL_NAME=true\` to enable an individual tool`,
` - Common mistake: Using \`XCODEBUILDMCP_BUILD_IOS_SIM=true\` instead of \`XCODEBUILDMCP_GROUP_BUILD_IOS_SIM=true\``,
].join('\n');
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
/**
* Registers the diagnostic tool with the dispatcher.
* This tool is only registered when the XCODEBUILDMCP_DEBUG environment variable is set.
* @param server The McpServer instance.
*/
export function registerDiagnosticTool(server: McpServer): void {
server.tool(
'diagnostic',
'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.',
{
enabled: z.boolean().optional().describe('Optional: dummy parameter to satisfy MCP protocol'),
},
async (): Promise<ToolResponse> => {
return runDiagnosticTool();
},
);
}