Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-1850Done

Public REST API — the versioned `/api/v1` integration surface every client shares

Motir has no public API. Every programmatic surface it exposes today is either the web app's internal cookie-authenticated app/api/** tree or the MCP endpoint at /api/mcp. For an open-source Jira alternative that is a product gap, not plumbing: a self-hoster cannot script it, no third party can integrate with it, and there is no surface an SDK, a CI action or a Terraform provider could target.

This epic builds that surface — one public, versioned, documented HTTP API, and makes every client a peer consumer of it.

Why this exists (Yue, 2026-07-29)

The trigger was a narrower question — is the CLI using MCP, and is that standard? It is (exclusively: packages/cli/src/mcpClient.ts is the only file importing the MCP SDK), and it is not. The industry pattern for a product CLI is unambiguous and was verified per-product, not from memory:

ProductCLITalks to
GitLab (open-core)glabpublic REST /api/v4 + GraphQL
Gitea / Forgejoteapublic /api/v1, OpenAPI-documented
Sentry (open-core)sentry-clipublic Web API /api/0/
Plane (OSS PM — the closest product analogue)public REST /api/v1/ + API keys
MattermostmmctlREST v4
GitHubghpublic REST + GraphQL

The invariant is not the transport — it is that one public, versioned API is a first-class product surface and the CLI is merely its first client, with no private endpoints and no privileges. So the gap is not "the CLI needs REST" (a CLI-private API would itself be non-standard); the gap is that Motir has nothing for anyone else to call. Note the asymmetry already in the tree: 7.16.4b has Motir importing from Plane over Plane's public REST API, while offering no equivalent outward.

It also fixes a real structural defect. The MCP tool surface currently serves two consumers with opposed change-rate requirements: agents want churn (tool names, descriptions and argument shapes get rewritten for prompt-engineering reasons), while a published npm CLI wants a frozen contract. One unversioned surface serving both makes agent-driven churn into client breakage. After this epic each consumer gets a contract shaped for it — the REST API is stable because third parties depend on it, MCP stays fluid because only agents do. That is the shape GitLab and GitHub use: their MCP servers wrap their public APIs. Motir has it inverted today.

Target architecture

services (the single source of truth — the 4-layer stack, already built)
   ├── web app             → session cookie (unchanged)
   ├── public REST /api/v1 → PAT bearer · versioned · OpenAPI ──┬── @motir/cli
   │                                                             └── third parties, SDKs, CI
   └── MCP server          → thin adapter over the SAME services + response schemas

Both REST routes and MCP tools stay thin adapters over services — the MCP tools are NOT re-pointed at HTTP routes (that would add an internal network hop). What unifies them is the response SCHEMA, not the transport: one schema source, both surfaces derive from it, a conformance test proves they cannot drift.

What makes this cheaper than it looks (rung-2 verified on origin/main @ d5b03e5f)

motir-core's architecture discipline pays off here — almost no business logic moves. Verified substrate:

  • lib/apiTokens/routeAuth.tsauthenticateApiToken(req, requiredScope) already exists: generic Authorization: Bearer motir_pat_… auth for a plain REST route, shipped by MOTIR-1631 for the acceptance-video publish endpoint, returning { ok, userId, workspaceId } or { ok: false, reason: 'unauthenticated' | 'forbidden' }. The public API's auth is a REUSE, not a build.
  • lib/mcp/scopes.ts — the PAT scope model is shipped and total: read · work_items:write · work_items:archive · work_items:delete · sprints:write · integration, typed so an ungated operation fails typecheck.
  • The PAT-minting UI is shippedapp/(authed)/settings/account/api-tokens/ (ApiTokensManager, CreateTokenModal), with real non-route callers. "Mint a token" resolves to an existing surface; no card is missing for it.
  • The error envelope convention is established{ code, error } + an HTTP status, e.g. app/api/work-items/[id]/route.ts.
  • Opaque cursor pagination existslib/mcp/searchCursor.ts.
  • Every service is already DTO-returning and route-agnostic (workItemsService, sprintsService, projectsService), so a v1 route is a thin adapter exactly like an MCP tool.

Genuinely new: the /api/v1 tree itself, per-token rate limiting (no primitive exists anywhere in lib/), the OpenAPI spec + published reference, the shared response-schema source, and the CLI transport swap.

Scope BOUNDARY

This epic delivers: the /api/v1 route tree over existing services; PAT bearer auth + scope enforcement + rate limiting + cursor pagination + the error envelope; the work-item, project, sprint and ready-set resources; a published OpenAPI 3.1 spec and API reference; the CLI migrated onto it; and MCP payloads derived from the same schemas.

It ENDS at the HTTP surface and its first two clients. It does NOT:

  • Rewrite app/api/**. That tree is the web app's INTERNAL cookie-authenticated surface and stays exactly as it is. /api/v1 is a NEW, separately-versioned tree — never a promotion or rename of internal routes, which carry no stability promise.
  • Ship a GraphQL surface (GitLab and GitHub both have one; it is a separate, later decision — no card here defers work to it).
  • Ship SDKs, a Terraform provider or a GitHub Action. Those become possible once the spec exists and are their own epics.
  • Grow BEHAVIOUR at the edge. No new filter axis, access gate, response field or write path is invented in a v1 route: if an endpoint needs DATA a service does not expose, that is a card in the owning feature's epic, not here. The one bounded exception, recorded as an ADR amendment by 11.2.1 rather than assumed: a v1 endpoint may add a page ADDRESSING over an existing predicate — a keyset read, or a page-size parameter where the shipped read has a fixed one — because the result SET is unchanged and the conventions mandate keyset paging that no shipped read offers. Everything else about that read stays as it is.
  • Touch motir-ai. Every deliverable is motir-core — one repo, so no card in this epic straddles the open-core boundary.

Where it sits

blocked_by Epic 6: Search, reporting & admin — the PM core this API publishes. That epic is done, so this one is ready now.

Deliberately NOT wired as a blocker of Epic 8: Launch readiness: a public API is arguably launch-readiness work, but Epic 8 is already in_progress and inverting that edge would block shipped-in-flight work on an epic that has not started. The relationship is noted, not wired.

The CLI migration lives HERE rather than in Epic 7's CLI story (MOTIR-809) because it is this API's first consumer and its proof of completeness. Story 7.9 continues on MCP in the meantime and needs no rework: the transport is confined to ONE file, so the swap touches no command and no renderer.