Skip to content

Commit 53d0d21

Browse files
committed
Add log capture tool
1 parent 4874339 commit 53d0d21

File tree

9 files changed

+471
-34
lines changed

9 files changed

+471
-34
lines changed

README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The XcodeBuildMCP server provides the following tool capabilities:
5151
### Simulator management
5252
- **Simulator Control**: List, boot, and open iOS simulators
5353
- **App Deployment**: Install and launch apps on iOS simulators
54+
- **Log Capture**: Capture run-time logs from a simulator
5455

5556
### App utilities
5657
- **Bundle ID Extraction**: Extract bundle identifiers from iOS and macOS app bundles
@@ -166,13 +167,6 @@ To configure your MCP client to use the local XcodeBuildMCP server, add the foll
166167
}
167168
```
168169

169-
Remember after making changes to the server implemetation you'll need to rebuild and restart the server.
170-
171-
```bash
172-
npm run build
173-
node build/index.js
174-
```
175-
176170
### Debugging
177171

178172
You can use MCP Inspector via:

example_projects/iOS/MCPTest/ContentView.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import OSLog
910

1011
struct ContentView: View {
1112
var body: some View {
@@ -14,11 +15,26 @@ struct ContentView: View {
1415
.imageScale(.large)
1516
.foregroundStyle(.tint)
1617
Text("Hello, world!")
18+
19+
Button("Log something") {
20+
Logger.myApp.debug("Oh this is structured logging")
21+
debugPrint("I'm just plain old std out :-(")
22+
}
1723
}
1824
.padding()
25+
1926
}
2027
}
2128

2229
#Preview {
2330
ContentView()
2431
}
32+
33+
// OS Log Extension
34+
extension Logger {
35+
static let myApp = Logger(
36+
subsystem: "com.cameroncooke.MCPTest",
37+
category: "default"
38+
)
39+
}
40+

src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
registerOpenSimulatorTool,
5656
registerInstallAppInSimulatorTool,
5757
registerLaunchAppInSimulatorTool,
58+
registerLaunchAppWithLogsInSimulatorTool,
5859
} from './tools/simulator.js';
5960

6061
// Import bundle ID tools
@@ -72,6 +73,12 @@ import { registerDiscoverProjectsTool } from './tools/discover_projects.js';
7273
// Import utilities
7374
import { log } from './utils/logger.js';
7475

76+
// Import log capture tools
77+
import {
78+
registerStartSimulatorLogCaptureTool,
79+
registerStopAndGetSimulatorLogTool,
80+
} from './tools/log.js';
81+
7582
/**
7683
* Main function to start the server
7784
*/
@@ -118,6 +125,7 @@ async function main(): Promise<void> {
118125
// Register App installation and launch tools
119126
registerInstallAppInSimulatorTool(server);
120127
registerLaunchAppInSimulatorTool(server);
128+
registerLaunchAppWithLogsInSimulatorTool(server);
121129

122130
// Register Bundle ID tools
123131
registerGetMacOSBundleIdTool(server);
@@ -130,6 +138,10 @@ async function main(): Promise<void> {
130138
registerMacOSBuildAndRunTools(server);
131139
registerIOSSimulatorBuildAndRunTools(server);
132140

141+
// Register log capture tools
142+
registerStartSimulatorLogCaptureTool(server);
143+
registerStopAndGetSimulatorLogTool(server);
144+
133145
// Start the server
134146
await startServer(server);
135147

src/tools/build_ios_simulator.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,17 @@ async function _handleIOSSimulatorBuildAndRunLogic(params: {
324324
text: `✅ iOS simulator build and run succeeded for scheme ${params.scheme} targeting ${target}.
325325
326326
The app (${bundleId}) is now running in the iOS Simulator.
327-
If you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.`,
327+
If you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.
328+
329+
Next Steps:
330+
- Option 1: Capture structured logs only (app continues running):
331+
start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' })
332+
- Option 2: Capture both console and structured logs (app will restart):
333+
start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}', captureConsole: true })
334+
- Option 3: Launch app with logs in one step (for a fresh start):
335+
launch_app_with_logs_in_simulator({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' })
336+
337+
When done with any option, use: stop_and_get_simulator_log({ logSessionId: 'SESSION_ID' })`,
328338
},
329339
],
330340
};

