A thin, opinionless layer over a profoundly opinionated craft.
Scripture App Builder is the SIL-built tool translation teams have used since 2015 to package Paratext projects, MP3 audio, timing files, color schemes, splash screens and signing keys into a real Android or iOS application — shipped to the playstore, sideloaded, or carried into a village on a microSD card. It's free, it's astonishing, and it has hundreds of opinionated settings the agent has no business pretending to know. The MCP server here doesn't pretend to know any of that either. It exposes filesystem-shaped IO, content-addressed job submission, and gets out of the way.
The opinions live next door, in a canon repository served by oddkit. The agent talks to one MCP — this one. The docs(query) tool proxies canon retrieval upstream, so the agent's loop is ask docs · understand · act · observe across a single connection.
For translation teams
Hand a translation agent your Paratext project — any language, any script, any audio bundle — and get a real, signed, installable app back. The agent knows when to ask, what to tweak, and when to stop.
For agent builders
Three async tools. No domain quiz to pass. Submit a build, poll for status, cancel if it overruns. The server takes care of Gradle, the Android SDK, signing, audio sync, splash variants, and surfacing failures in language a model can reason about.
For systems people
Cloudflare Worker dispatches via service binding into a Container running SAB + Gradle + Android SDK + aeneas (audio sync). Durable Objects hold per-job state. R2 stores content-addressed .apk / .ipa outputs. SHA-256 of the canonical payload is the cache key.
"I am very grateful for the app of our culture, the New Testament app. I love this because God gives each ethnic group their culture to value." Pastor Arcesio · Guahibo people group · on a Scripture app in his language
Submit a build. Get a real, signed APK.
Both demo payloads are checked-in smoke fixtures from the repo's smoke/ directory and have already been built once, so they cache-hit and return instantly — zero container CPU. The artifact card below is a real R2-served APK.
Ask the docs tool anything.
The MCP server's docs(query) tool searches the project's canon — prose articles, build manifests, app.json schemas, and governance documents that give an agent enough context to drive Scripture App Builder. Type a question; see the actual answer plus the canon URIs that backed it.
Aeneas needs a phonetic profile to align audio to text. For a language with no eSpeak voice, SAB falls back to "sentence-level forced alignment" using a graphemic surrogate (closest eSpeak language) plus the language's own SFM verse markers as anchor points.
The agent should set audio.sync = "sentence" and provide a per-book timings/{book}.{chapter}.csv if greater precision is required. The build will not fail without timings — it will warn and proceed with verse-level highlighting only.
Three tools. One contract.
A signed Android build for a New Testament with audio takes 8–25 minutes. iOS with provisioning takes longer. Synchronous tools collide with every chat-shaped surface in existence. So the protocol is async: submit returns immediately, status is pollable, cancellation is honored.
submit_build
Hand it a project, an app.json, a target platform, signing config. Returns a job_id immediately and a predicted artifact URL. Identical payloads cache-hit.
// returns immediately { job_id: "4a17b9c2…", payload_hash: "4a17b9c2…", cached: true, predicted_artifact_url: "…/r2/…/apk", predicted_size_mb: 148 }
get_job_status
Per-stage progress, log tail, error list, gradle warning count. A human_summary string for downstream chat agents.
{
state: "succeeded",
stage: 7/7,
current: "sign-and-zipalign",
warnings: 3,
errors: [],
human_summary:
"Built. Signed. 148 MB."
}
cancel_job
A 25-minute Gradle build needs a kill switch. SIGTERM to the subprocess; partial outputs preserved on disk; state moves to cancelled.
{
ok: true,
was_running: false,
cancelled_at: "2026-05-01T13:24:00Z",
partial_artifacts: [
"…/staging/manifest.xml"
]
}
SHA-256 of the canonical payload (RFC 8785 JCS) is the only cache key.
No TTL. Identical builds cost zero CPU and return the same R2 artifact. Source of truth, not approximation.
Per-job timeout in the request, default 25 min for Android, 45 for iOS.
No platform-edge timeout exposed to the caller. The Worker hands off to a Container; the Container does the long work.
Per-stage, not per-percent.
Gradle doesn't expose useful per-percent progress for SAB. An honest "stage 4 of 7: synchronizing audio" beats fabricated bars.
// note · the three async tools are the build contract. The full live surface also exposes docs(query) for canon retrieval (proxied to oddkit) and telemetry_public · telemetry_policy · telemetry_schema for transparency (seven tools total). README and page enumerations drift — the deploy is authoritative. Ask tools/list against /mcp for the current surface.
No information asymmetry.
Every tool call against appbuilder.klappy.dev writes one structural data point to appbuilder_telemetry. Same data the maintainer sees, queried over MCP from this page in your browser, right now. Identify yourself with an x-appbuilder-client header and you'll appear on the consumer leaderboard below.
- isubmit_build—
- iiget_job_status—
- iiicancel_job—
- ivdocs—
- vtelemetry_public—
- vitelemetry_policy—
- viitelemetry_schema—
// querying telemetry_public…
- no consumers yet — set x-appbuilder-client to appear here
Vodka architecture.
Vodka architecture — coined in the broader klappy canon and applied across oddkit, ptxprint-mcp, and now this server — says each MCP server holds opinions about exactly one concern. The AppBuilder server holds none about translation craft, font resolution, or app design — only about subprocess lifecycle, content-addressed caching, signing-key vault access, and sandboxed file IO. Domain knowledge lives next door, in canon. Agents see one MCP; AppBuilder delegates canon retrieval to oddkit upstream when serving docs().
Agent
CLAUDE / GEMMA
GPT / OSS
appbuilder MCP
- submit · status · cancel
- docs · telemetry · policy
thin layer · zero domain opinions
SAB Builder
SAB · Gradle
Android SDK · Xcode
aeneas · eSpeak
DO + R2
per-job state
SHA-256 cache key
signed APK / IPA
Signing keys
Worker secrets
per-project release certs
iOS provisioning
oddkit MCP
canon retrieval
(invisible to agent)
ask docs · understand · act · observe
One server. One concern.
Two services in concert —
one of them invisible to the agent.
Opinionless server
No app.json validation. No font tables. No splash compositing rules. The server treats every file as opaque text and every subprocess as opaque action.
Content-addressed
Cache keys are SHA-256 hashes (RFC 8785 JCS) of the canonical payload. No TTL. No staleness. Two identical builds share one APK.
Async by design
Cloudflare's 30s Worker timeout collides with 25-minute Gradle builds. The two-step contract is the only honest answer.
Canon-governed
Every architectural decision is encoded in OLDC+H artifacts and stored under canon/. The repo is the spec.
Built on the shoulders of two giants.
Cloudflare
- WorkersMCP transport, auth, dispatch via service binding
- ContainersSAB + Gradle + Android SDK + aeneas (standard-3: 2 vCPU, 12 GiB)
- Durable Objectsper-job state, cancellation, polling
- R2content-addressed APK / IPA / build-log storage
- Secretssigning keys, provisioning profiles, vault
- Analytics Enginepublic usage telemetry · zero asymmetry
SIL & Paratext
- Scripture App BuilderSIL Global · pinned at v14.0 (released 24 Apr 2026) · headless build mode
- ParatextUSFM source, project conventions, scripture identifiers
- aeneasforced audio-text alignment for synchronized highlighting
- eSpeak NGphonetic surrogate engine for unsupported languages
- SIL Charis / Andikascript-aware fonts bundled by default
- BCP 47 + LFFlanguage tag → font + voice resolution
Built in canon-governed sessions for translation teams who need the shop floor to move at the speed of a conversation.