Asking Fate MCP Server
by Armulee
README.md
# AskingFate — MCP Server
Remote MCP server for [askingfate.com](https://askingfate.com) fortune-telling services, built with Next.js (App Router, API-only) and [`mcp-handler`](https://www.npmjs.com/package/mcp-handler).
**Endpoint:** `https://mcp.askingfate.com/mcp` (Streamable HTTP only — SSE is disabled), protected by OAuth 2.1 (see [Auth](#auth) below)
## Tools
| Tool | Description |
| --- | --- |
| `draw_tarot_spread` | Shuffles the full 78-card deck (crypto-random, no duplicates, upright/reversed) and opens an interactive MCP App UI where the user taps face-down cards arranged in a fan to complete the spread (`single`, `three_card`, or `celtic_cross`). The user's picks are sent back to the host for the model to interpret. |
| `get_thai_horoscope` | Structured Thai astrology data (the traditional Thaksa tables) for a birth date: day planet, element, lucky/kalakini colours, lucky numbers, the eight Thaksa positions, and an approximate ascendant when a birth time is given. |
| `get_zodiac_info` | Western (tropical) sign with element/quality/ruling planet, approximate Thai sidereal sign, and the Thai/Chinese 12-year animal with year-boundary caveats. |
| `get_auspicious_dates` | Auspicious dates in a month for a purpose (`wedding`, `business`, `moving`, `car`) rated by traditional Thai day-of-week principles, with reasons and days to avoid. |
All tools are read-only (`readOnlyHint: true`), computed from static data modules — no external APIs. Tool results return structured JSON for the model to interpret; the server does not generate fortune text.
## Development
```bash
npm install
cp .env.example .env.local # set OAUTH_DEV_USER_ID for local testing
npm run dev # then connect an MCP client to http://localhost:3000/mcp
npm run build # production build + strict TypeScript check
```
## Layout
- `app/[transport]/route.ts` — MCP handler, tool registration, CORS, `withMcpAuth` wrapper
- `app/oauth/` — OAuth 2.1 authorization server (`register`, `authorize`, `token`)
- `app/.well-known/` — OAuth metadata (RFC 9728 protected resource + RFC 8414 authorization server)
- `lib/oauth/` — token/store/session plumbing (see below)
- `lib/tarot/` — 78-card deck data, spread definitions, crypto-random shuffle
- `lib/astro/` — Thai Thaksa tables, horoscope/zodiac/auspicious-date logic
- `lib/mcp/tarot-app-html.ts` — self-contained card-picking MCP App (sandboxed iframe, no storage APIs)
## Auth
The MCP endpoint requires OAuth 2.1 Bearer tokens. This deployment is **both**
the authorization server and the protected resource (issuer =
`https://mcp.askingfate.com`), and it delegates login to the existing
askingfate.com account system.
### Flow (what Claude does when you add this as a custom connector)
1. `POST /mcp` without a token → `401` + `WWW-Authenticate` pointing at
`/.well-known/oauth-protected-resource`, which points at the authorization
server metadata (`/.well-known/oauth-authorization-server`).
2. Claude registers itself via **Dynamic Client Registration**
(`POST /oauth/register`, RFC 7591). Public clients
(`token_endpoint_auth_method: "none"`) and confidential clients
(`client_secret_post` / `client_secret_basic`, secret stored hashed) are
accepted; PKCE stays mandatory either way. Scope values are treated as
opaque labels (Claude sends `claudeai`) — only the format is validated.
Redirect URIs are checked against an allowlist
(`https://claude.ai/api/mcp/auth_callback`,
`https://claude.com/api/mcp/auth_callback`, plus
`OAUTH_ALLOWED_REDIRECT_URIS`). The endpoints are also aliased at the
MCP-spec default root paths (`/register`, `/authorize`, `/token`) for
clients that skip metadata discovery.
3. The user lands on `GET /oauth/authorize`. No session yet → redirect to
**askingfate.com/signin** with a `callbackUrl` pointing at
`/oauth/callback` here. After login (or immediately, if already logged in
on askingfate.com) the main site hands the Supabase access token back in
the URL fragment; `/oauth/callback` POSTs it to `/oauth/session`, which
verifies it server-side (same Supabase project) and sets a signed
HttpOnly session cookie for this deployment, then resumes the authorize
URL → a short consent page (app name, requested scopes, Allow/Deny).
4. Consent issues a single-use authorization code (60 s TTL, stored hashed,
bound to user + client + PKCE `code_challenge`) and redirects back to Claude.
5. `POST /oauth/token` exchanges the code — **PKCE S256 is verified on every
exchange, no fallback** — for a 1-hour HS256 JWT access token
(`user_id`, `scope`, `exp` in the payload) plus a 30-day refresh token
(stored hashed, rotated on every use).
6. Tools receive the caller via `extra.authInfo` — the askingfate user id is at
`extra.authInfo.extra.userId`.
### Storage
Three "tables", all keyed in Redis (Upstash REST / Vercel KV) with in-memory
fallback for local dev: `oauth_clients`, `oauth_codes`, `oauth_refresh_tokens`.
Codes and refresh tokens are stored **only as SHA-256 hashes**. Set
`UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN` (or the `KV_REST_API_*`
pair) in production.
### Wiring up the askingfate.com account system
The main site authenticates with Supabase **in the browser** (the session
lives in localStorage on askingfate.com), so no cookie ever reaches the
`mcp.` subdomain. Instead, the main site's `/signin`, `/signup` and Google
`/auth/callback` pages recognise a `callbackUrl` pointing at an allowlisted
external origin (this deployment — see `lib/external-auth-callback.ts` in
the simple-tarot repo) and hand the Supabase access token back in the URL
fragment; `/oauth/session` verifies it against the project and issues our
own session cookie. Deployment requirements:
1. Set `SUPABASE_URL` + `SUPABASE_ANON_KEY` (or their `NEXT_PUBLIC_`-prefixed
twins) to the exact values the main site uses — without them
`/oauth/authorize` refuses with a 503 saying so.
2. The main site must be deployed with the token-handoff support above.
`lib/oauth/session.ts`/`lib/oauth/supabase.ts` remain the single integration
point. When session establishment fails, `/oauth/callback` shows the exact
server error (wrong Supabase project, missing `JWT_SECRET`, origin
mismatch, …) and the reason is also logged server-side.
### Testing locally / MCP Inspector
```bash
# .env.local: OAUTH_DEV_USER_ID=user_dev_1 (bypasses the askingfate session
# check — dev builds only, ignored in production)
npm run dev
npx @modelcontextprotocol/inspector # connect to http://localhost:3000/mcp
```
Loopback redirect URIs (`http://localhost`, `http://127.0.0.1`) are allowed
outside production so the Inspector can complete the flow; in production they
are rejected unless `OAUTH_ALLOW_LOOPBACK_REDIRECTS=true`.
Quick smoke checks:
```bash
curl -si -X POST http://localhost:3000/mcp | grep -i www-authenticate
curl -s http://localhost:3000/.well-known/oauth-protected-resource | jq
curl -s http://localhost:3000/.well-known/oauth-authorization-server | jq
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues