<?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: Kiell Tampubolon</title>
    <description>The latest articles on DEV Community by Kiell Tampubolon (@kielltampubolon).</description>
    <link>https://dev.to/kielltampubolon</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890870%2Ff4c1760b-670f-4d29-b0a8-29dc39842afa.jpg</url>
      <title>DEV Community: Kiell Tampubolon</title>
      <link>https://dev.to/kielltampubolon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kielltampubolon"/>
    <language>en</language>
    <item>
      <title>I gave Claude a memory of everything I browse — here's the architecture</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Mon, 15 Jun 2026 08:05:42 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-gave-claude-a-memory-of-everything-i-browse-heres-the-architecture-3a7d</link>
      <guid>https://dev.to/kielltampubolon/i-gave-claude-a-memory-of-everything-i-browse-heres-the-architecture-3a7d</guid>
      <description>&lt;p&gt;Claude can read my files, my terminal, even my screen. But it had no idea what I read in my browser yesterday.&lt;/p&gt;

&lt;p&gt;That gap bugged me enough to build &lt;strong&gt;&lt;a href="https://github.com/glatinone/BraveMCP" rel="noopener noreferrer"&gt;BraveMCP&lt;/a&gt;&lt;/strong&gt;: a local-first "second brain" that gives Claude Desktop access to my browsing history, bookmarks, highlights, and notes through the Model Context Protocol (MCP). Everything stays on my machine. No cloud, no tracking.&lt;/p&gt;

&lt;p&gt;This is the technical write-up: the architecture, the one constraint that shaped the whole design, and the bugs that cost me the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;MCP servers talk to Claude Desktop over &lt;strong&gt;stdio&lt;/strong&gt;, a JSON-RPC stream on stdin/stdout. A browser extension lives in a sandbox and &lt;strong&gt;cannot speak stdio&lt;/strong&gt;. It can only make outbound HTTP requests.&lt;/p&gt;

&lt;p&gt;So the two halves of the system physically cannot talk to each other directly. That single fact drove the entire design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eyfvw7pw9kfotk5sr0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eyfvw7pw9kfotk5sr0z.png" alt="BraveMCP architecture" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is a small &lt;strong&gt;HTTP bridge&lt;/strong&gt;: an Express server running on port &lt;code&gt;3747&lt;/code&gt;, &lt;em&gt;inside the same process&lt;/em&gt; as the MCP server. The extension POSTs browsing events to it; the MCP server reads from the shared database when Claude calls a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The storage layer: hybrid search
&lt;/h2&gt;

&lt;p&gt;Keyword search and semantic search each miss things the other catches. So BraveMCP runs both and merges them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite with FTS5&lt;/strong&gt; for fast BM25 keyword ranking over titles, summaries, notes, and highlights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChromaDB&lt;/strong&gt; for cosine vector similarity, so "MCP security" still finds a page titled "Claude agent hardening."
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Merge keyword + vector hits; boost items that appear in both&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chromaMatches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;semantic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ftsMatches&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relevance&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// appears in both -&amp;gt; boost&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keyword&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ChromaDB is not running, the server degrades to FTS5-only instead of failing. Local-first means it has to work with whatever services you actually have up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI pipeline, and why the fallback matters
&lt;/h2&gt;

&lt;p&gt;When a page is captured, BraveMCP generates a summary and an embedding. It tries &lt;strong&gt;Ollama&lt;/strong&gt; first (fully local: &lt;code&gt;llama3.2&lt;/code&gt; for summaries, &lt;code&gt;nomic-embed-text&lt;/code&gt; for embeddings), then falls back to the &lt;strong&gt;Anthropic API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But here is the trap I walked into. The first version, when &lt;em&gt;no&lt;/em&gt; LLM was available, returned canned strings:&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;// before: this ignores the actual data entirely&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Synthesis on "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;": Relies on the gathered browser research database.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useless. It says the same thing no matter what you searched. So I rewrote every fallback to be &lt;strong&gt;extractive&lt;/strong&gt;: to build a real summary from the actual data, grouping matching pages by domain with real snippets pulled from SQLite. With no LLM at all, asking for a topic synthesis now returns the genuine sources. Different input produces different output. The "AI" tools stay useful even when there is no AI running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering forgotten pages
&lt;/h2&gt;

&lt;p&gt;The tool I use most is &lt;code&gt;find_forgotten_content&lt;/code&gt;. You give it a vague description and it does hybrid search, then re-ranks with &lt;strong&gt;time decay&lt;/strong&gt; and a &lt;strong&gt;visit-count boost&lt;/strong&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeDecay&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;daysElapsed&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;visitBoost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visitCount&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;adjusted&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relevance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;timeDecay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;visitBoost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page you opened three times last week beats one you glanced at once today. That matches how memory actually feels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feoa6vtgb3ar5f4faitf6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feoa6vtgb3ar5f4faitf6.png" alt="Before and after BraveMCP" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs that cost me hours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. dotenv v17 broke the entire protocol.&lt;/strong&gt; MCP communicates over stdout. dotenv v17 prints a status line to stdout by default. That one line corrupted the JSON-RPC channel and Claude Desktop refused to connect with a cryptic &lt;code&gt;Unexpected token&lt;/code&gt; error. The fix was pinning &lt;code&gt;dotenv@16&lt;/code&gt;. Two hours on a single log line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The dual-process state problem.&lt;/strong&gt; Claude Desktop and my dev client each spawn their own copy of the MCP server. Only the instance that grabs port &lt;code&gt;3747&lt;/code&gt; receives extension data. The other had empty in-memory state, so tab tools returned nothing. The fix: stop treating in-memory state as the source of truth and fall back to SQLite, which both processes share.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Manifest V3 extension (tab sync, bookmarks, context-menu highlights)&lt;/li&gt;
&lt;li&gt;An MCP server exposing &lt;strong&gt;13 tools&lt;/strong&gt; (&lt;code&gt;search_memory&lt;/code&gt;, &lt;code&gt;find_forgotten_content&lt;/code&gt;, &lt;code&gt;summarize_research_topic&lt;/code&gt;, &lt;code&gt;generate_weekly_digest&lt;/code&gt;, &lt;code&gt;suggest_tab_cleanup&lt;/code&gt;, and more)&lt;/li&gt;
&lt;li&gt;SQLite + ChromaDB hybrid search&lt;/li&gt;
&lt;li&gt;A test suite on Node's built-in runner, wired into CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is open source, MIT licensed: &lt;strong&gt;&lt;a href="https://github.com/glatinone/BraveMCP" rel="noopener noreferrer"&gt;https://github.com/glatinone/BraveMCP&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are building on MCP, the stdio-vs-HTTP bridge pattern is the part worth stealing. What would you want your AI to remember?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We thought we had location-based MFA. We had something else entirely</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Thu, 11 Jun 2026 09:27:14 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/we-thought-we-had-location-based-mfa-we-had-something-else-entirely-29go</link>
      <guid>https://dev.to/kielltampubolon/we-thought-we-had-location-based-mfa-we-had-something-else-entirely-29go</guid>
      <description>&lt;h1&gt;
  
  
  We thought we had location-based MFA. We had something else entirely
&lt;/h1&gt;

&lt;p&gt;Our CTO asked a simple question: when someone travels and signs in from outside our home country, do we prompt for MFA? Everyone's gut answer was yes. Users &lt;em&gt;do&lt;/em&gt; get prompted when they travel. Case closed, right?&lt;/p&gt;

&lt;p&gt;I decided to actually verify it. What I found changed the answer from "yes" to "yes, but not for the reason anyone thinks," and the whole audit turned into a small migration project. Here's the walkthrough, including the wrong turns, in case you need to run the same check on your tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this configuration actually lives
&lt;/h2&gt;

&lt;p&gt;My first instinct was Intune, since that's where our endpoint security lives. Wrong place. Intune handles device compliance. The "prompt MFA based on where you sign in from" logic lives in &lt;strong&gt;Microsoft Entra ID, under Conditional Access&lt;/strong&gt;. Intune only feeds into it as a signal (the "require compliant device" grant control).&lt;/p&gt;

