mcpbeat

Expo Tuft Development

expo/expo-tuft-development

Get an Expo app running on the user's phone from a Tuft machine. Read this at the start of any session where the user asks for an app — including brand-new apps, before scaffolding or picking how to deliver it; assume the deliverable is an installable build on their device, not a web preview, unless they say otherwise, and start that build before feature work so it lands mid-session. Also use for EAS account/Apple/device setup via the Tuft dashboard flow, making development builds with EAS, Expo dev-client iteration, remotely exposing Metro with `tuft host`, generating a dev-client deep link, diagnosing device-only failures with Tuft telemetry, deciding whether a native rebuild is required, and handing a working preview back to the user.

5k tokens
context cost
the whole folder, loaded on every use
2
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
0
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/expo/tuft-skills --skill expo-tuft-development

What comes with it

4 866 bytes besides the instruction
scripts/run-metro.sh

The instruction itself

12 sections, as written by the author

Expo + Tuft Development

You are the server

You run on a persistent machine with full control over it, and the machine outlives the conversation. Act like it:

  • The user has one job: use the app on their phone. Everything else — hosting, tokens, process management, provisioning, broken state — is your job on this machine, and you perform it yourself. The user's single hands-on step is the browser sign-in flow behind the Tuft setup link, which only they can complete.
  • The first version of anything must work with zero setup from the user: no accounts to create, no API keys to paste, no auth to configure. You are a server: when the app needs a backend, build the backend — run it on this machine, publish it with tuft host, and point the app at that URL. All the user ever sees is their app working.
  • Answer your own questions before they reach chat, in order: (1) look the answer up on this machine — platform from eas device:list, account from eas whoami, everything about the project from the repository itself; (2) failing that, pick a reasonable default, record it in your next status update, and keep building — a stated default takes the user one message to overturn, whenever they happen to read it. Save chat questions for decisions that are hard to undo: spending money, publishing publicly, deleting data.
  • Long-running processes belong to the machine, not the turn. Run Metro — and any backend you build — as a launchd agent (see “Run Metro for a physical device”) so it survives session teardown, restarts itself after a crash, and always restarts with the exact command and environment pinned in its plist. When something breaks, repair it, re-verify it, and mention the repair in your next update.

Establish context

  • Read the session and repository AGENTS.md files before acting.
  • Locate the Expo project — it lives in its own checkout; the Tuft session root is scratch space.
  • Inspect package.json, app.json/app.config.*, eas.json, the lockfile, and the current Git status.
  • Preserve unrelated user changes. Use the project’s existing package manager.
  • Send a short pre-flight update before editing: state what you found and what you will change.

Start the native build immediately

Native development builds are the primary means of distribution. The deliverable is an installed app on the user's phone; a web preview or Expo Go link is an interim, shared with a label like "here's a preview while the iPhone build finishes."

Builds run on EAS servers and take roughly 10–20 minutes, so sequence a session to hide that latency behind your own work instead of adding it after:

  • At session start, run eas whoami and eas device:list. If either is missing, send the Tuft setup link (see "Set up EAS") in your first message — the user's browser flow is the long pole in the whole session, so start it before writing any code.
  • Scaffold the app and settle its native surface: SDK version, native dependencies, config plugins, URL scheme. Build from the bare native surface — a development build encodes only the native project, and JavaScript loads from Metro afterward, so a build started now is exactly as current as one started after the features exist.
  • Kick off eas build --profile development --platform ios (platform from device:list) in the background the moment the native surface is settled.
  • Build features over Metro while the EAS build runs. When the build lands mid-session, send the install link the moment it arrives.
  • From then on, rebuild only for native changes.

Choose the iteration path

  • Reuse the installed development client for JavaScript, TypeScript, and asset changes that do not alter the native project.
  • When no dev client is installed yet, start the development build now — before feature work, per "Start the native build immediately" — and iterate over Metro while it runs. Also rebuild after adding or changing native dependencies, config plugins, entitlements, URL schemes, capabilities, or native configuration.
  • Prefer npx expo install <package> for Expo-managed dependencies so versions match the SDK.
  • Ship ordinary UI and business-logic changes over Metro alone; the installed client picks them up on reload.

Set up EAS on this machine (the user does it in a browser)

