<?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: Albert Alov</title>
    <description>The latest articles on DEV Community by Albert Alov (@vola-trebla).</description>
    <link>https://dev.to/vola-trebla</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3836100%2Fef7f69de-6efb-4fa6-9594-b4766a4ecead.jpg</url>
      <title>DEV Community: Albert Alov</title>
      <link>https://dev.to/vola-trebla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vola-trebla"/>
    <language>en</language>
    <item>
      <title>How react-render-profile-mcp works under the hood - and what it found in a real project</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Fri, 22 May 2026 13:26:21 +0000</pubDate>
      <link>https://dev.to/vola-trebla/how-react-render-profile-mcp-works-under-the-hood-and-what-it-found-in-a-real-project-he3</link>
      <guid>https://dev.to/vola-trebla/how-react-render-profile-mcp-works-under-the-hood-and-what-it-found-in-a-real-project-he3</guid>
      <description>&lt;p&gt;I've been building &lt;code&gt;react-render-profile-mcp&lt;/code&gt; for a few months — an MCP server that decodes React DevTools Profiler exports so AI agents can diagnose and now &lt;em&gt;fix&lt;/em&gt; render performance. Earlier posts covered &lt;a href="https://dev.to/vola-trebla/your-ai-agent-just-broke-your-react-performance-it-has-no-idea-4ghn"&gt;v0.1&lt;/a&gt; and &lt;a href="https://dev.to/vola-trebla/react-render-profile-mcp-v031"&gt;v0.3.1&lt;/a&gt;. This is v1.0.&lt;/p&gt;

&lt;p&gt;I want to do two things in this post: show what it actually found on a real open source project, then explain how the engine works inside — because "it's just an MCP wrapper" is not the right mental model. 🐸&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 1 — What it found on slash-admin
&lt;/h2&gt;