&lt;p&gt;So the audit checklist looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Conditional Access → Named locations: is there a location defined for your trusted country or IPs?&lt;/li&gt;
&lt;li&gt;Conditional Access → Policies: is there a policy that requires MFA when the sign-in location is outside those trusted locations?&lt;/li&gt;
&lt;li&gt;Sign-in logs: does reality match the config?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we found
&lt;/h2&gt;

&lt;p&gt;Named locations: one stale IP range from two years ago, not referenced by any policy. No trusted country, no office IPs.&lt;/p&gt;

&lt;p&gt;Policies: 18 policies total. Only 3 enabled, all device-related (block unmanaged devices, MAM). Zero location-based policies. Not even a disabled one.&lt;/p&gt;

&lt;p&gt;So the honest answer to the CTO was: no, we never configured travel MFA. And yet users were definitely getting MFA prompts when travelling. Something else was doing it. Hold that thought.&lt;/p&gt;

&lt;p&gt;Entra also showed us its own receipts. Under Policies there's a Security Alerts section that analyzes your last 7 days of sign-ins. Ours said 72% of sign-ins were out of scope of any Conditional Access policy, and a few hundred sign-ins were still using legacy authentication. If you've never looked at this panel, look. It's free ammunition for whatever security proposal you're trying to get approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the policy
&lt;/h2&gt;

&lt;p&gt;Management wanted trusted locations defined by IP ranges (office egress + VPN egress) rather than by country. That's the stricter and, I'd argue, better option. Country-based trust means anyone inside the country skips MFA, including an attacker on a VPN exit node in your city. IP-based trust means the corporate network is the boundary: on VPN, no prompt; off VPN, prompt. Wherever you physically are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: find your real egress IP
&lt;/h3&gt;

&lt;p&gt;Don't ask around or trust documentation. Test it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect the corporate VPN, open &lt;code&gt;ifconfig.me&lt;/code&gt;, note the IP.&lt;/li&gt;
&lt;li&gt;Sign in to any Microsoft 365 portal, then open Entra → Sign-in logs and check the IP address recorded on that sign-in.&lt;/li&gt;
&lt;li&gt;If the two IPs match, your M365 traffic goes through the VPN tunnel and the VPN egress IP is usable as a trusted location.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 matters more than it looks. A lot of VPN setups use split tunneling and exclude Microsoft 365 traffic from the tunnel (Microsoft themselves recommend this for performance). In that case Entra sees the user's local ISP address, your VPN-based trusted location never matches, and the policy will prompt everyone everywhere. Better to find out in a ten-minute test than after rollout. Ours matched, so we were fine.&lt;/p&gt;

&lt;p&gt;Also check your logs for IPv6 sign-ins. If clients sometimes authenticate over IPv6, an IPv4-only trusted range silently fails to match those.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: named location
&lt;/h3&gt;

