Metadata-Version: 2.4
Name: apple-instruments-mcp
Version: 1.2.1
Summary: MCP server for Apple platform profiling via xctrace/Instruments
Project-URL: Homepage, https://github.com/dogo/apple-instruments-mcp
Project-URL: Repository, https://github.com/dogo/apple-instruments-mcp
Project-URL: Issues, https://github.com/dogo/apple-instruments-mcp/issues
Project-URL: Changelog, https://github.com/dogo/apple-instruments-mcp/blob/main/CHANGELOG.md
Author-email: Diogo Autilio <diautilio@gmail.com>
License: MIT
License-File: LICENSE
Keywords: instruments,ios,macos,mcp,model-context-protocol,performance,profiling,xctrace
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: mcp>=1.0.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pyright>=1.1.380; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# apple-instruments-mcp

`apple-instruments-mcp` is a Python MCP server for profiling Apple platform apps and processes with Instruments through `xcrun xctrace`. It lets MCP clients such as Claude Desktop record traces, inspect existing `.trace` bundles, and return concise performance reports with actionable recommendations.

The server can target iOS, iPadOS, macOS, tvOS, watchOS, visionOS, simulators, physical devices, and host processes when those targets are visible to the installed Xcode/Instruments toolchain. Actual template and device support is determined by `xcrun xctrace list devices` and `xcrun xctrace list templates` on your Mac.

## Features

- Record new traces with `xctrace` from an MCP client.
- Analyze existing `.trace` bundles without recording again.
- Report launch time, memory allocations, leaks, CPU hot spots, and network activity.
- Launch apps by bundle ID or executable path.
- Attach to running processes by process name or PID.
- Record all processes when an Instruments template supports it.
- List available devices, simulators, runtimes, and Instruments templates.
- Return raw or JSON-structured device/template listings.
- Preview the generated `xctrace` command with `dry_run`.
- Keep `.trace` and XML artifacts for later inspection.
- Return human-readable summaries with top offenders and suggested fixes.
- Runs as a standard stdio MCP server.

## Requirements

- macOS
- Xcode with Instruments installed
- Xcode Command Line Tools: `xcode-select --install`
- Python 3.11 or newer
- A supported target visible to `xctrace`: booted simulator, connected device, local macOS app, running process, or all-processes recording.

Physical devices may require enabling UI automation in `Settings > Developer`.

## Installation

