Skip to content

Commit 373f870

Browse files
authored
Fix bug where AXe isn't bundled for Smithery installs, also improve AXe detection logic (#164)
Also: * Bundle AXe for CI runs to ensure StackBlitz deployments include AXe * Modernise package
1 parent f1b9c02 commit 373f870

24 files changed

Lines changed: 489 additions & 237 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ jobs:
2626
- name: Install dependencies
2727
run: npm ci
2828

29-
- name: Build (tsup)
30-
run: npm run build:tsup
29+
- name: Bundle AXe artifacts
30+
run: npm run bundle:axe
3131

3232
- name: Build (Smithery)
3333
run: npm run build
3434

35+
- name: Verify Smithery bundle
36+
run: npm run verify:smithery-bundle
37+
3538
- name: Lint
3639
run: npm run lint
3740

.github/workflows/release.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ jobs:
4848
- name: Bundle AXe artifacts
4949
run: npm run bundle:axe
5050

51-
- name: Build TypeScript (tsup)
52-
run: npm run build:tsup
53-
5451
- name: Build Smithery bundle
5552
run: npm run build
5653

.smithery/index.cjs

Lines changed: 134 additions & 134 deletions
Large diffs are not rendered by default.

docs/CONFIGURATION.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ Leave this unset for the streamlined session-aware experience; enable it to forc
5151

5252
If you do not wish to send error logs to Sentry, set `XCODEBUILDMCP_SENTRY_DISABLED=true`.
5353

54+
## AXe binary override
55+
56+
UI automation and simulator video capture require the AXe binary. By default, XcodeBuildMCP uses the bundled AXe when available, then falls back to `PATH`. To force a specific binary location, set `XCODEBUILDMCP_AXE_PATH` (preferred). `AXE_PATH` is also recognized for compatibility.
57+
58+
Example:
59+
60+
```
61+
XCODEBUILDMCP_AXE_PATH=/opt/axe/bin/axe
62+
```
63+
5464
## Related docs
5565
- Session defaults: [SESSION_DEFAULTS.md](SESSION_DEFAULTS.md)
5666
- Tools reference: [TOOLS.md](TOOLS.md)

docs/TROUBLESHOOTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ It reports on:
2525
2626
## Common issues
2727

28+
### UI automation reports missing AXe
29+
UI automation (describe/tap/swipe/type) and simulator video capture require the AXe binary. If you see a missing AXe error:
30+
- Ensure `bundled/` artifacts exist when installing from Smithery or npm.
31+
- Or set `XCODEBUILDMCP_AXE_PATH` to a known AXe binary path (preferred), or `AXE_PATH`.
32+
- Re-run the doctor tool to confirm AXe is detected.
33+
2834
### Tool timeouts
2935
Some clients have short tool timeouts. If you see timeouts, increase the client timeout (for example, `tool_timeout_sec = 600` in Codex).
3036

