Merge MCP auth pages into the sync frontend on a single domain (sync.parc.land).
Architecture: multi-page app with per-page SSR (React + styled-components) and
per-page client hydration. No SPA, no client-side router. The server is the router.
mcp/auth.tsmcp/mcp.ts HTTP entry point at mcp.sync.parc.landfrontend/index.tsx + React Router)sync.parc.land handles everything<a href>. It's just the web.Each page follows the same shape:
// In main.ts route handler
import { renderToString } from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
import { ManagePage } from "./frontend/pages/manage/ManagePage.tsx";
const sheet = new ServerStyleSheet();
const html = renderToString(sheet.collectStyles(<ManagePage vault={vault} />));
const css = sheet.getStyleTags();
return new Response(shell({ html, css, props: { vault }, entry: "/frontend/pages/manage/client.tsx" }));
// frontend/pages/manage/client.tsx
import { hydrateRoot } from "react-dom/client";
import { ManagePage } from "./ManagePage.tsx";
const props = JSON.parse(document.getElementById("__PROPS__")!.textContent!);
hydrateRoot(document.getElementById("root")!, <ManagePage {...props} />);
// shell() emits:
<html>
<head>${css}</head> <!-- styled-components extracted CSS -->
<body>
<div id="root">${html}</div>
<script id="__PROPS__" type="application/json">${serializedProps}</script>
<script type="module" src="${entry}"></script>
</body>
</html>
frontend/
shell.ts — HTML wrapper (head, props injection, script tag)
theme.ts — styled-components theme tokens, shared
components/ — shared across pages (Card, Button, StatusText, Toast...)
pages/
landing/
LandingPage.tsx — isomorphic component
client.tsx — hydrateRoot entry
dashboard/
DashboardPage.tsx — isomorphic component
client.tsx — hydrateRoot entry
docs/
DocPage.tsx — isomorphic component
client.tsx — hydrateRoot entry
authorize/
AuthorizePage.tsx — OAuth sign-in/register + consent
client.tsx — hydrateRoot entry
manage/
ManagePage.tsx — Vault, passkeys, recovery management
client.tsx — hydrateRoot entry
recover/
RecoverPage.tsx — Recovery token → new passkey
client.tsx — hydrateRoot entry
The MCP HTTP handler becomes a function imported by main.ts, not a separate entry point.
// main.ts
import { handleMcpRequest } from "./mcp/mcp.ts";
// In the route handler, before room routes:
if (url.pathname.startsWith("/mcp") ||
url.pathname.startsWith("/oauth/") ||
url.pathname.startsWith("/webauthn/") ||
url.pathname.startsWith("/manage") ||
url.pathname.startsWith("/recover") ||
url.pathname.startsWith("/vault") ||
url.pathname.startsWith("/.well-known/oauth")) {
return handleMcpRequest(req);
}
mcp/mcp.ts changes from export default async function to
export async function handleMcpRequest(req: Request): Promise<Response>.
mcp.sync.parc.land → sync.parc.land (or sync.parc.land/mcp)https://sync.parc.landparc.land — domain-level, unaffected)| Path | Method | Handler | Page/Response |
|---|---|---|---|
/ | GET | main.ts | SSR LandingPage |
/?room=X | GET | main.ts | SSR DashboardPage |
/?doc=X | GET | main.ts | SSR DocPage |
/manage | GET | main.ts → mcp | SSR ManagePage |
/recover | GET | main.ts → mcp | SSR RecoverPage |
/oauth/authorize | GET | main.ts → mcp | SSR AuthorizePage |
/mcp | POST | main.ts → mcp | MCP JSON-RPC |
/oauth/* | POST | main.ts → mcp | OAuth API |
/webauthn/* | POST | main.ts → mcp | WebAuthn API |
/manage/api/* | * | main.ts → mcp | Management API |
/recover/* | POST | main.ts → mcp | Recovery API |
/vault | * | main.ts → mcp | Vault API |
/.well-known/* | GET | main.ts → mcp | OAuth discovery |
/rooms/* | * | main.ts | Room API |
/frontend/* | GET | main.ts | Module proxy (esm.town) |
/reference/* | GET | main.ts | Reference docs |
Adopt the sync dashboard palette as the single source of truth. The MCP pages' slightly different dark theme (#0a0a0f vs #0d1117, #4a4aff vs #58a6ff) converges to the dashboard variables.
// frontend/theme.ts
export const theme = {
bg: '#0d1117',
fg: '#c9d1d9',
dim: '#484f58',
border: '#21262d',
accent: '#58a6ff',
green: '#3fb950',
yellow: '#d29922',
red: '#f85149',
surface: '#161b22',
surface2: '#1c2129',
purple: '#bc8cff',
orange: '#f0883e',
// Landing/docs (light default, dark media query)
landing: { ... },
};
frontend/components/
Card.tsx — Surface container
Button.tsx — Primary, secondary variants
StatusText.tsx — Status/error messages
Toast.tsx — Toast notifications
TokenBadge.tsx — Token type badges (room/agent/view)
PasskeyChip.tsx — Passkey credential display
VaultTable.tsx — Token vault table with actions
import {
startRegistration,
startAuthentication,
} from "https://esm.sh/@simplewebauthn/browser@13";
This is client-only — used in hydration scripts, not in SSR.
// frontend/hooks/useWebAuthn.ts
export function useWebAuthn(origin: string) {
const [status, setStatus] = useState("");
const [error, setError] = useState("");
// signIn(): Promise<string | null> — returns sessionId
// register(username): Promise<{ sessionId } | null>
return { signIn, register, status, error, setError };
}
WebAuthn requires JS (browser API). Forms that don't need WebAuthn can work
without JS via <form method="POST" action="...">. The server handles the POST,
re-renders the page with updated state.
Pages that require WebAuthn (all three MCP pages):
frontend/shell.ts — SSR HTML wrapperfrontend/theme.ts — shared design tokensfrontend/components/ shared componentsstyled-components ServerStyleSheet works in Val.town Deno runtimemcp/mcp.ts from default export to named exportGET /oauth/authorize/params)| Risk | Severity | Mitigation |
|---|---|---|
| styled-components ServerStyleSheet fails in Deno | High | Test early in Phase 1. Fallback: inline <style> tags |
| WebAuthn ESM import differs from UMD behavior | High | Test isolated. Fallback: keep UMD script tag in shell |
| MCP clients break when domain changes | High | Keep mcp.sync.parc.land redirect → sync.parc.land |
| SSR cold start adds latency | Medium | Templates are small, renderToString is fast. Monitor |
| OAuth authorize page fails mid-flow | High | Test with Claude.ai connector. Keep old handlers on branch |
| Hydration mismatch (server/client render differ) | Low | Keep components deterministic, no browser-only logic in render |
__PROPS__ script tag