&lt;p&gt;Conditional Access → Named locations → &lt;strong&gt;IP ranges location&lt;/strong&gt; (not Countries location, I clicked the wrong one first and you can't convert between types, only delete and recreate):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add your office and VPN egress ranges in CIDR (&lt;code&gt;/32&lt;/code&gt; for single IPs)&lt;/li&gt;
&lt;li&gt;Tick &lt;strong&gt;Mark as trusted location&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: the policy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Users: start with just yourself. Then a pilot group. Then everyone. One policy, widen the scope each phase. Don't create three policies, future-you will hate cleaning them up.&lt;/li&gt;
&lt;li&gt;Target resources: All resources for the monitoring phase. You want complete data.&lt;/li&gt;
&lt;li&gt;Network: Include Any location, Exclude your trusted IP location.&lt;/li&gt;
&lt;li&gt;Grant: Require multifactor authentication.&lt;/li&gt;
&lt;li&gt;State: &lt;strong&gt;Report-only&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Report-only evaluates the policy on every sign-in and records what &lt;em&gt;would&lt;/em&gt; have happened, without prompting anyone. Each sign-in event gets a Report-only tab showing "Failure" (would have been prompted) or "Not applied" (excluded or out of scope). After a week or two you know exactly how many prompts per day enforcement will generate, and which accounts would be affected. That's the data you bring to the meeting where someone has to say "turn it on."&lt;/p&gt;

&lt;p&gt;One gotcha while testing: check the result on a sign-in to a resource your policy actually covers. I initially scoped the policy to the Office 365 app group, then tested against the Azure portal and wondered why the policy didn't show up in the evaluation. It wasn't broken. The resource just wasn't in scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plot twist: who was prompting all along
&lt;/h2&gt;

&lt;p&gt;While reading sign-in logs I noticed something odd. Sign-ins showed "Multifactor authentication" as the authentication requirement even when every Conditional Access policy on the event said Not applied. MFA was being required by &lt;em&gt;nothing visible in CA&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;There's only one thing that does that: &lt;strong&gt;legacy per-user MFA&lt;/strong&gt;, the old Enabled/Enforced toggle buried in the per-user MFA portal, predating Conditional Access. Someone had switched it on for users years ago. It prompts for MFA everywhere, with a "remember this device for X days" option, which is exactly why users only noticed prompts when travelling: new browser, new device, hotel computer. It &lt;em&gt;felt&lt;/em&gt; location-based. It never was.&lt;/p&gt;

&lt;p&gt;This matters beyond trivia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-user MFA ignores trusted locations entirely. Our shiny new exclusion did nothing while it was active, which confused my first round of testing.&lt;/li&gt;
&lt;li&gt;Microsoft is deprecating per-user MFA and wants everyone on Conditional Access.&lt;/li&gt;
&lt;li&gt;The migration order is critical: enforce the CA policy first, &lt;em&gt;then&lt;/em&gt; disable per-user MFA in batches. Do it in the other order and you've created a window with no MFA at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the audit's final answer to the CTO: yes, users get MFA prompts when travelling, but from a deprecated mechanism that prompts everywhere and can't honor trusted networks. The new CA policy is both the fix and the migration path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things the audit shook loose
&lt;/h2&gt;

&lt;p&gt;Once you start reading policies carefully, you find things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A "block unmanaged devices" policy whose grant logic was actually "require compliant device OR app protection policy, require one." That's not a block, that's an either/or, and the policy name lies about it. Probably intentional for BYOD mobile. Worth confirming rather than assuming.&lt;/li&gt;
&lt;li&gt;A Microsoft-managed policy ("MFA for admins accessing admin portals") sitting in report-only for over two years, with a note in the fine print: leave it in report-only and Microsoft will enable it for you. Enforcement is coming whether you schedule it or not. Schedule it.&lt;/li&gt;
&lt;li&gt;Several abandoned test policies from old projects. Off, report-only, names like Group2, Group3, Group4. Every tenant has these. Delete or document them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist version
&lt;/h2&gt;

&lt;p&gt;If you want to run this same audit on your tenant, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Conditional Access → Named locations: anything defined? Trusted? Referenced by a policy?&lt;/li&gt;
&lt;li&gt;Policies: filter by State = On. Read the grant logic on each one. AND vs OR matters.&lt;/li&gt;
&lt;li&gt;Pick a user who travelled recently, open their sign-in, read the Conditional Access tab and the Authentication requirement column. If MFA is required but no policy applied it, you have per-user MFA lurking.&lt;/li&gt;
&lt;li&gt;Check the Security Alerts panel for out-of-scope and legacy auth numbers.&lt;/li&gt;
&lt;li&gt;Verify your VPN egress IP against actual sign-in log entries before trusting it in a policy.&lt;/li&gt;
&lt;li&gt;Build the location policy in report-only, scoped to yourself first, and widen from there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The config took an afternoon. Understanding what was already happening in the tenant took longer, and was worth more.&lt;/p&gt;

</description>
      <category>security</category>
      <category>azure</category>
      <category>microsoft</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Solstice — A Game About Holding the Light</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Sun, 07 Jun 2026 11:50:45 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/solstice-a-game-about-holding-the-light-55dg</link>
      <guid>https://dev.to/kielltampubolon/solstice-a-game-about-holding-the-light-55dg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/june-game-jam-2026-06-03"&gt;June Solstice Game Jam&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Game
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solstice — Hold the Light&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You are the last spark before the longest night. Move with your cursor, collect golden orbs to keep your light from fading, and avoid the shadows drifting in from the dark.&lt;/p&gt;

&lt;p&gt;The world dims as your light dims. When your light hits zero, darkness wins.&lt;/p&gt;

&lt;p&gt;🎮 &lt;strong&gt;Play it here:&lt;/strong&gt; &lt;a href="https://glatinone.github.io/solstice-game/" rel="noopener noreferrer"&gt;glatinone.github.io/solstice-game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/glatinone/solstice-game" rel="noopener noreferrer"&gt;github.com/glatinone/solstice-game&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Plays
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Move with mouse (desktop) or touch (mobile)&lt;/li&gt;
&lt;li&gt;Collect golden orbs to restore light&lt;/li&gt;
&lt;li&gt;Each orb is worth 1 point&lt;/li&gt;
&lt;li&gt;Avoid the drifting shadows; they drain your light on contact&lt;/li&gt;
&lt;li&gt;Your light decays continuously, faster as time goes on&lt;/li&gt;
&lt;li&gt;Game ends when light reaches zero&lt;/li&gt;
&lt;li&gt;Score = orbs collected before darkness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no win state. Only how long you can hold the light.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solstice Theme
&lt;/h2&gt;

&lt;p&gt;The brief was the June solstice: the longest day, the shortest night. I flipped it. What if you were on the other side of the year, watching the light slip away?&lt;/p&gt;

&lt;p&gt;Three things carry the theme:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual decay.&lt;/strong&gt; The background is a radial gradient centered on the player. At full light it is warm: amber, dusk, sunset reds. As light drains the colors shift cold and dark. Stars become more visible. The screen literally dims with you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Light as resource.&lt;/strong&gt; Light is not just visual. It is your health bar, your time, and your radius of awareness all at once. Lose light, lose the game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shadows that grow with time.&lt;/strong&gt; Difficulty scales with how long you have survived. More shadows, faster shadows, fewer orbs. The longer you hold the light, the harder the dark pushes back.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML5 Canvas&lt;/strong&gt; for rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt;, no frameworks, no build step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS&lt;/strong&gt; for the UI shell (start screen, HUD, game over)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localStorage&lt;/strong&gt; for high score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Pages&lt;/strong&gt; for hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole game is one HTML file. About 600 lines including the styles and script. No dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Decisions
&lt;/h2&gt;

&lt;p&gt;A few things I locked in early:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No menus, no levels, no upgrades.&lt;/strong&gt; A jam game should be playable in 30 seconds. Press Begin, move mouse, see what happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No instant fail.&lt;/strong&gt; Touching a shadow drains light, it doesn't kill you. This means a confident player can recover, but a careless one accumulates damage. Skill ceiling without frustration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visuals over mechanics.&lt;/strong&gt; The core loop is simple: collect, dodge, survive. What carries the experience is the way the world looks at full light versus near-zero light. Particle trails, radial glows, the slow color shift. The game's mood is the gameplay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One screen, no scrolling.&lt;/strong&gt; Mobile and desktop play identically. No camera systems, no levels to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Add With More Time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sound (a slow ambient drone that intensifies as light fades)&lt;/li&gt;
&lt;li&gt;A second orb type that gives bigger light boost but moves&lt;/li&gt;
&lt;li&gt;Boss shadow at extreme low light&lt;/li&gt;
&lt;li&gt;Daily seed leaderboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These would all stack on top of the existing loop without changing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection
&lt;/h2&gt;

&lt;p&gt;I have not built a game from scratch in years. Twenty-four hours from idea to playable submission, single file, no engine.&lt;/p&gt;

&lt;p&gt;The hardest part was not the code. It was knowing when to stop. A jam game asks you to ship something fun, not something complete. Every extra feature is a tax on the player's first impression.&lt;/p&gt;

&lt;p&gt;Solstice is a small game. That is the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with HTML5 Canvas. Hosted on GitHub Pages. Survival time is yours to discover.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gamechallenge</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Finishing a Read-Only MCP Server: From 6 Tools to 9</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Sun, 07 Jun 2026 08:05:04 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/finishing-a-read-only-mcp-server-from-6-tools-to-9-52n</link>
      <guid>https://dev.to/kielltampubolon/finishing-a-read-only-mcp-server-from-6-tools-to-9-52n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I took an unfinished open-source MCP server for DEV.to and added the missing half.&lt;/p&gt;

&lt;p&gt;The original repo (&lt;a href="https://github.com/nickytonline/dev-to-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;nickytonline/dev-to-mcp&lt;/code&gt;&lt;/a&gt;) was built by an actual DEV.to engineer and shipped six read-only tools: &lt;code&gt;get_articles&lt;/code&gt;, &lt;code&gt;get_article&lt;/code&gt;, &lt;code&gt;get_user&lt;/code&gt;, &lt;code&gt;get_tags&lt;/code&gt;, &lt;code&gt;get_comments&lt;/code&gt;, &lt;code&gt;search_articles&lt;/code&gt;. Useful for reading, useless for writing.&lt;/p&gt;

&lt;p&gt;I extended it with three write tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create_article&lt;/code&gt; for publishing new articles (draft or live)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_article&lt;/code&gt; for editing existing ones&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete_article&lt;/code&gt; for unpublishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a full read-write MCP server that lets Claude (or any MCP client) treat DEV.to like a CMS. This article was created and published using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The tool list in Claude Desktop after the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read-only tools (6):
  Get Articles, Get Article, Get User, Get Tags, Get Comments, Search Articles

Write/delete tools (3):
  Create Article, Update Article, Delete Article
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A draft creation call looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My new post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body_markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Hello world"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"webdev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server hits &lt;code&gt;POST https://dev.to/api/articles&lt;/code&gt; with the user's &lt;code&gt;DEVTO_API_KEY&lt;/code&gt; from env, returns the article ID, and Claude can immediately call &lt;code&gt;update_article&lt;/code&gt; against it. No browser, no copy-paste from chat to editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Journey
&lt;/h2&gt;

&lt;p&gt;The original repo was solid but limited. I asked myself: why use an MCP server that can only read?&lt;/p&gt;

&lt;p&gt;Setup was the first wall. The npm package wasn't published, so &lt;code&gt;npx -y @nickytonline/dev-to-mcp&lt;/code&gt; returned 404. Then &lt;code&gt;npm install -g github:...&lt;/code&gt; failed because the repo had no top-level &lt;code&gt;package.json&lt;/code&gt; at the install path npm expected. The fix was unglamorous: &lt;code&gt;git clone&lt;/code&gt;, &lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt;, point Claude Desktop's config at the local &lt;code&gt;dist/index.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was also a Windows-specific gotcha. Claude Desktop on Windows needs &lt;code&gt;npx.cmd&lt;/code&gt;, not &lt;code&gt;npx&lt;/code&gt;. The error message was just &lt;code&gt;Server disconnected&lt;/code&gt;. Logs showed &lt;code&gt;bad option: -y&lt;/code&gt; because the config still had the npx flag while the command had been swapped to &lt;code&gt;node&lt;/code&gt;. Small things, two hours.&lt;/p&gt;

&lt;p&gt;Once the read-only server was running, the actual finish-up work was straightforward. The codebase used a clean handler pattern: each tool was a function that called the DEV.to API and returned a typed response. I followed the same pattern for the three new tools:&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;// Pattern from the existing read tools&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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://dev.to/api/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&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;headers&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;api-key&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;DEVTO_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// New write tool, same shape&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleInput&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://dev.to/api/articles&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="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;headers&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;api-key&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&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="nx"&gt;article&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;Register the new handlers in the MCP server's tool list, rebuild with &lt;code&gt;npm run build&lt;/code&gt;, restart Claude Desktop. Done.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; for the server code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; for the build (12.83 kB output, builds in 133ms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Context Protocol SDK&lt;/strong&gt; for the server scaffolding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV.to API v1&lt;/strong&gt; as the backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Desktop&lt;/strong&gt; as the MCP client&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Two things stood out.&lt;/p&gt;

&lt;p&gt;First, finishing someone else's project is faster than starting from scratch. The repo had types, patterns, error handling, and tests already in place. Adding three tools meant matching an existing shape, not inventing one. The full add-rebuild-test cycle was under thirty minutes.&lt;/p&gt;

&lt;p&gt;Second, AI assistance works best when there's existing structure to imitate. I gave Claude Code the repo path and asked for three tools matching the existing pattern. It read the codebase, identified the handler signature, knew the DEV.to API endpoints from training data, and produced working code on the first build. Without the existing read-only tools as reference, the output would have needed more iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The MCP server still has gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No image upload (DEV.to requires base64 inline or external URLs)&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;get_followers&lt;/code&gt; or &lt;code&gt;get_following&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No comment write/delete&lt;/li&gt;
&lt;li&gt;No analytics endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are small additions following the same pattern. The hard part was the first three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;The forked repo with write tools: &lt;a href="https://github.com/glatinone/dev-to-mcp" rel="noopener noreferrer"&gt;github.com/glatinone/dev-to-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Original credit to &lt;a href="https://dev.to/nickytonline"&gt;@nickytonline&lt;/a&gt; for the read-only foundation.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Token Monitor 🚀 — Real-Time Claude Usage HUD for Your Windows Desktop</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Sat, 06 Jun 2026 17:55:16 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/claude-token-monitor-real-time-claude-usage-hud-for-your-windows-desktop-4njn</link>
      <guid>https://dev.to/kielltampubolon/claude-token-monitor-real-time-claude-usage-hud-for-your-windows-desktop-4njn</guid>
      <description>&lt;h1&gt;
  
  
  Claude Token Monitor 🚀
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Disclaimer:&lt;/strong&gt; This is an unofficial community-driven tool and is not affiliated, associated, endorsed by, or in any way officially connected with Anthropic, PBC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ever wonder how much of your Claude quota you've burned through mid-session? &lt;strong&gt;Claude Token Monitor&lt;/strong&gt; is a local-first desktop utility that gives you a beautiful, always-on-top floating HUD showing your Claude usage in real time — covering both &lt;strong&gt;Claude.ai Web App&lt;/strong&gt; quota and &lt;strong&gt;Claude Code CLI&lt;/strong&gt; token consumption.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/glatinone/claude-token-monitor-usage" rel="noopener noreferrer"&gt;github.com/glatinone/claude-token-monitor-usage&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📸 What It Looks Like
&lt;/h2&gt;

&lt;p&gt;The HUD uses a sleek &lt;strong&gt;glassmorphism&lt;/strong&gt; design with a circular progress ring that dynamically changes color:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;Green&lt;/strong&gt; — Low usage&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;Amber&lt;/strong&gt; — Medium usage&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Red&lt;/strong&gt; — High usage (time to slow down!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can drag it anywhere on your screen, collapse it into a tiny title pill with a double-click, and control it via the system tray.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Double-Sync Strategy:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone Mode&lt;/strong&gt; — Direct API polling using your saved &lt;code&gt;sessionKey&lt;/code&gt; cookie&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser Extension Sync&lt;/strong&gt; — A companion Chrome Extension pushes live stats the moment you send a message on claude.ai&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Always-on-Top Floating HUD&lt;/strong&gt; — Circular progress ring, remaining quota %, and reset timer&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Draggable &amp;amp; Position Memory&lt;/strong&gt; — Drag it wherever you want; position is saved on exit&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Claude Code CLI Integration&lt;/strong&gt; — Real-time token parsing (Input, Output, Cache Write, Cache Read) with USD cost estimation based on Claude Sonnet pricing&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Single-Instance Protection&lt;/strong&gt; — TCP socket lock prevents duplicate processes or tray icon spam&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;100% Private &amp;amp; Offline-First&lt;/strong&gt; — All data lives in &lt;code&gt;usage_data.json&lt;/code&gt; on your machine. The local server binds only to &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ Architecture
&lt;/h2&gt;

&lt;p&gt;Here's how the pieces fit together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Web Browser (claude.ai)
  └── Chrome Extension MV3
        └── POST JSON ──────────────────────┐
                                            ▼
Terminal / IDE                     Desktop App (Python)
  └── Claude Code CLI               ├── Local HTTP Server :9988
        └── Writes logs ──►         ├── Log Watcher
              ~/.claude/projects/   ├── Direct API Fetcher
              *.jsonl               ├── Storage Manager
                                    ├── CTk Floating HUD
                                    └── Tray Manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log Watcher&lt;/strong&gt; — Monitors &lt;code&gt;%USERPROFILE%\.claude\projects\*.jsonl&lt;/code&gt; for CLI token events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local HTTP Server (port 9988)&lt;/strong&gt; — Receives pushed JSON payloads from the Chrome Extension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Fetcher&lt;/strong&gt; — Polls the Claude API directly using your &lt;code&gt;sessionKey&lt;/code&gt; cookie when the extension isn't running&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🚀 Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10+&lt;/li&gt;
&lt;li&gt;Windows OS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Install the Desktop App
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/glatinone/claude-token-monitor-usage.git
&lt;span class="nb"&gt;cd &lt;/span&gt;claude-token-monitor-usage
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python &lt;span class="nt"&gt;-m&lt;/span&gt; app.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Windows users can just double-click &lt;code&gt;run.bat&lt;/code&gt; to install dependencies and launch in one step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Standalone Mode (Session Key Setup)
&lt;/h3&gt;

&lt;p&gt;To let the app query your quota directly without the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;claude.ai&lt;/a&gt; and log in&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;F12&lt;/code&gt; → &lt;strong&gt;Application&lt;/strong&gt; → &lt;strong&gt;Cookies&lt;/strong&gt; → &lt;code&gt;https://claude.ai&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the value of the &lt;code&gt;sessionKey&lt;/code&gt; cookie (starts with &lt;code&gt;sk-ant-sid...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Right-click the &lt;strong&gt;C&lt;/strong&gt; tray icon → &lt;strong&gt;Setup Session Key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Paste and click OK — the widget syncs automatically!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Chrome Extension (Optional but Recommended)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;chrome://extensions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Developer Mode&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Load Unpacked&lt;/strong&gt; → select the &lt;code&gt;extension/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Open claude.ai and send a message — your quota syncs instantly!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔒 Security &amp;amp; Privacy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cloud Sharing&lt;/strong&gt; — Usage data stays strictly in &lt;code&gt;app/usage_data.json&lt;/code&gt; on your local disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localhost Lock&lt;/strong&gt; — The API server binds to &lt;code&gt;127.0.0.1&lt;/code&gt; only; no LAN access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential Isolation&lt;/strong&gt; — The Chrome Extension never touches your &lt;code&gt;sessionKey&lt;/code&gt;. It's stored locally in &lt;code&gt;app/config.json&lt;/code&gt; (git-ignored)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;Contributions are very welcome! Whether it's Linux/macOS support, UI improvements, or better CLI log parsing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork the repo&lt;/li&gt;
&lt;li&gt;Create your branch: &lt;code&gt;git checkout -b feature/AmazingFeature&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Commit: &lt;code&gt;git commit -m 'Add AmazingFeature'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Push: &lt;code&gt;git push origin feature/AmazingFeature&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open a Pull Request&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  📄 License
&lt;/h2&gt;

&lt;p&gt;MIT License — free to use, modify, and distribute.&lt;/p&gt;




&lt;p&gt;🔗 &lt;strong&gt;Check out the full project on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/glatinone/claude-token-monitor-usage" rel="noopener noreferrer"&gt;github.com/glatinone/claude-token-monitor-usage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find it useful, drop a ⭐ on the repo and share it with fellow Claude power users!&lt;/p&gt;

</description>
      <category>python</category>
      <category>claude</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI Agents: The Future of Autonomous Intelligence</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Sat, 06 Jun 2026 16:51:03 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/ai-agents-the-future-of-autonomous-intelligence-onp</link>
      <guid>https://dev.to/kielltampubolon/ai-agents-the-future-of-autonomous-intelligence-onp</guid>
      <description>&lt;h1&gt;
  
  
  AI Agents: The Future of Autonomous Intelligence
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What is an AI Agent?
&lt;/h2&gt;

&lt;p&gt;An AI Agent is a system that perceives its environment, makes decisions, and takes actions to reach a goal with minimal human involvement.&lt;/p&gt;

&lt;p&gt;A chatbot answers questions. An AI Agent goes further. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search the web for up-to-date information&lt;/li&gt;
&lt;li&gt;Write and run code&lt;/li&gt;
&lt;li&gt;Read and write files&lt;/li&gt;
&lt;li&gt;Call external APIs&lt;/li&gt;
&lt;li&gt;Plan tasks across multiple steps and recover when something breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is not intelligence. It is autonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do AI Agents Actually Work?
&lt;/h2&gt;

&lt;p&gt;Most agents today run on a loop called ReAct (Reason + Act):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------+
|         Observe         |
|    Read current state   |
+-------------------------+
            |
            v
+-------------------------+
|          Think          |
|   Decide next action    |
+-------------------------+
            |
            v
+-------------------------+
|           Act           |
|    Use a tool or API    |
+-------------------------+
            |
            v
+-------------------------------+
|        Environment            |
|  Files, APIs, web, databases  |
+-------------------------------+
            |
            +-------------------+
                                |
                    (result feeds back)
                                |
                                v
                     Back to Observe...
                     until goal is reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent looks at the current state, decides what to do, does it, then checks again. This cycle continues until the goal is met or something breaks.&lt;/p&gt;

&lt;p&gt;What makes this powerful is not any single step. It is that the loop can run hundreds of times, across tools, APIs, and files, without a human in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of AI Agents
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Single-agent
&lt;/h3&gt;

&lt;p&gt;One agent handles everything. Fast to build, limited in scope. Good enough for most tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-agent
&lt;/h3&gt;

&lt;p&gt;Specialized agents working together. One researches, one writes, one reviews. More capable, more complex to orchestrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agentic pipelines
&lt;/h3&gt;

&lt;p&gt;A fixed sequence of agent steps. Think assembly line, not free-form reasoning. Reliable and predictable, useful for automation workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;td&gt;GitHub Copilot, Cursor, Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer support&lt;/td&gt;
&lt;td&gt;Bots that actually resolve issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;Browse, summarize, report autonomously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevOps&lt;/td&gt;
&lt;td&gt;Auto-fix failing pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content creation&lt;/td&gt;
&lt;td&gt;Write, edit, and publish without manual steps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Frameworks Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangChain&lt;/strong&gt; -- the most widely used, with a huge ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LlamaIndex&lt;/strong&gt; -- strong for retrieval-augmented generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AutoGen&lt;/strong&gt; (Microsoft) -- designed for multi-agent conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrewAI&lt;/strong&gt; -- role-based teams of agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude + MCP&lt;/strong&gt; -- Anthropic's approach using the Model Context Protocol&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol is a standard way to connect AI models to external tools and data. Instead of hardcoding tool integrations, MCP lets agents plug into anything that exposes an MCP server.&lt;/p&gt;

&lt;p&gt;With MCP, Claude can read files, query databases, call APIs, and publish articles to DEV.to. That is exactly how this article got here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can Go Wrong
&lt;/h2&gt;

&lt;p&gt;AI Agents are genuinely useful. They are also genuinely unreliable in specific ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hallucination&lt;/strong&gt; -- agents can produce confident, wrong output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite loops&lt;/strong&gt; -- without guardrails, they can spin indefinitely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; -- multi-step agentic tasks burn tokens fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; -- an agent with broad permissions is a large attack surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a dealbreaker. It just means you need to design with failure in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Is Heading
&lt;/h2&gt;

&lt;p&gt;Three things that matter most in the near term:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Better planning&lt;/strong&gt; -- agents that reason over longer time horizons without losing track&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent memory&lt;/strong&gt; -- actually remembering what happened in previous sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-correction&lt;/strong&gt; -- agents that notice when they have gone wrong and course-correct&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Multi-agent collaboration is already here. The hard part is not making agents work together. It is making them work together reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Does This Leave Us?
&lt;/h2&gt;

&lt;p&gt;AI Agents shift the relationship between humans and software. Instead of tools you operate, you get systems that operate on your behalf.&lt;/p&gt;

&lt;p&gt;That is a meaningful change. It is also early. The gap between what agents can do in a demo and what they can do reliably in production is still wide.&lt;/p&gt;

&lt;p&gt;But it is closing fast.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Kubernetes Alternative. It Changed My Perspective on Complexity.</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 27 May 2026 04:50:58 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-built-a-kubernetes-alternative-it-changed-my-perspective-on-complexity-4ja6</link>
      <guid>https://dev.to/kielltampubolon/i-built-a-kubernetes-alternative-it-changed-my-perspective-on-complexity-4ja6</guid>
      <description>&lt;p&gt;After three long nights of coding, I stood on the edge of quitting. The most recent error? 'Connection refused: too many retries.' How does one recover from such a humiliating message? I guess you just pick yourself up and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACT 1: The Confidence Built on Ignorance
&lt;/h2&gt;

&lt;p&gt;At first, I was cocky about my ability to develop a simpler alternative to Kubernetes. After all, I was drawing from over five years of experience building and managing containerized applications. Besides, I had graduated from a reputable programming boot camp, which ended up leading me to some great opportunities. I figured if I could wrangle Kubernetes, I could surely create something less complex and more approachable.&lt;/p&gt;

&lt;p&gt;When I began this project, I believed the only thing standing in my way was time. I had convinced myself that my technical skills were enough; all I had to do was write the code, and it would come together. I thought eliminating a few features and minimizing configurations would be all it took to create something user-friendly. I pictured it, lightweight, no more than 300 lines of code for my entire tool. Simple, right? &lt;/p&gt;

&lt;h2&gt;
  
  
  ACT 2: The Moment It Broke
&lt;/h2&gt;

&lt;p&gt;That illusion shattered when I hit my first major roadblock. After several days of coding with barely any sleep, I tried to deploy my work for the first time. I was so giddy with excitement; it was like I was seeing the light at the end of a tunnel. For about two seconds, it felt like everything was going great. And then came the error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: invalid configuration: failed to find a ready to use service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I thought, “How could this happen?” My tool was designed to simplify the deployment process, not make it more convoluted. After sweating over the terminal for hours, I tried everything. I rechecked the API calls, re-evaluated services, and even consulted the Kubernetes official documentation (which, let's be honest, is just a fancy way of saying I ended up down a rabbit hole of YAML files).&lt;/p&gt;

&lt;p&gt;At that moment, it hit me: I hadn’t just omitted complexity; I had disregarded the very principles that make Kubernetes powerful, its ability to manage the scalability and orchestration of applications in distributed environments. I realized simplicity doesn't always mean fewer lines of code. Sometimes, it means better abstractions that serve higher purposes. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Turning Point: What I Discovered
&lt;/h2&gt;

&lt;p&gt;I decided to roll my sleeves up, go back to the drawing board, and analyze what I was actually trying to achieve. Instead of just cutting features, I began to rethink some of them entirely. I analyzed critical aspects like service discovery and load balancing and realized that perhaps they weren’t&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>github</category>
    </item>
    <item>
      <title>I Dumped My Entire Codebase into Gemma 4. It Found a Bug My Team Missed for 3 Months.</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Mon, 18 May 2026 13:54:12 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-dumped-my-entire-codebase-into-gemma-4-it-found-a-bug-my-team-missed-for-3-months-3oa1</link>
      <guid>https://dev.to/kielltampubolon/i-dumped-my-entire-codebase-into-gemma-4-it-found-a-bug-my-team-missed-for-3-months-3oa1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Write About Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The bug had been in production for 90 days. Three sprint reviews. Two refactors nearby. One team member who actually touched that file and left a comment saying "looks fine."&lt;/p&gt;

&lt;p&gt;I found it in 4 minutes by pasting 47,000 tokens into Gemma 4 31B and asking one question.&lt;/p&gt;

&lt;p&gt;I want to tell you this is a story about AI being brilliant. It is not. It is a story about what happens when you stop treating a 128K context window as a spec sheet number and start treating it as a tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick context if you're new to Gemma 4:&lt;/strong&gt; It's Google DeepMind's latest open-weight model family, released April 2026 under Apache 2.0. The 31B dense variant is the largest in the family and the one that ships with a 128K context window and built-in reasoning mode. You can access it free via &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; with no credit card required.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Bug Was Invisible at File Level
&lt;/h2&gt;

&lt;p&gt;We run a mid-sized Python backend. Not a startup toy project, not a monolith from 2009 either. Around 18,000 lines across 200-ish files, with a payment reconciliation module that processes about 4,000 transactions a day.&lt;/p&gt;

&lt;p&gt;For three months we had an off-by-one error in how we accumulated daily totals. The kind of bug that doesn't crash anything, doesn't throw an exception, and stays quiet until someone runs a quarterly audit. Finance flagged it. Our first assumption was the database. Our second assumption was a race condition. Our third assumption was a rounding issue inherited from a third-party SDK.&lt;/p&gt;

&lt;p&gt;All wrong. The bug was in how we correlated timestamps across two functions that lived 800 lines apart in the same module. You could not see it by reading either function. You had to hold both in your head at the same time, along with the utility function they both relied on, to see the logic drift.&lt;/p&gt;

&lt;p&gt;Nobody held all three at once. Humans don't naturally do that across 800 lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Did
&lt;/h2&gt;

&lt;p&gt;I pulled the relevant module (about 1,400 lines), the shared utilities it imports (another 600 lines), and the test file (300 lines). Total: roughly 2,300 lines of Python. That's somewhere around 46,000 tokens.&lt;/p&gt;

&lt;p&gt;I chose Gemma 4 31B, accessed via OpenRouter, specifically because of the context window size and the reasoning capability. The 128K window was not the point in itself. The point was that 128K let me stop chunking.&lt;/p&gt;

&lt;p&gt;Every time I had tried to debug this with smaller-context tools I was feeding pieces of the problem. Paste function A, get feedback. Paste function B, get different feedback. Try to synthesize the two answers manually. That synthesis step is where the bug kept hiding.&lt;/p&gt;

&lt;p&gt;With 31B I pasted the full thing at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are reviewing Python source code for a financial reconciliation system.
Below is the complete module, its utility dependencies, and its test suite.

Find any logic errors related to timestamp handling, accumulation order,
or correlation between functions. Be specific about line numbers and
explain the interaction that causes the error, not just the error itself.

&amp;lt;module&amp;gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reconciliation_module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;/module&amp;gt;

&amp;lt;utilities&amp;gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shared_utils&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;/utilities&amp;gt;

&amp;lt;tests&amp;gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_suite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;/tests&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No chunking. No iterative back-and-forth. One completion.&lt;/p&gt;

&lt;p&gt;The response came back in about 40 seconds. It flagged three things. Two were style notes I already knew about. The third was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;accumulate_daily()&lt;/code&gt; uses &lt;code&gt;record.created_at&lt;/code&gt; for grouping (line 312), but &lt;code&gt;reconcile_period()&lt;/code&gt; filters by &lt;code&gt;record.settled_at&lt;/code&gt; (line 1089). For records that settle in a different calendar day than they were created, these two functions will assign the same record to different days. The test suite only creates records where &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;settled_at&lt;/code&gt; are within the same UTC day, so this has never been caught by the test harness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I sat there for a second. Then I checked the two functions side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# line 312 — in accumulate_daily()
&lt;/span&gt;&lt;span class="n"&gt;day_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- groups by creation date
&lt;/span&gt;
&lt;span class="c1"&gt;# line 1089 — in reconcile_period()
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;period_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- filters by settlement date
&lt;/span&gt;    &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a transaction is created on Monday but settles on Tuesday, &lt;code&gt;accumulate_daily()&lt;/code&gt; puts it in Monday's bucket. &lt;code&gt;reconcile_period()&lt;/code&gt; counts it toward Tuesday's filter window. Same record. Two different days. Neither function is wrong on its own. Together they silently drift.&lt;/p&gt;

&lt;p&gt;Then I checked the test fixtures. Every single test record had &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;settled_at&lt;/code&gt; set to the same UTC day. The tests never exercised the cross-day case.&lt;/p&gt;

&lt;p&gt;It was exactly right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Worked and Why It Didn't Work Before
&lt;/h2&gt;

&lt;p&gt;The test suite was the key detail. The model wasn't just spotting a timestamp mismatch between two functions. It was reading the tests and noticing the gap: the tests don't cover the case where these dates differ. That kind of cross-file reasoning requires the full context to be present simultaneously.&lt;/p&gt;

&lt;p&gt;I had tried a similar experiment with GPT-4 earlier in the week by pasting sections iteratively. It flagged the &lt;code&gt;created_at&lt;/code&gt; vs &lt;code&gt;settled_at&lt;/code&gt; discrepancy in isolation but didn't make the connection to the test fixtures because the test file wasn't in scope when it saw the functions. The insight was split across two separate completions, and I didn't connect them.&lt;/p&gt;

&lt;p&gt;With the full module in one pass, the model could see the claim ("here is how we accumulate") and the verification ("here is what the tests assert") in the same window, and notice they didn't match on the cross-day case.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Limits
&lt;/h2&gt;

&lt;p&gt;This is not a "just paste your codebase and it will find all your bugs" story. Three caveats that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency is real.&lt;/strong&gt; The 31B model via OpenRouter takes time on a 46K token completion. For quick questions I still reach for smaller models or a local Gemma 4 E4B setup. The 31B is the right tool when you need the full picture and can afford the wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A vague prompt gets vague answers.&lt;/strong&gt; My first attempt asked "find any issues." It returned generic style feedback. The version that found the bug asked specifically for "logic errors related to timestamp handling" because I had already narrowed the domain. The model amplified my hypothesis. It did not generate one from nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It still missed one bug.&lt;/strong&gt; I found it later through manual review. Not a humiliating miss. But a reminder that "found the bug I missed" and "found all the bugs" are very different sentences.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What This Actually Changes
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The 128K context window is not about being able to process more. It is about being able to stop decomposing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most debugging assistance with AI is a decomposition problem: I have to decide what to show and what to leave out, and that editorial decision is exactly where bugs hide. The moment I have to decide "this function is probably not relevant," I might be wrong about that, and the model will never tell me.&lt;/p&gt;

&lt;p&gt;When the window is large enough that I can stop curating, the model sees the same thing I should be seeing: the whole picture, including the parts I didn't think to highlight.&lt;/p&gt;

&lt;p&gt;That changes the failure mode from "I showed it the wrong piece" to "I asked the wrong question." The second failure mode is actually fixable.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it yourself:&lt;/strong&gt; grab a module you've been meaning to audit, load the full file plus its tests into Gemma 4 31B on &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; (free tier, no card), and ask specifically about the interaction between the functions, not just each function alone. Drop what you find in the comments. I'm genuinely curious whether the cross-file blindspot is a universal problem or just a symptom of how our specific codebase grew.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Was Wrong About AI. Here Is the Moment That Changed It.</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Mon, 18 May 2026 13:30:36 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-was-wrong-about-ai-here-is-the-moment-that-changed-it-1cb</link>
      <guid>https://dev.to/kielltampubolon/i-was-wrong-about-ai-here-is-the-moment-that-changed-it-1cb</guid>
      <description>&lt;p&gt;The debugging tool flagged a staggering 150 issues in my code almost instantly. I was astonished by how many mistakes I had made and how far I still had to go. This moment revealed the complexity of AI that I had underestimated all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment It Broke
&lt;/h2&gt;

&lt;p&gt;That breaking point was unexpected. I was toying with a fancy AI algorithm, thinking it was about to churn out perfect code. I had linked it with my code editor, set everything up, and watched with excitement as it typed out solutions based on prompts I fed it. After a few successful iterations, I made a huge mistake. I forgot to properly validate the inputs. &lt;/p&gt;

&lt;p&gt;One afternoon, I threw in a random input to test its limits. The console displayed the message that will haunt me: "Runtime Error: Unexpected Token."&lt;/p&gt;

&lt;p&gt;For a moment, I was frozen. I had ignored something critical: AI is a tool, not a solution in itself. More often than not, I’d been treating it as some kind of oracle instead of evaluating how it actually understood my requests. I should’ve known better. Sure, AI can assist in many ways, but nothing beats core programming principles.&lt;/p&gt;

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

&lt;p&gt;When I finally debugged the mess, I took a moment to reflect. I realized that I had neglected proper coding best practices in favor of a shiny new toy. AI works wonderfully when it augments your existing understanding and workflow. Here’s a snippet demonstrating the change I made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original flawed function without validation&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aiSuggestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;aiModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Improved function with validation&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;aiSuggestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input: Please provide a valid string.&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;aiModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;Just adding that validation step made a world of difference. I could trust my AI’s feedback more because I was guiding it with better input. It was a simple tweak, but it became pivotal in my project’s success. I still smiled when my AI echoed back code that was far more functional than my initial attempts, but now I was equipped with the knowledge that I needed to do my part first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Principle
&lt;/h2&gt;

&lt;p&gt;This lead to a bigger realization: tools are just extensions of ourselves. They don't replace the fundamentals. If you're a developer working with AI, you have to take responsibility for your code. Forgetting that turns you into a passive user, and honestly, nobody wants to be that. It’s like trying to build a house without knowing how to lay bricks; the walls might look good for a while, but they’re bound to crumble eventually.&lt;/p&gt;

&lt;p&gt;When I look back, what annoys me most is that I didn’t question my assumptions sooner. I could’ve saved time, energy, and probably a few hairs on my head. Relying solely on AI to deliver the goods is tempting, but it leads to dangerous shortcuts. Proper coding practices don’t just lead to better outcomes; they prevent mistakes down the road.&lt;/p&gt;

&lt;p&gt;In hindsight, I’d tell past-me to challenge the narrative that tech can solve everything. AI should elevate our skills, not replace them. So here’s my burning question: Are AI and automation a developer's best support system, or do they create a dangerous dependency that might weaken our core skills? What’s your take on this?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>javascript</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>I Struggled with Data Analysis. Claude for Excel Changed Everything.</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Fri, 08 May 2026 12:02:15 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-struggled-with-data-analysis-claude-for-excel-changed-everything-24o0</link>
      <guid>https://dev.to/kielltampubolon/i-struggled-with-data-analysis-claude-for-excel-changed-everything-24o0</guid>
      <description>&lt;p&gt;At 2 AM, I was ready to throw my laptop out the window. I had just spent hours gaining zero insights from a sprawling sea of data. Then I stumbled upon Claude for Excel, and suddenly everything clicked into place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Real Error or Messy Code
&lt;/h2&gt;

&lt;p&gt;Like many developers, especially those of us who aren't formally trained as data analysts, Excel can sometimes feel like a jungle. I was trying to generate reports that needed to highlight sales trends, which required me to wrangle all that data into a coherent narrative. I needed to create pivot tables to analyze the responses, but navigating through cumbersome formulas was turning my brain into mush. I spent hours merging cells, applying filters, and trying to remember whether I used SUM or AVERAGE last time.&lt;/p&gt;

&lt;p&gt;One day, I spent an entire Sunday trying to get a complex formula to work. I ended up with #VALUE! errors splattered across my sheet, leading to frustration that lasted longer than I care to admit. My family could see my stress levels rising as I muttered incoherently about Excel's cryptic syntax. I wanted to blame the software, but the truth was, I was the one wrestling with messy data and unrealistic expectations on myself. The more I struggled, the more irrational I became.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Enter Claude for Excel—a tool that instantly began to take the burden off my shoulders. After a brief introduction and tutorial, I realized this could be my new best friend in the world of data. The functionality was like getting handed a magic wand; it could analyze data without needing me to trip over complex formulas. &lt;/p&gt;

&lt;p&gt;For example, I was once stuck trying to calculate the year-over-year growth of sales figures across various regions, which involved a spreadsheet of about 3,000 rows. Typically, I would have diced and sliced this data manually, hoping to avoid errors along the way. Instead, using Claude, I could simply upload my data set and ask it for the required analysis in plain language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the sales data and show year-over-year growth by region.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within seconds, not only did Claude give me the results, but it also generated a pivot table for me. I couldn't believe my eyes. The tool had processed the entire sheet seamlessly, saving me at least three hours of work. I was able to use the generated table to make decisions swiftly and present findings during Monday’s meeting with confidence. This was not magic—it was just a smart combination of AI and my ongoing need for efficiency.&lt;/p&gt;

&lt;p&gt;Furthermore, when I felt extra adventurous, I asked Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me the top 5 products based on customer feedback.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, I received a neat summary sans the usual headache. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Principle Behind the Fix
&lt;/h2&gt;

&lt;p&gt;What I learned through this process was simple yet profound: embracing tools that streamline techniques is essential. Rather than getting tangled in the complexity of data manipulation, using Claude for Excel allowed me to focus on what truly mattered—insightful analysis and decision-making. I began to see an increase in my productivity, as I could spend time analyzing results rather than scrambling over formulas. In concrete terms, I've gone from spending about 20 hours a month on data analysis down to fewer than 10.&lt;/p&gt;

&lt;p&gt;So what's the bigger principle here? It's about understanding your limitations and recognizing when to employ technology that complements your skills. Claude for Excel shifted not only the way I handled data but also my perception of it. Instead of feeling overwhelmed, I started feeling empowered, knowing I had a sophisticated partner by my side.&lt;/p&gt;

&lt;p&gt;Reflecting on the time wasted before discovering this tool, I can’t help but chuckle. I wish I had found it sooner. If you're knee-deep in data analysis and feel like you’re drowning in formulas, I highly recommend you take a look at Claude. Just make sure to remember the old saying: it's not about knowing it all, but knowing how to ask the right questions. &lt;/p&gt;

&lt;p&gt;If you've had similar experiences with data tools, what’s the most significant win you've achieved with a new software? What one tool would you recommend to others drowning in data that could provide clarity without the headache?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>github</category>
    </item>
    <item>
      <title>I Struggled with a Bug for Days — Here's What I Learned About Clean Code and Debugging</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Thu, 07 May 2026 15:14:19 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-struggled-with-a-bug-for-days-heres-what-i-learned-about-clean-code-and-debugging-2665</link>
      <guid>https://dev.to/kielltampubolon/i-struggled-with-a-bug-for-days-heres-what-i-learned-about-clean-code-and-debugging-2665</guid>
      <description>&lt;p&gt;As I sat in front of my computer screen one humid evening in Batam, covered with the shadows of unfinished projects, I felt a sinking frustration deep in my chest. For days, I was plagued by a bug in a JavaScript function that seemed to mock me, with every test yielding inconsistent results. I was convinced I had a handle on clean code principles, but the hours spent chasing this elusive problem made me question everything I knew.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context &amp;amp; Stakes
&lt;/h3&gt;

&lt;p&gt;debugging effectively is essential for any developer, especially when working in tight deadlines or collaborative environments. I was leading a small team on a client project, a dynamic web application intended to streamline local business operations. Our success depended on delivering clean, efficient code, and I was the one responsible for reviewing and finalizing it. The stakes were high, and with each failed attempt to fix the issue, our timeline slipped further.&lt;/p&gt;

&lt;p&gt;The bug originated in a function designed to fetch user data from an API, but the data only loaded half the time. After scanning and re-scanning the code, everything seemed fine — I had followed all the recommended practices. Yet, the problem persisted. This relentless loop of testing and frustration forced me to confront my limitations head-on. Would I be able to solve this before our deadline? &lt;/p&gt;

&lt;h3&gt;
  
  
  CHALLENGE
&lt;/h3&gt;

&lt;p&gt;What I had overlooked was a crucial aspect of my code's asynchronous nature. Here’s the snippet that tripped me up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.example.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Network response was not ok&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&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;At first glance, this code looked correct, but the challenge was in the way it interacted with the rest of the application. I was using this function in multiple places within the codebase, but because the API could occasionally be slow, I hadn’t handled the failures correctly in the components calling it. This led to intermittent fraying of user experience, with components loading only half the time or throwing unhandled errors.&lt;/p&gt;

&lt;p&gt;I felt the weight of inefficiency weighing down my team. The pressure to deliver without compromising quality left me juggling multiple hats, and I realized that my understanding of clean, maintainable code had been superficial. I needed to dig deeper into both debugging techniques and code readability practices to navigate this complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  BREAKTHROUGH
&lt;/h3&gt;

&lt;p&gt;The breakthrough came when I decided to systematically approach the problem. Instead of diving straight into fixing it, I first resurfaced and rewrote the function to make it clearer and more manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.example.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Network response was not ok&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fetching user data failed: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Logging error with context&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Handle this gracefully on the calling side&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;By adding error handling and a &lt;code&gt;try/catch&lt;/code&gt; block, I octupled my chances of catching errors early. However, the biggest revelation was shifting my mindset from fixing bugs to devising solutions that make the code robust against future issues. I took the time to document the expected outcomes in the comments as a reminder to myself and my teammates. This transparency in the code not only made it easier for us to follow, but it also set the stage for better collaboration.&lt;/p&gt;

&lt;h3&gt;
  
  
  DEEPER INSIGHT
&lt;/h3&gt;

&lt;p&gt;Through this experience, I gleaned a larger principle: clean code is not just about aesthetics or following patterns; it is about making it maintainable and resilient. As developers, we must anticipate how our code may misbehave under unexpected conditions. The cleaner and more explicit we write our code, the easier it becomes for us and others to debug it — saving precious development time in the long run.&lt;/p&gt;

&lt;p&gt;Additionally, the experience reinforced the idea that debugging is as much about mindset as it is about skill. It’s easy to get lost in the weeds of our code; stepping back to reassess the problem can sometimes bring clarity, especially with peer programming, where you can have a fresh set of eyes or someone to help brainstorm.&lt;/p&gt;

&lt;h3&gt;
  
  
  WHAT I'D DO DIFFERENTLY
&lt;/h3&gt;

&lt;p&gt;Upon reflection, here are a few actionable steps I’d take to improve my approach to debugging and code quality moving forward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize Error Handling&lt;/strong&gt;: Always include explicit error handling strategies within asynchronous functions. Errors are inevitable; planning for them pays off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encourage Team Code Reviews&lt;/strong&gt;: Foster a culture of peer reviews within your team. Fresh perspectives often unearth issues that one might overlook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document Potential Errors&lt;/strong&gt;: Add comments to clarify how functions behave in edge cases. Documentation can be a lifesaver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test-Driven Development (TDD)&lt;/strong&gt;: Implement TDD practices for new features to catch bugs early in the development cycle, enhancing code reliability. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Up Monitoring&lt;/strong&gt;: Utilize tools like Sentry to catch runtime errors and log issues transparently into the application, so you can react in real-time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By taking these steps, I believe we can significantly minimize bugs and build a more resilient infrastructure for our applications. &lt;/p&gt;

&lt;h3&gt;
  
  
  CLOSING QUESTION
&lt;/h3&gt;

&lt;p&gt;What debugging techniques or coding practices have you found essential in your own projects? How have you dealt with seemingly insurmountable bugs? Let's discuss in the comments!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>I Deployed My First Cloudflare Worker — Here's What I Learned</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 06 May 2026 11:32:05 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/i-deployed-my-first-cloudflare-worker-heres-what-i-learned-hcf</link>
      <guid>https://dev.to/kielltampubolon/i-deployed-my-first-cloudflare-worker-heres-what-i-learned-hcf</guid>
      <description>&lt;p&gt;I sat there staring at the screen, my heart racing as I clicked 'deploy' on my first Cloudflare Worker. What should have been a straightforward process felt like a rollercoaster of confusion and excitement. Just moments before, I'd lined everything up: my code looked good, my environment seemed solid—how could anything go wrong?&lt;/p&gt;

&lt;p&gt;But then it hit me. 401 Unauthorized. What? I hadn’t encountered this in all my testing. Little did I know, the culprit was hiding in the shadows of a misconfigured secret token, like a ghost refusing to leave its haunted house.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Being a developer, I often toggle between creative solutions and technical bottlenecks. I’ve spent countless late nights coding, occasionally stacking hundreds of browser tabs filled with information, guides, and articles. Typical developer life, right? In an attempt to re-organize my chaotic digital world, I finally utilized Notion Web Clipper. It became my sidekick, ensuring that everything I stumbled upon would be neatly packed away for when I needed it. But nothing prepared me for the impending chaos with Cloudflare Workers. &lt;/p&gt;

&lt;p&gt;I was eager to build a simple API service that could run globally. After all, deploying on Cloudflare should have been a piece of cake. I picked a couple of templates and dived in, but secret management? Talk about a headache! I had breezed through the initial setup, barely taking note of how tokens should be correctly managed in the environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;The panic kicked in when my worker returned a loud, resounding &lt;strong&gt;401 Unauthorized&lt;/strong&gt; error. I felt defeated. It felt like grasping at straws, trying various approaches that led me further into confusion. I had mistakenly selected a template that wasn’t suitable for my use case, throwing my authorization strategy out the window like an old shoe.&lt;/p&gt;

&lt;p&gt;The main issue was that I lost sight of what tokens I was using for authentication. I had multiple API keys lying around from various services, and sure enough, I grabbed the wrong one. It reminded me of those ‘SELECT A TEMPLATE’ dropdown menus you see on documentation pages that you just don’t read. Instead of reading through the instructions, I assumed I knew it all. Classic developer hubris.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough
&lt;/h2&gt;

&lt;p&gt;Eventually, instead of banging my head against the desk, I took a deep breath and revisited the documentation for Cloudflare Workers. Turns out, the secret management was not just a convenient feature; it was crucial for keeping my application secure and functional. They even had detailed instructions on how to handle these secrets correctly.&lt;/p&gt;

&lt;p&gt;I quickly updated my API tokens using Cloudflare's secret management tools with these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Revisit the dashboard:&lt;/strong&gt; Ensure that you've saved your tokens properly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;.env&lt;/code&gt; file:&lt;/strong&gt; If you're unsure of where to place your secrets, Cloudflare Workers let you conveniently manage these in the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go with the right template:&lt;/strong&gt; Choose wisely! The correct template would seamlessly integrate with your authentication flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After implementing the right token, I deployed it again. It felt surreal to see the &lt;strong&gt;200 OK&lt;/strong&gt; response this time; everything felt right in the world again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deeper Insight
&lt;/h2&gt;

&lt;p&gt;What I realized through this whole ordeal is much larger than just the technical issue. It served as a reminder of the importance of understanding the tools we use—rushing into a setup without learning the fundamentals can be disastrous. This situation taught me that our tools can either propel our success or hinder it, and we should respect the complexity involved in building software. Reading documentation isn’t just for beginners; it’s for anyone looking to level up.&lt;/p&gt;

&lt;p&gt;In my experience, spending a bit more time upfront in understanding the scope and limits of what I was working on saved me hours of troubleshooting later. &lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Take Notes:&lt;/strong&gt; Throughout the cloud worker setup, I started losing track of what I was doing. If I’d kept notes or a checklist, I could’ve avoided such an embarrassing mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t Rush:&lt;/strong&gt; I was so eager to deploy my worker that I skimmed through critical documentation. In the future, I'll be more patient and thorough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging Tools:&lt;/strong&gt; Use Cloudflare's built-in development tools. They can help you pinpoint those pesky authorization issues sooner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for Help:&lt;/strong&gt; Instead of trying to figure it all out alone, asking in developer communities could save time—like moments of doubt and confusion I faced.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Question
&lt;/h2&gt;

&lt;p&gt;Have you ever overlooked an essential detail in your development work that led to a frustrating error? How did you recover, and what lessons did you take away from it? I’d love to hear your stories in the comments!&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>webdev</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
