# Sigao Li — personal MCP server [Health: Active]

**Category:** 🔄 Version Control  
**Repository:** https://github.com/SigaoLi/SigaoLi.github.io  
**GitHub Stars:** 0  
**Views:** 1  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/sigao-li-personal-mcp-server

## Description
Read-only access to Sigao Li's profile, CV and case studies. Bilingual (EN/ZH).

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "sigao-li-personal-mcp-server": {
    "command": "npx",
    "args": ["-y","astro"]
  }
}
```

## Documentation & README

# sigaoli.com

Personal website of **Sigao Li** — AI Product Manager · Spatial Data Scientist.
_From maps to models, and the products in between._

Bilingual (English at `/`, 中文 at `/zh/`), built with **Astro + Tailwind CSS v4 + GSAP**,
deployed to GitHub Pages via GitHub Actions. Launched 2026-06-11, replacing the previous
Jekyll (academicpages) site.

## Highlights

- **Generative canvas effects on a map motif** — an interactive particle field (home), contour terrain (work), a "river as timeline" with a flow field (CV), and a geo-network arc map (photography); all vanilla canvas/SVG, tuned to 60fps with reduced-motion and mobile fallbacks
- **Dotted world map** — land sampled from Natural Earth, with 79 GPS-extracted photo footprints across 6 countries; click a marker to open that country's gallery
- **Zoe, the digital doorcat** — Sigao's cat (驺虞) lives in the corner of every page as a set of AI-generated, chroma-keyed VP9-alpha video clips pinned to shared anchor poses, driven by a state machine: she dozes off when ignored, reacts to page switches, listens while you type, "types back" while the assistant streams, and keeps a few easter eggs (production handbook in `docs/`)
- **Built-in AI layer** — a floating chat assistant (fronted by Zoe) on every page — it suggests the single most relevant page as you ask, and greets a returning visitor by name (stored only in their own browser, opt-in) — plus a personal MCP server, both fed by a build-time knowledge pack generated from the same sources as the pages (see below)
- **Machine-readable by design** — [`/llms.txt`](https://sigaoli.com/llms.txt), [`/llms-full.txt`](https://sigaoli.com/llms-full.txt), [`/resume.json`](https://sigaoli.com/resume.json) (JSON Resume), [`/knowledge.json`](https://sigaoli.com/knowledge.json), [`/.well-known/mcp.json`](https://sigaoli.com/.well-known/mcp.json), JSON-LD, and a robots.txt that explicitly welcomes AI crawlers
- **Photographs file themselves** — drop the full-resolution originals into `_inbox/` and commit. A pre-commit hook works out the country from the coordinates against Natural Earth polygons, archives the originals outside git, derives 2560px masters with the EXIF stripped, asks a vision model for the bilingual caption, and registers everything in `photos.json` — the map, the counts and the assistant's knowledge all follow from there
- **Either language pulls the other along** — edit the Chinese or the English and commit. The hook works out which side moved and asks a model whether the edit actually changed what the other side must say. Polishing one language leaves the other alone; a changed fact is revised rather than rewritten, so hand-picked wording survives. Editing both in one commit counts as already handled by hand
- Lighthouse (mobile): 96–100 across all categories; zero cookies, no paid services, and a plain-language privacy notice at [`/privacy`](https://sigaoli.com/privacy)

## Commands

| Command | Action |
| --- | --- |
| `npm run dev` | Dev server at `localhost:4321` (Astro 7 runs it as a daemon — stop with `npx astro dev stop`) |
| `npm run build` | Production build to `dist/` |
| `npm run preview` | Serve the production build locally |
| `npm test` | Unit tests for both hook pipelines (`node --test`, no extra dependencies) |
| `npm run ingest` | Photo ingest by hand. Normally the pre-commit hook runs it; useful for watching what it does |
| `npm run sync` | Bilingual sync by hand. Same — the hook runs it, this is for watching |
| `node scripts/check-links.mjs` | Internal link integrity check over `dist/` |
| `node scripts/verify-nav.mjs` 等 | Playwright interaction suites (run against a local server) |
| `npm run dev` (in `worker/`) | Chat + MCP Worker at `localhost:8787` (wrangler; secrets in `worker/.dev.vars`, never committed) |
| `node scripts/verify-chat.mjs` | E2E chat-widget test (needs both dev servers running) |
| `node scripts/verify-zoe.mjs` | E2E for Zoe's action state machine (append `?zoe-fast` locally to compress minute-scale timers) |
| `node scripts/verify-typeroute.mjs` | E2E for the intent-driven typing clip and the bilingual 404 page |

> Any Playwright suite that waits on Zoe's state must pin the clock
> (`Date.prototype.getHours = () => 14`): between 23:00 and 06:00 she starts the
> session asleep, so `state` never reaches `idle` and the run just times out.

> When adding a Zoe clip, decide **who prewarms it and when** at the same time.
> A clip that is only fetched at playback stalls on a slow connection, and the
> stage shows nothing until it decodes. Prewarming has been missed three times
> already. Note `warm()` takes the *file* name (`sit-to-loaf`), not the `ZOE`
> key (`sitToLoaf`).

> The chat panel is rebuilt on every navigation — `transition:persist` keeps
> Zoe's stage, not the panel. **Anything that lives only in panel DOM is gone
> the moment a visitor clicks a link.** The streaming reply, the guidance chip
> and the unsent draft each had to be given module state plus a path back
> through `paint()`; the chip was lost for weeks before anyone noticed. So when
> adding persistent UI here, answer two questions up front: how does `paint()`
> rebuild it, and should it ride along in `sessionStorage` with the history?
> Measure geometry only once the panel is visible — `scrollHeight` is 0 while
> it is hidden, which silently writes `height: 0px`.

> **Turnstile guards `/chat` and `/classify`. It must never guard `/mcp`.**
> That endpoint exists so machines can read Sigao's profile — it is in the
> official registry — and Turnstile exists to stop machines. It also costs
> nothing to serve: the tools read the knowledge pack and never call a model.
> The static outlets (`llms.txt`, `knowledge.json`, `.well-known/mcp.json`) are
> served by Pages and never reach the Worker at all.

> **Locally, Turnstile uses Cloudflare's always-pass test keys** — sitekey in
> `site.ts` behind `import.meta.env.DEV`, secret in `worker/.dev.vars`. The real
> key rejects headless browsers, which is exactly its job, so every suite that
> drives a real Worker would fail against it. The real secret exists only in
> production, set with `wrangler secret put`. A corollary worth remembering:
> **the production happy path cannot be verified from a script** — reaching it
> needs a human in a real browser. Automation can still prove the gate is up
> (a request with no credential must return 403).

```
src/
├── pages/            # en routes + zh/ mirrors; llms.txt / resume.json / knowledge.json endpoints
├── components/       # Nav, Hero, WorldMap, Lightbox, CommandK, ChatWidget …
│   └── pages/        # shared page bodies rendered by both locales
├── content/          # cases & research (en) + cases-zh & research-zh (generated, reviewed)
├── data/             # cv.json / cv.zh.json / photos.json (GPS + bilingual alts)
│   └── knowledge/    # persona sources for the AI assistant (about / faq / guidelines / boundaries)
├── lib/              # i18n dict, GSAP lifecycle helper, site config
│   └── knowledge/    # knowledge-pack pipeline (same-source layers + build-time privacy guard)
└── assets/           # photo originals (optimized at build; originals never shipped)
worker/               # Cloudflare Worker: /chat (SSE) + /classify (intent) + /mcp (MCP server)
└── src/core/         # runtime-agnostic logic; Cloudflare specifics live only in src/adapter/
public/zoe/           # Zoe's clip library (600p VP9 alpha, lazy-loaded; idle loads first)
docs/                 # zoe-production-handbook.md — clip production specs & prompt cards
```

## AI layer

One knowledge layer, three outlets: `/llms-full.txt` for passive crawlers, a chat assistant
(`POST /chat`, SSE) for humans, and an MCP server (`/mcp`, Streamable HTTP, no auth — tools:
`get_profile` / `list_experience` / `get_case_study`) for visiting agents, both served from
`api.sigaoli.com` (Cloudflare Worker, code in `worker/`). The knowledge pack
([`/knowledge.json`](https://sigaoli.com/knowledge.json)) is assembled at build time from the
same sources as the pages — persona markdown, `cv.json`, case studies, photo stats — so any
content edit propagates to all three outlets on the next deploy, no manual step. A privacy
guard fails the build if sensitive patterns (phone numbers, IDs, coordinates) ever leak into
the pack.

Alongside each reply the chat runs a lightweight intent classifier (`POST /classify`, a small
model) to suggest the single most relevant page, and can remember a returning visitor's name —
both kept entirely in the visitor's own browser (opt-in, clearable via "Forget me"), never on a
server. Visitors in the EU/EEA/UK have their chat and classification routed to an EU-hosted
provider, never the China-direct API. What the site stores and sends is described in plain
language at [`/privacy`](https://sigaoli.com/privacy).

## Editing content

- **Case studies / research**: edit either language and commit. The pre-commit hook works out
  which side moved, asks a model whether the edit actually changed what the other side must
  say, and only then revises it — passing the existing translation along so a changed fact
  gets fixed without the rest of your wording being rewritten. Polish one language and the
  other is left alone. Edit both in one commit and that counts as already handled by hand.
  See `docs/bilingual-sync-design.md`.
- **CV**: edit `src/data/cv.json` or `cv.zh.json` — the hook keeps the other in step entry by
  entry, so changing one role doesn't touch the rest. The timeline, `/resume.json` and
  `/llms-full.txt` all render from it. Replace `public/files/pdf/CV__Sigao_Li.pdf` alongside.
  (`skills` is deliberately left out of the sync: the two languages use different shapes there
  and the renderer handles both.)
- **UI strings & hero copy**: hand-written bilingual dictionary in `src/lib/i18n.ts`.
- **Photos**: drop the full-resolution originals into `_inbox/` and commit. The pre-commit
  hook files them by GPS, archives the originals to `_originals/` (never committed), derives
  2560px serving masters, writes bilingual captions with a vision model, and registers them in
  `src/data/photos.json`. Photos without GPS go in `_inbox/<country-id>/` instead. Counts,
  the map and the AI knowledge pack all follow from `photos.json` automatically.
  See `docs/photo-ingest-design.md`.
- **AI assistant persona**: edit `src/data/knowledge/*.md`; the knowledge pack rebuilds on
  every deploy and the assistant follows within ~10 minutes (Worker-side cache TTL).
- **Zoe's actions**: source clips live outside the repo; the pipeline
  (`scripts/zoe-board2.mjs` → `zoe-qc2.mjs` → `zoe-prod2.mjs`) keys, QCs, mirrors and
  encodes them into `public/zoe/`. New actions = one clip + one row in the `ZOE` table in
  `ChatWidget.astro`; specs and prompt cards in `docs/zoe-production-handbook.md`.

## Local setup

Two chains run from a pre-commit hook: photo ingest, and bilingual sync. After
cloning, point git at the tracked hooks directory once:

```bash
git config core.hooksPath .githooks
```

Without this neither runs — photos dropped into `_inbox/` stay there, and the two
languages stop tracking each other. Both exit immediately when there is nothing to
do, so an ordinary commit is unaffected.

Both need `.env` (copy `.env.example`): the model calls for captions, translation
and the staleness check all go through that gateway.

**Regenerating the lock file.** `package-lock.json` has to be built on Linux. npm
records only the optional dependencies it can resolve on the machine it runs on,
so a lock made on Windows omits what sharp needs on a runner and `npm ci` refuses
to install. After changing dependencies here, run the **Relock on Linux** workflow
from the Actions tab; it rebuilds the lock, proves `npm ci` works, and commits the
result. Ordinary pushes never need it — and if you forget, the verify job says so.

## Deployment

Push to `master` → `deploy.yml` builds and deploys to Pages. Pushes to `v2` build
without deploying (verification).

Two more workflows watch rather than gate, each emailing on failure and neither able
to hold up a release:

| Workflow | When | What |
|---|---|---|
| `verify.yml` | every push | Builds, checks internal links, then drives eight browser scripts against the production build. Failures upload screenshots and Playwright traces — drop a trace into [trace.playwright.dev](https://trace.playwright.dev) to replay the run frame by frame |
| `audit.yml` | Mondays | `npm audit` over the site and the worker |

The audit used to gate the build. It reads the day's advisory database rather than
this repository, so an untouched commit could go from green to red overnight — which
is exactly what stopped a photo batch from shipping on 13 September. Knowing is worth
an email; being blocked is not.

The Worker deploys separately: `cd worker && npx wrangler deploy` (secrets via
`wrangler secret put`; custom domain `api.sigaoli.com` bound in the Cloudflare dashboard).
When a batch changes both, deploy the Worker **first** — the chat UI calls its endpoints, so a
site push ahead of the Worker leaves a brief window where those calls 404.

A deploy takes up to a minute to reach every edge location. Checking immediately reads the
previous version, which has twice looked like a broken deploy when nothing was wrong — wait,
then check.

Daily chat usage is at `https://api.sigaoli.com/usage` (last seven days, plus whether today has
hit the cap). The cap itself is `DAILY_CAP` in `worker/src/core/quota.ts`; when it trips it emails
once via Cloudflare Email Routing.

> ⚠️ **Never click "Sync fork".** This repository began as an academicpages fork; syncing
> would reset `master` to the upstream template. If that ever happens again:
> `git push --force origin <good-commit>:master`.

