Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git
.github
deploy
*.md
.env*
50 changes: 22 additions & 28 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
# Footics MCP — environment variables
# Copy this file to .env.local for local development, and set the same values in
# the Vercel project (Settings -> Environment Variables). Point at a staging
# project to test safely.
# Footics MCP (Go) — environment variables.
# The server is a thin authenticated client of the Footics REST API (/v1):
# it holds no database connection and no service-role key.

# Supabase database (SAME project as the Footics app). Use the pgbouncer pooler
# (port 6543, ?pgbouncer=true) — db/index.ts runs with prepare:false.
DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-eu-central-1.pooler.supabase.com:6543/postgres"
# Listen address.
HTTP_ADDR=":8080"

# Supabase project (used to verify tokens). Either naming convention is accepted.
NEXT_PUBLIC_SUPABASE_URL="https://<ref>.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="<anon-key>"
# Footics API base URL (every read/write goes through it, with the user's token).
FOOTICS_API_URL="https://api.footics.app"

# Public URL of the MCP server (custom domain). Used to build the OAuth resource URI.
# User-token verification — pick one mode:
# JWKS (asymmetric, production): comma-separated JWKS URL(s).
AUTH_JWKS_URL="https://api.footics.app/auth/v1/.well-known/jwks.json"
# HS256 (symmetric, local dev against a local GoTrue):
# AUTH_JWT_SECRET=""
AUTH_JWT_AUD="authenticated"

# OAuth resource metadata served to MCP clients (.well-known/oauth-protected-resource).
MCP_PUBLIC_URL="https://mcp.footics.app"
MCP_OAUTH_ISSUER="https://api.footics.app/auth/v1"

# Kill switch. "true" (default) = serving. "false" = /mcp returns 503 for every
# request WITHOUT checking the token or running a tool (no Supabase/DB traffic).
# Kill switch. "true" (default) = serving; "false" = /mcp answers 503 without
# checking tokens or running tools.
MCP_ENABLED="true"

# Require auth? "true" (default). "false" = temporary authless test mode only.
MCP_REQUIRE_AUTH="true"

# Allow writing predictions? "false" (default, read-only). Set "true" only when
# you are ready to expose submit_prediction (guardrails ported from the app).
MCP_ENABLE_WRITES="false"

# Token verification: "getuser" (default, robust) or "jwks" (local, asymmetric).
MCP_TOKEN_VERIFY="getuser"
# Allow writing predictions? "false" = read-only. Production runs "true".
MCP_ENABLE_WRITES="true"

# OAuth issuer advertised in the resource metadata. Default = SUPABASE_URL/auth/v1.
# MCP_OAUTH_ISSUER="https://<ref>.supabase.co/auth/v1"
# Per-user tool-call rate limit (calls/minute/user). 0 = off. Default: 30.
MCP_RATE_LIMIT_PER_MIN=30

# Identity fallback in authless mode (MCP_REQUIRE_AUTH=false): test account id.
# MCP_TEST_USER_ID="00000000-0000-0000-0000-000000000000"

# For `pnpm token` (mints a test Supabase access token):
# TEST_EMAIL="you@example.com"
# TEST_PASSWORD="your-password"

# Per-user tool-call rate limit (calls/minute/user). 0 = off. Default: 30.
MCP_RATE_LIMIT_PER_MIN=30
25 changes: 8 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,15 @@ on:
pull_request:

jobs:
build:
check:
name: build · vet · fmt · test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.2.1

- uses: actions/setup-node@v4
- uses: actions/setup-go@v5
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm typecheck

- name: Build
run: pnpm build
go-version-file: go.mod
- run: go build ./...
- run: go vet ./...
- run: test -z "$(gofmt -l .)"
- run: go test ./...
15 changes: 4 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
# deps
node_modules
.pnp
.pnp.*

# next
.next
out
next-env.d.ts
*.tsbuildinfo
# binaries
/mcp
footics-mcp-linux
*.exe

# env (never commit secrets)
.env
Expand All @@ -17,6 +11,5 @@ next-env.d.ts
# misc
.DS_Store
*.log
.vercel
.idea
.vscode
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# footics-mcp — multi-stage build → tiny static binary (same pattern as footics-api).

# ---- build ----
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0 GOOS=linux
RUN go build -trimpath -ldflags="-s -w" -o /mcp ./cmd/mcp

# ---- runtime ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /mcp /mcp
EXPOSE 8080
ENTRYPOINT ["/mcp"]
54 changes: 33 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,66 @@ account, and the assistant can read their matches, standings and predictions —
and, optionally, submit them. Inference runs on the user's side; Footics only
pays for hosting.