docs/investigations/issue-163.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Investigation: UI automation tools unavailable with Smithery install (issue #163)
2+
3+
## Summary
4+
Smithery installs ship only the compiled entrypoint, while the server hard-requires a bundled `bundled/axe` path derived from `process.argv[1]`. This makes UI automation (and simulator video capture) fail even when system `axe` exists on PATH, and Doctor can report contradictory statuses.
5+
6+
## Symptoms
7+
- UI automation tools (`describe_ui`, `tap`, `swipe`, etc.) fail with "Bundled axe tool not found. UI automation features are not available."
8+
- `doctor` reports system axe present, but UI automation unavailable due to missing bundled binary.
9+
- Smithery cache lacks `bundled/axe` directory; only `index.cjs`, `manifest.json`, `.metadata.json` present.
10+
11+
## Investigation Log
12+
13+
### 2026-01-06 - Initial Assessment
14+
**Hypothesis:** Smithery packaging omits bundled binaries and server does not fallback to system axe.
15+
**Findings:** Issue report indicates bundled path is computed relative to `process.argv[1]` and Smithery cache lacks `bundled/`.
16+
**Evidence:** GitHub issue #163 body (Smithery cache contents; bundled path logic).
17+
**Conclusion:** Needs code and packaging investigation.
18+
19+
### 2026-01-06 - AXe path resolution and bundled-only assumption
20+
**Hypothesis:** AXe resolution is bundled-only, so missing `bundled/axe` disables tools regardless of PATH.
21+
**Findings:** `getAxePath()` computes `bundledAxePath` from `process.argv[1]` and returns it only if it exists; otherwise `null`. No PATH or env override.
22+
**Evidence:** `src/utils/axe-helpers.ts:15-36`
23+
**Conclusion:** Confirmed. Smithery layout lacking `bundled/` will always return null.
24+
25+
### 2026-01-06 - UI automation and video capture gating
26+
**Hypothesis:** UI tools and video capture preflight fail when `getAxePath()` returns null.
27+
**Findings:** UI tools call `getAxePath()` and throw `DependencyError` if absent; `record_sim_video` preflights `areAxeToolsAvailable()` and `isAxeAtLeastVersion()`; `startSimulatorVideoCapture` returns error if `getAxePath()` is null.
28+
**Evidence:** `src/mcp/tools/ui-testing/describe_ui.ts:150-164`, `src/mcp/tools/simulator/record_sim_video.ts:80-88`, `src/utils/video_capture.ts:92-99`
29+
**Conclusion:** Confirmed. Missing bundled binary blocks all UI automation and simulator video capture.
30+
31+
### 2026-01-06 - Doctor output inconsistency
32+
**Hypothesis:** Doctor uses different checks for dependency presence vs feature availability.
33+
**Findings:** Doctor uses `areAxeToolsAvailable()` (bundled-only) for UI automation feature status, while dependency check can succeed via `which axe` when bundled is missing.
34+
**Evidence:** `src/mcp/tools/doctor/doctor.ts:49-68`, `src/mcp/tools/doctor/lib/doctor.deps.ts:100-132`
35+
**Conclusion:** Confirmed. Doctor can report `axe` dependency present but UI automation unsupported.
36+
37+
### 2026-01-06 - Packaging/Smithery artifact mismatch
38+
**Hypothesis:** NPM releases include `bundled/`, Smithery builds do not.
39+
**Findings:** `bundle:axe` creates `bundled/` and npm packaging includes it, but Smithery config has no asset inclusion hints. Release workflow bundles AXe before publish.
40+
**Evidence:** `package.json:21-44`, `.github/workflows/release.yml:48-55`, `smithery.yaml:1-3`, `smithery.config.js:1-6`
41+
**Conclusion:** Confirmed. Smithery build output likely omits bundled artifacts unless explicitly configured.
42+
43+
### 2026-01-06 - Smithery local server deployment flow
44+
**Hypothesis:** Smithery deploys local servers from GitHub pushes and expects build-time packaging to include assets.
45+
**Findings:** README install flow uses Smithery CLI; `smithery.yaml` targets `local`. `bundled/` is gitignored, so it must be produced during Smithery’s deployment build. Current `npm run build` does not run `bundle:axe`.
46+
**Evidence:** `README.md:11-74`, `smithery.yaml:1-3`, `.github/workflows/release.yml:48-62`, `.gitignore:66-68`
47+
**Conclusion:** Confirmed. Smithery deploy must run `bundle:axe` and explicitly include `bundled/` in the produced bundle.
48+
49+
### 2026-01-06 - Smithery config constraints and bundling workaround
50+
**Hypothesis:** Adding esbuild plugins in `smithery.config.js` overrides Smithery’s bootstrap plugin.
51+
**Findings:** Smithery CLI merges config via spread and replaces `plugins`, causing `virtual:bootstrap` resolution to fail when custom plugins are supplied. Side-effect bundling in `smithery.config.js` avoids plugin override and can copy `bundled/` into `.smithery/`.
52+
**Evidence:** `node_modules/@smithery/cli/dist/index.js:~2716600-2717500`, `smithery.config.js:1-47`
53+
**Conclusion:** Confirmed. Bundling must run outside esbuild plugins; Linux builders must skip binary verification.
54+
55+
## Root Cause
56+
Two coupled assumptions break Smithery installs:
57+
1) `getAxePath()` is bundled-only and derives the path from `process.argv[1]`, which points into Smithery’s cache (missing `bundled/axe`), so it always returns null.
58+
2) Smithery packaging does not include the `bundled/` directory, so the bundled-only resolver can never succeed under Smithery even if AXe is installed system-wide.
59+
60+
## Recommendations
61+
1. Add a robust AXe resolver: allow explicit env override and PATH fallback; keep bundled as preferred but not exclusive.
62+
2. Distinguish bundled vs system AXe in UI tools and video capture; only apply bundled-specific env when the bundled binary is used.
63+
3. Align Doctor output: show both bundled availability and PATH availability, and use that in the UI automation supported status.
64+
4. Update Smithery build to run `bundle:axe` and copy `bundled/` into the Smithery bundle output; skip binary verification on non-mac builders to avoid build failures.
65+
66+
## Preventive Measures
67+
- Add tests for AXe resolution precedence (bundled, env override, PATH) and for Doctor output consistency.
68+
- Document Smithery-specific install requirements and verify `bundled/` presence in Smithery artifacts during CI.

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
"mcpName": "com.xcodebuildmcp/XcodeBuildMCP",
55
"iOSTemplateVersion": "v1.0.8",
66
"macOSTemplateVersion": "v1.0.5",
7-
"main": "build/index.js",
87
"type": "module",
9-
"module": "src/smithery.ts",
8+
"exports": {
9+
".": "./build/index.js",
10+
"./package.json": "./package.json"
11+
},
1012
"bin": {
1113
"xcodebuildmcp": "build/index.js",
1214
"xcodebuildmcp-doctor": "build/doctor-cli.js"
1315
},
1416
"scripts": {
15-
"build": "npm run generate:version && npm run generate:loaders && npx smithery build",
17+
"build": "npm run build:tsup && npm run build:smithery",
18+
"build:smithery": "npx smithery build src/smithery.ts",
1619
"dev": "npm run generate:version && npm run generate:loaders && npx smithery dev",
1720
"build:tsup": "npm run generate:version && npm run generate:loaders && tsup",
1821
"dev:tsup": "npm run build:tsup && tsup --watch",
@@ -24,6 +27,7 @@
2427
"format": "prettier --write 'src/**/*.{js,ts}'",
2528
"format:check": "prettier --check 'src/**/*.{js,ts}'",
2629
"typecheck": "npx tsc --noEmit",
30+
"verify:smithery-bundle": "bash scripts/verify-smithery-bundle.sh",
2731
"inspect": "npx @modelcontextprotocol/inspector node build/index.js",
2832
"doctor": "node build/doctor-cli.js",
2933
"tools": "npx tsx scripts/tools-cli.ts",

scripts/bundle-axe.sh

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ else
9797
tar -xzf "axe-release.tar.gz"
9898

9999
# Find the extracted directory (might be named differently)
100-
EXTRACTED_DIR=$(find . -type d -name "*AXe*" -o -name "*axe*" | head -1)
100+
EXTRACTED_DIR=$(find . -type d \( -name "*AXe*" -o -name "*axe*" \) | head -1)
101101
if [ -z "$EXTRACTED_DIR" ]; then
102102
# If no AXe directory found, assume files are in current directory
103103
EXTRACTED_DIR="."
@@ -144,17 +144,23 @@ echo "📦 Copied $FRAMEWORK_COUNT frameworks"
144144
echo "🔍 Bundled frameworks:"
145145
ls -la "$BUNDLED_DIR/Frameworks/"
146146

147-
# Verify binary can run with bundled frameworks
148-
echo "🧪 Testing bundled AXe binary..."
149-
if DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version > /dev/null 2>&1; then
150-
echo "✅ Bundled AXe binary test passed"
147+
# Verify binary can run with bundled frameworks (macOS only)
148+
OS_NAME="$(uname -s)"
149+
if [ "$OS_NAME" = "Darwin" ]; then
150+
echo "🧪 Testing bundled AXe binary..."
151+
if DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version > /dev/null 2>&1; then
152+
echo "✅ Bundled AXe binary test passed"
153+
else
154+
echo "❌ Bundled AXe binary test failed"
155+
exit 1
156+
fi
157+
158+
# Get AXe version for logging
159+
AXE_VERSION=$(DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version 2>/dev/null || echo "unknown")
151160
else
152-
echo "❌ Bundled AXe binary test failed"
153-
exit 1
161+
echo "⚠️ Skipping AXe binary verification on non-macOS (detected $OS_NAME)"
162+
AXE_VERSION="unknown (verification skipped)"
154163
fi
155-
156-
# Get AXe version for logging
157-
AXE_VERSION=$(DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version 2>/dev/null || echo "unknown")
158164
echo "📋 AXe version: $AXE_VERSION"
159165

160166
# Clean up temp directory if it was used

scripts/verify-smithery-bundle.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
BUNDLE_DIR="$PROJECT_ROOT/.smithery/bundled"
7+
AXE_BIN="$BUNDLE_DIR/axe"
8+
FRAMEWORK_DIR="$BUNDLE_DIR/Frameworks"
9+
10+
if [ ! -f "$AXE_BIN" ]; then
11+
echo "❌ Missing AXe binary at $AXE_BIN"
12+
if [ -d "$PROJECT_ROOT/.smithery" ]; then
13+
echo "🔍 .smithery contents:"
14+
ls -la "$PROJECT_ROOT/.smithery"
15+
fi
16+
exit 1
17+
fi
18+
19+
if [ ! -d "$FRAMEWORK_DIR" ]; then
20+
echo "❌ Missing Frameworks directory at $FRAMEWORK_DIR"
21+
if [ -d "$BUNDLE_DIR" ]; then
22+
echo "🔍 bundled contents:"
23+
ls -la "$BUNDLE_DIR"
24+
fi
25+
exit 1
26+
fi
27+
28+
FRAMEWORK_COUNT="$(find "$FRAMEWORK_DIR" -maxdepth 2 -type d -name "*.framework" | wc -l | tr -d ' ')"
29+
if [ "$FRAMEWORK_COUNT" -eq 0 ]; then
30+
echo "❌ No frameworks found in $FRAMEWORK_DIR"
31+
find "$FRAMEWORK_DIR" -maxdepth 2 -type d | head -n 50
32+
exit 1
33+
fi
34+
35+
echo "✅ Smithery bundle includes AXe binary and $FRAMEWORK_COUNT frameworks"

smithery.config.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
import { execFileSync } from 'child_process';
2+
import { cpSync, existsSync, mkdirSync } from 'fs';
3+
import { dirname, join, resolve } from 'path';
4+
5+
const projectRoot = process.cwd();
6+
const bundledDir = join(projectRoot, 'bundled');
7+
const bundledAxePath = join(bundledDir, 'axe');
8+
9+
function resolveOutputDir() {
10+
const args = process.argv;
11+
const outIndex = args.findIndex((arg) => arg === '--out' || arg === '-o');
12+
if (outIndex !== -1 && args[outIndex + 1]) {
13+
return dirname(resolve(args[outIndex + 1]));
14+
}
15+
return join(projectRoot, '.smithery');
16+
}
17+
18+
const outputDir = resolveOutputDir();
19+
const bundledTargetDir = join(outputDir, 'bundled');
20+
21+
if (!existsSync(bundledAxePath)) {
22+
execFileSync('bash', [join(projectRoot, 'scripts', 'bundle-axe.sh')], {
23+
stdio: 'inherit',
24+
});
25+
}
26+
27+
if (existsSync(bundledAxePath)) {
28+
mkdirSync(outputDir, { recursive: true });
29+
cpSync(bundledDir, bundledTargetDir, { recursive: true });
30+
} else {
31+
throw new Error(`AXe bundle missing at ${bundledAxePath}`);
32+
}
33+
134
export default {
235
esbuild: {
336
format: 'cjs',

0 commit comments

Comments
 (0)