Build your own interface
A developer guide to building a custom app or UI directly on the Kula Intelligence MCP — connection and scopes, execute_query gotchas, the unified schema, and a worked reference implementation.
Kula Intelligence is a read-only AI layer over a studio's operational data: a Model Context Protocol (MCP) control plane that normalises GymMaster / Mindbody / Wix / Stripe / Xero etc. into one schema and exposes it through a small set of safe, org-scoped tools.
Any client that speaks MCP can read it — Claude has its own connect guide, and Cursor, Lovable and other clients are covered in Connect. This guide is for developers building their own app or UI directly against the MCP — a custom dashboard, a payroll tool, an internal report. A complete, open-source worked example exists: the Instructor Bonus Builder (github.com/kulaforbusiness/instructor-bonus-builder, live demo).
1. Get a connection (endpoint + token)
In your operator console (kula-org-admin): Settings → Integrations → MCP Tokens.
Create a token. Pick the scope matching what your app does:
ScopeSeesUse foranalyticsaggregates only, no names
charts, totals, anonymous analysis
operationsnames, no emails/phones
most apps — rosters, schedules, reports
admin+ emails/phones
CRM-style tools, member contact
full+ payment details (audit-logged)
only when you genuinely need raw PII
Least privilege wins —
operationsoradmincovers most products.The console shows the JWT token and the exact endpoint URL to use — e.g.
https://mcp.kula.digital/mcp(or your region's, such ashttps://au-mcp.kula.digital/mcp). You need both.
The token encodes your org_id and scope — everything is auto-scoped to your org; you never pass an org id. Treat the token like a database password: store it in a secret, not in code; rotate it every ~90 days; use a dedicated token per app (tokens are rate-limited independently, and it keeps billing/audit clean).
2. The protocol
Standard MCP over HTTP (JSON-RPC 2.0). Authenticate with Authorization: Bearer <token>.
tools/list— discover the full tool catalogue (your client registers these).tools/call— invoke a tool with arguments.
Discover what's available first:
The tools you'll use most:
list_tables,get_table_schema,get_semantic_catalogue— explore the data model (tables, columns, typical queries).execute_query— run read-only SQL (the workhorse; see §3).build_query— a structured SELECT builder if you'd rather not write raw SQL.get_business_context— operator-authored studio facts (class mix, goals, pricing).Graph tools —
get_staff_context,get_staff_concentration,get_member_affinity,get_open_signals(at-risk queue),simulate_departure(what-if a staff member leaves).
3. execute_query — the workhorse, with the gotchas
execute_query — the workhorse, with the gotchasMost custom apps are built on execute_query. These are the things that will trip you up if you don't know them (they're not obvious):
Argument name is
sql(notquery):{"name":"execute_query","arguments":{"sql":"SELECT ..."}}.Read-only —
SELECT/WITHonly.Event tables are read-guarded. Querying
bookings.class_sessionorbookings.attendancedirectly fails. Use the_guardedviews:bookings.class_session_guarded,bookings.attendance_guarded(they hide data newer than midnight-today in the org timezone). Dimension tables likepeople.location/people.staffare not guarded.500-row hard cap per call. No argument lifts it — only SQL
OFFSETworks. Page every bulk query:... ORDER BY <stable unique key> LIMIT 500 OFFSET <k>, looping until a page returns < 500 rows. A single un-paged query on a busy studio silently truncates (classic symptom: classes show but attendance/retention come back empty).Result shape. Rows arrive as a text content part whose
textis JSON:{ "rows": [...], "row_count": N, "columns": [...] }. Parserowsfrom there.Errors come back HTTP 200 with
result.isError === trueand the message inresult.content[0].text— check it and surface it, don't treat it as empty.Rate limit ~120 req/min per token (HTTP 429). Back off (exponential + jitter, honour
Retry-After) and serialise heavy paging rather than firing it all at once.PII is redacted by scope at request time — an
analyticstoken literally cannot see names;operationscan't see emails. Pick the scope your UI needs.
A robust executeQuery helper (TypeScript) looks like:
4. The unified schema (orientation)
Everything is normalised into a small set of family-canonical schemas, regardless of the underlying vendor:
people—member,staff,location,identity_linkbookings—class_session,attendance,appointment,facility_entrycommerce—sale,refund,plangraph— derived rollups:staff_concentration,member_affinity,risk_signal
Two things to know about keys:
Every row carries
source(e.g.com.gymmaster) andsource_external_id(the vendor's id). Joins match on both — keys are only unique with their source.attendance.member_id/attendance.class_session_idare loose FKs to thesource_external_idof the member / class. But the graph tools key on the canonicalpeople.staff.id, notsource_external_id— resolve it first (SELECT id FROM people.staff WHERE source_external_id = …) before callingget_staff_context.
Run get_semantic_catalogue for the authoritative, annotated column list.
5. The pattern that matters: your code computes, the AI narrates
The most important design lesson from the reference build: don't ask an AI to compute the numbers your users will act on. Earlier attempts that let an LLM "judge" performance produced figures nobody could trust or defend.
Instead:
Use the MCP (
execute_query, graph tools) to fetch clean, org-scoped data.Do the decision logic in your own deterministic, testable code (the Bonus Builder has a pure, zero-dependency engine with hand-checked + golden-master tests).
Use an LLM only for what it's good at — explaining the result in plain English and routing the user — never for the arithmetic.
Structure your app with a data-adapter seam: one function that turns MCP rows into your domain objects. Then the same logic runs against live data, a CSV export, or test fixtures.
6. Security for hosted apps
A browser-only token is fine for a local/single-user tool. For anything hosted and shared, do not ship the token to the browser — proxy the MCP call through a small server or edge function with the token held as a server-side secret. (The Bonus Builder includes a Supabase edge-function proxy you can copy.) This also sidesteps CORS.
Scope to least privilege; rotate tokens; one token per app.
7. Reference implementation
Instructor Bonus Builder is a complete, open-source app built exactly this way — a deterministic engine that splits a bonus pool across instructors, fed by an MCP adapter with the pagination + retry + guarded-view handling above, plus a CSV fallback and a swappable theme.
Code: https://github.com/kulaforbusiness/instructor-bonus-builder
Live: https://instructor-bonus-builder.vercel.app
The MCP adapter (the bits in §3, productionised):
packages/adapters/src/
Clone it as a starting point, or read it to see the patterns in context.
Last updated
Was this helpful?