Skip to content

MOS API — external builder quickstart

Build a "vibe app" that sits adjacent to MOS: your app holds a MOS team token and calls the API; MOS is the system of record, your app is a UX shell. You never touch the database — the token's team_id + RLS are the tenant boundary, so your app can only ever see the one team it was issued for.

The full surface map (what's exposed, what's a gap) is in agentic-rails.md. This is the 5-minute on-ramp.


1. Mint a token

UI: sign in to your team app (https://<your-team>.marduk.app) → Admin → Settings → API Keys (/admin/settings/api-keys) → New token. Copy the jst_… value once — it is shown only at creation and stored hashed.

Token types

Type Expiry Trust (on /api/v1/mcp/actions) Use for
service none (rotate manually) trusted — mutations execute immediately server-to-server integrations, your adjacent app's backend
human 1h access + 30d refresh untrusted — mutations queue for human approval a user acting through your app
break_glass 4h untrusted audited emergency access

Scopes. A token carries a JSONB scope list ([{resource, actions[]}]). Leaving it empty grants full team access (the wildcard *). Today most v1 routes require the wildcard, so least-privilege tokens are limited — see agentic-rails.md §G-1. Scope resources include contacts, forms, files, comms, emr, audit, tokens, *; actions are read / write / delete / admin.

Every request is Authorization: Bearer jst_….


2. The first three calls

Base URL is your team host, e.g. https://acme.marduk.app. (The versioned API is served from every team subdomain.)

a) List tables

curl -s https://acme.marduk.app/api/v1/tables/tables \
  -H "Authorization: Bearer $MOS_TOKEN"
# → { "tables": [ { "id": "...", "name": "...", "display_name": "..." }, ... ] }

⚠️ The route is tables/tables (not data/tables, which the /api/v1 index currently mis-advertises — G-2).

b) Create a row

curl -s -X POST https://acme.marduk.app/api/v1/tables/tables/$TABLE_ID/rows \
  -H "Authorization: Bearer $MOS_TOKEN" -H "Content-Type: application/json" \
  -d '{ "data": { "name": "Widget", "qty": 3 } }'

c) "Subscribe to changes" — ⚠️ not yet available. There is no token-facing events/webhook stream (G-16). Until it lands, poll: GET .../rows?sort=updated_at:desc (via MCP query_rows) on an interval, tracking the newest updated_at you've seen. A real event stream is the top-ranked gap.

Discover the whole surface at runtime:

curl -s https://acme.marduk.app/api/.well-known/openapi.json   # OpenAPI 3.1, no auth
curl -s https://acme.marduk.app/api/v1 -H "Authorization: Bearer $MOS_TOKEN"

3. MCP config (Claude / Cursor / any MCP client)

MOS speaks MCP over plain HTTP (JSON-RPC 2.0) at /api/v1/mcp — 10 tools: list_tables, query_rows, insert_row, list_forms, list_bases, list_workflows, create_invoice, create_quote, create_checkout, create_subscription.

{
  "mcpServers": {
    "mos": {
      "type": "http",
      "url": "https://acme.marduk.app/api/v1/mcp",
      "headers": { "Authorization": "Bearer jst_YOUR_TOKEN_HERE" }
    }
  }
}

The #1 gotcha: an empty token401 {code:-32000}. If you template the token from an env var ("Bearer ${MOS_MCP_TOKEN}") and the var is unset, the client sends Bearer with no value and every call 401s. The server now tells you exactly which case you hit ("empty Bearer token…", "not a MOS team API key…", "invalid, revoked, or expired"). Sanity-check with curl before wiring a client:

curl -s -X POST https://acme.marduk.app/api/v1/mcp \
  -H "Authorization: Bearer $MOS_MCP_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# → {"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"jst-client-api",...}}}

The URL must include /api/v1/mcp. A bare host (https://acme.marduk.app) gives the client's own ENDPOINT_NOT_FOUND.

There is a second, richer MCP server at /api/v1/mcp/actions that exposes every MOS action — but it is off by default (per-tenant modules.mcp_export) and returns 403 until enabled. See agentic-rails.md.


4. Rate limits

Per token, sliding window, fail-open (a flaky limiter never takes the API down):

Token type Limit
service 10 000 / min
human, break_glass 1 000 / min

Over the limit → 429 with a Retry-After header (seconds). Back off and retry.


5. What "adjacent app" means

  • Your app holds the token. Store it server-side; never ship a service token to a browser. For end-user flows, mint short-lived human tokens.
  • MOS is the system of record. Don't mirror-and-diverge; read and write through the API so RLS, audit logging, and approval gates stay authoritative.
  • The team boundary is enforced for you. A token is bound to one team_id; every query is scoped to it. You cannot read another team's data even by id.
  • Mutations may be gated. On the actions MCP surface, human/break_glass tokens propose changes for human review rather than applying them — design your UX to show "pending approval" states.

Questions or a gap that blocks you? The ranked gap list (contacts, files, events/webhooks, …) is in agentic-rails.md; each is a tracked task under mos-hq6q4.

Last verified: 2026-09-04 (task mos-hq6q4.76).


© 2026 MOS Labs LLC. All rights reserved.