src/tools/common.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ export const appPathSchema = z
5050
export const bundleIdSchema = z
5151
.string()
5252
.describe("Bundle identifier of the app (e.g., 'com.example.MyApp')");
53-
export const dummySchema = z
54-
.boolean()
55-
.optional()
56-
.describe('This is a dummy parameter. You must still provide an empty object {}.');
5753
export const launchArgsSchema = z
5854
.array(z.string())
5955
.optional()

src/tools/log.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Log Tools - Functions for capturing and managing iOS simulator logs
3+
*
4+
* This module provides tools for capturing and managing logs from iOS simulators.
5+
* It supports starting and stopping log capture sessions, and retrieving captured logs.
6+
*
7+
* Responsibilities:
8+
* - Starting and stopping log capture sessions
9+
* - Managing in-memory log sessions
10+
* - Retrieving captured logs
11+
*/
12+
13+
import { startLogCapture, stopLogCapture } from '../utils/log_capture.js';
14+
import { z } from 'zod';
15+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16+
import { ToolResponse } from '../types/common.js';
17+
import { validateRequiredParam } from '../utils/validation.js';
18+
import { registerTool, createTextContent } from './common.js';
19+
20+
/**
21+
* Registers the tool to start capturing logs from an iOS simulator.
22+
*
23+
* @param server The MCP Server instance.
24+
*/
25+
export function registerStartSimulatorLogCaptureTool(server: McpServer): void {
26+
const schema = {
27+
simulatorUuid: z
28+
.string()
29+
.describe('UUID of the simulator to capture logs from (obtained from list_simulators).'),
30+
bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'),
31+
captureConsole: z
32+
.boolean()
33+
.optional()
34+
.default(false)
35+
.describe('Whether to capture console output (requires app relaunch).'),
36+
};
37+
38+
async function handler(params: {
39+
simulatorUuid: string;
40+
bundleId: string;
41+
captureConsole?: boolean;
42+
}): Promise<ToolResponse> {
43+
const validationResult = validateRequiredParam('simulatorUuid', params.simulatorUuid);
44+
if (!validationResult.isValid) {
45+
return validationResult.errorResponse!;
46+
}
47+
48+
const { sessionId, error } = await startLogCapture(params);
49+
if (error) {
50+
return {
51+
content: [createTextContent(`Error starting log capture: ${error}`)],
52+
isError: true,
53+
};
54+
}
55+
return {
56+
content: [
57+
createTextContent(
58+
`Log capture started successfully. Session ID: ${sessionId}.\n\n${params.captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_and_get_simulator_log' with session ID '${sessionId}' to stop capture and retrieve logs.`,
59+
),
60+
],
61+
};
62+
}
63+
64+
registerTool(
65+
server,
66+
'start_simulator_log_capture',
67+
'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs. Use captureConsole:true to also capture console output (will relaunch the app).',
68+
schema,
69+
handler,
70+
);
71+
}
72+
73+
/**
74+
* Registers the tool to stop log capture and retrieve the content in one operation.
75+
*
76+
* @param server The MCP Server instance.
77+
*/
78+
export function registerStopAndGetSimulatorLogTool(server: McpServer): void {
79+
const schema = {
80+
logSessionId: z.string().describe('The session ID returned by start_simulator_log_capture.'),
81+
};
82+
83+
async function handler(params: { logSessionId: string }): Promise<ToolResponse> {
84+
const validationResult = validateRequiredParam('logSessionId', params.logSessionId);
85+
if (!validationResult.isValid) {
86+
return validationResult.errorResponse!;
87+
}
88+
const { logContent, error } = await stopLogCapture(params.logSessionId);
89+
if (error) {
90+
return {
91+
content: [
92+
createTextContent(`Error stopping log capture session ${params.logSessionId}: ${error}`),
93+
],
94+
isError: true,
95+
};
96+
}
97+
return {
98+
content: [
99+
createTextContent(
100+
`Log capture session ${params.logSessionId} stopped successfully. Log content follows:\n\n${logContent}`,
101+
),
102+
],
103+
};
104+
}
105+
106+
registerTool(
107+
server,
108+
'stop_and_get_simulator_log',
109+
'Stops an active simulator log capture session and returns the captured logs.',
110+
schema,
111+
handler,
112+
);
113+
}

0 commit comments

Comments
 (0)