Skip to content

Commit 14c223c

Browse files
committed
Progress reporting
1 parent 0ee5651 commit 14c223c

File tree

13 files changed

+640
-58
lines changed

13 files changed

+640
-58
lines changed

HOW_TO_TEST.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Running XcodeBuildMCP Progress Update Tests
2+
3+
This document provides instructions for testing the new progress update system for XcodeBuildMCP.
4+
5+
## Prerequisites
6+
7+
Make sure you have:
8+
1. Node.js installed (v16 or newer)
9+
2. Built the latest version of the XcodeBuildMCP project
10+
11+
To build the project:
12+
```bash
13+
npm run build
14+
```
15+
16+
## Running the Test
17+
18+
The test script provides a user-friendly way to see progress updates in action:
19+
20+
1. Open a terminal in the project root directory
21+
2. Run the test script:
22+
```bash
23+
node test-progress.mjs
24+
```
25+
3. Watch the terminal for color-coded progress messages
26+
- Progress updates are highlighted based on completion percentage
27+
- New operations are clearly marked with headers
28+
- The test automatically runs a clean and build operation on the example project
29+
- Test automatically exits after completion (about 15 seconds)
30+
31+
### Example Output
32+
33+
```
34+
===================================================
35+
XcodeBuildMCP Progress Update Test
36+
===================================================
37+
38+
Testing with project: ./example_projects/macOS/MCPTest.xcodeproj, scheme: MCPTest
39+
40+
Waiting for server initialization...
41+
[2025-05-04T20:37:43.532Z] [INFO] Server initialized (version 1.1.0)
42+
[2025-05-04T20:37:43.534Z] [INFO] Progress service initialized
43+
44+
===== EXECUTING CLEAN OPERATION =====
45+
46+
--- New Operation Started: 550e8400-e29b-41d4-a716-446655440000 ---
47+
>> Operation [550e8400-e29b-41d4-a716-446655440000]: RUNNING - Starting clean operation... (0%)
48+
>> Operation [550e8400-e29b-41d4-a716-446655440000]: RUNNING - Cleaning build files... (50%)
49+
>> Operation [550e8400-e29b-41d4-a716-446655440000]: COMPLETED - Clean completed successfully (100%)
50+
51+
===== EXECUTING BUILD OPERATION =====
52+
53+
--- New Operation Started: a67890bc-d12e-4f56-ga7b-8927ea1cde0b ---
54+
>> Operation [a67890bc-d12e-4f56-ga7b-8927ea1cde0b]: RUNNING - Starting Xcode build... (0%)
55+
>> Operation [a67890bc-d12e-4f56-ga7b-8927ea1cde0b]: RUNNING - CompileSwift phase... (25%)
56+
>> Operation [a67890bc-d12e-4f56-ga7b-8927ea1cde0b]: RUNNING - Processing file 10 of 20 (50%)
57+
>> Operation [a67890bc-d12e-4f56-ga7b-8927ea1cde0b]: RUNNING - Linking phase... (75%)
58+
>> Operation [a67890bc-d12e-4f56-ga7b-8927ea1cde0b]: COMPLETED - Build completed successfully (100%)
59+
60+
===== TEST COMPLETE =====
61+
```
62+
63+
## Testing Individual Operations
64+
65+
If you want to test with your own projects or specific operations:
66+
67+
1. Start the server in one terminal:
68+
```bash
69+
node build/index.js
70+
```
71+
72+
2. In a second terminal, use the MCP CLI tool to call specific operations:
73+
```bash
74+
npx @modelcontextprotocol/cli call-tool --server-command="node build/index.js" macos_build_project '{"projectPath": "./path/to/your/project.xcodeproj", "scheme": "YourScheme"}'
75+
```
76+
77+
3. Watch for progress updates in the server terminal
78+
79+
## Troubleshooting
80+
81+
If you don't see progress updates:
82+
83+
1. Make sure you've built the latest version:
84+
```bash
85+
npm run build
86+
```
87+
88+
2. Check that the project exists and is accessible:
89+
```bash
90+
ls -la example_projects/macOS/MCPTest.xcodeproj
91+
```
92+
93+
3. Verify that Xcode command line tools are installed:
94+
```bash
95+
xcode-select --install
96+
```
97+
98+
4. Try modifying the test script to use a different project if needed:
99+
- Edit `test-progress.mjs`
100+
- Update the `PROJECT_PATH` and `SCHEME` constants

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ The XcodeBuildMCP server provides the following tool capabilities:
5555
- **Bundle ID Extraction**: Extract bundle identifiers from iOS and macOS app bundles
5656
- **App Launching**: Launch built applications on both simulators and macOS
5757