Every eas command that acts on the user’s account — eas build, eas credentials, eas device:*, eas submit, eas update — needs EAS CLI on this machine to be logged in.

  • Check first, non-interactively:
   eas whoami

Success prints the account; failure means the machine is not logged in.

  • When it is not logged in, sign-in happens in the user's browser — send them the Tuft setup link:
   https://dash.tuft.dev/expo/setup

The flow runs entirely in their browser and walks through three steps against this machine: sign EAS CLI in with their Expo account, connect Apple Developer, and register their iPhone. That page is the only channel for credentials — it keeps secrets out of chat, and it stands in for the interactive commands (eas login, eas device:create) that would hang this machine's non-interactive shell.

  • Wait for setup in the background instead of blocking the conversation, and bound the wait:
   timeout 300 bash -c 'until eas whoami >/dev/null 2>&1; do sleep 10; done'

Exit 0 means the login landed; exit 124 means the user has not finished — follow up in chat rather than looping forever.

  • Before a physical-device iOS build, additionally require a registered device:
   eas device:list

If it is empty, point the user back at the same setup link — the last step registers their iPhone by UDID.

Setup state is normal EAS CLI state on this machine; a user who prefers the terminal can run eas login locally and the checks above observe it identically.

Make a development build

  • Ensure eas.json has a development profile; create a minimal one if missing:
   {
     "build": {
       "development": {
         "developmentClient": true,
         "distribution": "internal"
       }
     }
   }
  • Build for the physical device:
   eas build --profile development --platform ios

(Android: --platform android.) iOS internal distribution signs against the registered device list and the Apple Developer access from setup; EAS manages certificates and the provisioning profile remotely; leave signing entirely to it.

  • Builds run on EAS servers. Track progress with the build URL the command prints (or eas build:list). When it finishes, send the user the build page link — it shows the install QR/link for their iPhone.
  • After the dev client is installed once, iterate over Metro (below); rebuild only for native changes.

Run Metro for a physical device

  • Run the bundled script — it performs the whole sequence in the right order and verifies every layer:
   scripts/run-metro.sh <project-dir> <stable-project-name>

The ordering it encodes: publish the port through tuft host first, because Metro embeds EXPO_PACKAGER_PROXY_URL in every manifest and bundle URL it serves — the public URL has to exist before Metro starts. It then runs Metro as a launchd agent with that URL pinned in the plist (so it outlives the turn, and every restart — manual or crash — serves public URLs), picks a free port when other projects' Metros share the machine, and verifies local status, public status, and that the served manifest belongs to this project and references the public host. It prints the public URL, the dev-client deep link, the log path, and the restart/stop commands.

  • Reuse the same name on later turns: the script is idempotent, keeping the existing binding and port and re-verifying the full path. Environment the bundle needs at runtime (telemetry values, EXPO_PUBLIC_*) lives in the project’s env files, which Metro reads on every restart.
  • Send the user both links the script prints: the clickable dev-client URL and the plain HTTPS Metro URL.
  • If the device remains on “Loading from Metro,” re-run the script — it restarts Metro and re-verifies every layer, and its first failing check names the broken one. For a wedged bundle, read the log at the path the script printed, then launchctl kickstart -k and re-verify before resending the same stable link. If all checks pass and the device still spins, compare the installed dev client against the current native project — a native change since the last build means a rebuild.

Instrument with Tuft telemetry

