# MCPedia β€” Phase Status Legend: βœ… built Β· 🟑 partial Β· ⬜ deferred ## Phase 1 β€” MVP (βœ… DONE) | Capability | Status | Notes | | --------------------- | ------ | ----- | | Monorepo (bun + Turbo)| βœ… | apps/{web,mcp}, packages/{types,config,db,parser,search,core}, scripts | | Content as Markdown | βœ… | `content/{docs,writeups,research,notes}/`, Git-tracked | | Frontmatter parsing | βœ… | `@mcpedia/parser` (gray-matter) | | Postgres metadata | βœ… | `@mcpedia/db` Drizzle, `documents` table | | Postgres FTS | βœ… | weighted `tsvector` (title A / body B), GIN index, `ts_rank`+`ts_headline` | | Core services | βœ… | Document / Content / Search β€” single business-logic layer | | Indexer | βœ… | `scripts/indexer.ts` walks content/ β†’ upserts | | Web UI (Next 16) | βœ… | home (list), doc view (SSG), search (dynamic). react-markdown render | | MCP server (stdio) | βœ… | 4 tools; in-memory smoke test passing | | Hybrid/semantic search| ⬜ | Phase 2 | ## Phase 2 β€” Semantic + API - [x] `packages/embeddings` β€” `EmbeddingProvider` interface + OpenRouter provider (via 9router `/v1`, `encoding_format:"float"`); `chunkText` + `embedChunks` batcher. `EMBED_DIM=2048` discovered live. - [x] Schema `document_chunks` (id, document_idβ†’documents.id cascade, slug, chunk_index, content, `embedding real[]`). Stored as `real[]` because pgvector **is not installed** on the shared imrnes Postgres (installing needs host-level apt β€” deferred). Cosine computed in-app; instant for a KB-sized corpus. - [x] `scripts/indexer.ts` β€” chunks + embeds + upserts (per-doc replace). - [x] `@mcpedia/search` β€” `semanticSearch` (cosine) + `hybridSearch` (FTS + cosine, RRF fusion). `keywordSearch` unchanged. - [x] `apps/api` β€” Hono + tRPC v11 (`@trpc/server` fetch adapter, `@hono/node-server` on :4020): `search`, `semanticSearch`, `hybridSearch`, `getDocument`, `listDocuments`, `related`. - [x] MCP server β€” added `semantic_search` + `hybrid_search` tools (6 total). - [x] Web search β€” keyword/hybrid toggle (`?mode=hybrid`), hybrid reaches semantically-related docs keyword misses. ## Phase 3 β€” Async + Scale βœ… DONE - [x] **Redis + BullMQ background indexing / embedding workers** β€” `packages/queue` (ioredis singleton + BullMQ `Queue`/`Worker`, prefix `mcpedia:` on shared imrnes Redis `:6379`); `apps/worker` runs `startWorker()`. Three job types: `index-doc`, `index-all`, `reindex`. Single indexing entry point `indexContentFile`/`runFullIndex` in `@mcpedia/core` shared by the script, worker, and git hook. Verified end-to-end against live Redis (job enqueue β†’ worker β†’ Postgres write). - [x] **Git synchronization hook (auto-reindex on push)** β€” API webhook `POST /hooks/reindex` (full) and `POST /hooks/index?slug=` (single) enqueue BullMQ jobs. Wire a Git provider (GitHub/Gitea) post-receive / webhook to `POST /hooks/reindex` to auto-reindex on push. `scripts/enqueue.ts` is a one-shot enqueue helper (`bun run enqueue --all` / ``). - [x] **Document revision system (`document_revisions`)** β€” `packages/db` migration `0002_document_revisions.sql`. Indexer snapshots a revision only when the body actually changes vs the latest revision (pure metadata edits don't bloat history). `listRevisions` / `getRevision` / `restoreRevision` in `@mcpedia/core`; exposed as tRPC `revisions` / `getRevision` / `restoreRevision` and the `mcpedia://docs/{+slug}/revisions` MCP Resource. - [x] **MCP Resources (`mcpedia://docs/...`)** β€” alongside the 6 tools: `mcpedia://docs` (list), `mcpedia://docs/{+slug}` (body from disk), `mcpedia://docs/{+slug}/chunks` (chunk preview), `mcpedia://docs/{+slug}/revisions` (history). `{+slug}` uses RFC 6570 reserved expansion so slugs containing `/` match. ### New/changed commands ``` bun run index # full reindex (runFullIndex, writes revisions) bun run enqueue --all # enqueue a full reindex job (no worker needed) bun run enqueue # enqueue a single-doc reindex job bun run worker # start the BullMQ indexing worker (long-running) bun run api # Hono+tRPC API on :4020 (added /hooks/* webhooks) ``` ### Verification done (real, against imrnes Redis + Postgres) - `turbo run typecheck` green across all 13 packages. - BullMQ e2e: enqueue `index-doc` β†’ worker completes β†’ `documents` + `document_chunks` + `document_revisions` rows present. - Revision dedup proven: editing a body creates a new revision; metadata-only reindex does not; `restoreRevision` writes history back into the live row. - MCP smoke test passes (tools + all 4 resources). - API webhook `POST /hooks/reindex` enqueues β†’ worker drains queue β†’ `queueStatus` reflects counts. ## Phase 4 β€” Operability & Correctness Hardening βœ… DONE > Reinterpreted from the original "Scale-out" plan: OpenSearch/object-storage/ > multi-tenant were flagged YAGNI at KB scale (4 docs), so Phase 4 = make the > Phase 3 async + revision machinery **correct, secure, observable, deployable**. - [x] **T1 β€” `restoreRevision` rebuilds semantic chunks (CORRECTNESS BUG)** β€” previously restore wrote the old body into `documents` but left `document_chunks` on the *new* body, so semantic/hybrid search went stale after a restore. `@mcpedia/core` `reindexChunks(slug)` now re-chunks + re-embeds from the live body; `restoreRevision` calls it after the update (embed failure is logged, not thrown). Verified: restore β†’ `document_chunks` count matches re-chunk of the restored body. - [x] **T2 β€” Secure git-sync webhook (SECURITY)** β€” `/hooks/*` now require an `x-webhook-secret` header matching `WEBHOOK_SECRET` (401 otherwise). API fails fast at startup if `WEBHOOK_SECRET` is unset (no open endpoint). Added `WEBHOOK_SECRET` to `@mcpedia/config` + `.env.example`; generated a real secret in the local `.env` (gitignored). - [x] **T3 β€” Web UI revisions view (UX)** β€” doc page now shows a "History" panel (revision no, reason, date, body length) with a per-revision Restore button. Restore POSTs to `apps/web/app/api/revisions/restore/route.ts` β†’ `restoreRevision` β†’ `revalidatePath` (server-component only, no client JS). - [x] **T4 β€” Paginate `listRevisions`** β€” added `offset` param (summary never includes body). API `revisions` + MCP resource use the summary. - [x] **T5 β€” Deploy as supervised services (OPS)** β€” `deploy/mcpedia-api.service` + `deploy/mcpedia-worker.service` systemd units (`Restart=on-failure`, `EnvironmentFile=.env`, `WorkingDirectory=/home/code/mcpedia`). Enable with: `cp deploy/*.service /etc/systemd/system && systemctl daemon-reload && systemctl enable --now mcpedia-api mcpedia-worker`. (Not auto-enabled on host without explicit user go-ahead.) ### Verification done (real, against imrnes Redis + Postgres) - `turbo run typecheck` + `turbo run build` green (incl. `next build` with the History panel). - T1: edit β†’ reindex (new revision + chunks) β†’ restore rev #1 β†’ `document_chunks` count for that slug matches re-chunk of rev #1; `semanticSearch` on a term unique to rev #1 returns it. - T2: `curl -XPOST /hooks/reindex` β†’ 401; with `-H "x-webhook-secret: $WEBHOOK_SECRET"` β†’ 200 + jobId; job drains via worker. - T3: History panel renders; restore route rebuilds chunks (T1 path). - T4: `revisions` returns summaries (no body); `offset` paging works. - T5: `systemd-analyze verify deploy/*.service` passes (off-host safe check). ## Phase 5 β€” Deferred scale-out (only when needed) - [ ] Dedicated search engine (OpenSearch/Elasticsearch) β€” YAGNI until FTS is insufficient - [ ] pgvector migration (install on imrnes Postgres) β€” when `real[]` cosine stalls - [ ] Object storage for assets - [ ] Advanced ranking, distributed workers, observability, multi-tenant ## Phase 6 β€” Network deployment + review hardening βœ… DONE > Closed the real gaps found during review: the MCP server was stdio-only (unreachable > over the network) and the tRPC API was not routed on the domain (swallowed by web β†’ > `/trpc/*` returned Next.js 404). Also found + fixed a security hole. - [x] **MCP over Streamable HTTP** β€” `apps/mcp/src/http.ts` serves the 6 tools + 4 resources via MCP 2025-03-26 Streamable HTTP on `:4021`, stateless mode (`sessionIdGenerator: undefined`, one server+transport per request, CORS on `/mcp`). Deployed as `mcpedia-mcp.service`; reachable at `https://mcp.asepharyana.my.id/mcp`. Stdio entry (`bun run mcp`) retained for local subprocess use. - [x] **tRPC API routed on the domain** β€” Caddy `wiki.asepharyana.my.id` now forwards `/trpc/*` (+ `/hooks/*`, `/health`) to the API on `:4020`; web stays on `:4016`. Read-only procedures (search, list, revisions, job status) are public; the `restoreRevision` mutation is gated by `x-webhook-secret` (see security fix below). - [x] **Security: lock down `restoreRevision`** β€” the state-changing tRPC mutation was anonymously callable over the network. Now requires `x-webhook-secret` (consistent with `/hooks` auth). The Web UI calls `@mcpedia/core` directly in a server component, so the gate does not affect the UI's restore button. Verified: no-secret β†’ 401-class rejection, with-secret β†’ reaches handler. - [x] **All four services live + supervised** β€” `mcpedia-web` (:4016), `mcpedia-api` (:4020), `mcpedia-worker` (BullMQ), `mcpedia-mcp` (:4021) all `active`, reboot-safe. GitHub push webhook β†’ `https://wiki.asepharyana.my.id/hooks/reindex` (verified 200, worker drains, 0 failed). ### Verification done (real, against live services) - `https://mcp.asepharyana.my.id/mcp` initialize β†’ 200 + serverInfo; tools/list β†’ 6; resources/list β†’ 4 (Streamable HTTP SSE framing). - `https://wiki.asepharyana.my.id/` β†’ 200; `/health` β†’ 200; `/trpc/listDocuments` β†’ 200. - GitHub push webhook delivers 200; `queueStatus` shows completed:N, failed:0. - `turbo run typecheck` green across all 4 apps. ## Phase 7 β€” Corpus, MCP write-tools + auth, observability βœ… DONE > Closed the remaining review gaps: tiny corpus (4 docs), MCP read-only (no write/auth), > no observability. - [x] **Content corpus grown** β€” added 5 real docs (Caddy reverse proxy, BullMQ workers, MCP Streamable HTTP, Postgres FTS, Cloudflare-525 debugging writeup) across docs/writeups/notes. `bun run index` reindexed: **9 documents, 17 chunks, 5 new revisions** (was 4 docs). Search/semantic/revisions now operate on a real corpus. - [x] **MCP write-tools + auth** β€” added `index_document`, `reindex_all`, `restore_revision` (write, require `x-webhook-secret`) and `queue_status` (public). `createMcpServer(authSecret?)` threads the request header; stdio keeps write tools open (trusted local). Verified: unauthenticated `index_document` β†’ `isError` + "unauthorized"; authenticated β†’ enqueues job, worker drains. - [x] **Observability** β€” `GET /metrics` on the API (Prometheus text exposition: `mcpedia_uptime_seconds`, `mcpedia_queue_jobs{state=...}`). Exposed on the domain at `https://wiki.asepharyana.my.id/metrics`. Public, safe to scrape. ### Verification done (real, against live services) - `https://wiki.asepharyana.my.id/metrics` β†’ 200 Prometheus text (uptime + queue gauges). - `tools/list` over MCP β†’ 10 tools (6 read + 4 new). `index_document` auth gate works. - Worker drained the MCP-enqueued job (completed count incremented, failed:0). - `turbo run typecheck` green across all 4 apps. ## Phase 8 β€” Dashboard (observability UI) βœ… DONE > The metrics endpoint existed (Phase 7) but had no consumer. Added a zero-dependency > dashboard so the KB is actually observable + searchable from a browser. - [x] **`GET /dashboard`** on the API β€” self-contained HTML (no build, no deps) that: - pulls `/metrics` (same origin) and renders queue gauges (waiting/active/completed/ failed/delayed) + uptime, refreshing every 5s with a live-dot status indicator; - runs a live **search box** that calls the MCP `hybrid_search` tool directly from the browser (MCP `/mcp` is CORS-open), returning ranked hits that link to the web doc page (`/docs/...`). - [x] XSS hardening: all KB-sourced fields (`slug`/`title`/`section`/error message) are `esc()`-escaped before `innerHTML` (defense-in-depth; data is server-trusted). - [x] Caddy: `wiki.asepharyana.my.id/dashboard` β†’ :4020. ### Verification done (real) - `https://wiki.asepharyana.my.id/dashboard` β†’ 200, serves the page (title + JS present). - `/metrics` β†’ 200, 7 gauge lines including `mcpedia_queue_jobs{state=...}`. - MCP `hybrid_search` from browser path returns real ranked hits (verified the exact `tools/call` payload the dashboard issues; shape `{doc:{slug,title,section},rank}`). - Dashboard link points to working web doc route `/docs/` (verified 200). - `turbo run typecheck` green. ## Phase 9 β€” Test coverage + CI gating βœ… DONE > Before Phase 9 the only test was an integration smoke (`apps/mcp/src/smoke.test.ts`) > requiring a live DB; its assertions had also rotted (expected 6 tools, now 10). Added > a real `bun:test` suite that runs green in CI with **no external services** via > in-process module mocking. - [x] **Test infra** β€” `turbo.json` `test` task (cache:false); `test` script on every package/app that has `.test.ts` files; `@types/bun` added to root devDeps; `tsconfig.base.json` registers `types: ["bun","node"]`; CI step `bun run test` added after `Build`. - [x] **`@mcpedia/embeddings`** (5 tests) β€” `chunkText`: empty input, single chunk, multi-chunk split, overlap/word-boundary integrity, default options. - [x] **`@mcpedia/parser`** (5 tests) β€” `parseFile`: frontmatter extraction, section derivation from top-level dir, invalid type/status fallbacks, missing-field defaults, body excludes delimiter. - [x] **`@mcpedia/search`** (8 tests) β€” `cosine` (orthogonal/identical/zero-vector/ mismatched-length/negative) + `toTsQuery` (AND-prefix, sanitization, empty/garbage). - [x] **`@mcpedia/core`** (4 tests) β€” `shouldCreateRevision` dedup truth table (no prior revision β†’ snapshot; identical body β†’ skip; changed body β†’ snapshot; empty vs non-empty). `restoreRevision` gained an `opts.reindex` seam for the chunk-rebuild contract. - [x] **`apps/api`** (8 tests) β€” refactored `index.ts` β†’ `app.ts` `createApp(deps?)` factory (pure construction, injectable `QueueLike`); `dashboard.ts` extracted; `/health`, `/metrics`, `/hooks/reindex` (401 w/o secret, 200 w/ secret), `/hooks/index` (400 w/o slug, 200 w/ slug+secret, 401 wrong secret), `/dashboard`. - [x] **`apps/mcp`** (6 tests) β€” write-tool auth gates via `InMemoryTransport`: `index_document`/`reindex_all`/`restore_revision` error without secret and enqueue with secret (mocked `@mcpedia/queue` + `@mcpedia/core`); `queue_status` public; tool discovery lists all 10 tools regardless of secret (gate is in handler). - [x] **Fixed rot** β€” renamed `smoke.test.ts` β†’ `smoke.ts` (so `bun test` doesn't run the integration smoke as a unit test) and updated stale assertions (10-tool set, 4 docs in `docs` section). ### Verification done (real) - `bun run test` β†’ 32 tests green across 6 packages, **no DB/Redis** (all fakes). - `bun run typecheck` β†’ 4 apps green (no test-only type errors). - `bun --cwd apps/mcp run smoke` β†’ SMOKE OK (integration, live DB). - Live API (temp port): `/health`β†’200, `/metrics`β†’200 gauges, `/hooks/reindex`β†’401/200. - CI workflow now runs `bun run test`. ### Files changed ``` new: apps/api/src/app.ts # createApp factory new: apps/api/src/dashboard.ts # dashboard HTML module new: packages/embeddings/src/chunk.test.ts new: packages/parser/src/parse.test.ts new: packages/search/src/cosine.test.ts new: packages/core/src/index.service.test.ts new: apps/api/src/app.test.ts new: apps/mcp/src/auth.test.ts mod: turbo.json, package.json, tsconfig.base.json, .github/workflows/ci.yml mod: apps/api/src/index.ts (thin re-export), apps/api/src/router.ts (unchanged) renamed: apps/mcp/src/smoke.test.ts -> smoke.ts (fixed stale assertions) ``` ## Phase 10 β€” Audit + Bug fixes + CI/CD deploy (live verification) > Full feature audit against live services. Found + fixed one real bug. Added the > missing CI/CD deploy pipeline. ### Bug: Frontmatter leaking into rendered doc pages + MCP doc body - **Symptom:** Doc pages showed raw YAML frontmatter (`id: websocket-contract`, `title: WebSocket Contract`, etc.) as visible plain text between `
` markers. MCP `mcpedia://docs/{+slug}` resource had the same leak. - **Root cause:** `getDocument()` in `@mcpedia/core` preferred the on-disk file via `readFileSync(abs, "utf8")` β€” returning **raw** file content including the `---` frontmatter block. The indexer correctly stripped frontmatter via `parseFile` (gray-matter), but `getDocument` bypassed it. `ReactMarkdown` rendered `---` as `
` and the YAML as paragraphs. - **Fix:** `packages/core/src/document.service.ts` β€” replaced `readFileSync` with `parseFile(abs, row.path).body` (same frontmatter stripping as the indexer). DB fallback (`row.body`) unchanged (already clean). - **Verified:** 9/9 doc pages render clean (no frontmatter `id:` text, proper `

