<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jon Watts</title>
    <description>The latest articles on DEV Community by Jon Watts (@watts4).</description>
    <link>https://dev.to/watts4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3848380%2F7b7a1fff-e16f-4511-87c0-2b69e3e00de5.jpeg</url>
      <title>DEV Community: Jon Watts</title>
      <link>https://dev.to/watts4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/watts4"/>
    <language>en</language>
    <item>
      <title>Please Verify: An Obstacle Course of Web UX Hell</title>
      <dc:creator>Jon Watts</dc:creator>
      <pubDate>Sun, 05 Apr 2026 04:51:24 +0000</pubDate>
      <link>https://dev.to/watts4/please-verify-an-obstacle-course-of-web-ux-hell-2i01</link>
      <guid>https://dev.to/watts4/please-verify-an-obstacle-course-of-web-ux-hell-2i01</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Please Verify&lt;/strong&gt; is a satirical single-page web app disguised as a legitimate SaaS verification system — clean design, professional fonts, a fake browser chrome — that traps you in an endless loop of the internet's most infuriating UX patterns.&lt;/p&gt;

&lt;p&gt;It looks like a real product. It has trust badges. It says "Secure. Simple. Streamlined." It is none of these things.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://please-verify-app.web.app" rel="noopener noreferrer"&gt;https://please-verify-app.web.app&lt;/a&gt;&lt;br&gt;&lt;br&gt;
💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/watts4/please-verify" rel="noopener noreferrer"&gt;https://github.com/watts4/please-verify&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The gauntlet, in order:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cookie Consent (×4)&lt;/strong&gt; — Four rounds of increasingly unhinged cookie categories. Round 4 includes "Void Telemetry" ("The void remembers. The void consents on your behalf.") and "Final Form Data Collection" ("Acceptance is non-reversible across all planes of existence."). All categories must be accepted to continue. Every time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Age Verification (animal years)&lt;/strong&gt; — Select your spirit animal and calculate your age in that animal's measurement system. Dog: ×7. Cat: odd years ×15, even years ×8 (parity modifier applied). Tortoise: age × π × 4. Elephant: convert to base-8 first, then ×3. Wrong answer? New animal. After 3 failures, we give up verifying your age and proceed anyway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terms of Service (procedural, never-ending)&lt;/strong&gt; — 30 clauses that load in batches as you scroll. You cannot click "Accept" until you've read everything. When you finally do click it — the checkbox unchecks itself and Section 47.3 appears, which requires you to scroll through the entire document again before agreeing. Clauses include "No Warranty of Vibe" and "Governing Law: a jurisdiction to be determined at a later date."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CAPTCHA (25% pass rate)&lt;/strong&gt; — Select all images matching increasingly impossible instructions: "Click all squares that are lying." "Identify the squares that feel like a Wednesday." You have a 25% chance of passing regardless of what you select. After 3 failures, Enhanced Verification activates (a loading spinner, then resets).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Password Creation (progressive impossible requirements)&lt;/strong&gt; — Requirements reveal themselves as you type, escalating from "8+ characters" to "must contain a haiku," "must evoke the feeling of a Tuesday afternoon," and "must not begin with the letter you are currently thinking of." The last character also deletes itself every 3 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create Account&lt;/strong&gt; — A normal-looking form. Your password field is blank. "Not transferred for security reasons."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sign In → 418 I'm a teapot&lt;/strong&gt; — Incorrect password (always). "Reset it here" → link expires in 3 seconds → "Your account doesn't exist" → Create New Account → repeat. The server's response: &lt;code&gt;HTTP 418 I'm a teapot — server refuses to brew authentication.&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/9xNL7VgqQV0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/watts4" rel="noopener noreferrer"&gt;
        watts4
      &lt;/a&gt; / &lt;a href="https://github.com/watts4/please-verify" rel="noopener noreferrer"&gt;
        please-verify
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A satirical web UX obstacle course — cookie consent hell, animal-years age verification, procedural ToS, impossible passwords, circular login loops.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;React + TypeScript + Vite&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.&lt;/p&gt;
&lt;p&gt;Currently, two official plugins are available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react" rel="noopener noreferrer"&gt;@vitejs/plugin-react&lt;/a&gt; uses &lt;a href="https://oxc.rs" rel="nofollow noopener noreferrer"&gt;Oxc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc" rel="noopener noreferrer"&gt;@vitejs/plugin-react-swc&lt;/a&gt; uses &lt;a href="https://swc.rs/" rel="nofollow noopener noreferrer"&gt;SWC&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;React Compiler&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The React Compiler is not enabled on this template because of its impact on dev &amp;amp; build performances. To add it, see &lt;a href="https://react.dev/learn/react-compiler/installation" rel="nofollow noopener noreferrer"&gt;this documentation&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Expanding the ESLint configuration&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:&lt;/p&gt;
&lt;div class="highlight highlight-source-js notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;default&lt;/span&gt; &lt;span class="pl-en"&gt;defineConfig&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;
  &lt;span class="pl-en"&gt;globalIgnores&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;'dist'&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;files&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;'**/*.{ts,tsx}'&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;extends&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Other configs...&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Remove tseslint.configs.recommended and replace with this&lt;/span&gt;
      &lt;span class="pl-s1"&gt;tseslint&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;configs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;recommendedTypeChecked&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Alternatively, use this for stricter rules&lt;/span&gt;
      &lt;span class="pl-s1"&gt;tseslint&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;configs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;strictTypeChecked&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Optionally, add this for stylistic rules&lt;/span&gt;
      &lt;span class="pl-s1"&gt;tseslint&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;configs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;stylisticTypeChecked&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;

      &lt;span class="pl-c"&gt;// Other configs...&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/watts4/please-verify" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React + TypeScript + Vite&lt;/li&gt;
