Skip to content

Commit 13f64f0

Browse files
committed
Fix
1 parent f1b9c02 commit 13f64f0

File tree

22 files changed

+470
-227
lines changed

22 files changed

+470
-227
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
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

.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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"format": "prettier --write 'src/**/*.{js,ts}'",
2525
"format:check": "prettier --check 'src/**/*.{js,ts}'",
2626
"typecheck": "npx tsc --noEmit",
27+
"verify:smithery-bundle": "scripts/verify-smithery-bundle.sh",
2728
"inspect": "npx @modelcontextprotocol/inspector node build/index.js",
2829
"doctor": "node build/doctor-cli.js",
2930
"tools": "npx tsx scripts/tools-cli.ts",

scripts/bundle-axe.sh

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
exit 1
13+
fi
14+
15+
if [ ! -d "$FRAMEWORK_DIR" ]; then
16+
echo "❌ Missing Frameworks directory at $FRAMEWORK_DIR"
17+
exit 1
18+
fi
19+
20+
FRAMEWORK_COUNT="$(find "$FRAMEWORK_DIR" -maxdepth 2 -type d -name "*.framework" | wc -l | tr -d ' ')"
21+
if [ "$FRAMEWORK_COUNT" -eq 0 ]; then
22+
echo "❌ No frameworks found in $FRAMEWORK_DIR"
23+
exit 1
24+
fi
25+
26+
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',

src/mcp/tools/doctor/lib/doctor.deps.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
collectToolNames,
77
resolveSelectedWorkflows,
88
} from '../../../../utils/workflow-selection.ts';
9-
import { areAxeToolsAvailable } from '../../../../utils/axe/index.ts';
9+
import { areAxeToolsAvailable, resolveAxeBinary } from '../../../../utils/axe/index.ts';
1010
import {
1111
isXcodemakeEnabled,
1212
isXcodemakeAvailable,
@@ -98,9 +98,26 @@ export interface DoctorDependencies {
9898
export function createDoctorDependencies(executor: CommandExecutor): DoctorDependencies {
9999
const binaryChecker: BinaryChecker = {
100100
async checkBinaryAvailability(binary: string) {
101-
// If bundled axe is available, reflect that in dependencies even if not on PATH
102-
if (binary === 'axe' && areAxeToolsAvailable()) {
103-
return { available: true, version: 'Bundled' };
101+
if (binary === 'axe') {
102+
const axeBinary = resolveAxeBinary();
103+
if (!axeBinary) {
104+
return { available: false };
105+
}
106+
107+
let version: string | undefined;
108+
try {
109+
const res = await executor([axeBinary.path, '--version'], 'Get AXe Version');
110+
if (res.success && res.output) {
111+
version = res.output.trim();
112+
}
113+
} catch {
114+
// ignore
115+
}
116+
117+
return {
118+
available: true,
119+
version: version ?? 'Available (version info not available)',
120+
};
104121
}
105122
try {
106123
const which = await executor(['which', binary], 'Check Binary Availability');
@@ -113,7 +130,6 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen
113130

114131
let version: string | undefined;
115132
const versionCommands: Record<string, string> = {
116-
axe: 'axe --version',
117133
mise: 'mise --version',
118134
};
119135

0 commit comments

Comments
 (0)