feat: Phase 11 — CRUD (create/update/delete) + auth + web UI

- Core: createDocument/updateDocument/deleteDocument (file + DB + revision + chunks)
- Parser: stringifyFile (serialize markdown with frontmatter to disk)
- API: tRPC CRUD routers (auth-gated via requireWriteAuth middleware)
- API: createContext now passes expectedSecret from deps (request-scoped auth)
- MCP: 3 new write tools (create_document, update_document, delete_document)
- Web: /create page + ?edit=1 form, /api/auth/login (cookie-based), /api/docs REST CRUD
- Web: Edit buttons on homepage + doc pages (auth-gated)
- Tests: 8 new tests (5 tRPC CRUD + 3 MCP CRUD auth), 40 total all green
This commit is contained in:
asepharyana committed 2026-08-20 13:08:27 +07:00
1 parent 2d974b8952
commit 57f90018da
19 files changed
+1215 -23

No files matched your search

+29 -8
View File
@@ -1,6 +1,7 @@
import Link from "next/link";
import { listDocuments } from "@mcpedia/core";
import type { DocumentMeta } from "@mcpedia/core";
import { cookies } from "next/headers";
// Render at request time — content is in Postgres (not available at build in
// CI, which has no DB). Request-time rendering is instant at this corpus scale.
@@ -15,6 +16,10 @@ export default async function Home() {
docs: all.filter((d) => d.section === section),
}));
// Auth check for edit/create buttons.
const cookieStore = await cookies();
const canEdit = cookieStore.get("mcpedia_admin")?.value != null;
return (
<div className="space-y-8">
<section>
@@ -23,6 +28,14 @@ export default async function Home() {
A content-first knowledge base. Humans read the Web UI; AI agents use
the MCP server. Both share one Core.
</p>
{canEdit && (
<Link
href="/create"
className="inline-block mt-3 px-4 py-2 text-sm border border-zinc-300 dark:border-zinc-700 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
+ Create Document
</Link>
)}
</section>
{bySection.map(({ section, docs }) => (
@@ -33,17 +46,25 @@ export default async function Home() {
) : (
<ul className="divide-y divide-zinc-200 dark:divide-zinc-800">
{docs.map((d: DocumentMeta) => (
<li key={d.slug} className="py-2">
<li key={d.slug} className="py-2">
<Link
href={`/${d.slug}`}
className="hover:underline font-medium"
>
{d.title}
</Link>
{canEdit && (
<Link
href={`/${d.slug}`}
className="hover:underline font-medium"
href={`/${d.slug}/edit`}
className="ml-2 text-xs text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
>
{d.title}
✎
</Link>
<div className="text-xs text-zinc-500 mt-0.5">
{d.tags.map((t) => `#${t}`).join(" ")}
</div>
</li>
)}
<div className="text-xs text-zinc-500 mt-0.5">
{d.tags.map((t) => `#${t}`).join(" ")}
</div>
</li>
))}
</ul>
)}