&lt;li&gt;Tailwind CSS v3&lt;/li&gt;
&lt;li&gt;Google Gemini API (for procedurally-generated ToS clauses)&lt;/li&gt;
&lt;li&gt;Firebase Hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key files:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/components/CookieConsent.tsx&lt;/code&gt; — 4-round cookie consent with 20 categories of nightmare tracking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/AgeVerification.tsx&lt;/code&gt; — Spirit animal calculation engine (6 animals, 6 formulas)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/TermsOfService.tsx&lt;/code&gt; — Scroll-triggered clause loading + Gemini integration + self-unchecking checkbox&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/Captcha.tsx&lt;/code&gt; — 25% random pass rate, 10 rotating impossible instructions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/PasswordCreation.tsx&lt;/code&gt; — Progressive requirements + 3-second character deletion timer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/Login.tsx&lt;/code&gt; — Circular authentication trap + HTTP 418&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The whole thing is a React state machine. &lt;code&gt;App.tsx&lt;/code&gt; tracks a &lt;code&gt;step&lt;/code&gt; variable cycling through: &lt;code&gt;landing → cookie → age → tos → captcha → password → create-account → login&lt;/code&gt;. Each component calls an &lt;code&gt;onComplete()&lt;/code&gt; prop when it decides you've suffered enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The step counter lies.&lt;/strong&gt; Steps skip from 3 to 5 (no Step 4), repeat Step 5 twice, then go to "Step 8 of 7" and "Step 9 of 7". This was deliberate — a small detail that makes it feel like something has gone wrong with reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Gemini for the ToS:&lt;/strong&gt; On mount, &lt;code&gt;TermsOfService.tsx&lt;/code&gt; hits &lt;code&gt;gemini-1.5-flash&lt;/code&gt; with a prompt asking for 5 absurd legal-sounding clauses. The result replaces the first batch of fallback clauses, making each visit's terms of service unique. If no API key is set (open-source deployment), it falls back to 30 handwritten clauses that are somehow funnier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The API call is client-side — key is in .env, never committed&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generate 5 absurd, legal-sounding Terms of Service clauses...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The self-unchecking checkbox:&lt;/strong&gt; When you finally reach the ToS checkbox, clicking it sets &lt;code&gt;rereadRequired: true&lt;/code&gt;, unchecks itself, and injects Section 47.3 at the bottom of the document. The "Accept &amp;amp; Continue" button becomes disabled until you scroll to the bottom again. This works via a scroll event listener that sets &lt;code&gt;rereadScrolled: true&lt;/code&gt; only when &lt;code&gt;scrollTop + clientHeight &amp;gt;= scrollHeight - 40&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookie consent architecture:&lt;/strong&gt; It's just 4 separate arrays of &lt;code&gt;CookieCategory&lt;/code&gt; objects with a round counter. The trick is that the "Accept All" button only sets all categories to true — you still have to click "Save &amp;amp; Continue" separately. And all non-required categories must be accepted before continue works. This creates just enough friction to feel authentic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 418:&lt;/strong&gt; &lt;code&gt;HTTP 418 I'm a teapot&lt;/code&gt; is RFC 2324, written by Larry Masinter in 1998 as an April Fools joke about a Hyper Text Coffee Pot Control Protocol. It was never removed from the HTTP spec. The server in Please Verify refuses to brew authentication — which is only appropriate, because there is no server, there is no authentication, and there never was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Ode to Larry Masinter&lt;/strong&gt; — The &lt;code&gt;HTTP 418 I'm a teapot&lt;/code&gt; error is displayed on every failed login attempt: &lt;em&gt;"Server responded: 418 I'm a teapot — server refuses to brew authentication."&lt;/em&gt; It's fitting: RFC 2324 is itself a satire of over-specified internet protocols. Please Verify is a satire of over-engineered user verification flows. Larry Masinter would understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Google AI Usage&lt;/strong&gt; — Gemini 1.5 Flash generates the opening ToS clauses on each visit. The prompt asks specifically for "absurd, legal-sounding" clauses — and Gemini delivers: "The Company reserves the right to interpret your inaction as action, your silence as consent, and your confusion as a binding agreement." Each visitor's terms are unique.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built in one session for the DEV April Fools Challenge 2026. The site does nothing. It verifies no one. It has never successfully authenticated a single user. That was always the plan.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>WriteRight — AI Writing Coach for K–8 Teachers, Built with Notion MCP</title>
      <dc:creator>Jon Watts</dc:creator>
      <pubDate>Sun, 29 Mar 2026 00:13:51 +0000</pubDate>
      <link>https://dev.to/watts4/writeright-ai-writing-coach-for-k-8-teachers-built-with-notion-mcp-1h82</link>
      <guid>https://dev.to/watts4/writeright-ai-writing-coach-for-k-8-teachers-built-with-notion-mcp-1h82</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WriteRight&lt;/strong&gt; is an AI-powered writing analysis tool that helps K–8 teachers give differentiated feedback to every student — without spending hours reading and planning.&lt;/p&gt;

&lt;p&gt;Teachers store student writing samples in a Notion database. WriteRight reads them all via Notion MCP, analyzes each one against Common Core State Standards (CCSS) for the student's grade level, and generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-student teaching points&lt;/strong&gt; (2–3 next-step instructional moves)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt; (what the student is doing well, tied to standards)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards addressed&lt;/strong&gt; (e.g. W.3.1, L.3.2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart small groups&lt;/strong&gt; — students clustered by shared instructional need, with a suggested activity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full analysis saves back to Notion in one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://write-right-app.web.app" rel="noopener noreferrer"&gt;https://write-right-app.web.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/watts4/write-right" rel="noopener noreferrer"&gt;https://github.com/watts4/write-right&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem I Solved
&lt;/h3&gt;

&lt;p&gt;A third-grade teacher with 28 students has 28 different writing stages. Reviewing every sample, finding the right CCSS standard, planning differentiated groups — that's 3–4 hours of prep per unit. WriteRight does it in under 2 minutes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/gsT0A9gwJio"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us the code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/watts4/write-right" rel="noopener noreferrer"&gt;https://github.com/watts4/write-right&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18 / TypeScript / Vite / Tailwind frontend (Firebase Hosting)&lt;/li&gt;
&lt;li&gt;Node.js / Express / TypeScript backend (Google Cloud Run)&lt;/li&gt;
&lt;li&gt;Notion OAuth 2.0 + PKCE flow&lt;/li&gt;
&lt;li&gt;MCP agentic read loop with parallel tool execution&lt;/li&gt;
&lt;li&gt;CCSS Writing + Language standards K–8&lt;/li&gt;
&lt;li&gt;Seed script for mock Grade 3 class (8 students with deliberate skill gaps)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;Notion MCP is the &lt;strong&gt;core reading engine&lt;/strong&gt; of WriteRight — not a thin wrapper, but the actual mechanism that makes agentic, schema-flexible reading of student writing possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-Phase Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notion DB ──► Phase 1: MCP Agentic Read
                   │
                   ▼
             Phase 2: Claude Analysis (CCSS-anchored JSON)
                   │
                   ▼
           UI Results ──► /api/save ──► Notion (REST write-back)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Phase 1 — Notion MCP Agentic Read
&lt;/h3&gt;

&lt;p&gt;The backend spawns &lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt; (the official npm package) as a subprocess on a random local port. Claude then runs an agentic tool-use loop — calling MCP tools to query the database, list student pages, and fetch the block content (the actual writing) for each one.&lt;/p&gt;

&lt;p&gt;The key performance optimization: all &lt;code&gt;tool_use&lt;/code&gt; blocks within each response are executed in &lt;strong&gt;parallel&lt;/strong&gt; via &lt;code&gt;Promise.all&lt;/code&gt;. This cut Phase 1 from 4+ minutes to ~90 seconds for a class of 30.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spawn Notion MCP server with teacher's OAuth token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcpServer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notion-mcp-server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--port&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;OPENAPI_MCP_HEADERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notionToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notion-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2022-06-28&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Agentic loop — Claude calls tools until all student data is extracted&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iterations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_ITERATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mcpTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Execute all tool_use blocks in parallel ← the key performance win&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_use&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end_turn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;iterations&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 2 — Claude Analysis (No Tools)
&lt;/h3&gt;

&lt;p&gt;A single Claude API call receives all the extracted writing samples + grade-level CCSS standards and returns structured JSON: per-student teaching points, strengths, standards addressed, and small group recommendations with activity suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3 — Notion REST Write-Back
&lt;/h3&gt;

&lt;p&gt;The "Save to Notion" action uses &lt;code&gt;@notionhq/client&lt;/code&gt; REST API — not MCP. MCP is ideal for &lt;strong&gt;flexible agentic reads&lt;/strong&gt; where the schema is unknown; REST is faster and more predictable for &lt;strong&gt;batch writes&lt;/strong&gt; with a known structure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why &lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt; (npm) — not the hosted server
&lt;/h3&gt;

&lt;p&gt;I initially tried Notion's hosted &lt;code&gt;mcp.notion.com&lt;/code&gt; server via Anthropic's &lt;code&gt;mcp_servers&lt;/code&gt; parameter. It only accepts Notion's own OAuth flow — incompatible with injecting a teacher's access token server-side.&lt;/p&gt;

&lt;p&gt;The npm package solves this cleanly: spawned as a subprocess with the token injected via &lt;code&gt;OPENAPI_MCP_HEADERS&lt;/code&gt;, giving each teacher session a fully authorized MCP connection.&lt;/p&gt;




&lt;h3&gt;
  
  
  Other Engineering Wins
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Parallel tool execution cut time by ~70%&lt;/strong&gt;&lt;br&gt;
Each MCP loop iteration may produce multiple &lt;code&gt;tool_use&lt;/code&gt; blocks. Running them sequentially was the original bottleneck. &lt;code&gt;Promise.all&lt;/code&gt; on every batch reduced Phase 1 from 4+ minutes to ~90 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateless analyze route + separate save route&lt;/strong&gt;&lt;br&gt;
Cloud Run buffers responses until the Node.js event loop is idle. Fire-and-forget Notion writes inside the analyze route caused requests to hang. Solution: &lt;code&gt;/api/analyze&lt;/code&gt; is pure read + analyze; &lt;code&gt;/api/save&lt;/code&gt; is a separate, explicit user action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;24-hour in-memory session store&lt;/strong&gt;&lt;br&gt;
Teacher OAuth tokens stored in a session map (UUID → token + database ID) with auto-expiry. No database required for MVP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 18, TypeScript, Vite, Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Node.js, Express, TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Claude Sonnet 4.6 (Anthropic SDK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion MCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt; (npm subprocess)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion Auth&lt;/td&gt;
&lt;td&gt;OAuth 2.0 + PKCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion Writes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@notionhq/client&lt;/code&gt; (REST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Firebase Hosting + Google Cloud Run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standards&lt;/td&gt;
&lt;td&gt;CCSS Writing + Language K–8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