58+
### Operation Progress
59+
- **Real-time Feedback**: Get detailed progress updates during long-running operations
60+
- **Build Phase Tracking**: Monitor compilation, linking, and code signing phases
61+
- **Status Reporting**: View operation status, estimated progress, and error information
62+
- [Learn more about progress updates](docs/progress-updates.md)
63+
5864

5965
## Getting started
6066

docs/progress-updates.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Progress Updates for Long-Running Operations
2+
3+
This document describes the progress update system implemented in the XcodeBuildMCP server to provide feedback during long-running operations.
4+
5+
## Overview
6+
7+
XcodeBuildMCP now provides real-time progress updates during long-running operations such as building applications. This helps clients understand the current state of operations, estimated completion percentage, and any important status messages.
8+
9+
## Implementation Details
10+
11+
### Progress Update Structure
12+
13+
Progress updates follow a standardized format:
14+
15+
```typescript
16+
interface ToolProgressUpdate {
17+
operationId: string; // Unique identifier for the operation
18+
status: 'running' | 'completed' | 'failed'; // Current status
19+
progress?: number; // 0-100 percentage
20+
message: string; // Human-readable status message
21+
timestamp: string; // ISO timestamp of the update
22+
details?: string; // Optional additional details
23+
}
24+
```
25+
26+
### Progress Service
27+
28+
The `progress.ts` module provides a centralized service for handling progress updates:
29+
30+
- `initProgressService(server)`: Initializes the progress service with an MCP server instance
31+
- `sendProgressUpdate(update)`: Sends a progress update for an operation
32+
- `createProgressCallback(operationName)`: Creates a callback function for a specific operation
33+
- `getActiveOperations()`: Returns a list of active operations
34+
35+
### How Progress Updates Work
36+
37+
1. When a long-running command is executed, it is given a unique operation ID
38+
2. The command execution monitors the output and periodically sends progress updates
39+
3. Progress is estimated based on various heuristics:
40+
- Build phase detection (CompileC, CompileSwift, Linking, CodeSign)
41+
- File count detection (x of y files)
42+
- Phase transitions
43+
4. Updates are sent to the client via console output (to stderr)
44+
5. On completion, a final update with status "completed" or "failed" is sent
45+
46+
### Progress Estimation Heuristics
47+
48+
The system uses several techniques to estimate build progress:
49+
50+
- **Phase Detection**: Identifies different build phases and uses them to estimate overall progress
51+
- **File Counting**: Detects "x of y files" patterns in the output
52+
- **Time-Based**: Provides periodic updates even when no concrete progress information is available
53+
- **Error Detection**: Highlights warnings and errors as they occur
54+
55+
## Client Integration
56+
57+
Clients can monitor the console output for progress messages in the format:
58+
59+
```
60+
Operation [operation-id]: STATUS - Message (progress%)
61+
```
62+
63+
For example:
64+
```
65+
Operation [550e8400-e29b-41d4-a716-446655440000]: RUNNING - CompileSwift phase... (25%)
66+
```
67+
68+
## Future Improvements
69+
70+
Future versions may include:
71+
- WebSocket-based progress updates
72+
- More precise progress estimation
73+
- Support for cancellation of long-running operations
74+
- A UI component for displaying progress

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
},
4040
"dependencies": {
4141
"@modelcontextprotocol/sdk": "^1.6.1",
42+
"@types/uuid": "^10.0.0",
43+
"uuid": "^11.1.0",
4244
"zod": "^3.24.2"
4345
},
4446
"devDependencies": {

src/server/server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
33
import { log } from '../utils/logger.js';
4+
import { initProgressService } from '../utils/progress.js';
45
import { version } from '../version.js';
56

67
/**
@@ -26,6 +27,9 @@ export function createServer(): McpServer {
2627

2728
// Log server initialization
2829
log('info', `Server initialized (version ${version})`);
30+
31+
// Initialize the progress service with the server instance
32+
initProgressService(server);
2933

3034
return server;
3135
}

src/tools/build_macos.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { promisify } from 'util';
88
import { log } from '../utils/logger.js';
99
import {
1010
executeXcodeCommand,
11+
ProgressCallback,
1112
} from '../utils/xcode.js';
13+
import { createProgressCallback } from '../utils/progress.js';
1214
import {
1315
createTextResponse,
1416
} from '../utils/validation.js';
@@ -63,7 +65,10 @@ async function _handleMacOSBuildLogic(params: {
6365

6466
command.push('build');
6567

66-
const result = await executeXcodeCommand(command, 'macOS Build');
68+
// Create a progress callback for this operation
69+
const progressCallback = createProgressCallback(`macOS Build ${params.scheme}`);
70+
71+
const result = await executeXcodeCommand(command, 'macOS Build', progressCallback);
6772

6873
let match;
6974
while ((match = warningRegex.exec(result.output)) !== null) {
@@ -110,23 +115,26 @@ async function _getAppPathFromBuildSettings(params: {
110115
derivedDataPath?: string;
111116
extraArgs?: string[];
112117
}): Promise<{ success: boolean; appPath?: string; error?: string }> {
113-
try {
114-
const getAppSettingsCommand = ['xcodebuild'];
118+
try {
119+
const getAppSettingsCommand = ['xcodebuild'];
120+
121+
if (params.workspacePath) {
122+
getAppSettingsCommand.push('-workspace', params.workspacePath);
123+
} else if (params.projectPath) {
124+
getAppSettingsCommand.push('-project', params.projectPath);
125+
} // Path guaranteed by caller validation
126+
getAppSettingsCommand.push(
127+
'-scheme',
128+
params.scheme,
129+
'-configuration',
130+
params.configuration,
131+
'-showBuildSettings'
132+
);
115133

116-
if (params.workspacePath) {
117-
getAppSettingsCommand.push('-workspace', params.workspacePath);
118-
} else if (params.projectPath) {
119-
getAppSettingsCommand.push('-project', params.projectPath);
120-
} // Path guaranteed by caller validation
121-
getAppSettingsCommand.push(
122-
'-scheme',
123-
params.scheme,
124-
'-configuration',
125-
params.configuration,
126-
'-showBuildSettings'
127-
);
128-
129-
const settingsResult = await executeXcodeCommand(getAppSettingsCommand, 'Get Build Settings for Launch');
134+
// Create a progress callback for getting build settings
135+
const settingsProgressCallback = createProgressCallback(`Get Build Settings ${params.scheme}`);
136+
137+
const settingsResult = await executeXcodeCommand(getAppSettingsCommand, 'Get Build Settings for Launch', settingsProgressCallback);
130138
if (!settingsResult.success) {
131139
return { success: false, error: settingsResult.error };
132140
}
@@ -166,10 +174,10 @@ async function _handleMacOSBuildAndRunLogic(params: {
166174
const buildResult = await _handleMacOSBuildLogic(params);
167175

168176
// 1. Check if the build itself failed
169-
if (buildResult.status !== 'success') {
177+
if (buildResult.isError) {
170178
return buildResult; // Return build failure directly
171179
}
172-
const warningMessages = buildResult.content?.filter(c => c.type === 'text' && !c.isError) ?? [];
180+
const warningMessages = buildResult.content?.filter(c => c.type === 'text') ?? [];
173181

174182
// 2. Build succeeded, now get the app path using the helper
175183
const appPathResult = await _getAppPathFromBuildSettings(params);

0 commit comments

Comments
 (0)