Skip to content

The two MCP servers (don't confuse them)

  1. /api/v1/mcp — always on. Registry-generated since mos-7kajg.37: the original 10 tools (list_tables, query_rows, insert_row, list_forms, list_bases, list_workflows, create_invoice, create_quote, create_checkout, create_subscription — now tokenOnly alias actions in src/lib/server/actions/v1Tools.ts, byte-compatible names + schemas) plus every other registered defineAction that declares a token scope resource (e.g. the task actions with project_task:read|write). tools/list is filtered by the caller's token scopes — a tables:read-only token sees only the tables read tools — and write/delete calls additionally require the modules.mcp_export flag (mcp*export is the WRITE gate only — except for an OAuth-connected app, whose gate is the team's Connected apps opt-in). Writes route through executeCommand with the trust model (service executes; an OAuth-connected app executes unless the team's modules.agent_connect.write_mode is propose; a pasted human key proposes — mos-zase8). This is what an external Claude/Cursor config points at. Auth: Bearer jst*\*. Verified working live — a minted key resolves and reaches tools/call.
  2. /api/v1/mcp/actions — write-gated by modules.mcp_export (off by default: writes/deletes → isError; read-scope actions still callable with the right token scope; tools/list shows only read actions while off). Exposes every registered defineAction with the full module-gate + scope + approval semantics: service tokens execute immediately (recorded changeset); an OAuth-connected app follows its team's write mode (direct by default); pasted human/break-glass tokens are forced to propose mode (queued for human review). This is the richer, safer surface — but writing needs the tenant opt-in.

On the 401 that stranded the MCP configs: the header-auth path is correct. The 401 {code:-32000} seen this session came from an empty token env (MOS_MCP_TOKEN unset → the client sent Bearer with no value). The 401 now returns a diagnostic message naming the exact cause (fixed in mos-hq6q4.76). See quickstart § "MCP config".


Connecting an outside agent (the doors)

Every harness connects to the same tool registry — the defineAction registry — served through three doors. There is no per-vendor tool list: a tool added to the registry appears on every door, filtered by the caller's token scopes and the team's enabled modules.

Harness How URL
ChatGPT custom MCP connector, OAuth https://<team>.marduk.app/api/v1/mcp
Claude.ai custom connector, OAuth same URL
Claude Code claude mcp add --transport http — OAuth or a Bearer jst_ header same URL
Codex mos CLI, or native MCP with a bearer token scripts/mos-cli/ or same URL
pi mos CLI + the mos skill scripts/mos-cli/ + docs/agent-door/skills/mos/SKILL.md
Any OpenAI-compatible API (Kimi, Z.ai, SambaNova, OpenRouter…) function-calling door GET /api/v1/tools?format=openai|anthropic|gemini|mcp + POST /api/v1/tools/call

The gate (Joe's risk rules, enforced in code):

  • Opt-in per team by an owner/admin — Admin → Settings → Connected apps (modules.agent_connect.enabled, default OFF). Off means every door answers 403 agent_connect_disabled.
  • A user can only grant what they can already see — the OAuth consent page (/oauth/authorize) offers four tool presets (Projects & knowledge, Tables, Calendar, Read-only everything), each capped at the consenting member's own team role; a viewer's grant has every write stripped. Messaging, money, health records and secrets are in no preset (asserted by presets.test.ts).
  • Isolated/HIPAA tenants: hard 404/403, never a warning — an isolated container (PRIVATE_TENANT_ISOLATED / PRIVATE_AUTH_COOKIE_PREFIX) serves no OAuth route, no discovery document, no consent page, and the function-calling door answers isolated_tenant. BAA teams are never offered at consent either.
  • Tokens are scoped, expiring, revocable — OAuth access tokens live 1h, refresh tokens 30d single-use (replaying a rotated one revokes the grant); Connected apps → Disconnect kills the grant and every token it issued. An OAuth-connected app's writes follow the team's write mode (Connected apps page): Read + write (default) executes each one as the member who connected it — a recorded changeset, attributed and undoable from Admin → Changesets, with changed in the result; Read + propose queues each one for an admin (queued: true + changeset_url).
  • Nothing leaves MOS without an explicit confirmation — the exposure ledger (src/lib/server/actions/tokenExposure.ts) classifies every token-exposed action: internal is never listed or callable; confirm (sends outside MOS, moves money, deletes, admin/provisioning) advertises confirm: true and the dispatcher refuses the call without it.

Per-harness setup guides (steps, admin enablement, presets, revoke, data-leaves-MOS, HIPAA note) live in the repo at docs/agent-door/ — README.md plus one page per harness: connect-chatgpt.md, connect-claude-ai.md, connect-claude-code.md, connect-codex.md, connect-pi.md, connect-openai-compatible.md. The admin surface is Admin → Settings → Connected apps (/admin/settings/connected-apps).


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 token → 401 {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.

For a person connecting ChatGPT, Claude.ai, Claude Code, Codex or pi, prefer the OAuth door over a pasted token: the same URL, discovered via /.well-known/oauth-protected-resource, with a scoped, expiring, revocable consent grant. See agentic-rails § "Connecting an outside agent" and the per-harness guides in docs/agent-door/.



© 2026 MOS Labs LLC. All rights reserved.