Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,15 @@ XcodeBuildMCP uses a **workflow-based architecture** with tools organized into
- `touch` - Perform touch down/up events at specific coordinates
- `type_text` - Type text (supports US keyboard characters)

#### 11. Simulator Environment Configuration (`simulator-environment`)
**Purpose**: Simulator environment and configuration management (5 tools)
- `reset_network_condition` - Resets network conditions to default in the simulator
#### 11. Simulator Management (`simulator-management`)
**Purpose**: Manage simulators and their environment (7 tools)
- `boot_sim` - Boots an iOS simulator using its UUID
- `list_sims` - Lists available iOS simulators with their UUIDs
- `open_sim` - Opens the iOS Simulator app
- `reset_simulator_location` - Resets the simulator's location to default
- `set_network_condition` - Simulates different network conditions in the simulator
- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator
- `set_simulator_location` - Sets a custom GPS location for the simulator
- `sim_statusbar` - Sets the data network indicator and status bar overrides in the iOS simulator

#### 12. Logging & Monitoring (`logging`)
**Purpose**: Log capture and monitoring across platforms (4 tools)
Expand Down
110 changes: 55 additions & 55 deletions scripts/tools-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,14 @@ if (command === 'help' || command === 'h') {
*/
async function executeReloaderoo(reloaderooArgs) {
const buildPath = path.resolve(__dirname, '..', 'build', 'index.js');

if (!fs.existsSync(buildPath)) {
throw new Error('Build not found. Please run "npm run build" first.');
}

const tempFile = `/tmp/reloaderoo-output-${Date.now()}.json`;
const command = `npx reloaderoo@latest inspect ${reloaderooArgs.join(' ')} -- node "${buildPath}"`;
const command = `npx -y reloaderoo@latest inspect ${reloaderooArgs.join(' ')} -- node "${buildPath}"`;

return new Promise((resolve, reject) => {
const child = spawn('bash', ['-c', `${command} > "${tempFile}"`], {
stdio: 'inherit'
Expand All @@ -225,22 +225,22 @@ async function executeReloaderoo(reloaderooArgs) {
}

const content = fs.readFileSync(tempFile, 'utf8');

// Remove stderr log lines and find JSON
const lines = content.split('\n');
const cleanLines = [];

for (const line of lines) {
if (line.match(/^\[\d{4}-\d{2}-\d{2}T/) || line.includes('[INFO]') || line.includes('[DEBUG]') || line.includes('[ERROR]')) {
continue;
}

const trimmed = line.trim();
if (trimmed) {
cleanLines.push(line);
}
}

// Find JSON start
let jsonStartIndex = -1;
for (let i = 0; i < cleanLines.length; i++) {
Expand All @@ -249,12 +249,12 @@ async function executeReloaderoo(reloaderooArgs) {
break;
}
}

if (jsonStartIndex === -1) {
reject(new Error(`No JSON response found in output.\nOutput: ${content.substring(0, 500)}...`));
return;
}

const jsonText = cleanLines.slice(jsonStartIndex).join('\n');
const response = JSON.parse(jsonText);
resolve(response);
Expand Down Expand Up @@ -282,21 +282,21 @@ async function getRuntimeInfo() {
try {
const toolsResponse = await executeReloaderoo(['list-tools']);
const resourcesResponse = await executeReloaderoo(['list-resources']);

let tools = [];
let toolCount = 0;

if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) {
toolCount = toolsResponse.tools.length;
tools = toolsResponse.tools.map(tool => ({
tools = toolsResponse.tools.map(tool => ({
name: tool.name,
description: tool.description
}));
}

let resources = [];
let resourceCount = 0;

if (resourcesResponse.resources && Array.isArray(resourcesResponse.resources)) {
resourceCount = resourcesResponse.resources.length;
resources = resourcesResponse.resources.map(resource => ({
Expand All @@ -305,7 +305,7 @@ async function getRuntimeInfo() {
description: resource.title || resource.description || 'No description available'
}));
}

return {
tools,
resources,
Expand All @@ -328,11 +328,11 @@ function isReExportFile(filePath) {
const lines = content.split('\n').map(line => line.trim());

const codeLines = lines.filter(line => {
return line.length > 0 &&
!line.startsWith('//') &&
!line.startsWith('/*') &&
!line.startsWith('*') &&
line !== '*/';
return line.length > 0 &&
!line.startsWith('//') &&
!line.startsWith('/*') &&
!line.startsWith('*') &&
line !== '*/';
});

if (codeLines.length === 0) {
Expand All @@ -352,7 +352,7 @@ function isReExportFile(filePath) {
function getWorkflowDirectories() {
const workflowDirs = [];
const entries = fs.readdirSync(toolsDir, { withFileTypes: true });

for (const entry of entries) {
if (entry.isDirectory()) {
const indexPath = path.join(toolsDir, entry.name, 'index.ts');
Expand All @@ -361,7 +361,7 @@ function getWorkflowDirectories() {
}
}
}

return workflowDirs;
}

Expand All @@ -372,7 +372,7 @@ async function getStaticInfo() {
try {
// Get workflow directories
const workflowDirs = getWorkflowDirectories();

// Find all tool files
const files = await glob('**/*.ts', {
cwd: toolsDir,
Expand All @@ -387,23 +387,23 @@ async function getStaticInfo() {
for (const file of files) {
const toolName = path.basename(file, '.ts');
const workflowDir = path.basename(path.dirname(file));

if (!toolsByWorkflow.has(workflowDir)) {
toolsByWorkflow.set(workflowDir, { canonical: [], reExports: [] });
}

if (isReExportFile(file)) {
reExportFiles.push({
name: toolName,
file,
reExportFiles.push({
name: toolName,
file,
workflowDir,
relativePath: path.relative(projectRoot, file)
});
toolsByWorkflow.get(workflowDir).reExports.push(toolName);
} else {
canonicalTools.set(toolName, {
canonicalTools.set(toolName, {
name: toolName,
file,
file,
workflowDir,
relativePath: path.relative(projectRoot, file)
});
Expand Down Expand Up @@ -431,20 +431,20 @@ async function getStaticInfo() {
function displaySummary(runtimeData, staticData) {
console.log(`${colors.bright}${colors.blue}📊 XcodeBuildMCP Tools Summary${colors.reset}`);
console.log('═'.repeat(60));

if (runtimeData) {
console.log(`${colors.green}🚀 Runtime Analysis:${colors.reset}`);
console.log(` Mode: ${runtimeData.dynamicMode ? 'Dynamic' : 'Static'}`);
console.log(` Tools: ${runtimeData.toolCount}`);
console.log(` Resources: ${runtimeData.resourceCount}`);
console.log(` Total: ${runtimeData.toolCount + runtimeData.resourceCount}`);

if (runtimeData.dynamicMode) {
console.log(` ${colors.yellow}ℹ️ Dynamic mode: Only enabled workflow tools shown${colors.reset}`);
}
console.log();
}

if (staticData) {
console.log(`${colors.cyan}📁 Static Analysis:${colors.reset}`);
console.log(` Workflow directories: ${staticData.workflowDirs.length}`);
Expand All @@ -460,16 +460,16 @@ function displaySummary(runtimeData, staticData) {
*/
function displayWorkflows(staticData) {
if (!options.workflows || !staticData) return;

console.log(`${colors.bright}📂 Workflow Directories:${colors.reset}`);
console.log('─'.repeat(40));

for (const workflowDir of staticData.workflowDirs) {
const workflow = staticData.toolsByWorkflow.get(workflowDir) || { canonical: [], reExports: [] };
const totalTools = workflow.canonical.length + workflow.reExports.length;

console.log(`${colors.green}• ${workflowDir}${colors.reset} (${totalTools} tools)`);

if (options.verbose) {
if (workflow.canonical.length > 0) {
console.log(` ${colors.cyan}Canonical:${colors.reset} ${workflow.canonical.join(', ')}`);
Expand All @@ -487,11 +487,11 @@ function displayWorkflows(staticData) {
*/
function displayTools(runtimeData, staticData) {
if (!options.tools) return;

if (runtimeData) {
console.log(`${colors.bright}🛠️ Runtime Tools (${runtimeData.toolCount}):${colors.reset}`);
console.log('─'.repeat(40));

if (runtimeData.tools.length === 0) {
console.log(' No tools available');
} else {
Expand All @@ -506,11 +506,11 @@ function displayTools(runtimeData, staticData) {
}
console.log();
}

if (staticData && options.static) {
console.log(`${colors.bright}📁 Static Tools (${staticData.toolCount}):${colors.reset}`);
console.log('─'.repeat(40));

if (staticData.tools.length === 0) {
console.log(' No tools found');
} else {
Expand All @@ -534,10 +534,10 @@ function displayTools(runtimeData, staticData) {
*/
function displayResources(runtimeData) {
if (!options.resources || !runtimeData) return;

console.log(`${colors.bright}📚 Resources (${runtimeData.resourceCount}):${colors.reset}`);
console.log('─'.repeat(40));

if (runtimeData.resources.length === 0) {
console.log(' No resources available');
} else {
Expand All @@ -560,41 +560,41 @@ async function main() {
try {
let runtimeData = null;
let staticData = null;

// Gather data based on options
if (options.runtime) {
console.log(`${colors.cyan}🔍 Gathering runtime information...${colors.reset}`);
runtimeData = await getRuntimeInfo();
}

if (options.static) {
console.log(`${colors.cyan}📁 Performing static analysis...${colors.reset}`);
staticData = await getStaticInfo();
}

// For default command or workflows option, always gather static data for workflow info
if (options.workflows && !staticData) {
console.log(`${colors.cyan}📁 Gathering workflow information...${colors.reset}`);
staticData = await getStaticInfo();
}

console.log(); // Blank line after gathering

// Display based on command
switch (command) {
case 'count':
case 'c':
displaySummary(runtimeData, staticData);
displayWorkflows(staticData);
break;

case 'list':
case 'l':
displaySummary(runtimeData, staticData);
displayTools(runtimeData, staticData);
displayResources(runtimeData);
break;

case 'static':
case 's':
if (!staticData) {
Expand All @@ -603,7 +603,7 @@ async function main() {
}
displaySummary(null, staticData);
displayWorkflows(staticData);

if (options.verbose) {
displayTools(null, staticData);
console.log(`${colors.bright}🔄 Re-export Files (${staticData.reExportCount}):${colors.reset}`);
Expand All @@ -614,16 +614,16 @@ async function main() {
});
}
break;

default:
// Default case (no command) - show runtime summary with workflows
displaySummary(runtimeData, staticData);
displayWorkflows(staticData);
break;
}

console.log(`${colors.green}✅ Analysis complete!${colors.reset}`);

} catch (error) {
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
process.exit(1);
Expand Down
19 changes: 10 additions & 9 deletions src/mcp/tools/discovery/__tests__/discover_tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('discover_tools', () => {

it('should have correct description', () => {
expect(discoverTools.description).toBe(
'Analyzes a natural language task description to enable a relevant set of Xcode and Apple development tools. For best results, specify the target platform (iOS, macOS, watchOS, tvOS, visionOS) and project type (.xcworkspace or .xcodeproj).',
'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging, diagnostics) and Swift packages.',
);
});

Expand Down Expand Up @@ -656,9 +656,10 @@ describe('discover_tools', () => {
const prompt = requestCall[0].messages[0].content.text;

expect(prompt).toContain(taskDescription);
expect(prompt).toContain('Project Type Selection Guide');
expect(prompt).toContain('Platform Selection Guide');
expect(prompt).toContain('Available Workflows');
expect(prompt).toContain('Select EXACTLY ONE workflow');
expect(prompt).toContain('Primary (project/workspace-based) workflows:');
expect(prompt).toContain('Secondary (task-based, no project/workspace needed):');
expect(prompt).toContain('All available workflows:');
});

it('should provide clear selection guidelines in prompt', async () => {
Expand Down Expand Up @@ -693,11 +694,11 @@ describe('discover_tools', () => {
const requestCall = requestCalls[0];
const prompt = requestCall[0].messages[0].content.text;

expect(prompt).toContain('Choose ONLY ONE workflow');
expect(prompt).toContain('If working with .xcworkspace files');
expect(prompt).toContain('If working with .xcodeproj files');
expect(prompt).toContain('iOS development on simulators');
expect(prompt).toContain('macOS development');
expect(prompt).toContain('Select EXACTLY ONE workflow');
expect(prompt).toContain('.xcworkspace');
expect(prompt).toContain('.xcodeproj');
expect(prompt).toContain('simulator-management');
expect(prompt).toContain('macOS');
expect(prompt).toContain('Respond with ONLY a JSON array');
});
});
Expand Down
Loading