xcodemcp
Version:
Model Context Protocol server for Xcode build automation and log parsing
597 lines • 25.8 kB
JavaScript
import { spawn } from 'child_process';
import { Logger } from '../utils/Logger.js';
import { EventEmitter } from 'events';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export class McpLibrary extends EventEmitter {
initialized = false;
cliPath;
constructor() {
super();
// Path to the CLI executable
this.cliPath = join(__dirname, '../cli.js');
}
/**
* Initialize the MCP library if not already initialized
*/
async initialize() {
if (this.initialized) {
return;
}
try {
// Test CLI is available by calling help
await this.spawnCli(['--help']);
this.initialized = true;
Logger.debug('MCP library initialized successfully');
}
catch (error) {
Logger.error('Failed to initialize MCP library:', error);
throw error;
}
}
/**
* Get all available tools with their schemas
*/
async getTools() {
await this.initialize();
try {
// Get tools from CLI subprocess
const result = await this.spawnCli(['--json', 'list-tools']);
if (result.exitCode !== 0) {
throw new Error(`Failed to get tools: ${result.stderr || result.stdout}`);
}
// Parse and return tool definitions
const toolsData = JSON.parse(result.stdout);
if (Array.isArray(toolsData)) {
return toolsData.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}));
}
// Fallback to hardcoded tools if CLI doesn't return expected format
Logger.warn('CLI returned unexpected format, using fallback tool definitions');
}
catch (error) {
Logger.error('Failed to get tools from CLI, using fallback:', error);
}
// Fallback to hardcoded tool definitions
return [
{
name: 'xcode_open_project',
description: 'Open an Xcode project or workspace',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_close_project',
description: 'Close the currently active Xcode project or workspace (automatically stops any running actions first)',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_build',
description: 'Build a specific Xcode project or workspace with the specified scheme. If destination is not provided, uses the currently active destination.',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file to build (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
scheme: {
type: 'string',
description: 'Name of the scheme to build',
},
destination: {
type: 'string',
description: 'Build destination (optional - uses active destination if not provided)',
},
},
required: ['xcodeproj', 'scheme'],
},
},
{
name: 'xcode_get_schemes',
description: 'Get list of available schemes for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_set_active_scheme',
description: 'Set the active scheme for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
schemeName: {
type: 'string',
description: 'Name of the scheme to activate',
},
},
required: ['xcodeproj', 'schemeName'],
},
},
{
name: 'xcode_clean',
description: 'Clean the build directory for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_test',
description: 'Run tests for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
commandLineArguments: {
type: 'array',
items: { type: 'string' },
description: 'Additional command line arguments',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_run',
description: 'Run a specific project with the specified scheme',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
scheme: {
type: 'string',
description: 'Name of the scheme to run',
},
commandLineArguments: {
type: 'array',
items: { type: 'string' },
description: 'Additional command line arguments',
},
},
required: ['xcodeproj', 'scheme'],
},
},
{
name: 'xcode_debug',
description: 'Start debugging session for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
scheme: {
type: 'string',
description: 'Scheme name (optional)',
},
skipBuilding: {
type: 'boolean',
description: 'Whether to skip building',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_stop',
description: 'Stop the current scheme action',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'find_xcresults',
description: 'Find all XCResult files for a specific project with timestamps and file information',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_get_run_destinations',
description: 'Get list of available run destinations for a specific project',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_get_workspace_info',
description: 'Get information about a specific workspace',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_get_projects',
description: 'Get list of projects in a specific workspace',
inputSchema: {
type: 'object',
properties: {
xcodeproj: {
type: 'string',
description: 'Absolute path to the .xcodeproj file (or .xcworkspace if available) - e.g., /path/to/project.xcodeproj',
},
},
required: ['xcodeproj'],
},
},
{
name: 'xcode_open_file',
description: 'Open a file in Xcode',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Absolute path to the file to open',
},
lineNumber: {
type: 'number',
description: 'Optional line number to navigate to',
},
},
required: ['filePath'],
},
},
{
name: 'xcode_health_check',
description: 'Perform a comprehensive health check of the XcodeMCP environment and configuration',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'xcresult_browse',
description: 'Browse XCResult files - list all tests or show details for a specific test. Returns comprehensive test results including pass/fail status, failure details, and browsing instructions. Large console output (>20 lines or >2KB) is automatically saved to a temporary file.',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Optional test ID or index number to show details for a specific test',
},
include_console: {
type: 'boolean',
description: 'Whether to include console output and test activities (only used with test_id)',
default: false,
},
},
required: ['xcresult_path'],
},
},
{
name: 'xcresult_browser_get_console',
description: 'Get console output and test activities for a specific test in an XCResult file. Large output (>20 lines or >2KB) is automatically saved to a temporary file.',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Test ID or index number to get console output for',
},
},
required: ['xcresult_path', 'test_id'],
},
},
{
name: 'xcresult_summary',
description: 'Get a quick summary of test results from an XCResult file',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
},
required: ['xcresult_path'],
},
},
{
name: 'xcresult_get_screenshot',
description: 'Get screenshot from a failed test at specific timestamp - extracts frame from video attachment using ffmpeg',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Test ID or index number to get screenshot for',
},
timestamp: {
type: 'number',
description: 'Timestamp in seconds when to extract the screenshot. WARNING: Use a timestamp BEFORE the failure (e.g., if failure is at 30.71s, use 30.69s) as failure timestamps often show the home screen after the app has crashed or reset.',
},
},
required: ['xcresult_path', 'test_id', 'timestamp'],
},
},
{
name: 'xcresult_get_ui_hierarchy',
description: 'Get UI hierarchy attachment from test. Returns raw accessibility tree (best for AI), slim AI-readable JSON (default), or full JSON.',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Test ID or index number to get UI hierarchy for',
},
timestamp: {
type: 'number',
description: 'Optional timestamp in seconds to find the closest UI snapshot. If not provided, uses the first available UI snapshot.',
},
full_hierarchy: {
type: 'boolean',
description: 'Set to true to get the full hierarchy (several MB). Default is false for AI-readable slim version.',
},
raw_format: {
type: 'boolean',
description: 'Set to true to get the raw accessibility tree text (most AI-friendly). Default is false for JSON format.',
},
},
required: ['xcresult_path', 'test_id'],
},
},
{
name: 'xcresult_get_ui_element',
description: 'Get full details of a specific UI element by index from a previously exported UI hierarchy JSON file',
inputSchema: {
type: 'object',
properties: {
hierarchy_json_path: {
type: 'string',
description: 'Absolute path to the UI hierarchy JSON file (the full version saved by xcresult_get_ui_hierarchy)',
},
element_index: {
type: 'number',
description: 'Index of the element to get details for (the "j" value from the slim hierarchy)',
},
include_children: {
type: 'boolean',
description: 'Whether to include children in the response. Defaults to false.',
},
},
required: ['hierarchy_json_path', 'element_index'],
},
},
{
name: 'xcresult_list_attachments',
description: 'List all attachments for a specific test - shows attachment names, types, and indices for export',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Test ID or index number to list attachments for',
},
},
required: ['xcresult_path', 'test_id'],
},
},
{
name: 'xcresult_export_attachment',
description: 'Export a specific attachment by index - can convert App UI hierarchy attachments to JSON',
inputSchema: {
type: 'object',
properties: {
xcresult_path: {
type: 'string',
description: 'Absolute path to the .xcresult file',
},
test_id: {
type: 'string',
description: 'Test ID or index number that contains the attachment',
},
attachment_index: {
type: 'number',
description: 'Index number of the attachment to export (1-based, from xcresult_list_attachments)',
},
convert_to_json: {
type: 'boolean',
description: 'If true and attachment is an App UI hierarchy, convert to JSON format',
},
},
required: ['xcresult_path', 'test_id', 'attachment_index'],
},
},
];
}
/**
* Spawn CLI subprocess and execute command
*/
async spawnCli(args) {
return new Promise((resolve, reject) => {
const child = spawn('node', [this.cliPath, ...args], {
stdio: ['inherit', 'pipe', 'pipe'],
env: process.env
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ stdout, stderr, exitCode: code || 0 });
});
child.on('error', (error) => {
reject(error);
});
});
}
/**
* Call a tool with the given name and arguments
* This spawns the CLI subprocess to execute the tool
*/
async callTool(name, args = {}, options = {}) {
await this.initialize();
try {
Logger.debug(`Calling tool: ${name} with args:`, args);
// Convert tool name to CLI command name
const commandName = name.replace(/^xcode_/, '').replace(/_/g, '-');
// Build CLI arguments
const cliArgs = ['--json', commandName, '--json-input', JSON.stringify(args)];
// Execute CLI subprocess
const result = await this.spawnCli(cliArgs);
// Parse events from stderr if callback provided
if (options.onEvent && result.stderr) {
const lines = result.stderr.split('\n');
for (const line of lines) {
if (line.startsWith('event:')) {
const eventType = line.replace('event:', '');
const nextLine = lines[lines.indexOf(line) + 1];
if (nextLine?.startsWith('data:')) {
try {
const eventData = JSON.parse(nextLine.replace('data:', ''));
options.onEvent({ type: eventType, data: eventData });
}
catch (parseError) {
Logger.debug('Failed to parse SSE event:', parseError);
}
}
}
}
}
// Handle CLI exit code
if (result.exitCode !== 0) {
throw new Error(`CLI command failed with exit code ${result.exitCode}: ${result.stderr || result.stdout}`);
}
// Parse JSON output from CLI
try {
const parsedResult = JSON.parse(result.stdout);
Logger.debug(`Tool ${name} completed successfully`);
return parsedResult;
}
catch (parseError) {
// If JSON parsing fails, wrap stdout in text content
Logger.debug(`Tool ${name} completed with non-JSON output`);
return { content: [{ type: 'text', text: result.stdout }] };
}
}
catch (error) {
Logger.error(`Tool ${name} failed:`, error);
throw error;
}
}
/**
* Get CLI path (for advanced use cases)
*/
getCliPath() {
return this.cliPath;
}
}
// Export convenience functions for direct usage
let defaultLibrary = null;
/**
* Get or create the default MCP library instance
*/
export function getDefaultLibrary() {
if (!defaultLibrary) {
defaultLibrary = new McpLibrary();
}
return defaultLibrary;
}
/**
* Call a tool using the default library instance
*/
export async function callTool(name, args = {}, options = {}) {
const library = getDefaultLibrary();
return library.callTool(name, args, options);
}
/**
* Get all available tools using the default library instance
*/
export async function getTools() {
const library = getDefaultLibrary();
return library.getTools();
}
//# sourceMappingURL=index.js.map