` + `` elements in SSR HTML); MCP `mcpedia://` doc body resource returns clean markdown (starts with `# WebSocket Contract`). ### Audit findings (all phases verified live) | Phase | Feature | Live check | Status | |-------|---------|------------|--------| | P1 | Web UI `/docs/
/` | 200, renders markdown | βœ… | | P1 | Search page (`?q=` + `?mode=hybrid`) | 200, returns results | βœ… | | P1 | MCP stdio + HTTP (`/4021`) | 10 tools, 4 resources | βœ… | | P2 | Semantic/hybrid search | returns ranked chunks | βœ… | | P2 | tRPC API on domain (`/trpc/*`) | listDocuments β†’ 4 docs | βœ… | | P3 | BullMQ worker drains jobs | queue completed 17β†’19 after enqueue | βœ… | | P3 | Revision system | listRevisions β†’ rev #1 "phase4-final-clean" | βœ… | | P3 | Git webhook auth gate | 401 w/o secret, 200 w/ secret | βœ… | | P4 | Dashboard | `/dashboard` β†’ 200 HTML | βœ… | | P6 | All 4 systemd services | web/api/mcp/worker all `active` | βœ… | | P6 | restoreRevision mutation locked | 401 w/o secret, executes w/ secret | βœ… | | P7 | 10 MCP tools (6 read + 4 write) | tools/list β†’ 10 | βœ… | | P7 | Write-tool auth gate | reindex_all w/o secret β†’ isError | βœ… | | P7 | Prometheus metrics | `/metrics` β†’ 7 gauges, 200 | βœ… | | P8 | Dashboard live search | `fetch("/metrics")` + `hybrid_search` via `/mcp` | βœ… | | P9 | Test suite | 6/6 packages, 32 tests, 0 fail | βœ… | ### Notes / non-bugs - Doc URLs follow `/
/` (e.g. `/docs/caddy/reverse-proxy`, `/writeups/infra/cloudflare-525`, `/notes/postgres/full-text-search`). The route is `[section]/[...slug]` β€” `/docs/websocket/contract` works because the section IS `docs` for that doc; `/notes/postgres/fts` does not (the correct slug is `notes/postgres/full-text-search`). - `restoreRevision` via tRPC needs the `x-webhook-secret` as an **HTTP header** (not inside the JSON body) β€” the fetch adapter reads `c.req.raw.headers`. ### CI/CD deploy pipeline (Phase 10 addition) Before this audit the CI workflow only built+tested β€” it did **not** deploy. The VPS services were configured manually (systemd units in `deploy/`). Added a `deploy.yml` workflow per the nix-ci-deploy pattern (CI builds, deploy is separate): - **Trigger:** `workflow_run` on `CI` completion (only runs if CI passes). - **Build:** same as CI (bun install + typecheck + web build) on the GitHub runner β€” fails fast if the build is broken. - **Deploy:** SSHes to the VPS over the public IP (`45.127.35.244`, not the Tailscale `100.79.111.61` which GitHub runners can't reach), pulls from git, reinstalls deps, rebuilds the web app, and restarts all 4 services via `sudo systemctl restart` (NOPASSWD already configured for `code` user). - **Secrets** (GitHub repo secrets, not files): `SSH_DEPLOY_HOST`, `SSH_DEPLOY_PORT`, `SSH_DEPLOY_USER`, `SSH_DEPLOY_KEY` (ed25519 deploy key). Deploy key's public half is in `/home/code/.ssh/authorized_keys` on the VPS. #### Gotchas discovered + fixed during setup 1. **SSH host = public IP, not Tailscale IP.** `100.79.111.61` is a Tailscale `tailscale0` interface IP (CGNAT `100.64.0.0/10`); GitHub Actions runners can't route to it. Use the real public IP `45.127.35.244` (port 22 open in iptables). 2. **SSH key storage.** Storing the key via shell variable (`gh secret set --body "$VAR"`) mangles newlines β†’ `ssh.ParsePrivateKey: no key found`. Store directly from file: `cat keyfile | gh secret set SSH_DEPLOY_KEY --repo ...` Use `appleboy/ssh-action@v1` (not `@v1.1.0`) which correctly parses the key. Add `-o IdentitiesOnly=yes` to prevent "too many authentication failures". 3. **Key rotation.** Force-pushing amended commits changes the SHA but CI triggers on `push: branches: [main]` (CI) + `workflow_run` (deploy) β€” both fire correctly. ### Verification done (real) - CI `workflow_run` β†’ Deploy triggers after CI success; all 4 services `active`. - All public URLs return 200: web `/`, `/docs/...`, `/search`, `/dashboard`, `/metrics`, `/trpc/*`, `mcp.asepharyana.my.id/mcp`. - Doc pages render clean markdown (no frontmatter); History panel + Restore work. ## Phase 11 β€” CRUD + Auth + Web UI βœ… DONE > User requested: "perbagus agar jadi CRUD, pastikan ada autentikasi dan bisa > manual dari web atau lewat agent melalui MCP, dan perbaui UI/UXnya." ### Backend (Core + API + MCP) - [x] **`packages/parser` β€” `stringifyFile()`** β€” serialize `DocumentMeta` + body back to a markdown file with YAML frontmatter (gray-matter). Round-trip stable with `parseFile`. - [x] **`@mcpedia/core` β€” CRUD functions:** - `createDocument({slug, title, section, body, type?, status?, author?, tags?})` β€” writes file to `content/{section}/{slug}.md`, upserts `documents` row, snapshots revision, indexes chunks. - `updateDocument(slug, {...})` β€” writes file, updates DB row, snapshots revision (if body changed), reindexes chunks. - `deleteDocument(slug)` β€” removes file + `documents`/`document_chunks`/ `document_revisions` rows. - Slug validation: `[a-z0-9][a-z0-9/_-]*`, no `//`, no `..` traversal. - [x] **`apps/api` β€” tRPC CRUD routers** β€” `createDocument`, `updateDocument`, `deleteDocument` (all `.use(requireWriteAuth)`). Fixed `requireWriteAuth` to compare against `ctx.expectedSecret` (injected from deps) instead of the module-level `WEBHOOK_SECRET` env constant β€” latent bug that made the middleware untestable without env manipulation. - [x] **`apps/mcp` β€” 3 new write tools** β€” `create_document`, `update_document`, `delete_document` (all require `x-webhook-secret`). Tools: 10 β†’ 13. - [x] **Auth** β€” MCP/API writes reuse the existing `WEBHOOK_SECRET` / `x-webhook-secret` pattern. Web CRUD adds cookie-based auth: `ADMIN_PASSWORD` env + `/api/auth/login` (HMAC-signed `mcpedia_admin` cookie, HttpOnly). ### Web UI - [x] **`/create` page** β€” form (section/type/status/title/slug/tags/author/body), POSTs to `/api/docs` with `x-webhook-secret`. - [x] **`?edit=1` on doc pages** β€” inline edit form (`DocForm` component), PUTs to `/api/docs/{slug}`. - [x] **`/login` page** β€” password β†’ `/api/auth/login` β†’ cookie β†’ redirect `/create`. - [x] **Edit buttons** β€” homepage "+ Create Document" + per-doc "✎" (auth-gated); doc page "Edit" button (auth-gated). - [x] **TOC** β€” doc page auto-generates a table of contents from `h2` headings. - [x] **Dark mode** β€” toggle persisted in `localStorage`, defaults to system. - [x] **`/api/docs` REST routes** β€” POST (create), PUT (update), DELETE (delete), all `x-webhook-secret` gated. ### Files changed ``` new: apps/web/app/api/auth/login/route.ts # cookie-based login + verify new: apps/web/app/api/docs/route.ts # REST CRUD new: apps/web/app/components/DocForm.tsx # shared create/edit form new: apps/web/app/create/page.tsx # create UI new: apps/web/app/login/page.tsx # login UI new: apps/web/app/components/TOC.tsx # auto-generated TOC mod: apps/web/app/page.tsx # edit/create buttons (auth-gated) mod: apps/web/app/[section]/[...slug]/page.tsx # ?edit=1 + TOC + dark mode mod: packages/core/src/document.service.ts # createDocument/updateDocument/deleteDocument mod: packages/core/src/index.service.ts # export snapshotRevision mod: packages/core/src/index.ts # re-export CRUD + types mod: packages/parser/src/index.ts # stringifyFile mod: packages/config/src/index.ts # ADMIN_PASSWORD mod: apps/api/src/router.ts # CRUD routers + fix requireWriteAuth mod: apps/api/src/app.ts # createContext passes expectedSecret mod: apps/api/src/trpc.ts # Context.expectedSecret mod: apps/mcp/src/index.ts # 3 new CRUD write tools mod: apps/mcp/src/auth.test.ts # +4 CRUD auth tests mod: apps/api/src/app.test.ts # +5 tRPC CRUD auth tests mod: .env.example # ADMIN_PASSWORD ``` ### Gotchas / lessons 1. **tRPC fetch adapter** expects input directly as JSON body, NOT JSON-RPC envelope (`{"slug":...}` not `{"jsonrpc":"2.0","method":...,"params":{...}}`). 2. **`requireWriteAuth` env-constant bug** β€” comparing `ctx.webhookSecret !== WEBHOOK_SECRET` (module-level env constant) is untestable. Fix: thread `expectedSecret` through `Context` from `createApp(deps)`. 3. **Next.js catch-all routes** β€” `[...slug]/edit/` is invalid (catch-all must be last). Used `?edit=1` query param instead. 4. **`stringifyFile` YAML** β€” quote string values with `JSON.stringify` for special-char safety; arrays use `[...]` syntax. ## Decisions locked (from initial planning) - **Tooling:** bun workspaces + Turborepo (repo already used bun; pnpm rejected to minimize churn). - **DB:** imrnes Postgres `100.121.180.82:6432/mcpedia` for both dev and deploy; driver `prepare:false` (PgBouncer). Docker Compose reserved for future prod. - **Phase 1 scope:** Core + Web + MCP only. tRPC/Hono API, pgvector, auth, BullMQ deferred (YAGNI).