Remote (hosted) mode
Point an MCP client at https://spellingcreator.org/mcp and it connects over Streamable HTTP with a real OAuth 2.1 "Connect" flow — no token to copy, paste, or store. This is the recommended way to connect a client that supports remote MCP servers (claude.ai, Claude Desktop's remote connectors, Cursor, etc.); the local stdio setup remains the CLI-first path.
How it works
- The client discovers the server's OAuth metadata and registers itself automatically (RFC 7591 Dynamic Client Registration) — nothing to set up on your end.
- It opens your browser to
/authorize, which redirects to an ordinary page of the web app at/oauth/authorize. If you're not already signed in, it offers the same magic-link sign-in as/login. - You see a consent screen — which client is connecting and what it can do — and choose Approve or Deny.
- The client receives its own access/refresh token pair and starts calling tools. No Supabase token is ever shown to you or passed to the client; the server holds your session and mints requests to the hub's normal endpoints on your behalf.
The tool layer (src/tools.js) and API client (src/api.js) are the exact same code the stdio server uses — the same tools, the same validation, the same author attribution.
Worker-side implementation
The whole thing is implemented in apps/api, not apps/mcp — the MCP package only supplies the two remote-specific pieces the Worker composes:
src/worker.js(@spelling-creator/mcp/worker) —buildMcpServer(build a connection-scopedMcpServergiven any auth provider) andgrantAuth(an auth provider seeded from an OAuth grant'sprops, with the same getAccessToken()/forceRefresh() shape the stdio auth provider has).src/auth.js(@spelling-creator/mcp/auth) —refreshSupabaseSession, the plain Supabase refresh-token-exchange call shared by the stdio server,grantAuth's fallback, and the Worker's token endpoint (below).
On the Worker side (apps/api):
src/routes/mcp.jswires up@cloudflare/workers-oauth-providerwithHubMcp(anMcpAgentfrom the Cloudflare Agents SDK, one per MCP connection) as the/mcpAPI handler, and the existing Honoappas thedefaultHandlerfor everything else. Supabase is the upstream identity: a grant'spropscarry a Supabase session captured at consent time, and atokenExchangeCallbackrotates it in step with the MCP client's own OAuth token refresh (kept a little shorter than Supabase's ~1h JWT lifetime), so a tool call's Supabase access token is normally already fresh with no extra round trip.grantAuthis the defense-in-depth fallback for a connection that outlives that cadence.src/routes/oauth.jsimplements the consent flow's two small endpoints (GET /oauth/request,POST /oauth/approve) that the/oauth/authorizeweb page calls./authorizeitself,/token, and dynamic client registration (/register) are otherwise implemented by the OAuthProvider library.- The consent page is
apps/web/src/pages/OAuthAuthorizePage.jsx, an ordinary route (/oauth/authorize) of the existing SPA.
Infrastructure
Deploying this needs a KV namespace for the OAuth provider's grant/token storage (also used for the consent flow's short-lived pending-request state) and a Durable Object binding for HubMcp:
wrangler kv namespace create OAUTH_KVthen fill the returned id into OAUTH_KV in apps/api/wrangler.jsonc (the HubMcp Durable Object binding and its SQLite migration are already declared). No new secrets are needed — the flow reuses the existing SUPABASE_SERVICE_ROLE_KEY (via the same verifySupabaseUser every other route uses) and the publishable SUPABASE_ANON_KEY/SPELLING_CREATOR_API_URL vars already in wrangler.jsonc.
A note on the agents dependency
apps/api depends on agents (the Cloudflare Agents SDK, for McpAgent) and @cloudflare/workers-oauth-provider. One of agents' own transitive dependencies, @cfworker/json-schema, probes self.location at module load to pick a default base URI and throws on Workers' location global under this Worker's compatibility settings — crashing the whole Worker before any request is handled. This is patched via pnpm patch (see patches/@cfworker__json-schema@4.1.1.patch and the patchedDependencies entry in pnpm-workspace.yaml) to fall back to the library's own safe default instead of throwing; pnpm install applies it automatically.