The package is published on PyPI. The recommended way is to run it through [`uvx`](https://github.com/astral-sh/uv), which downloads and caches the server on demand without polluting your environment:

```bash
uvx apple-instruments-mcp
```

Alternatively, install it globally with `pipx` or `pip`:

```bash
pipx install apple-instruments-mcp
# or
python -m pip install apple-instruments-mcp
```

Either path provides the `apple-instruments-mcp` executable.

## MCP Client Configuration

`apple-instruments-mcp` is a standard stdio MCP server, so any MCP-compatible client works — Claude Desktop, Claude Code, Codex CLI, Kiro, Cursor, Cline, Continue, Windsurf, Zed, or anything else that speaks MCP.

The simplest config uses `uvx`, so the client launches the server on demand with no separate install step. Example for Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "apple-instruments": {
      "command": "uvx",
      "args": ["apple-instruments-mcp"]
    }
  }
}
```

To pin a specific version, replace the arg with `apple-instruments-mcp@1.2.1`.

For Claude Code:

```bash
claude mcp add apple-instruments -- uvx apple-instruments-mcp
```

If you prefer a global install (`pipx` or `pip`), point the client at the resulting binary instead:

```json
{
  "mcpServers": {
    "apple-instruments": {
      "command": "apple-instruments-mcp"
    }
  }
}
```

Other clients (Codex CLI, Kiro, Cursor, Cline, Continue, Windsurf, Zed) accept the same `command` / `args` shape under their own MCP server config — check the client's docs for the exact file path. Restart the client after saving.

## Example Prompts

```text
Run the xctrace doctor and tell me if recording is currently possible.
List the devices and Instruments templates available on this Mac.
Profile com.example.app on this simulator with the full preset for 15 seconds.
Record an App Launch trace for com.example.app on this iPhone simulator and show the startup offenders.
Launch /Applications/MyMacApp.app with Time Profiler and show CPU hot methods.
Time-profile com.example.app, then zoom into the 8000ms–12000ms window and flag main-thread stalls above 200ms.
Time-profile this release build and symbolicate raw addresses with ~/Build/MyApp.dSYM.
Attach Allocations to the running process named MyMacApp for 30 seconds.
Attach Time Profiler to PID 1234.
Record all processes with Time Profiler for 10 seconds.
Analyze network activity for com.example.app and flag slow requests.
Analyze the existing trace at ~/Desktop/launch.trace.
Compare ~/Desktop/baseline.trace and ~/Desktop/candidate.trace for launch regressions.
```

## Tools

| Tool | Instruments template | Description |
| --- | --- | --- |
| `doctor` | - | One-shot health check: xctrace version/path, online/offline device counts, CoreSimulator access, templates/instruments, and targeted diagnostics for wedges or sandbox failures. Run this first if anything looks wedged. |
| `list_devices` | - | Lists devices, simulators, runtimes, and host targets visible to `xctrace`. |
| `list_devices_structured` | - | Returns the device/runtime listing as JSON. |
| `list_templates` | - | Lists Instruments templates installed on this Mac. |
| `list_templates_structured` | - | Returns the template listing as JSON. |
| `profile_preset` | `cpu` / `memory` / `network` / `full` | Records a bundle of instruments in one trace, then reports each family that produced data. `full` covers CPU + Allocations + Leaks + Network in a single run. |
| `profile_ios_app` | Selected by `profile_type` | Profiles an iOS, iPadOS, tvOS, watchOS, or visionOS app by bundle ID on a simulator/device. |
| `profile_mac_app` | Selected by `profile_type` | Profiles a macOS `.app` or executable by launch path. |
| `profile_process` | Selected by `profile_type` | Profiles a running process by process name or PID. |
| `profile_all_processes` | Selected by `profile_type` | Profiles all processes on the host or selected device. |
| `analyze_launch` | App Launch | Records and analyzes app startup time. |
| `analyze_launch_trace` | App Launch | Analyzes an existing App Launch `.trace`. |
| `analyze_allocations` | Allocations | Records memory allocations and reports live heap/VM totals plus top allocation categories. |
| `analyze_allocations_trace` | Allocations | Analyzes an existing Allocations `.trace`. |
| `analyze_leaks` | Leaks | Records a Leaks trace and reports exported leak details when available. |
| `analyze_leaks_trace` | Leaks | Analyzes an existing Leaks `.trace` when leak details are exportable. |
| `analyze_time_profiler` | Time Profiler | Records CPU samples and reports hot methods. Supports `scope_start_ms`/`scope_end_ms` windowing, main-thread hang detection, user-binary filtering, and `dsym_path` for symbolicating release-build frames. |
| `analyze_time_profiler_trace` | Time Profiler | Analyzes an existing Time Profiler `.trace`. Same scope/hang/user-binary/dSYM options as `analyze_time_profiler`. |
| `analyze_network` | Network | Records network requests, latency, transfer sizes, and status codes. |
| `analyze_network_trace` | Network | Analyzes an existing Network `.trace`. |
| `compare_launch_traces` | App Launch | Compares two App Launch traces and reports startup deltas. |
| `compare_memory_traces` | Allocations | Compares two Allocations traces and reports memory deltas. |
| `compare_cpu_traces` | Time Profiler | Compares two Time Profiler traces and reports CPU deltas. |
| `build_xctrace_command` | Any | Returns the exact `xcrun xctrace record` command for a target without executing it. |

## Recording Targets

Recording tools accept exactly one target mode:

- `bundle_id`: app bundle identifier, for example `com.example.app`. Use with `device_id` for iOS, iPadOS, tvOS, watchOS, and visionOS simulator/device launches.
- `launch_path`: executable path or `.app` path. Use this for macOS apps and command-line tools.
- `process_name`: attach to a running process by name.
- `pid`: attach to a running process by process ID.
- `all_processes`: record all processes when the selected template supports it.

Additional recording arguments:

- `device_id`: optional simulator UUID or physical device identifier from `list_devices`. Omit it for host macOS profiling.
- `launch_args`: optional shell-style arguments passed after `launch_path`.
- `time_limit_seconds`: recording duration, from 5 to 120 seconds. Defaults to 20.
- `dry_run`: return the generated `xctrace` command without recording.
- `keep_trace`: keep the run directory and generated `.trace`/XML artifacts, including on failures, and include their paths in the report.
- `output_dir`: optional parent directory for generated artifacts. Each run creates a dedicated subdirectory inside it.

Examples:

```json
{ "bundle_id": "com.example.app", "device_id": "SIMULATOR-UUID" }
{ "launch_path": "/Applications/MyMacApp.app" }
{ "launch_path": "/usr/bin/python3", "launch_args": "--version" }
{ "process_name": "MyMacApp" }
{ "pid": 1234 }
{ "all_processes": true }
{ "launch_path": "/Applications/MyMacApp.app", "dry_run": true }
{ "bundle_id": "com.example.app", "device_id": "SIMULATOR-UUID", "keep_trace": true, "output_dir": "~/Desktop/traces" }
```

The `profile_*` tools are higher-level wrappers around the generic `analyze_*` tools. Prefer them when you know the target shape:

```json
{ "profile_type": "launch", "bundle_id": "com.example.app", "device_id": "SIMULATOR-UUID" }
{ "profile_type": "time_profiler", "launch_path": "/Applications/MyMacApp.app" }
{ "profile_type": "allocations", "process_name": "MyMacApp" }
{ "profile_type": "time_profiler", "pid": 1234 }
{ "profile_type": "time_profiler" }
```

Supported `profile_type` values are `launch`, `allocations`, `leaks`, `time_profiler`, and `network`.

## Custom Thresholds

The generic `analyze_*` and `analyze_*_trace` tools expose parser-specific thresholds. Use these when the defaults are too strict or too loose for a device class, platform, workload, or CI budget.

Launch:

- `launch_good_ms`
- `launch_critical_ms`
- `offender_warning_ms`
- `offender_critical_ms`

Allocations:

- `memory_warning_mb`
- `memory_critical_mb`
- `memory_cache_warning_mb`

Leaks:

- `leak_critical_count`

Time Profiler:

- `total_good_ms` — good threshold for normalized CPU load, measured as CPU-active milliseconds per wall-clock second.
- `total_critical_ms` — critical threshold for the same normalized CPU-load metric. Defaults of 100/500 correspond to roughly 10%/50% of one fully occupied core.
- `method_warning_ms`
- `method_critical_ms`

Network:

- `request_warning_ms`
- `request_critical_ms`
- `slow_request_critical_count`
- `transfer_warning_mb`

Example:

```json
{
  "bundle_id": "com.example.app",
  "device_id": "SIMULATOR-UUID",
  "launch_good_ms": 700,
  "launch_critical_ms": 1500,
  "offender_warning_ms": 150,
  "offender_critical_ms": 500
}
```

## Trace Analysis Arguments

Trace analysis tools accept:

- `trace_path`: absolute path to an existing `.trace` bundle.
- `bundle_id`: optional target name used in the generated report.

Trace comparison tools accept:

- `baseline_trace_path`: absolute path to the baseline `.trace` bundle.
- `candidate_trace_path`: absolute path to the candidate `.trace` bundle.
- `bundle_id`: optional target name used in the generated report.

Example:

```json
{
  "baseline_trace_path": "~/Desktop/baseline.trace",
  "candidate_trace_path": "~/Desktop/candidate.trace",
  "bundle_id": "com.example.app"
}
```

## Example Report

```markdown
# App Launch Analysis - com.example.app