Separate Vercel project from the main site, **same Supabase database**. Built with
Next.js 16 (App Router) and [`mcp-handler`](https://github.com/vercel/mcp-handler).
Written in **Go** (`cmd/mcp`) with the official
[`modelcontextprotocol/go-sdk`](https://github.com/modelcontextprotocol/go-sdk).
The server is a **thin authenticated client of the Footics REST API** (`/v1`):
it holds no database connection and no service-role key — every read and write
is forwarded to the API with the calling user's token, so the API stays the
single source of truth and the single write point.

## Architecture

```
AI assistant (Claude / ChatGPT) ──MCP/HTTP──► app/[transport]/route.ts (/mcp)
│ withMcpAuth → lib/auth (verify Supabase JWT)
│ lib/tools → lib/queries (reads)
│ → lib/predictions (writes, gated)
AI assistant (Claude / ChatGPT) ──MCP/HTTP──► cmd/mcp (/mcp)
│ internal/auth verify user JWT (JWKS or HS256)
│ internal/tools → internal/apiclient (calls /v1 with the user token)
Supabase Postgres (shared with the site)
Footics API (api.footics.app) ──► Postgres
```

- **`lib/queries.ts`** — lean reads that return raw JSON for the model, not the UI view-models.
- **`lib/predictions.ts`** — writes with the same guardrails as the main app. Off by default (`MCP_ENABLE_WRITES=false`).
- **`db/schema.ts`**, **`db/index.ts`**, **`lib/jokers.ts`** — mirror the main app's schema and rules so column contracts never drift.
- **`internal/auth`** — verifies Supabase-issued user JWTs (JWKS asymmetric in
prod, HS256 for local dev), same JWKS cache + refresh cooldown as the API.
- **`internal/apiclient`** — forwards each tool call to the Footics `/v1` API
with the user's bearer token (no privileged credentials here).
- **`internal/tools`** — the MCP tool definitions + per-user rate limiting.

## Tools

`whoami` · `list_matches` · `get_match` · `get_my_standing` · `get_my_predictions` ·
`get_leaderboard` · `list_my_groups` · `get_joker_status` · `search` ·
`submit_prediction` *(gated)*
`submit_prediction` *(gated by `MCP_ENABLE_WRITES`)*

## Configuration

| Variable | Default | Effect |
|---|---|---|
| `MCP_ENABLED` | `true` | `false` → `/mcp` returns 503 with no Supabase/DB call (kill switch) |
| `FOOTICS_API_URL` | — | base URL of the Footics REST API |
| `AUTH_JWKS_URL` | — | JWKS URL(s) for asymmetric token verification (prod) |
| `AUTH_JWT_SECRET` | — | HS256 secret (local dev alternative to JWKS) |
| `MCP_ENABLED` | `true` | `false` → `/mcp` returns 503 without verifying tokens (kill switch) |
| `MCP_REQUIRE_AUTH` | `true` | require a valid bearer token (401 otherwise) |
| `MCP_ENABLE_WRITES` | `false` | allow `submit_prediction` |
| `MCP_TOKEN_VERIFY` | `getuser` | `getuser` (robust) or `jwks` (local, asymmetric) |
| `MCP_RATE_LIMIT_PER_MIN` | `30` | tool calls per minute per user (`0` = off) |

See [`.env.example`](./.env.example) for the full list, including the Supabase
connection variables.
See [`.env.example`](./.env.example) for the full list.

## Development

```bash
pnpm install
cp .env.example .env.local # fill in — point at a staging project, not production
pnpm dev # http://localhost:3000/mcp
pnpm token # mint a test access token + ready-to-paste MCP Inspector commands
cp .env.example .env # point AUTH at a local/staging GoTrue, not production
go run ./cmd/mcp # serves on $HTTP_ADDR (default :8080), /mcp
```

`pnpm token` prints an MCP Inspector command: run `tools/list`, then `whoami`,
then any tool. Without an `Authorization` header the endpoint returns `401`.
Without an `Authorization: Bearer <token>` header the endpoint returns `401`
(unless `MCP_REQUIRE_AUTH=false`). Build a static binary with
`go build ./cmd/mcp`; the container image is the multi-stage
[`Dockerfile`](./Dockerfile) (distroless).

## Deployment

Runs on the Footics VPS as a Dokploy service (`footics-mcp`), build-from-git via
[`deploy/compose.dokploy.yml`](./deploy/compose.dokploy.yml) → published at
`mcp.footics.app`. Runbook lives in the private infra repo.

## Security

Expand Down
71 changes: 0 additions & 71 deletions app/[transport]/route.ts

This file was deleted.

27 changes: 0 additions & 27 deletions app/api/oauth-prm/route.ts

This file was deleted.

Binary file removed app/apple-icon.png
Binary file not shown.
36 changes: 0 additions & 36 deletions app/health/route.ts

This file was deleted.

7 changes: 0 additions & 7 deletions app/icon.svg

This file was deleted.

Loading
Loading