MCP — drive omg from Claude Code
OAuth-authenticated MCP server at mcp.omg.dev. Install, tool surface, how the auth dance works.
mcp.omg.dev is an OAuth-protected Model Context Protocol server. Hook
it up to Claude Code (or any MCP client that speaks Streamable HTTP +
OAuth 2.1) and you get a small toolbelt for driving your omg account
from the editor: create apps from a prompt, steer existing apps with
follow-up instructions, inspect runs.
The Worker is hosted on Cloudflare with a custom-domain route. The
Authorization Server is auth.omg.dev (the same Better Auth instance
that mints session JWTs for omg.dev). Tokens are JWT access tokens
with aud=https://mcp.omg.dev/.
Install in Claude Code
claude mcp add --transport http omg https://mcp.omg.dev/mcpThat's the whole install. On first use Claude Code does Dynamic Client
Registration against auth.omg.dev/api/auth/oauth2/register, runs
PKCE-authorize, opens a browser for sign-in + consent, and stores the
resulting access + refresh tokens. Subsequent sessions are silent.
Verify:
claude mcp list
# omg: https://mcp.omg.dev/mcp (HTTP) - ✓ ConnectedIf you see ! Needs authentication, run a tool from any session and
Claude Code will trigger the OAuth flow. The refresh token (issued
under the offline_access scope) lives for 30 days, so reauthentication
is rare.
Tools
| Tool | Args | Purpose |
|---|---|---|
omg_status | — | Health + configured-subsystems probe. Use first when something seems off. |
omg_create_app | prompt | Spin up a fresh project and kickoff agent run. Returns {projectId, runId, slug}. |
omg_steer | runId, text | Send a natural-language follow-up to an existing app. Not a code diff. |
omg_get_run | runId | Current run state (queued/running/done/failed) + most recent message. |
omg_tail_run | runId, afterMessageCreatedAt? | Incremental tail of assistant + tool messages. |
All tool calls forward the same JWT to Convex's /api/cli/* endpoints,
which accept dual-audience tokens (legacy aud=vibes from
auth.omg.dev/token and new aud=https://mcp.omg.dev/ from this
flow). See Auth for the JWT model.
OAuth flow
// .well-known discovery, per MCP spec
// Step 1 — Resource Server metadata (RFC 9728)
GET https://mcp.omg.dev/.well-known/oauth-protected-resource
→ {
"resource": "https://mcp.omg.dev",
"authorization_servers": ["https://auth.omg.dev"],
"scopes_supported": ["omg:apps"]
}
// Step 2 — Authorization Server metadata (RFC 8414)
GET https://auth.omg.dev/.well-known/oauth-authorization-server
→ {
"issuer": "https://auth.omg.dev",
"authorization_endpoint": "https://auth.omg.dev/api/auth/oauth2/authorize",
"token_endpoint": "https://auth.omg.dev/api/auth/oauth2/token",
"registration_endpoint": "https://auth.omg.dev/api/auth/oauth2/register",
"jwks_uri": "https://auth.omg.dev/api/auth/jwks",
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid","profile","email","offline_access","omg:apps"]
}PKCE is mandatory (S256 only). Dynamic Client Registration is open and unauthenticated, per MCP-client-onboarding norms. Consent is shown the first time a given client requests a given scope; later authorize calls from the same client + user return the redirect immediately.
Scopes
| Scope | What the token can do |
|---|---|
omg:apps | Read + write the user's omg apps (everything the tool surface above exposes) |
offline_access | Mint refresh tokens — without this the access token expires in 1h and the user has to re-authorize |
openid / profile / email | OIDC scopes; not currently required by the Worker but available |
The Worker validates aud=https://mcp.omg.dev/ and iss=https://auth.omg.dev
on every /mcp request via the JWKS at /api/auth/jwks (EdDSA Ed25519).
401 responses include WWW-Authenticate: Bearer resource_metadata=… so
clients can auto-reauthenticate on stale tokens.
Token shape
// Decoded access token issued by /api/auth/oauth2/token
{
"sub": "PmcFDWIjUum…", // omg user id
"email": "you@example.com", // via customAccessTokenClaims
"name": "",
"aud": ["https://mcp.omg.dev/", // the resource indicator
"https://auth.omg.dev/api/auth/oauth2/userinfo"],
"azp": "gJHYnPVlNx…", // DCR-issued client_id
"scope": "openid email omg:apps",
"iss": "https://auth.omg.dev",
"iat": 1780050232,
"exp": 1780053832 // 1h default
}The email claim is added by apps/auth/src/auth.ts via
customAccessTokenClaims. Convex's requireSessionUserId uses it to
look up the omg user — keeping legacy /token-minted JWTs and the new
OAuth tokens interchangeable at the Convex boundary.
Troubleshooting
! Needs authentication in claude mcp list. No tokens stored yet,
or the refresh token expired (30d). Call any omg_* tool from a Claude
Code session — it'll trigger the OAuth flow.
401 from /mcp with WWW-Authenticate: Bearer resource_metadata=….
Access token expired and refresh failed (rare). Same fix: trigger any
tool to reauthorize.
Tool call works but returns Unauthorized from Convex. The token's
aud doesn't match Convex's accepted audiences. The deployed Convex
verifier accepts both https://mcp.omg.dev and the trailing-slash form
https://mcp.omg.dev/. If a custom client is sending something else,
that won't match.
requested resource invalid at the token endpoint. The resource
parameter (RFC 8707) wasn't in validAudiences. Two forms are accepted
(…dev and …dev/). Anything else is rejected. Update the client to
match.
Request path
The Worker verifies each Bearer token's EdDSA signature against
auth.omg.dev's JWKS before forwarding to Convex. The tokens themselves
come from the OAuth flow above — a separate
Claude-Code-to-auth.omg.dev handshake (PKCE authorize + token), not part
of this hot path.
Source lives at apps/mcp/ in the monorepo. The contributor-facing
README at apps/mcp/README.md covers how to add a new tool, the
deploy pipeline (push → deploy-mcp.yml → wrangler deploy), and the
trailing-slash / validAudiences gotcha.