Status: Accepted — framework implemented; cloud surfacing pending (proposed 2026-06-06 · calibrated 2026-06-12)
Deciders: ObjectStack Protocol Architects
Builds on: ADR-0011 (actions/objects as AI tools), ADR-0024 (MCP as connectors — the inbound sibling: ObjectStack consuming external MCP servers; this ADR is the outbound direction), ADR-0033 (AI builds the metadata; publishing makes it live)
Consumers: @objectstack/rest (REST CRUD + /api/v1/discovery), new HTTP transport in @objectstack/mcp, @objectstack/platform-objects (sys_api_key), ../objectui (Developer Hub → new Integrations page; publish surface), ../cloud (per-env public hostname = the API/MCP base)
Premise: pre-launch, no back-compat debt — specify the target end-state directly.
Design center: the moment you build an object in ObjectStack, it is already a programmable REST API and an agent-ready MCP toolset — we just have to make that visible and one-click connectable. This is the differentiator Lovable / Airtable / Power Apps structurally cannot match: their output is a screen; ours is a governed data backend that any program or AI agent can drive. The magic moment (one sentence → a working, populated app) should end not at "here's your screen" but at "here's your screen — and here's the API + the agent tools it exposes."
After Phase 1a/2 landed, three decisions refine (and in one case supersede) the sections below. They take precedence where they conflict.
A. Package name → @objectstack/mcp. The outbound MCP-server package was renamed from @objectstack/plugin-mcp-server (legacy plugin- prefix) to @objectstack/mcp at the top level (packages/mcp), parallel to @objectstack/rest. Both are "your app exposed over a protocol", so rest (old surface) + mcp (new surface) sit as peers; inbound MCP (consuming external servers) stays @objectstack/connector-mcp. The old name is removed (pre-launch clean break; only internal @objectstack/cli depended on it).
B. Granularity is the environment, not the app. One MCP server per env (<env-host>/api/v1/mcp) covers all of that env's apps — apps are metadata groupings of objects, and the data surface is objects. A customer with dozens of apps in one env has one MCP connection, not dozens. Dynamic apps are a non-issue: tool discovery is live (list_objects / describe_object), so a newly built app shows up on the next call with zero reinstall. Narrowing an agent to a subset of objects/apps is done via sys_api_key.scopes (per-key), never by standing up separate servers.
C. Distribution = "skills + MCP", not vendor config snippets (supersedes §3/§4's "generate claude_desktop_config.json / Cursor snippet" as the primary artifact). Verified 2026-06: Agent Skills (SKILL.md) is now an open, cross-platform standard (Claude Code, OpenAI Codex CLI, Gemini CLI, GitHub Copilot, Cursor, Cline, Windsurf, …; MCP under the Linux Foundation). Skills and MCP are complementary: MCP = the live, authenticated, RLS-governed access layer (the moat — a static skill can never be the live data gateway to a specific tenant env); a Skill = the portable "how to use it" layer on top. So the primary cross-agent distributable is one generic ObjectStack Skill (install once; teaches the tool conventions, query/RLS idioms, workflows — it does not enumerate schema, which is discovered live), plus the env's remote-MCP URL + a show-once sys_api_key. A one-click "Add to Claude/Cursor" deeplink (or a .claude-plugin bundle that registers the MCP server) is a convenience on top, not the core artifact — we do not hand-maintain N vendor snippets.
A metadata-driven ObjectStack app already auto-generates a full REST CRUD API (/api/v1/data/{object}) discoverable at /api/v1/discovery, and the @objectstack/mcp already maps every object's CRUD (and the registered AI tools) to MCP tools. Two things are missing to turn that latent capability into a product: (1) the MCP server only speaks stdio, so a remote agent (Claude Desktop / Cursor) can't connect to a cloud env; (2) none of it is surfaced — a user who just built an app has no idea it's an API, no self-serve key, no connect button.
Decision. Treat "expose this app as an API + MCP server" as a first-class, open framework capability and surface it right after build:
- REST: keep the auto-generated CRUD + discovery (no change); surface it (base URL, per-object endpoints, sample code, the existing API Console).
- Auth: per-env, self-serve API keys (
sys_api_key, modelled but not yet enforced — wiring the key Bearer into the auth path is Phase 1a) — Bearer tokens that resolve to a principal and run under the key's existing permissions + RLS; a show-once generation UX. - MCP: add an HTTP transport to
@objectstack/mcp(a stable/api/v1/mcpendpoint, authed by the same Bearer), and generate the Claude/Cursor connection config for the env. - Surface: a Developer-Hub Integrations & APIs page + a "View API / Connect an agent" affordance on publish success.
Open-core boundary: the REST serve path, the MCP HTTP transport, discovery, and sys_api_key are the open mechanism (framework); the AI/intelligence that authored the app stays closed; the surfacing UI lives in objectui.
| Capability | Where | Status |
|---|---|---|
REST CRUD per object (/api/v1/data/{object} + OData query) |
@objectstack/rest registerCrudEndpoints() |
✅ auto-generated on publish |
API discovery (/api/v1/discovery) |
@objectstack/rest |
✅ |
| API Console (interactive REST debugger) | objectui apps/console/.../developer/ApiConsolePage |
✅ shipped |
| MCP server: object CRUD + AI tools → MCP tools | @objectstack/mcp |
✅ but stdio only, default off |
| Per-user/env API keys (create/revoke, key returned once) | platform-objects/.../sys-api-key.object.ts |
|
Per-env public hostname (<env>.objectos.app) = API/MCP base |
cloud env registry | ✅ |
- MCP HTTP transport — stdio works for a local CLI; a hosted env needs an HTTP endpoint (streamable-http / SSE) so external agents can connect over the network.
- External auth wiring — the REST/MCP surface must accept a
sys_api_keyBearer and enforce that key's scopes/permissions (RLS still applies). - Surfacing — the whole capability is invisible: no base-URL display, no self-serve key, no connect-an-agent flow, no post-publish nudge.
Lovable/Airtable/Power Apps produce a UI bound to their runtime. ObjectStack produces a governed backend: typed objects, validations, permissions, RLS — and the same metadata that renders the screen also defines the API and the agent tools. Surfacing "your app is an API + MCP server" converts our architectural fact into a felt advantage, and it is the natural on-ramp to programmatic + agentic use (and, later, paid tiers).
No runtime change. A new Integrations & APIs page reads /api/v1/discovery + /api/v1/meta/object, shows the env Base URL (window.location.origin), lists each published object's endpoints (GET/POST /data/{object}, GET/PATCH/DELETE /data/{object}/:id), links to the existing API Console, and renders copy-paste cURL / JS / Python samples using the user's key.
Correction (verified 2026-06-06). The pinned better-auth 1.6.13 has NO
apiKeyplugin (itsdist/pluginsships access, admin, anonymous, bearer, captcha, jwt, magic-link, mcp, oidc-provider, organization, two-factor, username, … but not api-key). So "just enable the plugin" is not an option on this version. (Note for Phase 2: better-auth 1.6.13 does ship anmcpplugin —mcp()+withMcpAuth+ OAuth discovery — which Phase 2 can use.)
Mechanism (revised): hand-roll the API-key check as a small, isolated
auth step in the runtime request pipeline (where the session principal is
resolved today): if Authorization: Bearer <key> is present and matches a
sys_api_key (constant-time hash compare; not revoked; not expired), resolve it
to its user_id and establish the same principal context the session path
produces — then the existing object permissions + RLS apply unchanged (no
parallel auth, no escalation). Reuse the platform's hashing util; store only the
hash (the model already does). last_used_at bumped on use.
This is the most security-sensitive change in this ADR — hand-rolled auth.
It MUST be its own focused, fully-tested change (see the Security note below),
not bundled. Alternative (rejected for now): bump better-auth to a version that
ships apiKey — a global auth-surface version bump is higher blast-radius than a
small, well-tested key-verification step.
- UI "Generate API key" →
POST /api/v1/data/sys_api_key; the raw key is returned once, shown in a copy-once panel, prefix-only thereafter. - Keys are per environment; revoke/restore + expiry already on the object.
Security note: this is a deliberate, security-sensitive change — it opens the tenant data backend to non-browser callers. It must be implemented as a focused, well-tested piece (key-create → keyed request → resolves to principal → object permissions + RLS enforced → revoked/expired key rejected), not bundled into unrelated work.
- Extend
@objectstack/mcpwith a streamable-http (with SSE fallback) transport mounted at a stable route (/api/v1/mcp), gated byOS_MCP_SERVER_ENABLEDper env (explicit opt-in — exposing tools is a deliberate act). - Auth: the same
sys_api_keyBearer; the advertisedtoolsare the env's object CRUD + any AI tools the key is scoped for. - The Integrations page generates the connection config (Claude Desktop
claude_desktop_config.json/ Cursor snippet) with the env's MCP URL + a generated key, and a one-click copy/download.
- Developer Hub: add an Integrations & APIs card → the new page.
- Publish success: the Publish & Open flow (ADR-0033 / the chat publish CTA) gains a secondary "View API · Connect an agent" link, so the API/MCP surface is discovered at the moment of "it works".
Correction (verified 2026-06-06).
sys_api_keyis modelled but NOT enforced for auth — the REST/data API today accepts only the better-auth session (cookie, or "Bearer = session token"); there is no key-verification path. The API Console "works" only because it runs inside the logged-in console (session cookie). So external programmatic access — the whole point — does not work yet. Wiringsys_api_keyBearer into the auth layer is therefore the foundation of Phase 1, not a pre-existing capability. Phase 1 is not objectui-only.
- Phase 1a (framework — the foundation): authenticate
Authorization: Bearer <sys_api_key>on the runtime auth path — verify the key (hash match, not revoked, not expired), resolve it to its principal, and run the request as that principal under the existing permissions + RLS (reuse the auth/permission layer; never a parallel one, never an escalation). This is what makes "your app is an external API" actually true. Unit-tested locally. - Phase 1b (objectui — surfacing): Integrations & APIs page (base URL + per-object endpoints + API Console link + self-serve API-key generation, show-once + cURL/JS samples) + Developer-Hub card + publish-success "View API" link. Locally testable against the keyed auth path.
- Phase 2 (framework + objectui): MCP Streamable HTTP transport (single
/api/v1/mcp, same Bearer auth), per-env opt-in (OS_MCP_SERVER_ENABLED), + connection-config generator and "Connect to Claude/Cursor" button. (Streamable HTTP, not the deprecated HTTP+SSE — single endpoint fits the Worker→container path; confirm streaming pass-through.)
- stdio-only MCP (status quo): fine for a local CLI, useless for a hosted env an external agent must reach over the network. Rejected for the cloud surface.
- No auth / public read API: a tenant data backend must never be open; the key + RLS model is mandatory.
- A separate API-gateway product: unnecessary — the runtime already serves the REST API and (with Phase 2) the MCP endpoint from the same env host. Adding a gateway would split the security model.
- Auto-enable MCP for every env: rejected — exposing tools to external agents is a deliberate, opt-in act (
OS_MCP_SERVER_ENABLED), surfaced as an explicit "Enable" in the Integrations page.
- Positive: the magic moment ends on a programmable, agent-ready backend; a clean on-ramp to programmatic + agentic use and to paid tiers; no new runtime for REST (pure surfacing in Phase 1).
- Cost: Phase 2's MCP HTTP transport + per-key auth is real framework work (transport, session, Bearer enforcement); the API-key show-once UX needs care (never re-display the raw key).
- Security: every external entry runs as a scoped
sys_api_keyprincipal under existing object permissions + RLS; MCP is opt-in per env; keys are revocable.
- Phase 1a (framework auth) — shipped (#1624):
sys_api_keyBearer/header verified on the runtime auth path → principal under existing permissions + RLS. - Phase 1b (objectui surfacing) — Integrations page + show-once key + publish "View API" link.
- Phase 2 (framework MCP HTTP transport) — shipped (#1626; package since renamed to
@objectstack/mcp): Streamable HTTP at/api/v1/mcp, opt-inOS_MCP_SERVER_ENABLED, fail-closed auth, principal-bound object-CRUD tools. - Key generation (framework) — shipped:
POST /api/v1/keysmints asys_api_keyand returns the raw secret once (only the hash is stored;user_idpinned to the caller; fail-closed auth). The backend for the show-once key UX. - Generic ObjectStack Skill — shipped (#1628):
renderSkillMarkdown({ mcpUrl })produces the portable, cross-agentSKILL.md. - Phase 2b (objectui surfacing, per Amendment C) — env-level remote-MCP connect page wiring
discovery.mcp+POST /keys(show-once) + skill download; not per-app, not hand-maintained vendor snippets.