Warning: Launch time 1240ms - above 400ms. Users may notice the delay.

**Total:** 1240ms

## Phases
- **pre-main (dyld + static init):** 434ms (35%)
- **post-main (AppDelegate + UI):** 806ms (65%)

## Top Offenders

Critical: `-[DatabaseManager setup]` [post-main]
   Self: **540ms** | Total: 540ms | 43%
   Suggestion: Move database initialization to a background queue or use lazy loading.

Warning: `+[AnalyticsSDK configure:]` [post-main]
   Self: **210ms** | Total: 210ms | 17%
   Suggestion: Defer analytics SDK initialization after first frame is rendered.

## Recommendations
- Fix 1 critical offender(s) - each adds 300ms+ to launch.
- Target: total launch under 400ms.
```

## Notes

- Temporary `.trace` bundles created by recording tools are cleaned up after each run.
- A failed recording preserves its partial `.trace` only when it contains non-empty artifacts; empty and zero-byte bundles are removed.
- The server only terminates the `xctrace` child process it started through its watchdog. It does not sweep unrelated `xctrace` sessions running on the Mac.
- Recording teardown has a 15-second grace period after the requested time limit. Known tap-disconnection output aborts immediately instead of leaving an `xctrace` child wedged.
- All analysis is based on the XML exported by `xcrun xctrace export`.
- Reports include an `Analysis Quality` section when a successful XML export is empty or contains no recognizable data for the selected parser. Failed exports are reported as `Analysis Inconclusive` under `Export Error`, and the parser is not run against missing or partial XML.
- If you already have a trace, prefer the `_trace` tools to avoid recording again.
- Current Xcode versions expect `xcrun xctrace record --output` paths to use the `.trace` extension; `.xctrace` may be rejected before recording starts.

## Parser Status

Each parser is wired to a specific `xctrace export --xpath` query. The tooling has been validated end-to-end against real `.trace` bundles for the ✅ rows.

| Template | xpath / schema | Status |
| --- | --- | --- |
| Time Profiler | `time-profile` | ✅ validated against real traces |
| App Launch | `time-profile` (CPU samples, idle leaves filtered) | ✅ validated against real traces |
| Allocations | `tracks/track[@name="Allocations"]/details/detail[@name="Statistics"]` | ✅ validated against real traces |
| Leaks | `tracks/track[@name="Leaks"]/details/detail[@name="Leaks"]` | ✅ validated against real traces |
| Network | `network-connection-detected` (socket-level fallback) | ✅ validated against a real physical-device trace; HTTP/HAR rows may be absent if Instruments does not export URLSession task details |

### App Launch caveats

The App Launch parser uses the same `time-profile` schema as Time Profiler and classifies frames into pre-main (dyld / static init / Swift runtime) vs post-main (AppDelegate / UIApplication / Cocoa methods) by symbol heuristic. Two things to keep in mind:

- `total_launch_ms` is **CPU-active** launch time, not wall-clock. xctrace does not expose a clean "app became active" marker we can read.
- Samples whose leaf frame is a known kernel wait (`mach_msg2_trap`, `start_wqthread`, `semaphore_wait_trap`, etc.) are dropped, matching Instruments' default "Hide System Libraries" behaviour. Without this, a long recording that idles after launch would dominate the totals with worker threads parked in wait.

## Troubleshooting

Recording tools run a few pre-flight checks before invoking `xctrace`. A target is rejected up front when `launch_path` does not exist on this Mac, `pid` is not a positive integer or no longer running, or `bundle_id` contains characters Instruments does not accept. When `xctrace` itself fails, the error message is classified per target (bundle not installed, dead process, unresolved device, invalid `launch_path`, missing Xcode tools) and the report includes a fix hint scoped to that target instead of a generic checklist.

If recording fails, check these first:

- The target app is installed on the selected simulator or device, or the local launch path exists.
- The simulator/device is booted and visible in `xcrun xctrace list devices`.
- A physical device must appear under `Devices`, not `Devices Offline`. `devicectl` reporting it as available only confirms CoreDevice connectivity; it does not prove that `xctrace` can record.
- The process exists when using `process_name` or `pid`.
- Xcode and command line tools are selected correctly: `xcode-select -p`.
- The Instruments template exists on your machine: use `list_templates`.
- Physical devices are trusted, unlocked, connected, and allowed to run UI automation.

Use `build_xctrace_command` to inspect the exact command that would run, without recording. The returned command also surfaces any pre-flight warnings detected against the chosen target.

You can also verify `xctrace` directly:

```bash
xcrun xctrace list devices
xcrun xctrace list templates
```

For the currently observed physical-device failure mode, open Xcode, keep the device unlocked, and wait until `xcrun xctrace list devices` moves it from `Devices Offline` to `Devices`. Opening Instruments alone does not establish Xcode's persistent device preparation and is not a substitute. The project still treats requiring the Xcode GUI as a toolchain limitation to investigate, not as the desired long-term workflow.

If `doctor` reports a sandbox restriction, run the MCP server outside that sandbox or grant its process access to the reported state directories. `xctrace` currently needs `~/Library/Caches/com.apple.dt.InstrumentsCLI`; simulator diagnostics also need CoreSimulator state, including `~/Library/Logs/CoreSimulator`. These failures happen before target profiling and must be resolved before interpreting a trace wedge.

## Development

Clone the repository and install the editable build with the `dev` extra:

```bash
git clone git@github.com:dogo/apple-instruments-mcp.git
cd apple-instruments-mcp
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"
```

Run linters, type check, and the test suite:

```bash
ruff check src tests
pyright
PYTHONPATH=src python -m unittest discover -s tests -v
```

The package ships a `py.typed` marker so downstream consumers can type-check against its public API. A `uv.lock` is committed for reproducible dependency installs with [uv](https://github.com/astral-sh/uv); CI runs the same checks across Python 3.11–3.13 and includes a macOS smoke test against the installed `xcrun xctrace`.

Run the MCP server locally:

```bash
python -m apple_instruments_mcp
```

### Releasing

Releases are published to PyPI through GitHub Actions trusted publishing — no API tokens stored in the repo. Steps:

1. Bump `version` in `pyproject.toml`, commit, and push to `main`.
2. Tag the commit with `X.Y.Z` (no `v` prefix) matching that version and push the tag.
3. The `Release` workflow builds `sdist` + `wheel`, verifies the tag matches `pyproject.toml`, and uploads to PyPI via OIDC.

One-time PyPI setup: on https://pypi.org, register the project name and add a Trusted Publisher pointing at this repository, the `release.yml` workflow, and the `pypi` environment.

## License

MIT
