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 holaos app", "帮我创建一个 holaos app", "build a holaos app for ", 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>"
# e.g.: bash scripts/scaffold-from-gcal.sh stripe stripe "Stripe" Stripe
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 # required first time on macOS
pnpm run typecheck # vite.config.ts will warn — that's a pre-existing gcalendar issue, ignore
pnpm run test:e2e # all green
6. Register the app
Three files in the repo root — see references/registration.md for full schemas:
marketplace.json— append entry withname,description,icon,category,tags,path,provider_id,credential_source: "platform"pnpm-workspace.yaml— add- "<slug>"(alphabetical order).github/workflows/build-apps.yml— append<slug>toLEGACY_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_actionswith<slug>_record_id,<slug>_deep_link, outcome, duration. Don't skip this. - Deep links: when the provider has a web UI, set
<slug>_deep_linkto 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 viaPORT/MCP_PORTenv).
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 —
threadswas discovered missing only after the app shipped. - Don't add a local app→provider table in desktop. The
provider_idfield inapp.runtime.yamlflows throughmarketplace.json→ backend → desktop catalog. Adding apps requires zero desktop edits. pnpm rebuild better-sqlite3is 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.yamlreferences/registration.md— exact format of marketplace.json entries, pnpm-workspace.yaml, build-apps.yml diffsreferences/live-testing.md— composio:broker setup, per-provider connect commands, env vars for write-path tests