Use telemetry liberally, and instrument up front, as you build each feature. Every device build and every feature you touch should already be emitting events, because the payoff is having the evidence in hand the moment the user hits an error: querying an event that already fired beats asking them to reload the app and reproduce the problem after the fact. Instrument new code as you write it, and err on the side of more decision-boundary events (they are small and cheap). Tuft telemetry is a per-machine event store: the app posts small structured events to this machine’s collector, and you query them with SQL.

  • Provision the stream with the setup_expo_telemetry MCP tool, passing the project’s absolute path. It creates (or reuses) this machine’s stream for the project and returns the collector URL plus, for a new stream, a write-only token as ready-made EXPO_PUBLIC_TUFT_TELEMETRY_* environment values. Pass rotate: true only to deliberately revoke and reissue the token.
  • Put the returned values in an uncommitted env file (for example .env.local). That file is the only place the token — like any credential — ever appears.
  • Install the tuft-telemetry npm package and initialize it once near the app root with those env values.
  • Emit small structured events at decision boundaries. Events carry kind (event or marker), level, name, optional route, trace_id/span_id for grouping a flow, and a payload of attributes:
  • screen or auth flow opened;
  • request started/completed/failed;
  • host and path rather than full URLs;
  • HTTP status and provider;
  • navigation allowed/blocked;
  • merge or hydration outcome;
  • stable anonymous IDs only when necessary.
  • Query with the search_telemetry MCP tool — one bounded read-only SQLite statement over streams(id, name, project_identity, created_at, last_seen_at) and events(id, stream_id, event_id, received_at, occurred_at, launch_id, sequence, kind, level, name, trace_id, span_id, route, update_id, schema_version, payload). Patterns that cover most debugging:
   -- Most recent events for a stream
   SELECT occurred_at, level, name, payload FROM events
   WHERE stream_id = '…' ORDER BY id DESC LIMIT 100;

   -- Recent errors across streams
   SELECT occurred_at, name, payload FROM events
   WHERE level = 'error' ORDER BY id DESC LIMIT 50;

   -- One flow, in order
   SELECT occurred_at, name, payload FROM events
   WHERE trace_id = '…' ORDER BY sequence;

   -- A user-reported marker code
   SELECT occurred_at, payload FROM events
   WHERE kind = 'marker'
     AND json_extract(payload, '$.attributes.reportCode') = 'ABC123'
   ORDER BY id DESC LIMIT 1;
  • For agent-driven reproduction, arm a wait before triggering the action, then have the user (or the app) perform it:
   npx tuft-telemetry wait <event-name> --timeout 30s

It exits the moment the new event arrives (timeout exits 124), so you confirm the exact event fired rather than assuming.

  • On a bug report, query recent telemetry before changing code. Establish the exact failing boundary, then patch it.
  • Confirm the fix using new telemetry after the user retries when the behavior depends on a physical device or third-party service.

Treat HAR files and telemetry payloads as sensitive: extract only the minimum fields needed, and keep any secrets they contain inside the store they came from.

Debug systematically

Follow this loop:

  • Reproduce or inspect telemetry.
  • State the verified cause, distinguishing it from a hypothesis.
  • Add or update a regression test first when the behavior is testable.
  • Make the smallest scoped change.
  • Run the targeted test, then TypeScript and any relevant broader tests.
  • Confirm Metro and the Tuft binding are still healthy.
  • Ask the user to retry only after the updated bundle is reachable.

For third-party authentication:

  • Treat the flow as complete when verifiable success data arrives — a token, profile, or callback payload; navigation alone is progress, not success.
  • Allow required HTTPS challenge and identity-provider pages, and keep navigation rules permissive enough for CAPTCHA and cross-origin dependencies.
  • Intercept custom callback schemes deliberately.
  • Support cookies as required by the flow, and isolate or clear them when the product requires explicit account switching.
  • Log flow outcomes and metadata only; raw credentials, access tokens, refresh tokens, cookies, passkeys, and MFA values stay out of every log.

For map/data-provider work:

  • Keep catalog authority, live hydration, and rendering separate.
  • Preserve provider health separately from an individual station’s absence, so a dead feed reads as “provider down” rather than “station missing.”
  • Log match inputs and outcomes as metadata, with the same secret handling as telemetry.
  • Add fixtures for matching, grouping, availability reconciliation, and unmatched records.

Validate proportionally

At minimum, run:

npm run typecheck
npm test
curl -fsS --max-time 5 http://127.0.0.1:8081/status

Use the repository’s equivalent scripts when these names differ. Report failures exactly, and report a check as passed only after running it.

Hand off

Keep chat updates short:

  • Begin with the outcome.
  • Include the clickable dev-client link when device testing is needed, or the EAS build install link after a new development build.
  • Present the install link as the deliverable; label any earlier web preview as an interim and say where the build stands.
  • When EAS setup is pending, restate the single setup link and the step the user is on.
  • State which tests passed.
  • Mention whether a new native build is required.
  • If waiting for a device retry, say exactly what action to take and watch telemetry for the result.

Every URL you send must be reachable from the user’s device: for anything served from this machine, that is its persistent tuft host URL.

How to use it

Copy the folder

Take expo/expo-tuft-development from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npx. Without those the skill loads but fails at the first command.