| name | create-holaos-app |
| description | Build a new holaOS App — a TanStack Start MCP server that proxies a third-party API through Composio. Use when the user says "create a holaos app", "make me a <provider> holaos app", "帮我创建一个 <provider> holaos app", "build a holaos app for <X>", or names a Composio toolkit they want wired up that way (e.g. "shopify holaos app", "klaviyo holaos app", "linear holaos app"). Covers the full flow end-to-end: Composio toolkit verification, scaffold from gcalendar, provider-specific client + tools + connection probe, audit log, e2e + live tests, marketplace + workspace + CI registration. |
Create a holaOS App
A holaOS App is a self-contained TanStack Start application that exposes a Composio-backed third-party service over MCP. Each app: own SQLite, own MCP server (SSE on /mcp/sse), own audit log, own tiny web UI. No shared packages — copy-paste over abstraction.
When to use
User asks for a new holaOS App wrapping a Composio toolkit (e.g. "create a shopify holaos app", "帮我做一个 figma holaos app", "build a holaos app for linear"). Existing examples in repo: stripe, figma, calendly, discordbot, linear, gcalendar, gdrive, slack, youtube, mailchimp, notion, instagram.
For publishing-style apps (drafts → queue → publish, e.g. twitter/linkedin/reddit), this skill does NOT apply — those follow a different pattern with a SQLite job queue and publisher.ts.
The 7-step flow
1. Verify the Composio toolkit exists FIRST
Don't assume — Threads / TikTok / Pinterest / Buffer are NOT in Composio. Curl the catalog before making promises.
COMPOSIO_API_KEY=<key> curl -s "https://backend.composio.dev/api/v3/toolkits?limit=500" \
-H "x-api-key: $COMPOSIO_API_KEY" -o /tmp/toolkits.json
python3 -c 'import json; d=json.load(open("/tmp/toolkits.json"))
for t in d["items"]:
if "<keyword>" in t["slug"].lower(): print(t["slug"], t.get("composio_managed_auth_schemes"))'
Note composio_managed_auth_schemes: if it's ["OAUTH2"], the toolkit is managed (zero-config user OAuth). If empty, the user must pass --api-key/credentials at connect time. Both work; managed is preferred.
API key location: frontend/apps/server/.env → COMPOSIO_API_KEY.
2. Scaffold from gcalendar
cd /Users/joshua/holaboss-ai/holaboss/hola-boss-apps
bash scripts/scaffold-from-gcal.sh <slug> <provider> "<Display>" "<Pascal>"
The script clones gcalendar/, sed-renames prefix tokens (gcal_* → <slug>_*, GCalError → <Pascal>Error, GCAL_BASE → <UPPER>_BASE, etc.) and clears the eight files you must rewrite per-provider.
Then update package.json name:
sed -i '' "s/\"name\": \"gcalendar\"/\"name\": \"<slug>\"/" <slug>/package.json
3. Write the provider-specific files
Eight files per app — see references/file-templates.md for working snippets:
| File | Content |
|---|
app.runtime.yaml | mcp.tools list (+ tool prefix), data_schema (audit/usage/settings tables), integration.destination = Composio slug |
src/lib/types.ts | Error code enum, ToolSuccessMeta, <UPPER>_CONFIG |
src/server/<slug>-client.ts | createIntegrationClient(<slug>) proxy + status mapping (200→ok, 404→not_found, 429→rate_limited, 401/403→not_connected, 4xx→validation_failed, 5xx→upstream_error) |
src/server/connection.ts | Cheap identity probe (e.g. GET /me or GET /account) |
src/server/mcp.ts | name: "<Display> App" |
src/server/tools.ts | 8–13 MCP tools: registerTools + impl functions exported for tests |
src/components/connection-status-bar.tsx | Polls /api/connection-status |
src/routes/__root.tsx + src/routes/index.tsx | Page title + heading |
Tool naming: snake_case, prefixed with the brand (stripe_list_customers, discord_send_channel_message even when slug is discordbot). Tool descriptions follow hola-boss-apps/docs/MCP_TOOL_DESCRIPTION_CONVENTION.md — agent only sees name+description+inputSchema+annotations.
Mandatory tool: <prefix>_get_connection_status. Always first in app.runtime.yaml's mcp.tools. Wraps getConnectionStatus() from connection.ts.
GraphQL APIs (Linear): write a gql<T>(query, vars) helper instead of apiGet/apiPost. Demote GraphQL errors[] to canonical Holaboss codes by extensions.type (AUTHENTICATION_ERROR → not_connected, INVALID_INPUT → validation_failed, etc.). See linear/src/server/linear-client.ts.
4. Write tests
test/e2e.test.ts — uses MockBridge (already cloned in fixtures/), 5–6 cases:
- MCP health endpoint live
- get_connection_status with bridge succeeding
- One write tool writes audit row with correct
<slug>_record_id + deep_link
- not_connected short-circuits when bridge throws
- validation_failed maps a 400
- (provider-specific) rate_limited maps 429 with retry_after
test/live.test.ts — describe.skipIf(!process.env.LIVE), shape-only assertions, write tests gated behind LIVE_WRITE=1 plus an env-var-named test resource id (so a test runner can never delete real customer data by accident).
See references/file-templates.md for skeletons.
5. Verify locally
cd <slug>
pnpm rebuild better-sqlite3
pnpm run typecheck
pnpm run test:e2e
6. Register the app
Three files in the repo root — see references/registration.md for full schemas:
marketplace.json — append entry with name, description, icon, category, tags, path, provider_id, credential_source: "platform"
pnpm-workspace.yaml — add - "<slug>" (alphabetical order)
.github/workflows/build-apps.yml — append <slug> to LEGACY_MODULES (env-var name is historical; it's the build allowlist)
Then from repo root: pnpm install to register the new workspace package.
7. Live test (the user runs this; we just commit)
User starts the broker, connects each provider once via OAuth, then LIVE=1 pnpm --filter <slug> run test:live. Connection metadata persists in .composio-connections.json (gitignored). Full instructions in references/live-testing.md.
Conventions you must follow
- Audit log: every tool wrapped via
wrapTool("<prefix>_<name>", impl) — writes a row to <slug>_agent_actions with <slug>_record_id, <slug>_deep_link, outcome, duration. Don't skip this.
- Deep links: when the provider has a web UI, set
<slug>_deep_link to the canonical URL for that resource (e.g. https://dashboard.stripe.com/customers/cus_xyz). Agents surface these to users.
- Error envelope: only the seven canonical codes (
not_found, invalid_state, validation_failed, not_connected, rate_limited, upstream_error, internal). Don't invent new ones.
- No raw credentials: ALL provider calls go through
createIntegrationClient(<slug>).proxy(...). The bridge prepends auth. Never read tokens from env or .env.
- Live tests are shape-only:
expect(Array.isArray(r.data.x)).toBe(true) — never assert on values, since user data drifts.
- One container, two processes: web app on
:3000, MCP server on :3099 (overridden by sandbox runtime via PORT/MCP_PORT env).
Pitfalls (from prior incidents)
- Don't skip the catalog probe (step 1). Building an app for a slug that doesn't exist on Composio is wasted work —
threads was discovered missing only after the app shipped.
- Don't add a local app→provider table in desktop. The
provider_id field in app.runtime.yaml flows through marketplace.json → backend → desktop catalog. Adding apps requires zero desktop edits.
pnpm rebuild better-sqlite3 is required the first time you run e2e tests on macOS; the prebuilt native binding doesn't always resolve correctly across new pnpm projects.
- MockBridge matches via
endsWith — query strings break suffix matchers. Test impls without query params, or refactor the impl to the bare endpoint, or extend mock-bridge if you really need it.
References (load on demand)
references/file-templates.md — copy-pasteable skeletons for client.ts, connection.ts, tools.ts, e2e.test.ts, live.test.ts, app.runtime.yaml
references/registration.md — exact format of marketplace.json entries, pnpm-workspace.yaml, build-apps.yml diffs
references/live-testing.md — composio:broker setup, per-provider connect commands, env vars for write-path tests