Features
AI connectors (API, CLI & MCP)
Make your SaaS queryable by AI agents: a customer creates an API token in your app, pastes it into Claude Code, Cursor or claude.ai, and their agent answers questions with your product's data. Battle-tested in production on MRRscope (July 2026), including a working claude.ai/Cowork custom connector.
One contract, four paths
- Versioned REST API (/v1) — scripts and backends, Bearer tok_… tokens
- npm CLI — terminal, CI, shell agents (zero runtime dependencies)
- Local MCP server (stdio) — Claude Code, Claude Desktop, Cursor
- Remote MCP server (HTTP) — claude.ai / Claude Cowork custom connectors
The CLI and both MCP servers are thin adapters over the public API: auth, subscription gating and scoping live in ONE place. Never duplicate a business rule in an adapter.
Setup order (Supabase kit)
terminal
supabase db push # 0006: api_tokens table + RPCs
supabase functions deploy account-tokens # token management from the app (JWT)
supabase functions deploy public-api --no-verify-jwt # token-authenticated public API
supabase functions deploy mcp-server --no-verify-jwt # remote MCP adapterThen drop <ApiTokensCard /> into your dashboard, add your product routes to public-api (the “PRODUCT ROUTES” section), and mirror your tool catalog in mcp-server/handler.ts AND cli/src/mcp.ts — keep both identical.
Non-negotiable rules (paid for in debugging)
- Store the hash only; show the token once. No “view my token again”.
- Never return a token that was not persisted (guaranteed silent 401 later).
- Gate on subscription AT USAGE TIME, fail-closed — a trial token dies with the trial without being deleted.
- Agents get read-only tools by default: no token management over MCP, no writes without an explicit decision. The agent holds the token.
- A public contract is a commitment: /v1, stable error codes { error, code }, amounts in cents with a _cents suffix (no unit ambiguity for an LLM).
- Tool descriptions are what the LLM reads — spell out units, bounds and defaults.
The two claude.ai connector pitfalls
1 — A 401 on claude.ai's initial probe switches it to OAuth discovery + dynamic client registration, which fails with “Unable to register with the sign-in service”. Fix (wired in mcp-server): initialize, tools/list and ping answer WITHOUT auth; the token is only required at tools/call, and a missing token returns an isError result with HTTP 200 — never a 401.
2 — OAuth discovery hits /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server on YOUR domain. A SPA fallback answering 200 with HTML (soft-404) makes claude.ai believe the server is OAuth-protected. Fix: an explicit 404 before the SPA fallback:
public/_redirects
/.well-known/* /404.html 404
/* /index.html 200Bonus: claude.ai connectors cannot set headers, so the token travels in the URL (Zapier pattern) — prefer a path segment (/mcp/<tok_…>) over a query string, which some clients strip. The URL IS the secret: say so in your user docs, and remind users the token is read-only and revocable. After a failed connector attempt, delete the entry and re-create it — claude.ai remembers the auth mode per entry.
Test recipe
terminal
# API: clean 401 without a token, data with one
curl -s -X POST https://<ref>.supabase.co/functions/v1/public-api/me
# Remote MCP: handshake WITHOUT a token must be 200 (not 401!)
curl -s -X POST https://<ref>.supabase.co/functions/v1/mcp-server \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
# well-known MUST be 404 on your domain
curl -s -o /dev/null -w "%{http_code}\n" https://<domain>/.well-known/oauth-protected-resource
# Local MCP in Claude Code
claude mcp add <product> --env <PRODUCT>_TOKEN=tok_xxx -- npx -y <package> mcpAutomated tests ship with the kit: deno test supabase/tests/mcp_handler_test.ts (MCP core) and cd cli && npm test (arg parsing + stdio core).