Hyperstack MCP Connector
Overview
Expose Convex functions as tools that Claude and ChatGPT can call as the
signed-in user, over a hosted remote MCP server with real OAuth. One server
works across every MCP client. The whole thing lives inside Convex — no
separate web server, no bridge, no keypair.
Built on the convex-mcp-gateway component. It hosts the MCP server as a
Convex httpAction, handles the JSON-RPC/Streamable-HTTP protocol, the OAuth
discovery doc, and an audit log. You supply three things:
- Tools —
defineMcpQuery / defineMcpMutation / defineMcpAction map a
Convex function to a tool. Each identity-scoped tool takes an injected
caller arg (identityArg), because Convex strips ctx.auth across the
component boundary — so you scope on caller.subject, not ctx.auth.
- Identity — a
resolveIdentity that validates Clerk's opaque OAuth
access token at Clerk's OIDC userinfo endpoint and returns
{ subject, claims }. Clerk is the OAuth authorization server (with Dynamic
Client Registration); the gateway is the resource server.
- Mount — one
httpAction in convex/http.ts that calls
gateway.handleMcpRequest(...), plus the protected-resource discovery route.
Claude/ChatGPT --OAuth (DCR)--> Clerk (authorization server)
| opaque access token
v
convex.site/mcp (gateway httpAction, requireAuth)
| resolveIdentity → Clerk userinfo → { subject }
v
gateway injects `caller` → internal Convex fn scopes by caller.subject
(runs as that user; every owner-scoped function works unchanged)
When to use
- You want an AI client to do something in your app (save, fetch, recall,
trigger) and have it run as the actual user.
- Your backend is Convex + Clerk — Convex is your source of truth.
- You want it addable in Claude and ChatGPT without per-client backends.
Not for: local stdio MCP servers (no OAuth needed — a token is enough); apps
not on Convex+Clerk (the identity resolver assumes Clerk; the tool model assumes
Convex components); read-only public data with no user identity (skip OAuth
entirely).
Why not host the MCP server in a Next.js/Vercel route and bridge to Convex? You
can, but for a Convex-centric app it adds a separate server, a signed bridge JWT,
a JWKS endpoint, and key rotation — all to reproduce an identity Convex can
already carry. The gateway keeps everything in convex/.
Build steps
- Install + register:
npm install convex-mcp-gateway, then app.use(mcpGateway)
in convex/convex.config.ts (setup-and-gotchas.md §1).
- Tools — copy
references/mcp.ts to convex/mcp.ts. Replace the sample
tools with one defineMcp{Query,Mutation,Action} per function you expose. Every
identity-scoped tool declares caller: mcpCallerValidator + identityArg: "caller"
and points fn at an internal function. Keep authorize, resolveClerkIdentity,
and initializeInstructions.
- Tool impls — copy
references/mcp-tools.ts to convex/mcpTools.ts. These are
internalQuery/internalMutation/internalActions that owner-scope by
caller.subject (map it to your user via whatever index keys users on the Clerk id).
- Mount — merge
references/http.ts into convex/http.ts (the /mcp routes +
protected-resource discovery + requireAuth + exposed headers).
- Env — set
MCP_AUTH_SERVER_URL (your Clerk issuer) per deployment, or rely on
an existing CLERK_JWT_ISSUER_DOMAIN (setup-and-gotchas.md §2).
- Enable Dynamic Client Registration in the Clerk dashboard — required, one
manual toggle; leave "access tokens as JWTs" OFF (
setup-and-gotchas.md §3).
- Deploy (
npx convex deploy), then add the connector at
https://<deployment>.convex.site/mcp in Claude/ChatGPT (setup-and-gotchas.md §4).
Reference files
| File |
Goes to |
What it is |
references/mcp.ts |
convex/mcp.ts |
gateway + tool descriptors + authorize + resolveClerkIdentity (Clerk userinfo) + initializeInstructions |
references/mcp-tools.ts |
convex/mcpTools.ts |
the internal tool impls, owner-scoped by the injected caller.subject |
references/http.ts |
convex/http.ts |
the /mcp mount + OAuth discovery route + requireAuth + exposed headers |
references/setup-and-gotchas.md |
— |
convex.config, env, Clerk DCR, deploy, add-connector, gotchas, round-trip test |
Common mistakes
- Reading the user from
ctx.auth in a tool → always null. The gateway strips
ctx.auth across the component boundary; use the injected caller arg.
- Exposing tool functions as
public (api.*) → any Convex client can call them
with a forged caller and read another user's data. Make them internal.
- Skipping Dynamic Client Registration in Clerk (or leaving it unsaved) → the
client can't register, OAuth never starts.
registration_endpoint must appear in
Clerk's oauth-authorization-server metadata (not openid-configuration).
- Omitting
requireAuth: true → browser clients (claude.ai) see an empty
tools/list, think they're connected, and never start OAuth (they only react to a 401).
- Not exposing
mcp-session-id / www-authenticate on CORS → the browser client
can't read the session or the auth challenge.
- Convex
subject mismatch → tools authenticate but owner-scoped queries return
nothing. caller.subject (the Clerk user id) must be what your functions key on.
- Weak tool descriptions → the model defaults to web search instead of your
connector. Name the concrete data and say when to use it; set
initializeInstructions. Re-sync by starting a new chat after changing them.
See references/setup-and-gotchas.md for the full gotcha list (opaque-vs-JWT
tokens, the circular-type annotation, ChatGPT caveats, DCR verification) and the
round-trip test.
Reference implementation
Padscanner's connector (github.com/tfohlmeister/convex-mcp-gateway is the
component; padscanner exposes its rental-search tools to Claude as the signed-in
user) is built exactly this way and runs in production — the same pattern powers a
savethis "save & recall from Claude" connector. Use it as the worked example, but
keep this skill stack-generic.
1---2name: hyperstack-mcp-connector3description: Use when exposing Convex functions as OAuth-secured tools that Claude, ChatGPT, or another MCP client can call as the signed-in user — i.e. building a remote/hosted MCP connector for a Convex + Clerk app, adding Clerk OAuth to an MCP server, or wiring a "save to / do X in / recall from <your app>" connector. Stack-specific — Convex + Clerk.4---56# Hyperstack MCP Connector78## Overview910Expose Convex functions as tools that Claude and ChatGPT can call **as the11signed-in user**, over a hosted remote MCP server with real OAuth. One server12works across every MCP client. The whole thing lives **inside Convex** — no13separate web server, no bridge, no keypair.1415Built on the **`convex-mcp-gateway`** component. It hosts the MCP server as a16Convex `httpAction`, handles the JSON-RPC/Streamable-HTTP protocol, the OAuth17discovery doc, and an audit log. You supply three things:18191. **Tools** — `defineMcpQuery` / `defineMcpMutation` / `defineMcpAction` map a20 Convex function to a tool. Each identity-scoped tool takes an injected21 `caller` arg (`identityArg`), because Convex strips `ctx.auth` across the22 component boundary — so you scope on `caller.subject`, not `ctx.auth`.232. **Identity** — a `resolveIdentity` that validates Clerk's **opaque** OAuth24 access token at Clerk's OIDC **userinfo** endpoint and returns25 `{ subject, claims }`. Clerk is the OAuth authorization server (with Dynamic26 Client Registration); the gateway is the resource server.273. **Mount** — one `httpAction` in `convex/http.ts` that calls28 `gateway.handleMcpRequest(...)`, plus the protected-resource discovery route.2930```31Claude/ChatGPT --OAuth (DCR)--> Clerk (authorization server)32 | opaque access token33 v34convex.site/mcp (gateway httpAction, requireAuth)35 | resolveIdentity → Clerk userinfo → { subject }36 v37gateway injects `caller` → internal Convex fn scopes by caller.subject38 (runs as that user; every owner-scoped function works unchanged)39```4041## When to use4243- You want an AI client to *do something* in your app (save, fetch, recall,44 trigger) and have it run as the actual user.45- Your backend is **Convex + Clerk** — Convex is your source of truth.46- You want it addable in Claude **and** ChatGPT without per-client backends.4748**Not for:** local stdio MCP servers (no OAuth needed — a token is enough); apps49not on Convex+Clerk (the identity resolver assumes Clerk; the tool model assumes50Convex components); read-only public data with no user identity (skip OAuth51entirely).5253> Why not host the MCP server in a Next.js/Vercel route and bridge to Convex? You54> can, but for a Convex-centric app it adds a separate server, a signed bridge JWT,55> a JWKS endpoint, and key rotation — all to reproduce an identity Convex can56> already carry. The gateway keeps everything in `convex/`.5758## Build steps59601. **Install + register:** `npm install convex-mcp-gateway`, then `app.use(mcpGateway)`61 in `convex/convex.config.ts` (`setup-and-gotchas.md` §1).622. **Tools** — copy `references/mcp.ts` to `convex/mcp.ts`. Replace the sample63 tools with one `defineMcp{Query,Mutation,Action}` per function you expose. Every64 identity-scoped tool declares `caller: mcpCallerValidator` + `identityArg: "caller"`65 and points `fn` at an **internal** function. Keep `authorize`, `resolveClerkIdentity`,66 and `initializeInstructions`.673. **Tool impls** — copy `references/mcp-tools.ts` to `convex/mcpTools.ts`. These are68 `internalQuery`/`internalMutation`/`internalAction`s that owner-scope by69 `caller.subject` (map it to your user via whatever index keys users on the Clerk id).704. **Mount** — merge `references/http.ts` into `convex/http.ts` (the `/mcp` routes +71 protected-resource discovery + `requireAuth` + exposed headers).725. **Env** — set `MCP_AUTH_SERVER_URL` (your Clerk issuer) per deployment, or rely on73 an existing `CLERK_JWT_ISSUER_DOMAIN` (`setup-and-gotchas.md` §2).746. **Enable Dynamic Client Registration** in the Clerk dashboard — required, one75 manual toggle; leave "access tokens as JWTs" OFF (`setup-and-gotchas.md` §3).767. **Deploy** (`npx convex deploy`), then **add the connector** at77 `https://<deployment>.convex.site/mcp` in Claude/ChatGPT (`setup-and-gotchas.md` §4).7879## Reference files8081| File | Goes to | What it is |82|------|---------|------------|83| `references/mcp.ts` | `convex/mcp.ts` | gateway + tool descriptors + `authorize` + `resolveClerkIdentity` (Clerk userinfo) + `initializeInstructions` |84| `references/mcp-tools.ts` | `convex/mcpTools.ts` | the **internal** tool impls, owner-scoped by the injected `caller.subject` |85| `references/http.ts` | `convex/http.ts` | the `/mcp` mount + OAuth discovery route + `requireAuth` + exposed headers |86| `references/setup-and-gotchas.md` | — | `convex.config`, env, Clerk DCR, deploy, add-connector, gotchas, round-trip test |8788## Common mistakes8990- **Reading the user from `ctx.auth` in a tool** → always null. The gateway strips91 `ctx.auth` across the component boundary; use the injected `caller` arg.92- **Exposing tool functions as `public` (api.*)** → any Convex client can call them93 with a forged `caller` and read another user's data. Make them `internal`.94- **Skipping Dynamic Client Registration** in Clerk (or leaving it unsaved) → the95 client can't register, OAuth never starts. `registration_endpoint` must appear in96 Clerk's **oauth-authorization-server** metadata (not openid-configuration).97- **Omitting `requireAuth: true`** → browser clients (claude.ai) see an empty98 `tools/list`, think they're connected, and never start OAuth (they only react to a 401).99- **Not exposing `mcp-session-id` / `www-authenticate`** on CORS → the browser client100 can't read the session or the auth challenge.101- **Convex `subject` mismatch** → tools authenticate but owner-scoped queries return102 nothing. `caller.subject` (the Clerk user id) must be what your functions key on.103- **Weak tool descriptions** → the model defaults to web search instead of your104 connector. Name the concrete data and say *when* to use it; set105 `initializeInstructions`. Re-sync by starting a **new** chat after changing them.106107See `references/setup-and-gotchas.md` for the full gotcha list (opaque-vs-JWT108tokens, the circular-type annotation, ChatGPT caveats, DCR verification) and the109round-trip test.110111## Reference implementation112113Padscanner's connector (github.com/tfohlmeister/convex-mcp-gateway is the114component; padscanner exposes its rental-search tools to Claude as the signed-in115user) is built exactly this way and runs in production — the same pattern powers a116savethis "save & recall from Claude" connector. Use it as the worked example, but117keep this skill stack-generic.