&lt;p&gt;I ran the full cycle on &lt;a href="https://github.com/d3george/slash-admin" rel="noopener noreferrer"&gt;slash-admin&lt;/a&gt;, a real React admin dashboard with Zustand and React Router. Target: &lt;code&gt;UserProfile&lt;/code&gt; in &lt;code&gt;src/pages/management/user/profile/index.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diagnostics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get_render_summary → find_spurious_renders → analyze_compiler_efficacy → suggest_memoization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;12 spurious renders on &lt;code&gt;UserProfile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;42ms wasted&lt;/li&gt;
&lt;li&gt;Trigger: &lt;code&gt;UNSTABLE_PARENT_REF&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Invalidation Index: 22.5&lt;/li&gt;
&lt;li&gt;ROI score: 2.5 (threshold is 1.5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem was two inline constants inside the render body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&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;bgStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`url(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bannerImage&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="na"&gt;backgroundSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundRepeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-repeat&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;tabs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Profile&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; },&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Followers&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; },&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;// ...3 more&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New object reference on every render. Every memoized child downstream gets invalidated on every pass, regardless of whether anything actually changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-remediation and the edge case
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;remediate_component&lt;/code&gt; runs three AST passes. On this file it hit a real edge case immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type-only import. Adding &lt;code&gt;React.memo&lt;/code&gt; to the default export without fixing this would produce a runtime &lt;code&gt;ReferenceError&lt;/code&gt;. The remediator detected it, rewrote the import, and continued:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-import type { CSSProperties } from "react";
&lt;/span&gt;&lt;span class="gi"&gt;+import React, { CSSProperties } from "react";
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the three passes ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-function UserProfile() {
-  const bgStyle: CSSProperties = { position: "absolute", inset: 0, ... };
-  const tabs = [{ icon: , title: "\"Profile\" }, ...];"
&lt;/span&gt;&lt;span class="gi"&gt;+const bgStyle: CSSProperties = { position: "absolute", inset: 0, ... };
+const tabs = [{ icon: , title: "\"Profile\" }, ...];"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+function UserProfile() {
&lt;/span&gt;   const { avatar, username } = useUserInfo();
   return ( ... );
 }
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-export default UserProfile;
&lt;/span&gt;&lt;span class="gi"&gt;+export default React.memo(UserProfile);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gap in the original implementation was found by running on real code. A new unit test now covers the &lt;code&gt;import type&lt;/code&gt; case. 57 tests passing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;42ms of spurious renders eliminated. Zero code written manually.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 2 — How it actually works inside
&lt;/h2&gt;

&lt;p&gt;This is the part that matters if you want to understand what's happening, not just that it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: decoding the profiler format
&lt;/h3&gt;

&lt;p&gt;React DevTools exports version 5 — a format most developers have never had to parse manually. The tricky parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;changeDescriptions&lt;/code&gt; is serialized as &lt;code&gt;Map.entries()&lt;/code&gt;&lt;/strong&gt;, not as a JSON object:&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="nl"&gt;"changeDescriptions"&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="mi"&gt;3&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="nl"&gt;"isFirstMount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="mi"&gt;4&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="nl"&gt;"props"&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 parser normalizes this on load into &lt;code&gt;Record&lt;/code&gt; so the rest of the code can work uniformly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;operations&lt;/code&gt; is an opcode integer array&lt;/strong&gt; encoding tree mutations. Format:&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="err"&gt;rendererID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rootFiberID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;stringTableSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...strings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...opcodes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Opcode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(ADD):&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;parentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ownerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nameStringIdx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keyIdx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Opcode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(REMOVE):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Opcode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(REORDER_CHILDREN):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...childIds&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 parser walks this array to reconstruct the fiber name map, parent-child relationships, and unmount counts — all without a React runtime, just integer arithmetic and string table lookups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two name resolution strategies&lt;/strong&gt;, with fallback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Primary: &lt;code&gt;snapshots&lt;/code&gt; map (human-readable, always preferred)&lt;/li&gt;
&lt;li&gt;Fallback: decode the string table from &lt;code&gt;operations&lt;/code&gt; opcodes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This dual strategy is what makes the parser robust against different DevTools export configurations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spurious render detection&lt;/strong&gt; relies on one non-obvious React behavior: when a component re-renders due to an unstable prop reference but no prop &lt;em&gt;values&lt;/em&gt; actually changed, React records &lt;code&gt;props: []&lt;/code&gt; (empty array) in &lt;code&gt;changeDescriptions&lt;/code&gt;. &lt;code&gt;props: null&lt;/code&gt; means unknown. &lt;code&gt;props: ["value"]&lt;/code&gt; means the &lt;code&gt;value&lt;/code&gt; prop genuinely changed. The parser uses exactly this signal:&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;function&lt;/span&gt; &lt;span class="nf"&gt;isSpurious&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fiberID&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="nx"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProfileCommit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeDescriptions&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fiberID&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;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isFirstMount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;didHooksChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;React 18 concurrent mode false-positive prevention&lt;/strong&gt;: &lt;code&gt;startTransition&lt;/code&gt; and &lt;code&gt;useDeferredValue&lt;/code&gt; can cause a component to render multiple times as React speculatively renders and discards incomplete trees. These show up with &lt;code&gt;priorityLevel: "Low Priority"&lt;/code&gt; or &lt;code&gt;"Idle"&lt;/code&gt;. The parser tracks these separately so they don't get flagged as regressions.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 2: the Invalidation Index
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;analyze_compiler_efficacy&lt;/code&gt; computes a score per component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I = (spurious_count / total_renders) × wasted_ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because raw &lt;code&gt;wasted_ms&lt;/code&gt; alone can be misleading. A component with 100ms wasted across 100 renders (1ms each) has a very different profile than one with 100ms across 2 renders (50ms each). The index captures both the frequency and the cost — which is what determines whether &lt;code&gt;React.memo&lt;/code&gt; overhead is actually worth it.&lt;/p&gt;

&lt;p&gt;The threshold for &lt;code&gt;suggest_memoization&lt;/code&gt; is &lt;code&gt;avgSelfMs &amp;gt; 2ms&lt;/code&gt;. Below that, the &lt;code&gt;Object.is&lt;/code&gt; comparison overhead of &lt;code&gt;React.memo&lt;/code&gt; can exceed the render cost, making memoization actively harmful.&lt;/p&gt;




&lt;h3&gt;
  
  
  Layer 3: the AST remediation engine
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ASTPerformanceRemediator&lt;/code&gt; uses &lt;code&gt;ts-morph&lt;/code&gt; (TypeScript compiler API wrapper) to perform three passes over the source file:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 1 — static hoisting.&lt;/strong&gt; Walks the render body looking for &lt;code&gt;VariableStatement&lt;/code&gt; nodes whose initializers are &lt;code&gt;ObjectLiteralExpression&lt;/code&gt; or &lt;code&gt;ArrayLiteralExpression&lt;/code&gt;. For each one, checks if any &lt;code&gt;Identifier&lt;/code&gt; inside references a component-scope binding (props, state, hooks). If not — it's static and gets hoisted to module scope. This runs in a loop until no more hoistable statements are found, handling multiple declarations per component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 2 — &lt;code&gt;useCallback&lt;/code&gt; wrapping.&lt;/strong&gt; Walks &lt;code&gt;ArrowFunction&lt;/code&gt; nodes inside the render body. For each one matching an unstable prop name, calls &lt;code&gt;resolveReactiveDependencies&lt;/code&gt; — which walks all &lt;code&gt;Identifier&lt;/code&gt; descendants of the arrow function and intersects them with the component's local scope bindings. This gives the dependency array automatically, without requiring the developer to reason about it manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 3 — &lt;code&gt;React.memo&lt;/code&gt; wrapping.&lt;/strong&gt; Checks if ROI score exceeds 1.5, then finds the default export assignment and rewrites it. The import check (the edge case from slash-admin) now runs first: if the file only has &lt;code&gt;import type ... from "react"&lt;/code&gt;, it rewrites it to a value import before applying the wrapper.&lt;/p&gt;

&lt;p&gt;All three passes operate on the live AST and call &lt;code&gt;saveSync()&lt;/code&gt; once at the end — no intermediate file writes, no risk of partial state.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why no React runtime dependency
&lt;/h3&gt;

&lt;p&gt;Everything described above — opcode decoding, name resolution, spurious render detection, cascade tracing — runs on pure JSON and TypeScript. No React, no DevTools, no browser. This is intentional: the server needs to be fast to start (it runs as an &lt;code&gt;npx&lt;/code&gt; command on every MCP client connection) and safe to run in any environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&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;"mcpServers"&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;"react-render-profile"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-render-profile-mcp"&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="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;Export a profile: React DevTools → Profiler tab → Record → interact → Stop → Save icon (💾). Pass the &lt;code&gt;.json&lt;/code&gt; path as &lt;code&gt;profile_path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/vola-trebla/react-render-profile-mcp" rel="noopener noreferrer"&gt;vola-trebla/react-render-profile-mcp&lt;/a&gt;&lt;br&gt;
npm: &lt;code&gt;npx react-render-profile-mcp&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;Questions about the opcode decoder or the dependency inference logic welcome. 🐸&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>react-render-profile-mcp v0.3.1 - 4 new diagnostic tools for React Compiler, hydration, Zustand, and state cascades</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Fri, 22 May 2026 12:14:59 +0000</pubDate>
      <link>https://dev.to/vola-trebla/react-render-profile-mcp-v031-4-new-diagnostic-tools-for-react-compiler-hydration-zustand-33ld</link>
      <guid>https://dev.to/vola-trebla/react-render-profile-mcp-v031-4-new-diagnostic-tools-for-react-compiler-hydration-zustand-33ld</guid>
      <description>&lt;p&gt;A few weeks ago I published a post about &lt;a href="https://dev.to/vola-trebla/your-ai-agent-just-broke-your-react-performance-it-has-no-idea-4ghn"&gt;react-render-profile-mcp&lt;/a&gt; — an MCP server that decodes React DevTools Profiler exports so AI agents can actually diagnose render performance instead of guessing at raw fiber IDs.&lt;/p&gt;

&lt;p&gt;v0.1 shipped with 5 tools. v0.3.1 adds 4 more, each targeting a class of problems the original couldn't touch. Here's what's new. 🐸&lt;/p&gt;




&lt;h2&gt;
  
  
  What was already there (v0.1)
&lt;/h2&gt;

&lt;p&gt;Quick recap for anyone who missed the first post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_render_summary&lt;/code&gt; — total commits, render time, top components, lifecycle anomaly flags&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;find_spurious_renders&lt;/code&gt; — components that re-rendered with no actual prop/state change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_hottest_components&lt;/code&gt; — ranked by CPU self-time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trace_render_cascade&lt;/code&gt; — what triggered a commit and what re-rendered as a result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suggest_memoization&lt;/code&gt; — ROI-scored &lt;code&gt;React.memo&lt;/code&gt; recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These covered the "something is rendering too much" class of problems. What they couldn't tell you: &lt;em&gt;why&lt;/em&gt; your React Compiler isn't helping, &lt;em&gt;where&lt;/em&gt; hydration broke, &lt;em&gt;which&lt;/em&gt; Zustand selector is in a loop, or &lt;em&gt;how deep&lt;/em&gt; a context update actually propagates.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's new in v0.3.1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;analyze_compiler_efficacy&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;React Compiler (React 19) and manual &lt;code&gt;React.memo&lt;/code&gt; should eliminate spurious renders. Often they don't — because of inline object allocations or unstable parent refs that bypass memoization entirely.&lt;/p&gt;

&lt;p&gt;This tool computes an &lt;strong&gt;Invalidation Index&lt;/strong&gt; per component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I = (spurious_count / total_count) × wasted_ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;High index = memoization is present but not working. The tool tells you exactly why:&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;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"component_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProductList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ineffective_render_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wasted_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;84.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger_cause"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UNSTABLE_PARENT_PROP_REFERENCE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Memoize parent props with useMemo/useCallback or hoist static objects out of the parent render function."&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;Without this, your agent might suggest adding &lt;code&gt;React.memo&lt;/code&gt; to a component that already has it — and has it for a reason that makes it useless.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. &lt;code&gt;diagnose_hydration_and_suspense&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Two separate problems, one tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hydration mismatches&lt;/strong&gt; — when React discards server HTML and remounts the entire tree from scratch. Shows up as an abnormally long initial mount with a spike of unmounts immediately after. The tool flags these as &lt;code&gt;HYDRATION_MISMATCH_RECOVERY&lt;/code&gt; with the affected Suspense boundaries and blocking duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspense waterfalls&lt;/strong&gt; — nested boundaries fetching sequentially instead of in parallel. Detected by measuring the gap between consecutive Suspense resolves against a configurable &lt;code&gt;waterfall_threshold_ms&lt;/code&gt; (default: 100ms).&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;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WARNING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anomaly_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NESTED_MOUNT_FETCH_WATERFALL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"root_component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProfileDetails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocking_duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;120.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Prefetch data at the parent level or use Promise.all."&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;h3&gt;
  
  
  3. &lt;code&gt;evaluate_external_store_performance&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Zustand and Redux with &lt;code&gt;useSyncExternalStore&lt;/code&gt; have two failure modes that are hard to catch manually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unstable selector object allocation&lt;/strong&gt; — a selector returns a new object reference every call, triggering rapid consecutive renders. The tool detects components that render multiple times in consecutive frames and flags the selector as the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync concurrency bypass&lt;/strong&gt; — a heavy store update runs synchronously in a high-priority lane, blocking the main thread. Should be in &lt;code&gt;startTransition&lt;/code&gt; instead.&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;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"impacted_components"&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;"CartSummary"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_infinite_loop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger_cause"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UNSTABLE_SELECTOR_OBJECT_ALLOCATION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrap the selector in useCallback or return primitive values."&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;h3&gt;
  
  
  4. &lt;code&gt;trace_state_cascade_footprint&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Given a commit index, this reconstructs the virtual component tree and measures how far an update actually propagated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which component triggered the update&lt;/li&gt;
&lt;li&gt;Whether it went through a context provider or a store subscriber&lt;/li&gt;
&lt;li&gt;How many levels deep it reached&lt;/li&gt;
&lt;li&gt;How many consumer components re-rendered
&lt;/li&gt;
&lt;/ul&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;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH_FOOTPRINT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"update_trigger_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ThemeButton"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propagation_channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONTEXT_PROVIDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cascade_render_depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rendered_consumer_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Split the context provider or memoize its value and children."&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;This is the tool that answers "why did 28 components re-render when I clicked one button."&lt;/p&gt;




&lt;h2&gt;
  
  
  Updated: &lt;code&gt;find_spurious_renders&lt;/code&gt; trigger classification
&lt;/h2&gt;

&lt;p&gt;The existing tool now classifies &lt;em&gt;why&lt;/em&gt; a render was spurious, not just &lt;em&gt;that&lt;/em&gt; it was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UNSTABLE_PARENT_REF&lt;/code&gt; — parent passed a new object/array/function reference with identical values&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONTEXT_UPDATE&lt;/code&gt; — context changed, but this component doesn't actually use the changed value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INTENTIONAL_CONCURRENT_YIELD&lt;/code&gt; — React's scheduler, not a bug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because &lt;code&gt;React.memo&lt;/code&gt; fixes the first case but can't help with the second. The recommendation now reflects the correct fix path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended agent workflow (updated)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. get_render_summary              → overview + lifecycle_anomaly flags
2. find_spurious_renders           → classify unnecessary renders by trigger type
3. analyze_compiler_efficacy       → check where React.memo / Compiler is being bypassed
4. diagnose_hydration_and_suspense → catch hydration recovery + Suspense waterfalls
5. evaluate_external_store_performance → Zustand/Redux selector loops + sync bypasses
6. trace_state_cascade_footprint   → propagation depth for expensive commits
7. suggest_memoization             → ROI-scored final recommendations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setup (unchanged)
&lt;/h2&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;"mcpServers"&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;"react-render-profile"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-render-profile-mcp"&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="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;Export a profile from React DevTools → Profiler tab → Record → Save. Pass the &lt;code&gt;.json&lt;/code&gt; path as &lt;code&gt;profile_path&lt;/code&gt; to any tool.&lt;/p&gt;

&lt;p&gt;npm: &lt;a href="https://www.npmjs.com/package/react-render-profile-mcp" rel="noopener noreferrer"&gt;react-render-profile-mcp&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/vola-trebla/react-render-profile-mcp" rel="noopener noreferrer"&gt;vola-trebla/react-render-profile-mcp&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Happy to answer questions about the hydration detection heuristics or the Invalidation Index math. And if you're hitting a React performance pattern this doesn't cover yet — open an issue. 🐸&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>react</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your AI agent just read your .env file. You have no idea what it did next.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sun, 17 May 2026 19:10:59 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ai-agent-just-read-your-env-file-you-have-no-idea-what-it-did-next-58d1</link>
      <guid>https://dev.to/vola-trebla/your-ai-agent-just-read-your-env-file-you-have-no-idea-what-it-did-next-58d1</guid>
      <description>&lt;p&gt;AI agents are helpful, not malicious. That's what makes them dangerous around secrets. Here's an MCP server that catches secret exposure before the agent gets there.&lt;/p&gt;

&lt;p&gt;Here's a scene that's more common than anyone admits.&lt;/p&gt;

&lt;p&gt;You're debugging a config issue. You ask your AI agent to look at &lt;code&gt;src/config.ts&lt;/code&gt;. The file has this:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&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;DB_PASSWORD&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;secret&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;JWT_SECRET&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Added during debugging last Tuesday, never removed&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Config loaded:&lt;/span&gt;&lt;span class="dl"&gt;"&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;config&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;log&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;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads the file, executes the tool, and your AWS secret key is now sitting in its context window. It might summarize it. It might include it in generated code. It might pass it to another tool that logs everything.&lt;/p&gt;

&lt;p&gt;None of this requires the agent to be malicious. It just needs to be helpful.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;env-secret-exposure-analyzer-mcp&lt;/code&gt; catches this before it happens. 🔐&lt;/p&gt;




&lt;h2&gt;
  
  
  🙈 The three ways secrets leak
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Hardcoded in source files
&lt;/h3&gt;

&lt;p&gt;The classic. Someone puts a token directly in code "just to test" and commits it. Or copies a &lt;code&gt;.env&lt;/code&gt; value into a constant because it's easier. Or the most common one: a connection string with the password embedded right there in the URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;postgres&lt;/span&gt;://&lt;span class="n"&gt;admin&lt;/span&gt;:&lt;span class="n"&gt;p&lt;/span&gt;@&lt;span class="n"&gt;ssw0rd123&lt;/span&gt;@&lt;span class="n"&gt;prod&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt;.&lt;span class="n"&gt;internal&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent reading any file that imports this config now has your prod database password.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;.env&lt;/code&gt; not in &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; has 40 keys. AWS credentials, Stripe keys, JWT secrets, OAuth tokens, encryption keys. It's not in &lt;code&gt;.gitignore&lt;/code&gt;. One &lt;code&gt;git push&lt;/code&gt; and it's in the repository forever — even if you delete it, it stays in git history.&lt;/p&gt;

&lt;p&gt;The agent doesn't know this is dangerous. It reads files. That's its job.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;console.log&lt;/code&gt; that never got removed
&lt;/h3&gt;

&lt;p&gt;The debug line that gets committed on Friday and nobody notices until Monday. Except by then, every time the app starts, it dumps credentials to stdout. If you have log aggregation, those secrets are now in your observability platform too.&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="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Starting server with config:&lt;/span&gt;&lt;span class="dl"&gt;"&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;config&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// config contains { db: { password: "..." }, jwt: { secret: "..." } }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔧 How the scanner works
&lt;/h2&gt;

&lt;p&gt;Three tools, three attack surfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;scan_for_secrets&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Scans source files, config files, and &lt;code&gt;.env&lt;/code&gt; files for 20+ patterns. Returns severity, file, line, and a &lt;strong&gt;masked&lt;/strong&gt; preview — the scanner never returns the full secret value, even in its own output.&lt;/p&gt;

&lt;p&gt;After building the initial version with the obvious patterns (AWS, GitHub, Stripe), we did something simple: created a realistic &lt;code&gt;.env&lt;/code&gt; with everything a real project might have and ran the scanner against it.&lt;/p&gt;

&lt;p&gt;First run: &lt;strong&gt;3 findings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's not good enough. A real &lt;code&gt;.env&lt;/code&gt; has database URLs, JWT secrets, SendGrid keys, Twilio tokens, Google OAuth secrets, Sentry DSNs, webhook secrets, encryption keys. We were missing most of it.&lt;/p&gt;

&lt;p&gt;We fixed the patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database URLs with embedded credentials — had to handle passwords containing &lt;code&gt;@&lt;/code&gt; (the character that separates credentials from host)&lt;/li&gt;
&lt;li&gt;Stripe webhook secrets (&lt;code&gt;whsec_&lt;/code&gt;), Google OAuth (&lt;code&gt;GOCSPX-&lt;/code&gt;), Sentry DSN with flexible key length&lt;/li&gt;
&lt;li&gt;Generic patterns for JWT secrets, session secrets, encryption keys — with a &lt;code&gt;(?!process\.env)&lt;/code&gt; lookahead to avoid false positives on &lt;code&gt;password: process.env.X&lt;/code&gt; which is actually correct code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After fixes: &lt;strong&gt;16 findings&lt;/strong&gt;. Everything in the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;check_gitignore_coverage&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Walks the project root and checks if sensitive files (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;secrets.json&lt;/code&gt;, private keys, certificates) are covered by &lt;code&gt;.gitignore&lt;/code&gt;. Correctly ignores &lt;code&gt;.env.example&lt;/code&gt; and &lt;code&gt;.env.sample&lt;/code&gt; — those are supposed to be committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;scan_for_log_leaks&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Scans for &lt;code&gt;console.log&lt;/code&gt; / &lt;code&gt;logger&lt;/code&gt; calls that print &lt;code&gt;process.env&lt;/code&gt; variables or objects with secret-sounding names. Catches both direct leaks:&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="nx"&gt;console&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;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;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// HIGH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And indirect ones:&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="nx"&gt;console&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Config:&lt;/span&gt;&lt;span class="dl"&gt;"&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;config&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// HIGH — config contains env vars&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;env:&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="c1"&gt;// CRITICAL — dumps everything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🐸 The most ironic moment of this build
&lt;/h2&gt;

&lt;p&gt;When writing the tests, I created fixture files with fake-but-realistic secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt;=&lt;span class="n"&gt;ghp_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&lt;/span&gt;
&lt;span class="n"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;=&lt;span class="n"&gt;AKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub push protection blocked the push.&lt;/p&gt;

&lt;p&gt;A secret scanner — blocked by GitHub's secret scanner — because its own test fixtures looked too much like real secrets. Even the obviously fake ones. &lt;code&gt;ghp_&lt;/code&gt; followed by 40 A's is still &lt;code&gt;ghp_&lt;/code&gt; followed by 40 alphanumeric characters, which is exactly the GitHub token format.&lt;/p&gt;

&lt;p&gt;The fix: create fixture files programmatically in &lt;code&gt;os.tmpdir()&lt;/code&gt; at test time. They never touch the git repo. The tests pass, the push goes through, and somewhere in the universe a security engineer nods approvingly.&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="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;leakyDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdtempSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tmpdir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;env-mcp-leaky-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leakyDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.env&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="s2"&gt;`GITHUB_TOKEN=ghp_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&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="s2"&gt;`AWS_ACCESS_KEY_ID=AKIA&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leakyDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;h2&gt;
  
  
  ✅ Does it actually work?
&lt;/h2&gt;

&lt;p&gt;After publishing, I verified the full MCP protocol layer — not just the functions in isolation, but the actual stdio transport that Claude Desktop uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize",...}\n
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"scan_for_secrets",...}}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | node dist/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&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;"result"&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;"content"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Secret Scan Results&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  Files scanned: 9&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  Findings: 16&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;  [CRITICAL] .env:7 — Database URL with password&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  [CRITICAL] .env:17 — AWS Access Key&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;Error handling too — bad path returns &lt;code&gt;"isError": true&lt;/code&gt; with a readable message, doesn't crash the server.&lt;/p&gt;

&lt;p&gt;For a security tool, "it compiles" is not good enough. The protocol has to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Setup
&lt;/h2&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;"mcpServers"&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;"secret-scanner"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"env-secret-exposure-analyzer-mcp"&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="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;Then ask your agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Scan this project for exposed secrets, check if .env is in .gitignore, and find any console.log calls that might be leaking credentials."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🐸 The pattern
&lt;/h2&gt;

&lt;p&gt;Every tool in this series exists because an AI agent hits a wall — something it structurally cannot know without the right tool.&lt;/p&gt;

&lt;p&gt;With secrets, the wall is subtle. The agent doesn't know what's sensitive just by looking. It doesn't know &lt;code&gt;p@ssw0rd123&lt;/code&gt; in a connection string is a production password. It doesn't know that &lt;code&gt;JWT_SECRET=abc123verysecretkey&lt;/code&gt; in a &lt;code&gt;.env&lt;/code&gt; file means something if that file gets committed.&lt;/p&gt;

&lt;p&gt;The scanner knows. It has the patterns, the gitignore logic, the log analysis. It tells the agent exactly what's dangerous before the agent gets near it.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/env-secret-exposure-analyzer-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/env-secret-exposure-analyzer-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/env-secret-exposure-analyzer-mcp" rel="noopener noreferrer"&gt;github.com/vola-trebla/env-secret-exposure-analyzer-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>mcp</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Your AI agent reads tsconfig.json. It has absolutely no idea what it means</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sun, 17 May 2026 18:43:30 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ai-agent-reads-tsconfigjson-it-has-absolutely-no-idea-what-it-means-5ge9</link>
      <guid>https://dev.to/vola-trebla/your-ai-agent-reads-tsconfigjson-it-has-absolutely-no-idea-what-it-means-5ge9</guid>
      <description>&lt;p&gt;Your agent sees "extends": "@tsconfig/strictest" and hallucinates the rest. Here's an MCP server that uses the TypeScript compiler API to resolve the full inheritance chain and return what actually applies.&lt;/p&gt;

&lt;p&gt;Here's a scene that happens more than you'd think.&lt;/p&gt;

&lt;p&gt;You ask your AI agent to help with a TypeScript error. It reads your &lt;code&gt;tsconfig.json&lt;/code&gt;, sees 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;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@tsconfig/strictest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&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;"paths"&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;"@/*"&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;"./src/*"&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="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;And confidently suggests:&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;// "This should work fine — I don't see strict mode enabled"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUsers&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;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ❌ Object is possibly 'undefined'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's wrong. &lt;code&gt;@tsconfig/strictest&lt;/code&gt; sets &lt;code&gt;noUncheckedIndexedAccess: true&lt;/code&gt;. &lt;code&gt;users[0]&lt;/code&gt; is &lt;code&gt;User | undefined&lt;/code&gt;, not &lt;code&gt;User&lt;/code&gt;. The agent doesn't know this because it never looked inside &lt;code&gt;@tsconfig/strictest&lt;/code&gt;. It just guessed.&lt;/p&gt;

&lt;p&gt;This is &lt;code&gt;tsconfig-inheritance-flattener-mcp&lt;/code&gt;. 🔍&lt;/p&gt;




&lt;h2&gt;
  
  
  🙈 What the agent actually sees
&lt;/h2&gt;

&lt;p&gt;When your agent reads &lt;code&gt;tsconfig.json&lt;/code&gt;, it reads exactly what's in the file. Nothing more.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;extends&lt;/code&gt; field points to a package or another file — and the agent stops there. It doesn't chase the chain. It doesn't know what that base config sets. It fills in the blanks from training data, and training data is not your project.&lt;/p&gt;

&lt;p&gt;In a typical monorepo this chain can be 3 levels deep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web/tsconfig.json
  → tsconfig.base.json
    → node_modules/@tsconfig/strictest/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt;, &lt;code&gt;moduleResolution&lt;/code&gt;, &lt;code&gt;strict&lt;/code&gt;, &lt;code&gt;paths&lt;/code&gt;, &lt;code&gt;baseUrl&lt;/code&gt; — all of it is scattered across these files. Without resolving the full chain, the agent is flying blind.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 What the TypeScript compiler API actually knows
&lt;/h2&gt;

&lt;p&gt;Here's the thing: TypeScript already solves this problem. Every time &lt;code&gt;tsc&lt;/code&gt; runs, it resolves the full chain and produces a single merged set of compiler options. The API is right there:&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readConfigFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseJsonConfigFileContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;configPath&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// parsed.options = fully merged CompilerOptions ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're not reimplementing anything. We're just exposing what TypeScript already computes — via MCP, so your agent can ask.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Three tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_effective_compiler_options&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Resolves the full &lt;code&gt;extends&lt;/code&gt; chain and returns the merged options that actually apply. Enums come back as readable strings, not magic numbers (&lt;code&gt;"ES2022"&lt;/code&gt; not &lt;code&gt;9&lt;/code&gt;, &lt;code&gt;"NodeNext"&lt;/code&gt; not &lt;code&gt;199&lt;/code&gt;).&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="err"&gt;Effective&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;TypeScript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Configuration&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Config:&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;/project/apps/web/tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Inheritance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;chain:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/project/apps/web/tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
                     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/project/tsconfig.base.json&lt;/span&gt;&lt;span class="w"&gt;
                     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;node_modules/@tsconfig/strictest/tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Compiler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(merged):&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;target:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;module:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;moduleResolution:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;strict:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;noUncheckedIndexedAccess:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;exactOptionalPropertyTypes:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;baseUrl:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/project"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;paths:&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;"@/*"&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;"apps/web/src/*"&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;Now the agent knows why &lt;code&gt;users[0]&lt;/code&gt; is &lt;code&gt;User | undefined&lt;/code&gt;. No guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;resolve_module_alias&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Maps &lt;code&gt;@/hooks/useAuth&lt;/code&gt; to the physical file on disk. Uses the resolved &lt;code&gt;paths&lt;/code&gt; and &lt;code&gt;baseUrl&lt;/code&gt; from the full inheritance chain — not just what's in the nearest &lt;code&gt;tsconfig.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alias Resolution: @/hooks/useAuth
  Config:   /project/apps/web/tsconfig.json
  Base URL: /project

Resolved physical paths:
  /project/apps/web/src/hooks/useAuth.ts   ✓ exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent needs to navigate to the file behind an import, it no longer has to guess the folder structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;analyze_project_references&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Validates the &lt;code&gt;references&lt;/code&gt; array in monorepo root configs. Checks that each referenced package has &lt;code&gt;composite: true&lt;/code&gt; — without it, TypeScript's incremental build silently breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project References Analysis
  Config: /project/tsconfig.json
  References found: 2

  [✓] packages/shared → /project/packages/shared/tsconfig.json
  [✗] packages/legacy → /project/packages/legacy/tsconfig.json (NOT FOUND)

Violations:
  ✗ packages/shared is referenced but does not have composite: true
    Fix: add "composite": true to packages/shared/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ Setup
&lt;/h2&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;"mcpServers"&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;"tsconfig-flattener"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsconfig-inheritance-flattener-mcp"&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="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;That's it. Works in Claude Desktop, Cursor, or any MCP-compatible client.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐸 The pattern
&lt;/h2&gt;

&lt;p&gt;Every MCP server in this series follows the same logic: find a place where an AI agent is structurally blind — not because it's dumb, but because it literally cannot see the data — and expose that data via a tool.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tsconfig.json&lt;/code&gt; inheritance is a perfect example. The agent isn't hallucinating out of laziness. It's hallucinating because the information it needs is locked inside a chain of files it never opened, or inside a npm package it can't inspect at runtime.&lt;/p&gt;

&lt;p&gt;The TypeScript compiler API already resolves all of this. We just asked it nicely. 🔍&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/tsconfig-inheritance-flattener-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/tsconfig-inheritance-flattener-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/tsconfig-inheritance-flattener-mcp" rel="noopener noreferrer"&gt;github.com/vola-trebla/tsconfig-inheritance-flattener-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>mcp</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Your CI Is Always Broken. Your AI Agent Has No Idea What to Do About It.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 21:05:58 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ci-is-always-broken-your-ai-agent-has-no-idea-what-to-do-about-it-p2j</link>
      <guid>https://dev.to/vola-trebla/your-ci-is-always-broken-your-ai-agent-has-no-idea-what-to-do-about-it-p2j</guid>
      <description>&lt;p&gt;In any real codebase, CI always has something failing. The hard part isn't finding failures — it's knowing which ones block a release. Here's an MCP server that answers that question automatically.&lt;/p&gt;

&lt;p&gt;Here's the situation every engineer knows:&lt;/p&gt;

&lt;p&gt;You open CI. Something's failing. You need to ship.&lt;/p&gt;

&lt;p&gt;Is it a real regression? A known flaky test? An infra blip that'll pass on retry? You open the logs, grep for errors, cross-reference with last week's run history, check what files changed in the PR, and 20 minutes later you have an answer.&lt;/p&gt;

&lt;p&gt;Your AI agent can't do any of that. It sees the same raw logs you do. It doesn't know your flakiness history. It doesn't know which tests are affected by the code change. It guesses.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;release-readiness-triage-mcp&lt;/code&gt; fixes this. 🚦&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The three signals that actually matter
&lt;/h2&gt;

&lt;p&gt;Triaging a CI failure requires correlating three things simultaneously:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Error signature deduplication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If 40 tests failed with &lt;code&gt;ECONNREFUSED 127.0.0.1:5432&lt;/code&gt;, that's one problem (database didn't start), not 40. Grouping by normalized error signature tells you the real shape of the failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Flakiness history&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some tests fail 70% of the time on a good day. If a test has a 0.73 flaky probability in your history, its failure today tells you nothing about the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Code-change correlation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Button.test.tsx&lt;/code&gt; is failing and &lt;code&gt;Button.tsx&lt;/code&gt; is in the diff, that's suspicious. If &lt;code&gt;AuthFlow.test.tsx&lt;/code&gt; is failing and nothing in auth changed, that's noise.&lt;/p&gt;

&lt;p&gt;Without all three signals in one place, you can't answer "is this safe to release?" You just accumulate tabs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The 4 tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;aggregate_suite_failures&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;First pass: normalize, deduplicate, categorize.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI Run Summary
  Total tests:   847
  Passed:        842
  Failed:        5
  Failure rate:  0.59%
  Error groups:  3

Failure Groups (by frequency):
  [NETWORK] 2x — connect ECONNREFUSED 127.0.0.1:Xms
    • API Suite &amp;gt; health check
    • API Suite &amp;gt; readiness probe
  [ASSERTION] 2x — expect(received).toBe(expected)
    • Search Suite &amp;gt; debounce timing
    • Search Suite &amp;gt; sort order
  [ASSERTION] 1x — Expected null, got &amp;lt;button&amp;gt;Submit&amp;lt;/button&amp;gt;
    • Button Suite &amp;gt; renders button correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supports &lt;code&gt;customInfraPatterns&lt;/code&gt; — pass cloud-specific strings like &lt;code&gt;"GCP quota exceeded"&lt;/code&gt; or &lt;code&gt;"No space left on device"&lt;/code&gt; to classify them as infrastructure noise instead of unknown failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cross_reference_flakiness&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Takes your flakiness database and scores each failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flakiness Cross-Reference

  [KNOWN FLAKY] Auth Suite &amp;gt; login with expired token
    Flaky probability: 73%
  [MILDLY FLAKY] Search Suite &amp;gt; debounce timing
    Flaky probability: 22%
  [NO HISTORY] Button Suite &amp;gt; renders button correctly
    Not found in flakiness database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;correlate_code_changes&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Matches changed files against failing tests. Works standalone or with pre-computed affected test lists from &lt;a href="https://www.npmjs.com/package/ast-impact-mapper-mcp" rel="noopener noreferrer"&gt;ast-impact-mapper-mcp&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code Change Correlation
  Changed files: 2
  Pre-identified affected tests: 1

  [CORRELATED] Button Suite &amp;gt; renders button correctly
    → Matched via affected test list
  [NOT CORRELATED] Search Suite &amp;gt; debounce timing
  [NOT CORRELATED] Auth Suite &amp;gt; login with expired token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;generate_release_recommendation&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The final step. Everything combined into one verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 🔴 Release Recommendation: NO_GO (75% confidence)&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; 1 confirmed regression(s) directly correlated with code changes. Do not release.&lt;/span&gt;

| Category            | Count |
|---|---|
| Total failures      | 5     |
| 🔴 Real regressions | 1     |
| 🟡 Known flaky      | 2     |
| ⚪ Infra blips      | 2     |
| ❓ Unknown          | 0     |

&lt;span class="gu"&gt;### 🔴 Blockers (must fix before release)&lt;/span&gt;

&lt;span class="gs"&gt;**Button Suite &amp;gt; renders button correctly**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Test is directly affected by code changes in this commit
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`Expected null, got &amp;lt;button&amp;gt;Submit&amp;lt;/button&amp;gt;`&lt;/span&gt;

&lt;span class="gu"&gt;### ✅ Safe to ignore&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ~~Auth Suite &amp;gt; login with expired token~~ — Historically flaky: 73% failure rate in history
&lt;span class="p"&gt;-&lt;/span&gt; ~~API Suite &amp;gt; health check~~ — Error pattern matches infrastructure issues (network)
&lt;span class="p"&gt;-&lt;/span&gt; ~~Search Suite &amp;gt; debounce timing~~ — Mildly flaky: 22% historical failure rate
&lt;span class="p"&gt;-&lt;/span&gt; ~~Storage Suite &amp;gt; upload avatar~~ — Error pattern matches infrastructure issues (network)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;format: "markdown"&lt;/code&gt; and the output is ready to paste directly into a GitHub PR comment or Slack message.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 It's a meta-orchestrator
&lt;/h2&gt;

&lt;p&gt;This MCP is designed to sit on top of the other tools in the ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/flakiness-knowledge-graph-mcp" rel="noopener noreferrer"&gt;flakiness-knowledge-graph-mcp&lt;/a&gt;&lt;/strong&gt; builds the flakiness database from run history — feed its output into &lt;code&gt;cross_reference_flakiness&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/ast-impact-mapper-mcp" rel="noopener noreferrer"&gt;ast-impact-mapper-mcp&lt;/a&gt;&lt;/strong&gt; computes which tests are affected by a code change via TypeScript AST — feed its output into &lt;code&gt;correlate_code_changes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/playwright-trace-decoder-mcp" rel="noopener noreferrer"&gt;playwright-trace-decoder-mcp&lt;/a&gt;&lt;/strong&gt; decodes trace files for individual failure root-cause — use it after getting a &lt;code&gt;NO_GO&lt;/code&gt; to understand the blocker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent orchestrates the chain. Each MCP handles one thing it couldn't do without tool access.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Setup
&lt;/h2&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;"mcpServers"&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;"release-readiness-triage"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-readiness-triage-mcp"&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="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;Then just ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Here are the failures from CI, our flakiness history, and the files changed in this PR. Is it safe to release?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One answer. No log reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/release-readiness-triage-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/release-readiness-triage-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/release-readiness-triage-mcp" rel="noopener noreferrer"&gt;github.com/vola-trebla/release-readiness-triage-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx release-readiness-triage-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ci</category>
      <category>mcp</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI Agent Just Broke Your React Performance. It Has No Idea</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 20:27:07 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ai-agent-just-broke-your-react-performance-it-has-no-idea-4ghn</link>
      <guid>https://dev.to/vola-trebla/your-ai-agent-just-broke-your-react-performance-it-has-no-idea-4ghn</guid>
      <description>&lt;p&gt;React DevTools Profiler gives you all the data you need to fix re-renders. Your AI agent can't read it. Here's how to fix that with an MCP server.&lt;/p&gt;

&lt;p&gt;You open React DevTools Profiler, record a session, and export the &lt;code&gt;.json&lt;/code&gt; file. It's got everything: which components re-rendered, how long each one took, which props changed, which hooks fired.&lt;/p&gt;

&lt;p&gt;Then you paste it into Claude or Cursor and ask: &lt;em&gt;"What's causing the performance problem?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent stares at raw JSON — thousands of lines of fiber IDs, opcode arrays, and microsecond timestamps — and does its best. But it's reading tea leaves. It doesn't know what a "spurious render" is in this format. It can't decode the operations integer array. It doesn't know that &lt;code&gt;props: []&lt;/code&gt; means a component re-rendered even though no props actually changed.&lt;/p&gt;

&lt;p&gt;That's the gap &lt;code&gt;react-render-profile-mcp&lt;/code&gt; fills. 🔍&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What the profiler actually exports
&lt;/h2&gt;

&lt;p&gt;React DevTools serializes profiler data in a specific binary-ish format that most developers have never had to parse manually:&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataForRoots"&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;"commitData"&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;"changeDescriptions"&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="mi"&gt;3&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="nl"&gt;"isFirstMount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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="mi"&gt;4&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="err"&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;"fiberActualDurations"&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.2&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fiberSelfDurations"&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.9&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.9&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&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;"snapshots"&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="mi"&gt;3&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="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"children"&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]}]],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operations"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;112&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;112&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;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;A few non-obvious things buried in there:&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;changeDescriptions&lt;/code&gt; is an array of pairs, not an object.&lt;/strong&gt; It's serialized as &lt;code&gt;Map.entries()&lt;/code&gt; — &lt;code&gt;[[fiberID, desc], ...]&lt;/code&gt;. If you parse it naively as JSON object keys, you'll get garbage.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;props: []&lt;/code&gt; means spurious render.&lt;/strong&gt; An empty array means the component re-rendered but zero prop keys actually changed — the reference was unstable. &lt;code&gt;props: null&lt;/code&gt; means unknown. &lt;code&gt;props: ["value"]&lt;/code&gt; means the &lt;code&gt;value&lt;/code&gt; prop genuinely changed.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;operations&lt;/code&gt; is an opcode array.&lt;/strong&gt; It encodes tree mutations (mount, unmount, reorder) with a string table at the start. You need to decode it to map fiber IDs to component names when snapshots aren't present.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;fiberSelfDuration&lt;/code&gt; ≠ &lt;code&gt;fiberActualDuration&lt;/code&gt;.&lt;/strong&gt; Self = time in this component only. Actual = self + children. For finding hotspots, you want self time.&lt;/p&gt;

&lt;p&gt;The MCP server handles all of this so the agent doesn't have to.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The 5 tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_render_summary&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;High-level overview of the entire recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total commits: 12
Total render time: 847.3ms
Spurious renders: 8
Top components by self time:
  ProductList — 312.4ms (6 renders)
  SearchInput — 89.1ms (12 renders)
  Sidebar — 44.2ms (3 renders)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;find_spurious_renders&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Detects components that re-rendered but had no actual prop or state changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spurious renders found:
  ProductList — 6 spurious renders, 312.4ms wasted
    → props ref changed but no keys differed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the classic "missing React.memo" pattern. The component re-renders every time a parent renders, even though nothing it depends on changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_hottest_components&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Components ranked by total self time across all commits. Useful for finding where to optimize first.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;trace_render_cascade&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Given a commit index, shows what triggered it and what else re-rendered as a result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Commit 3 — triggered by: SearchInput (hook changed)
Cascade:
  ProductList — re-rendered (unstable props)
  Sidebar — re-rendered (context changed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how you find the root cause instead of just the symptom.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;suggest_memoization&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Combines spurious render detection with self-time data to generate specific recommendations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Suggestions:
  ProductList → React.memo
    Reason: 6 spurious renders, 312.4ms wasted
    Self time: 52.1ms avg per render
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ Setup
&lt;/h2&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;"mcpServers"&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;"react-render-profile"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-render-profile-mcp"&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="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;Then record a session in React DevTools → Profiler → export as JSON → give the file path to your agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 The workflow
&lt;/h2&gt;

&lt;p&gt;Instead of pasting raw JSON and hoping:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Load the profile at &lt;code&gt;/tmp/myapp.profile.json&lt;/code&gt; and find spurious renders"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent calls &lt;code&gt;find_spurious_renders&lt;/code&gt; with the file path, gets back structured data with component names, counts, and wasted milliseconds — and gives you a concrete list of what to fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Load the profile and trace what caused the slow commit at index 3"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;trace_render_cascade&lt;/code&gt; returns the trigger component, the cascade, and the reason for each re-render.&lt;/p&gt;

&lt;p&gt;The agent goes from &lt;em&gt;"I see some fiber IDs with large durations"&lt;/em&gt; to &lt;em&gt;"ProductList is re-rendering 6 times spuriously, wrapping it in React.memo would save 312ms."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/react-render-profile-mcp" rel="noopener noreferrer"&gt;npmjs.com/package/react-render-profile-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/react-render-profile-mcp" rel="noopener noreferrer"&gt;github.com/vola-trebla/react-render-profile-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx react-render-profile-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>mcp</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your AI Agent Hallucinates Tailwind Classes. Here's the Fix</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 19:38:01 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ai-agent-hallucinates-tailwind-classes-heres-the-fix-mk2</link>
      <guid>https://dev.to/vola-trebla/your-ai-agent-hallucinates-tailwind-classes-heres-the-fix-mk2</guid>
      <description>&lt;p&gt;Your AI agent is confidently writing Tailwind classes that don't exist in your project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bg-primary-500&lt;/code&gt;? Your project uses &lt;code&gt;bg-brand-primary&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;p-18&lt;/code&gt;? Only valid if your spacing scale includes it.&lt;br&gt;
&lt;code&gt;tw-flex&lt;/code&gt;? Only if your config sets &lt;code&gt;prefix: "tw-"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent doesn't know. It's working from training data — the default Tailwind docs, not your config.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Epistemic Blindness
&lt;/h2&gt;

&lt;p&gt;When an agent generates a React component, it has no idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether &lt;code&gt;bg-brand-primary&lt;/code&gt; is a valid class &lt;strong&gt;in this project&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;What your custom spacing scale looks like (is &lt;code&gt;p-18&lt;/code&gt; valid here?)&lt;/li&gt;
&lt;li&gt;Whether you're using a custom prefix like &lt;code&gt;tw-&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Which brand colors exist beyond the Tailwind defaults&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;flex grid&lt;/code&gt; on the same element is a conflict&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It guesses. Custom tokens = hallucinations. Every time.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix: Give the Agent Your Actual Config
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vola-trebla/tailwind-context-resolver-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;tailwind-context-resolver-mcp&lt;/code&gt;&lt;/a&gt; is an MCP server that loads your &lt;code&gt;tailwind.config.ts/js&lt;/code&gt; and exposes its resolved design system as queryable tools.&lt;/p&gt;

&lt;p&gt;Before writing a component, the agent can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;→ get_config_summary        understand the project's design system
→ resolve_theme_tokens      query what colors/spacing actually exist
→ validate_class_string     verify the className before committing it
→ detect_css_conflicts      catch flex+grid on the same element
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from &lt;code&gt;validate_class_string&lt;/code&gt;:&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;"valid_classes"&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;"bg-brand-primary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text-white"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hover:dark:bg-brand-secondary"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invalid_classes"&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;"bg-fake-token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"possibly_valid_classes"&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;"btn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prose"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&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;"Conflicting multiple layout models: flex, grid"&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;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The server uses the same config loading strategy as the Tailwind CLI itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/unjs/jiti" rel="noopener noreferrer"&gt;&lt;code&gt;jiti&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; loads your &lt;code&gt;tailwind.config.ts&lt;/code&gt; at runtime — no &lt;code&gt;ts-node&lt;/code&gt; setup needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tailwindcss/resolveConfig&lt;/code&gt;&lt;/strong&gt; merges your config with Tailwind defaults → full resolved theme&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token-based validation&lt;/strong&gt; — checks that &lt;code&gt;bg-brand-primary&lt;/code&gt; maps to an actual &lt;code&gt;colors.brand.primary&lt;/code&gt; token — without running PostCSS or the full JIT pipeline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The class parser handles the full Tailwind syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;!hover:dark:bg-brand-primary/50&lt;/code&gt; → strips &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;hover:dark:&lt;/code&gt;, &lt;code&gt;/50&lt;/code&gt; before lookup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bg-[#ff0000]&lt;/code&gt; → arbitrary values are always valid&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-mt-4&lt;/code&gt; → negative prefix stripped, looks up spacing token &lt;code&gt;4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;group-hover:text-white&lt;/code&gt; → multi-word variants handled correctly&lt;/li&gt;
&lt;li&gt;Unknown classes when plugins are active → &lt;code&gt;possibly_valid_classes&lt;/code&gt; (can't verify &lt;code&gt;btn&lt;/code&gt; or &lt;code&gt;prose&lt;/code&gt; without PostCSS)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Add to Claude Desktop / Cursor / any MCP client:&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;"mcpServers"&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;"tailwind-context-resolver"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tailwind-context-resolver-mcp"&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="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;Then pass your config path on each tool call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/absolute/path/to/tailwind.config.ts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tailwind v3 Only
&lt;/h2&gt;

&lt;p&gt;v4 uses a CSS-based config format — the programmatic &lt;code&gt;resolveConfig&lt;/code&gt; API doesn't apply. The server detects v4 and returns a clear error instead of silently failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;a href="https://www.npmjs.com/package/tailwind-context-resolver-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/vola-trebla/tailwind-context-resolver-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Part of a series of MCP tools for making AI agents actually useful in real codebases. Also check out &lt;a href="https://www.npmjs.com/package/v8-cpu-profile-decoder-mcp" rel="noopener noreferrer"&gt;v8-cpu-profile-decoder-mcp&lt;/a&gt; for Node.js performance profiling.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>tailwindcss</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Your Node.js App Is Slow. Your AI Agent Can't Help - Until Now</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 19:05:16 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-nodejs-app-is-slow-your-ai-agent-cant-help-until-now-5c0o</link>
      <guid>https://dev.to/vola-trebla/your-nodejs-app-is-slow-your-ai-agent-cant-help-until-now-5c0o</guid>
      <description>&lt;p&gt;You've been here before.&lt;/p&gt;

&lt;p&gt;The API is dragging. P99 latency is climbing. Someone says "just profile it" — so you run &lt;code&gt;node --cpu-prof&lt;/code&gt; and get a file like &lt;code&gt;CPU.20260516.154355.cpuprofile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You open it. It's 28MB of 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;"nodes"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"callFrame"&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;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"processRequest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scriptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"94"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;847&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"columnNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&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;"hitCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3241&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"children"&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="mi"&gt;1483&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1490&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1501&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1483&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"callFrame"&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;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parseBody"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scriptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"94"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/middleware.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"columnNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&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;"hitCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"children"&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="mi"&gt;1484&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="err"&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;"samples"&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="mi"&gt;1482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1483&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1490&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1501&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1482&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1483&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"timeDeltas"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;124&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;112&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;Thousands of nodes. Millions of samples. Raw memory addresses. Microsecond tick intervals.&lt;/p&gt;

&lt;p&gt;You paste it into Claude. The context window collapses. It hallucinates an answer. You're back to square one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the problem this MCP solves.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why AI Agents Are Blind to CPU Profiles
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.cpuprofile&lt;/code&gt; isn't just "a big file." It's a specific format that requires real computation to be useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exclusive time&lt;/strong&gt; (self time) = how long a function ran &lt;em&gt;on its own&lt;/em&gt;, without counting callees = &lt;code&gt;hitCount × avg(timeDeltas)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inclusive time&lt;/strong&gt; = self time + all descendant inclusive times = requires a recursive DFS over the call tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot path&lt;/strong&gt; = which chain of callers led to the bottleneck = requires building a reverse parent map&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An LLM can't do any of this. It can't execute algorithms. It can't traverse a tree across 50,000 nodes. Even if you somehow got the file into context, it would just pattern-match on function names and guess.&lt;/p&gt;

&lt;p&gt;The agent needs a bridge — something that runs the math locally and hands back a 10-line summary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing &lt;code&gt;v8-cpu-profile-decoder-mcp&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;An MCP server that decodes V8 CPU profiles into token-efficient bottleneck summaries for AI agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx v8-cpu-profile-decoder-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tools. Each one answers a specific question the agent would otherwise be blind to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 1: &lt;code&gt;extract_hottest_functions&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Which function is burning the most CPU?"&lt;/em&gt;&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;"profile_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/app/profiles/CPU.20260516.cpuprofile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"top_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"min_self_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&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;Response:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashPassword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/auth/crypto.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selfTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1842.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1842.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selfPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;61.32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hitCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3241&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;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parseJsonBody"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/middleware/body.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selfTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;412.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selfPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;13.71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hitCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;724&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;61% of all CPU time in one function. The agent now knows exactly where to look — without ever reading the raw profile.&lt;/p&gt;

&lt;p&gt;V8 internals (&lt;code&gt;(program)&lt;/code&gt;, &lt;code&gt;(garbage collector)&lt;/code&gt;, &lt;code&gt;(idle)&lt;/code&gt;) are filtered out by default. You get user code only.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 2: &lt;code&gt;analyze_call_tree_path&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"What's calling my slow function, and how often?"&lt;/em&gt;&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;"profile_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/app/profiles/CPU.20260516.cpuprofile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"function_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashPassword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"top_callers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Response:&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;"targetFunction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashPassword"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchedNodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalSelfTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1842.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;61.32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callers"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loginHandler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/routes/auth.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sampleCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attributedTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1235.4&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;span class="nl"&gt;"functionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"validateSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/middleware/auth.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"lineNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sampleCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1061&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"attributedTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;607.1&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;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;Now the agent knows the full picture: &lt;code&gt;hashPassword&lt;/code&gt; is slow, &lt;code&gt;loginHandler&lt;/code&gt; is responsible for 67% of those calls, and &lt;code&gt;validateSession&lt;/code&gt; is calling it unnecessarily on every request.&lt;/p&gt;

&lt;p&gt;Function name matching is partial and case-insensitive — you don't need to know the exact internal name.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool 3: &lt;code&gt;correlate_source_code&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Which TypeScript file and line is the bottleneck actually from?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After TypeScript compilation, your profile shows &lt;code&gt;dist/auth/crypto.js:42&lt;/code&gt;. That's not where you write code. This tool reads the &lt;code&gt;.js.map&lt;/code&gt; source maps and resolves back to the original TypeScript:&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;"resolved"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"generatedUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:///app/dist/auth/crypto.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"generatedLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"originalFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/auth/crypto.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"originalLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"originalColumn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"originalFunction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hashPassword"&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;"selfTimeMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1842.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"selfPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;61.32&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;span class="nl"&gt;"sourcemapErrors"&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="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 agent can now open &lt;code&gt;src/auth/crypto.ts&lt;/code&gt; at line 38 and fix the actual source — not a compiled artifact.&lt;/p&gt;

&lt;p&gt;Falls back gracefully to compiled JS locations if no &lt;code&gt;.map&lt;/code&gt; file is found.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Algorithm Works
&lt;/h2&gt;

&lt;p&gt;Three steps happen locally before anything reaches the agent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Build the node map&lt;/strong&gt;&lt;br&gt;
Parse the &lt;code&gt;nodes&lt;/code&gt; array into &lt;code&gt;Map&amp;lt;id, node&amp;gt;&lt;/code&gt; for O(1) lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Compute exclusive time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selfTimeMs = hitCount × avg(timeDeltas[1:])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;timeDeltas[0]&lt;/code&gt; is always 0 per V8 spec (offset from &lt;code&gt;startTime&lt;/code&gt;), so we skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compute inclusive time via DFS with memoization&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inclusiveTime(node) = selfTime(node) + Σ inclusiveTime(child)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cached to avoid recomputation on shared subtrees.&lt;/p&gt;

&lt;p&gt;The raw profile might have 50,000 nodes and 200,000 samples. What comes back to the agent is 10 ranked objects. That's the token compression that makes this useful.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; v8-cpu-profile-decoder-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt; (&lt;code&gt;~/.config/claude/claude_desktop_config.json&lt;/code&gt;):&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;"mcpServers"&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;"v8-cpu-profile-decoder-mcp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v8-cpu-profile-decoder-mcp"&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="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;&lt;strong&gt;Generate a profile:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--cpu-prof&lt;/span&gt; &lt;span class="nt"&gt;--cpu-prof-dir&lt;/span&gt; ./profiles your-script.js
&lt;span class="c"&gt;# or for a running server: kill -USR1 &amp;lt;pid&amp;gt; (with --cpu-prof flag)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Then ask your agent:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Profile is at &lt;code&gt;./profiles/CPU.cpuprofile&lt;/code&gt; — find the bottleneck and suggest a fix"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What the Agent Can Do With This
&lt;/h2&gt;

&lt;p&gt;With a decoded profile in context, an AI agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify the hot function and read its source code&lt;/li&gt;
&lt;li&gt;Trace who's calling it and how many times&lt;/li&gt;
&lt;li&gt;Suggest memoization, caching, or algorithmic improvements&lt;/li&gt;
&lt;li&gt;Spot unexpected callers (e.g., &lt;code&gt;validateSession&lt;/code&gt; calling &lt;code&gt;hashPassword&lt;/code&gt; on every request)&lt;/li&gt;
&lt;li&gt;Generate a PR with the fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this MCP, it's guessing from static code. With it, it's working from measured runtime data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/v8-cpu-profile-decoder-mcp" rel="noopener noreferrer"&gt;v8-cpu-profile-decoder-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/v8-cpu-profile-decoder-mcp" rel="noopener noreferrer"&gt;vola-trebla/v8-cpu-profile-decoder-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;More in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/vola-trebla/stop-copy-pasting-playwright-traces-into-chatgpt-do-this-instead-56ni"&gt;playwright-trace-decoder-mcp&lt;/a&gt; — same pattern for Playwright CI failures&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/vola-trebla/your-ai-agent-can-read-the-dom-it-cant-see-the-screen-1ic3"&gt;playwright-spatial-layout-mcp&lt;/a&gt; — geometric vision for browser layouts&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your AI Agent Can Read the DOM. It Can't See the Screen.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 03:32:46 +0000</pubDate>
      <link>https://dev.to/vola-trebla/your-ai-agent-can-read-the-dom-it-cant-see-the-screen-1ic3</link>
      <guid>https://dev.to/vola-trebla/your-ai-agent-can-read-the-dom-it-cant-see-the-screen-1ic3</guid>
      <description>&lt;p&gt;Here's a test that passes every time:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.checkout-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's what the AI agent doesn't know: the checkout button is at &lt;code&gt;y: 1450px&lt;/code&gt; on a 375px mobile viewport. It exists. It's visible according to the DOM. The test is green. The user can't reach it without scrolling three screens down, and on some devices a sticky cookie banner overlaps it by 60%.&lt;/p&gt;

&lt;p&gt;The agent read the accessibility tree. It didn't see the screen.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap between the DOM and the render
&lt;/h2&gt;

&lt;p&gt;When an AI agent analyzes a Playwright failure or writes a new test, it works with what Playwright exposes by default: roles, labels, text content, ARIA attributes. This is the right abstraction for functional testing.&lt;/p&gt;

&lt;p&gt;But layout bugs don't live in the DOM. They live in the render engine's output — in coordinates, z-indexes, bounding boxes, and intersection ratios. A button can be &lt;code&gt;display: block&lt;/code&gt;, &lt;code&gt;visibility: visible&lt;/code&gt;, &lt;code&gt;opacity: 1&lt;/code&gt;, and completely unreachable by a real user.&lt;/p&gt;

&lt;p&gt;Current tools for this problem are either pixel-diff based (noisy, breaks on anti-aliasing) or proprietary enterprise AI (Applitools, Percy). There's no open-source tool that gives an AI agent structured geometric data from a live browser.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;playwright-spatial-layout-mcp&lt;/code&gt; does.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The MCP server launches a headless Chromium browser, navigates to a URL, and extracts geometric data using &lt;code&gt;getBoundingClientRect()&lt;/code&gt; and &lt;code&gt;getComputedStyle()&lt;/code&gt; in a single &lt;code&gt;page.evaluate()&lt;/code&gt; call — one browser round-trip per element batch.&lt;/p&gt;

&lt;p&gt;Four tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;extract_bounding_boxes&lt;/code&gt;&lt;/strong&gt; — returns position, size, z-index, and viewport visibility for any set of selectors.&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.com/checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selectors"&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;".checkout-button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".cookie-banner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nav"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"viewport"&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;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;375&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;812&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;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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".checkout-button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"box"&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;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;892&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;343&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&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;"z_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_visible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_in_viewport"&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 button exists. It is not in the viewport. The agent now knows this.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;detect_visual_occlusion&lt;/code&gt;&lt;/strong&gt; — computes the intersection ratio between two elements' bounding boxes.&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.com/checkout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".checkout-button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overlay_selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".cookie-banner"&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;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;"is_occluded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intersection_ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occluded_area_px"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4128&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;61% of the button's area is under the cookie banner. The agent can now report this as a bug, not a passing test.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;verify_spatial_relationships&lt;/code&gt;&lt;/strong&gt; — validates layout rules and returns a pass/fail with a human-readable reason per rule.&lt;/p&gt;

&lt;p&gt;Six rule types: &lt;code&gt;left_of&lt;/code&gt;, &lt;code&gt;right_of&lt;/code&gt;, &lt;code&gt;above&lt;/code&gt;, &lt;code&gt;below&lt;/code&gt;, &lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;not_overlapping&lt;/code&gt;.&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"above"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"element_a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nav"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"element_b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".hero"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_overlapping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"element_a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".sidebar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"element_b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".main-content"&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;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;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;"passed"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"'nav' bottom (64px) is above '.hero' top (64px)"&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;span class="nl"&gt;"passed"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"'.sidebar' and '.main-content' overlap by 12%"&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;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;This is layout spec-as-code — the agent asserts design constraints the same way it asserts functional ones.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;compute_viewport_reflow&lt;/code&gt;&lt;/strong&gt; — tracks how element geometry changes across breakpoints. All viewports are processed in parallel.&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-app.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selectors"&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;".hero-cta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nav"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"viewports"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;375&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;812&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;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&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;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;720&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;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;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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".hero-cta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shifted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_delta_x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;442&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_delta_y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;318&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_delta_width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;897&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 CTA moved 442px horizontally and 318px vertically between mobile and desktop. The agent knows which element is most volatile across breakpoints.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the agent can do with this
&lt;/h2&gt;

&lt;p&gt;Before this MCP, an AI agent writing or debugging Playwright tests operated blind to rendering. It could tell you the button has &lt;code&gt;role="button"&lt;/code&gt; and &lt;code&gt;aria-label="Checkout"&lt;/code&gt;. It could not tell you where the button is on screen.&lt;/p&gt;

&lt;p&gt;With spatial data in context, the agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect that a passing test covers a button the user can't actually click&lt;/li&gt;
&lt;li&gt;Identify which elements are off-screen on mobile before a test suite runs&lt;/li&gt;
&lt;li&gt;Verify that a CSS refactor didn't break the layout without running visual regression diffs&lt;/li&gt;
&lt;li&gt;Catch z-index wars where one component silently slides under another after a merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shift is from "does this element exist in the DOM" to "can a real user reach this element on this device."&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; playwright-spatial-layout-mcp
npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to your Claude Desktop config:&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;"mcpServers"&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;"playwright-spatial-layout-mcp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright-spatial-layout-mcp"&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="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;Then ask your agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Check if the cookie banner is blocking the checkout button on a 375px viewport"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Verify that nav is above the hero section and sidebar doesn't overlap main content"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Which elements shift the most when resizing from desktop to mobile?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part of a larger ecosystem
&lt;/h2&gt;

&lt;p&gt;This is the fifth MCP server in a series of open-source tools for the Playwright/TypeScript testing ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vola-trebla/playwright-trace-decoder-mcp" rel="noopener noreferrer"&gt;playwright-trace-decoder-mcp&lt;/a&gt; — root-cause analysis from Playwright trace.zip files&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vola-trebla/flakiness-knowledge-graph-mcp" rel="noopener noreferrer"&gt;flakiness-knowledge-graph-mcp&lt;/a&gt; — knowledge graph of flaky test patterns over time&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vola-trebla/ast-impact-mapper-mcp" rel="noopener noreferrer"&gt;ast-impact-mapper-mcp&lt;/a&gt; — TypeScript AST-based test impact analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vola-trebla/zod-contract-mock-forge-mcp" rel="noopener noreferrer"&gt;zod-contract-mock-forge-mcp&lt;/a&gt; — deterministic mock generation from Zod schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one addresses a specific blind spot — what the agent can't reason about without structured tool access. Spatial layout was the most visible one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/playwright-spatial-layout-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/playwright-spatial-layout-mcp&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/vola-trebla/playwright-spatial-layout-mcp" rel="noopener noreferrer"&gt;https://github.com/vola-trebla/playwright-spatial-layout-mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>ai</category>
      <category>typescript</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Stop Copy-Pasting Zod Schemas to ChatGPT. Build a Mock Forge Instead.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 02:28:01 +0000</pubDate>
      <link>https://dev.to/vola-trebla/stop-copy-pasting-zod-schemas-to-chatgpt-build-a-mock-forge-instead-1ah9</link>
      <guid>https://dev.to/vola-trebla/stop-copy-pasting-zod-schemas-to-chatgpt-build-a-mock-forge-instead-1ah9</guid>
      <description>&lt;p&gt;If you are using Zod for API contract validation in TypeScript, you already know the pain of writing test data. &lt;/p&gt;

&lt;p&gt;You have a schema with 50 nested fields, UUIDs, enums, and email constraints. When it's time to write tests, what do you do? You copy the schema, paste it into Claude or ChatGPT, and ask: &lt;em&gt;"Generate a valid JSON mock for this."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then you need negative tests. So you ask: &lt;em&gt;"Now generate 10 invalid payloads that break different constraints."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then you need a Playwright test. &lt;em&gt;"Now write a Playwright script that sends this payload."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It works, but it's tedious. You are acting as a human clipboard between your IDE and the AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the AI could just read your files, generate the mocks, and scaffold the tests on its own?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;. &lt;/p&gt;




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

&lt;p&gt;The Model Context Protocol allows AI agents (like Claude Desktop or Cursor) to interact with local tools and data sources. Instead of you pasting data &lt;em&gt;to&lt;/em&gt; the AI, the AI fetches data &lt;em&gt;from&lt;/em&gt; your environment using defined tools.&lt;/p&gt;

&lt;p&gt;I built the &lt;a href="https://github.com/vola-trebla/zod-contract-mock-forge-mcp" rel="noopener noreferrer"&gt;&lt;strong&gt;zod-contract-mock-forge-mcp&lt;/strong&gt;&lt;/a&gt; to solve the exact problem of API contract testing. It bridges the gap between your Zod schemas and your testing frameworks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Mock Forge" Arsenal
&lt;/h2&gt;

&lt;p&gt;When you connect this MCP server to Claude Desktop, the AI suddenly gains a powerful set of tools:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. File Discovery (&lt;code&gt;read_schema_from_file&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Instead of pasting code, you tell the AI: &lt;em&gt;"Look at &lt;code&gt;user.schema.ts&lt;/code&gt;."&lt;/em&gt;&lt;br&gt;
The server automatically parses the file, extracts the specific Zod export, and feeds it to the AI. No manual copying.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deterministic Mocking (&lt;code&gt;generate_valid_mock&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Need test data? The server leverages &lt;code&gt;@anatine/zod-mock&lt;/code&gt; and Faker.js to generate valid, realistic JSON payloads on the fly. You get actual UUIDs, formatted emails, and structurally perfect objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Deep Boundary Violations (&lt;code&gt;generate_boundary_violations&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the killer feature for QA and SDETs. &lt;br&gt;
Simple mock generators can give you valid data, but testing requires &lt;em&gt;invalid&lt;/em&gt; data. The server performs &lt;strong&gt;recursive deep mutation&lt;/strong&gt; on your schema. It will traverse deep nested objects and arrays to intentionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Omit required fields.&lt;/li&gt;
&lt;li&gt;Inject type mismatches (e.g., sending a number instead of a string).&lt;/li&gt;
&lt;li&gt;Break string constraints (invalid emails, bad UUIDs, wrong URLs).&lt;/li&gt;
&lt;li&gt;Violate number and array boundaries (min/max).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI can generate a suite of 20 negative test cases in seconds, targeting specific edge cases you might not have thought of.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test Scaffolding (&lt;code&gt;scaffold_api_contract_test&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Once the mocks are ready, the AI can use this tool to generate the actual test boilerplate. It supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; (for E2E API tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jest / Vitest&lt;/strong&gt; (for unit testing contracts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MSW&lt;/strong&gt; (Mock Service Worker, for frontend mocking)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. The Smart Fixer (&lt;code&gt;suggest_contract_fix&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Tests failing? Hand the failing payload and the schema to the &lt;code&gt;suggest_contract_fix&lt;/code&gt; tool. It analyzes the Zod validation error and tells you exactly what went wrong:&lt;br&gt;
&lt;em&gt;"Issue: Expected string, received null at &lt;code&gt;user.profile.avatar&lt;/code&gt;. Fix: Make the schema &lt;code&gt;.nullable()&lt;/code&gt; or ensure the payload provides a string."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workflow in Action
&lt;/h2&gt;

&lt;p&gt;Here is what it looks like when you use an AI agent equipped with the Zod Mock Forge:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; &lt;em&gt;"Read the &lt;code&gt;CreateOrder&lt;/code&gt; schema from &lt;code&gt;src/schemas/order.ts&lt;/code&gt; and write a Playwright negative test suite testing all boundary violations."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI (automatically):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calls &lt;code&gt;read_schema_from_file&lt;/code&gt; to get the Zod code.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;generate_boundary_violations&lt;/code&gt; to get 15 different ways to break the payload.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;scaffold_api_contract_test&lt;/code&gt; to get the Playwright boilerplate.&lt;/li&gt;
&lt;li&gt;Writes out a complete &lt;code&gt;order-negative.spec.ts&lt;/code&gt; file with 15 distinct &lt;code&gt;test()&lt;/code&gt; blocks, each asserting the correct 400 Bad Request response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero copy-pasting. Complete test coverage. &lt;/p&gt;




&lt;h2&gt;
  
  
  Stop being a clipboard
&lt;/h2&gt;

&lt;p&gt;AI agents are meant to automate tasks, not just generate text. By equipping them with MCP servers like the Zod Contract Mock Forge, you turn a chat interface into a fully automated QA engineer.&lt;/p&gt;

&lt;p&gt;Try it out on your own projects:&lt;br&gt;
&lt;a href="https://github.com/vola-trebla/zod-contract-mock-forge-mcp" rel="noopener noreferrer"&gt;&lt;strong&gt;vola-trebla/zod-contract-mock-forge-mcp&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure it in your Claude Desktop, point it at your &lt;code&gt;.ts&lt;/code&gt; files, and watch it forge your tests.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>zod</category>
      <category>typescript</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Stop Spending Two Weeks Configuring Playwright. Use a Skeleton Built for AI Adaptation.🤖</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sat, 16 May 2026 00:03:29 +0000</pubDate>
      <link>https://dev.to/vola-trebla/stop-spending-two-weeks-configuring-playwright-use-a-skeleton-built-for-ai-adaptation-2ci9</link>
      <guid>https://dev.to/vola-trebla/stop-spending-two-weeks-configuring-playwright-use-a-skeleton-built-for-ai-adaptation-2ci9</guid>
      <description>&lt;p&gt;Every SDET joining a new project goes through the same ritual.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;npm init playwright@latest&lt;/code&gt;. Spend day one on ESLint, Prettier, Husky. Day two on folder structure debates. Day three writing a Base API client. By the time you write a test that actually validates business logic, a week is gone.&lt;/p&gt;

&lt;p&gt;Boilerplates exist to solve this — but they have a fundamental flaw: adapting one to your domain takes almost as long as starting from scratch. You're still hunting down every &lt;code&gt;SamplePageObject&lt;/code&gt; and &lt;code&gt;example-login.spec.ts&lt;/code&gt; and manually replacing them with your actual app's concepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the boilerplate came pre-wired for an AI agent to adapt it for you?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What is playwright-ai-skeleton?
&lt;/h2&gt;

&lt;p&gt;It's a production-ready E2E framework built on Playwright and TypeScript, following patterns I've used across multiple real projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page Object Model&lt;/strong&gt; with strictly private locators — business intent exposed, Playwright internals hidden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid testing&lt;/strong&gt; — API clients and UI Page Objects sharing the same Playwright fixture chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API-first data setup&lt;/strong&gt; — immutable data builders for fast, reliable state injection without UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zod validation&lt;/strong&gt; — fails fast if &lt;code&gt;.env&lt;/code&gt; or API responses don't match the expected schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker-scoped auth&lt;/strong&gt; — one login per worker instead of one per test, cutting CI time significantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-wired infrastructure&lt;/strong&gt; — GitHub Actions, Docker Compose, and a Slack reporter included&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the architecture is not the point. The point is what's inside &lt;code&gt;docs/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The adaptation guide
&lt;/h2&gt;

&lt;p&gt;The repo ships with two files designed not for human reading, but for AI input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docs/CONVENTIONS.md&lt;/code&gt; — strict architectural rules the AI must follow&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/ADAPTATION_GUIDE.md&lt;/code&gt; — a step-by-step prompt that drives the AI through customizing the skeleton for your specific domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire onboarding flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vola-trebla/playwright-ai-skeleton.git my-project
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project
&lt;span class="c"&gt;# Open in Cursor or Claude Code, then:&lt;/span&gt;
&lt;span class="c"&gt;# "Read docs/ADAPTATION_GUIDE.md and adapt this for an e-commerce app called ShopFrog"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI reads the guide, understands the architecture's constraints, and starts a structured conversation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;What is your base URL?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What are your core domain entities?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How does authentication work?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After you answer, it renames files, generates your API clients, scaffolds your Page Objects, sets up Zod schemas, and writes your first smoke test — all consistent with the conventions in &lt;code&gt;CONVENTIONS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;git clone&lt;/code&gt; to a compiling, project-specific framework in under 10 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why strict conventions matter for AI
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are fast but undirected — ask them to "write a Playwright framework" and you get a mix of outdated patterns, inconsistent selectors, and business logic leaking into locator files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CONVENTIONS.md&lt;/code&gt; constrains the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;getByRole&lt;/code&gt; — never raw CSS selectors&lt;/li&gt;
&lt;li&gt;Expose &lt;code&gt;login()&lt;/code&gt; — never &lt;code&gt;click()&lt;/code&gt; on the Page Object surface&lt;/li&gt;
&lt;li&gt;Data builders are immutable — no test mutates shared state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI doesn't just write code. It writes code that conforms to a specific architectural standard, enforced by the guide it was given.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vola-trebla/playwright-ai-skeleton.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed &lt;code&gt;docs/ADAPTATION_GUIDE.md&lt;/code&gt; to Claude or Copilot. Answer the questions. Ship your first test today.&lt;/p&gt;

&lt;p&gt;The infrastructure problem is solved. Your job is testing business logic — not configuring linters.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>ai</category>
      <category>testing</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Stop Running Your Entire Test Suite. Use the AST Instead.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Fri, 15 May 2026 23:16:54 +0000</pubDate>
      <link>https://dev.to/vola-trebla/stop-running-your-entire-test-suite-use-the-ast-instead-1bkh</link>
      <guid>https://dev.to/vola-trebla/stop-running-your-entire-test-suite-use-the-ast-instead-1bkh</guid>
      <description>&lt;p&gt;You just changed one utility function. CI kicks off. 2,000 Playwright tests start running.&lt;/p&gt;

&lt;p&gt;45 minutes later, you get your green light.&lt;/p&gt;

&lt;p&gt;This is the state of E2E testing at scale: every PR pays the price of the full suite, regardless of what actually changed. It's not a tooling problem — it's an information problem. Your CI doesn't know which tests depend on your change. So it runs everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/vola-trebla/ast-impact-mapper-mcp" rel="noopener noreferrer"&gt;ast-impact-mapper-mcp&lt;/a&gt;&lt;/strong&gt; fixes that.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The Idea: Import Graphs Don't Lie
&lt;/h2&gt;

&lt;p&gt;Every TypeScript project is a directed graph of imports. When you change &lt;code&gt;src/utils/auth.ts&lt;/code&gt;, the only tests that need to run are the ones that — directly or transitively — import it.&lt;/p&gt;

&lt;p&gt;This isn't guesswork based on filenames or folder structure. It's a precise traversal of your actual dependency graph.&lt;/p&gt;

&lt;p&gt;The tool uses &lt;a href="https://ts-morph.com/" rel="noopener noreferrer"&gt;&lt;code&gt;ts-morph&lt;/code&gt;&lt;/a&gt; to parse your TypeScript project (including &lt;code&gt;tsconfig.json&lt;/code&gt;, path aliases, JS/JSX files) and builds two graphs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forward graph&lt;/strong&gt;: file → files it imports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse graph&lt;/strong&gt;: file → files that import it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given a set of changed files, a BFS through the reverse graph finds every test that transitively depends on them. Everything else is safe to skip.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Eight Tools for Complete Impact Analysis
&lt;/h2&gt;

&lt;p&gt;Once connected to your AI assistant (Claude, Cursor), the MCP server exposes eight tools:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_affected_tests&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The core tool. Give it a list of changed files — or the raw output of &lt;code&gt;git diff --name-only&lt;/code&gt; — and get back every test file that transitively imports them.&lt;/p&gt;

&lt;p&gt;Supports TypeScript, JavaScript, and JSX. Matches &lt;code&gt;*.spec.ts&lt;/code&gt;, &lt;code&gt;*.test.ts&lt;/code&gt;, and files inside &lt;code&gt;__tests__/&lt;/code&gt; directories.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_affected_tests_by_branch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Same as above, but the server runs &lt;code&gt;git diff&lt;/code&gt; itself. Point it at a base branch (default: &lt;code&gt;main&lt;/code&gt;) and it handles everything — no manual file listing needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_dependency_graph&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Direct imports and importers for any file. Returns JSON by default, or a &lt;strong&gt;Mermaid flowchart&lt;/strong&gt; you can paste straight into GitHub markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="k"&gt;graph&lt;/span&gt; &lt;span class="nv"&gt;TD&lt;/span&gt;
  &lt;span class="s2"&gt;"src/fixtures/base-fixture.ts"&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"src/pages/google-home-page.ts"&lt;/span&gt;
  &lt;span class="s2"&gt;"src/fixtures/base-fixture.ts"&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"src/pages/google-results-page.ts"&lt;/span&gt;
  &lt;span class="s2"&gt;"tests/google-pom.spec.ts"&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"src/fixtures/base-fixture.ts"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;explain_impact&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Finds the shortest import chain from a test file to a changed source file. The AI can say exactly &lt;em&gt;why&lt;/em&gt; a test is affected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"checkout.spec.ts is affected because it imports CartPage.ts → PriceCalc.ts → the file you changed."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_coverage_gaps&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Finds source files that are not reachable from any test through the import graph — completely untested code, found statically in milliseconds, no coverage run needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_test_summary&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Project-wide health overview in one call: coverage rate, the 10 most-imported source files (highest blast radius if changed), and tests with the deepest import chains.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;refresh_project&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Clears the cached AST and dependency graphs for a project root. Call it after switching branches or pulling changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;get_dependency_graph&lt;/code&gt; with &lt;code&gt;format: "mermaid"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A visual flowchart of any file's import neighborhood — ready for GitHub, Notion, or any Mermaid renderer.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕵️‍♂️ Real-World Scenario: The Targeted Refactor
&lt;/h2&gt;

&lt;p&gt;You're refactoring &lt;code&gt;DateHelper.ts&lt;/code&gt;. You want to run only the affected tests locally before pushing — not the full suite.&lt;/p&gt;

&lt;p&gt;You ask your AI:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I'm about to change &lt;code&gt;src/utils/DateHelper.ts&lt;/code&gt;. Which tests should I run?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI calls &lt;code&gt;get_affected_tests&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"4 tests are affected: &lt;code&gt;calendar.spec.ts&lt;/code&gt;, &lt;code&gt;booking.spec.ts&lt;/code&gt;, &lt;code&gt;history.spec.ts&lt;/code&gt;, and &lt;code&gt;profile.spec.ts&lt;/code&gt;. Everything else is safe to skip."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You run those 4 tests in 2 minutes. CI confirms green 40 minutes later. &lt;strong&gt;That's the 20x feedback loop improvement&lt;/strong&gt; — not from running tests faster, but from running fewer of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Real-World Scenario: The Unknown Risk
&lt;/h2&gt;

&lt;p&gt;You're reviewing a PR that touches &lt;code&gt;src/api/client.ts&lt;/code&gt;. You want to know the blast radius before merging.&lt;/p&gt;

&lt;p&gt;You ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"How many tests depend on &lt;code&gt;src/api/client.ts&lt;/code&gt;? Show me the dependency graph."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI calls &lt;code&gt;get_test_summary&lt;/code&gt;, then &lt;code&gt;get_dependency_graph&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"&lt;code&gt;api/client.ts&lt;/code&gt; is imported by 11 other files and is the most-imported source file in the project. Changing it affects 23 test files — roughly 40% of your suite. I'd recommend reviewing this PR carefully."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's information you couldn't get from a code diff alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Example Output
&lt;/h2&gt;

&lt;p&gt;Given this project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  fixtures/base-fixture.ts   ← imports home-page and results-page
  pages/google-home-page.ts
  pages/google-results-page.ts
tests/
  google-pom.spec.ts         ← imports base-fixture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_affected_tests&lt;/code&gt;&lt;/strong&gt; after changing &lt;code&gt;google-home-page.ts&lt;/code&gt;:&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;"changed_files"&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;"/my-project/src/pages/google-home-page.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affected_tests"&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;"/my-project/tests/google-pom.spec.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_affected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;&lt;strong&gt;&lt;code&gt;explain_impact&lt;/code&gt;&lt;/strong&gt; — the exact import chain:&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;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"import_chain"&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="s2"&gt;"tests/google-pom.spec.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/fixtures/base-fixture.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/pages/google-home-page.ts"&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;h2&gt;
  
  
  🚀 The Complete AI-QA Pipeline
&lt;/h2&gt;

&lt;p&gt;This is the third tool in a series designed to give AI agents full visibility into a Playwright test suite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/vola-trebla/stop-copy-pasting-playwright-traces-into-chatgpt-do-this-instead-56ni"&gt;Trace Decoder&lt;/a&gt;&lt;/strong&gt; — &lt;em&gt;how&lt;/em&gt; did a test fail? Full network, DOM, and console analysis from the trace file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/vola-trebla/stop-guessing-why-your-tests-flake-build-a-knowledge-graph-instead-2k2"&gt;Flakiness Knowledge Graph&lt;/a&gt;&lt;/strong&gt; — &lt;em&gt;is&lt;/em&gt; this test reliable? Historical failure rates, trends, browser-specific patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AST Impact Mapper&lt;/strong&gt; — &lt;em&gt;which&lt;/em&gt; tests need to run? Precise impact analysis from the dependency graph.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, they cover the full lifecycle: knowing what to run, understanding if it's reliable, and diagnosing failures when they happen — all without opening a trace file or running the full suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡️ Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ast-impact-mapper-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add ast-impact-mapper ast-impact-mapper-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor / VS Code&lt;/strong&gt; (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;):&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;"mcpServers"&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;"ast-impact-mapper"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ast-impact-mapper-mcp"&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;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;&lt;strong&gt;Ask your AI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My project is at /my-project. I just pushed a PR.

1. get_affected_tests_by_branch — which tests are affected vs main?
2. explain_impact for the top result — why is that test affected?
3. get_coverage_gaps — what source files have zero test coverage?
4. get_test_summary — what's the overall health of the suite?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Stop running everything. Start running what matters.&lt;/em&gt; 🧠🕸️🐸&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
