<?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.us-east-2.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>In 2013 I wrote a diploma on elliptic curves. Last night my tool used them to check its own release.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sun, 01 Nov 2026 23:32:06 +0000</pubDate>
      <link>https://dev.to/vola-trebla/in-2013-i-wrote-a-diploma-on-elliptic-curves-last-night-my-tool-used-them-to-check-its-own-release-1k3i</link>
      <guid>https://dev.to/vola-trebla/in-2013-i-wrote-a-diploma-on-elliptic-curves-last-night-my-tool-used-them-to-check-its-own-release-1k3i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: In 2013 I wrote a university diploma on elliptic-curve cryptography, and it went into a drawer. Thirteen years later the same math signs most of what you &lt;code&gt;npm install&lt;/code&gt;. I taught &lt;strong&gt;croak&lt;/strong&gt; 🐸, my pre-install gate for AI agents, to check those signatures itself. Last night it checked its own fresh release: &lt;code&gt;verified&lt;/code&gt;. Then I gave it the same signature with a different tarball: &lt;code&gt;invalid&lt;/code&gt;. Below is what that check actually involves, and why the curve is the easy part.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The diploma in the drawer
&lt;/h2&gt;

&lt;p&gt;In 2013: a diploma on elliptic curves and key exchange. Points, groups, a lot of modular arithmetic. Defended, filed, forgotten.&lt;/p&gt;

&lt;p&gt;I went into test automation, and the curves stayed in the drawer.&lt;/p&gt;

&lt;p&gt;Then AI agents started running &lt;code&gt;npm install&lt;/code&gt; on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What croak does, in 30 seconds
&lt;/h2&gt;

&lt;p&gt;An install doesn't just download code. It &lt;strong&gt;runs&lt;/strong&gt; it: &lt;code&gt;postinstall&lt;/code&gt;, native builds, whatever a package wants. An agent does this all day, in your terminal, usually without reading anything. It also invents package names that sound right, and attackers register those names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;croak&lt;/strong&gt; sits between the agent and the install. It's a Claude Code hook: before a shell command runs, croak has npm or pnpm &lt;em&gt;plan&lt;/em&gt; the install (resolve it, run nothing), then reads that plan as data: install scripts, the lockfile, registry metadata, and the code of every new package, unpacked in memory and never executed. It gives a verdict and &lt;strong&gt;explains it to the agent&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;agent:  npm install reactt-domm

croak:  🐸 croak blocked this command before it ran.
        reactt-domm: not on npm; did you mean `react-dom`?
        Do not work around this check (another package manager, a different
        command, or editing files to install it); tell the user what croak
        found and let them decide.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;allow&lt;/strong&gt;: the hook stays silent.&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;warn&lt;/strong&gt;: Claude Code asks you. An esbuild-style &lt;code&gt;postinstall&lt;/code&gt; that downloads a binary lands here.&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;block&lt;/strong&gt;, or a check that couldn't finish: denied, with the reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also scans an unfamiliar repo before you open it (&lt;code&gt;croak repo&lt;/code&gt;: VS Code tasks that run on open, code hidden after hundreds of spaces, obfuscation), because "clone this take-home assignment and run it" is a real attack too.&lt;/p&gt;

&lt;p&gt;It's early, and it misses things. On its own eval it flags all 36 attacks but blocks only 27; the other nine go to a human as 🟡. I'd rather publish that number than a prettier one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an agent should care who built a package
&lt;/h2&gt;

&lt;p&gt;Most attacks give themselves away in the code: a &lt;code&gt;postinstall&lt;/code&gt; that pipes &lt;code&gt;curl&lt;/code&gt; into &lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;process.env&lt;/code&gt; posted somewhere. And install scripts are losing ground anyway: pnpm 10 and npm 12 no longer run a dependency's scripts unless the project allows them. I checked with a bait package that writes a marker file: npm 11 still runs them; npm 12 doesn't.&lt;/p&gt;

&lt;p&gt;That pushes attacks into code you &lt;em&gt;import&lt;/em&gt;, and that code can look exactly like the real thing. Someone steals a maintainer's token, publishes from a laptop, and the tarball looks like every release before it.&lt;/p&gt;

&lt;p&gt;That is the question code can't answer: &lt;strong&gt;who built this tarball, and from what?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;npm has an answer. It's called provenance. And it's elliptic curves all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm provenance in one minute
&lt;/h2&gt;

&lt;p&gt;When a package is published from CI with provenance, a few things happen in a couple of seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CI job proves who it is to &lt;strong&gt;Fulcio&lt;/strong&gt;, Sigstore's certificate authority, with its OIDC token.&lt;/li&gt;
&lt;li&gt;Fulcio issues a certificate for a brand-new key. The certificate says &lt;em&gt;"this key belongs to &lt;code&gt;release.yml&lt;/code&gt; in repo X, at commit Y"&lt;/em&gt;. It is valid for &lt;strong&gt;ten minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The job signs a statement: &lt;em&gt;"package Z, version V, has this SHA-512"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The signature and certificate go into &lt;strong&gt;Rekor&lt;/strong&gt;, a public append-only log. Rekor signs a promise: &lt;em&gt;"I saw this, at this time"&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's my own release from last night, read straight from the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leaf key:  ec { namedCurve: 'prime256v1' }      // P-256
valid:     Nov  1 23:13:03 2026 GMT -&amp;gt; Nov  1 23:23:03 2026 GMT   // 10 minutes
issuer:    O=sigstore.dev, CN=sigstore-intermediate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A key that lives for ten minutes and is then thrown away. The trust comes from the log, not from keeping a secret for years.&lt;/p&gt;

&lt;p&gt;Count the curves. The registry's own signature on every package: &lt;strong&gt;P-256&lt;/strong&gt;. The build's ten-minute key: &lt;strong&gt;P-256&lt;/strong&gt;. Rekor's key: &lt;strong&gt;P-256&lt;/strong&gt;. (Fulcio's CA uses its bigger sibling, P-384.)&lt;/p&gt;

&lt;p&gt;My diploma, signing my &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one line that isn't the hard part
&lt;/h2&gt;

&lt;p&gt;Here is the elliptic-curve part of croak's verifier:&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;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. &lt;code&gt;node:crypto&lt;/code&gt; does the curve math, and it does it better than I would. No point addition by hand, no hand-rolled anything. In 2026, writing your own ECDSA would be a red flag, not a skill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All the hard parts moved into the plumbing around that line.&lt;/strong&gt; A signature verifies &lt;em&gt;something&lt;/em&gt;. The question is whether it is the thing you think. croak's check, in order:&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;logged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readLogEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&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;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rekor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1. Rekor promised it saw this, and when&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;leafOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;checkCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fulcio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loggedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2. Fulcio issued it, valid at that moment&lt;/span&gt;
&lt;span class="c1"&gt;// 3. the envelope's signature verifies with the leaf key&lt;/span&gt;
&lt;span class="c1"&gt;// 4. the log entry is for this exact signature, certificate and payload&lt;/span&gt;
&lt;span class="nf"&gt;checkSubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                        &lt;span class="c1"&gt;// 5. the statement is about THIS tarball&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fulcioIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leaf&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things I only learned by building it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time comes from the log.&lt;/strong&gt; The certificate expired ten minutes after the build. Checking it against &lt;em&gt;now&lt;/em&gt; would fail every package ever published. So the question is "was it valid when Rekor logged it?", and that time is only trusted because Rekor signed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The statement can lie, the certificate can't.&lt;/strong&gt; The signed statement includes build details, but the publisher wrote those. &lt;em&gt;Who built it&lt;/em&gt; has to come from the certificate Fulcio issued. Node verifies certificate chains but won't show you Fulcio's custom extensions, so croak has about 170 lines of DER parsing to read them. Every length is checked against the bytes actually there, because the certificate comes from the registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signature is over an envelope, not the payload.&lt;/strong&gt; DSSE signs &lt;code&gt;"DSSEv1 &amp;lt;type length&amp;gt; &amp;lt;type&amp;gt; &amp;lt;payload length&amp;gt; &amp;lt;payload&amp;gt;"&lt;/code&gt;. Sign the raw JSON instead and a valid signature could be reinterpreted as a different type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin, don't fetch.&lt;/strong&gt; The Fulcio and Rekor keys are pinned in the code. Fetching them from the same place as the package would just ask the server to vouch for itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I can't tell" is not a pass.&lt;/strong&gt; A bundle shape croak doesn't support, like a newer Rekor log, comes back &lt;code&gt;unverifiable&lt;/code&gt;, not &lt;code&gt;verified&lt;/code&gt;. Only a bundle that claims something its signatures don't back is &lt;code&gt;invalid&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pointing it at itself
&lt;/h2&gt;

&lt;p&gt;Last night I tagged croak 0.1.4. The release workflow built it, packed it once, and published that exact tarball with provenance. Then I asked croak, through its own verifier, about itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;latest: 0.1.4
0.1.4: verified https://github.com/vola-trebla/croak/.github/workflows/release.yml@refs/tags/v0.1.4
       @ a598a5863c9146429859323e2858421e9db16f21 logged 2026-11-01T23:13:03.000Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow file, the tag, the commit. Logged at 23:13:03, the same second the ten-minute certificate was born.&lt;/p&gt;

&lt;p&gt;Then the check that matters: the same signature, but compared against the previous release's tarball hash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.1.4 bundle against 0.1.3's tarball: invalid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real signature over the wrong bytes. That's the tampered-tarball case, and it has to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The irony
&lt;/h2&gt;

&lt;p&gt;The first time croak looked at itself, it flagged itself.&lt;/p&gt;

&lt;p&gt;Version 0.1.0 shipped with my source comments in it. One of those comments explained the obfuscation detector by quoting the pattern it hunts for, &lt;code&gt;globalThis[key](…)&lt;/code&gt;. croak scanned its own installed files and found "obfuscated code".&lt;/p&gt;

&lt;p&gt;The fix was one compiler flag, &lt;code&gt;removeComments&lt;/code&gt;. Now CI installs the packed tarball and runs croak on it before every release. A security tool that can't pass its own scan doesn't get published.&lt;/p&gt;

&lt;h2&gt;
  
  
  What provenance does not prove
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to oversell.&lt;/p&gt;

&lt;p&gt;Provenance proves &lt;strong&gt;where a tarball came from&lt;/strong&gt;: this workflow, this repo, this commit. It does not prove &lt;strong&gt;the code is safe&lt;/strong&gt;. A malicious maintainer can publish malware with perfect provenance from their own CI.&lt;/p&gt;

&lt;p&gt;So in croak it's one signal among many, and it's used as a comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A package that &lt;strong&gt;used to&lt;/strong&gt; have provenance and suddenly doesn't, in a release that runs an install script? That's what a stolen token looks like. 🔴&lt;/li&gt;
&lt;li&gt;Provenance from a &lt;strong&gt;different repository&lt;/strong&gt; than the version before? Worth a hard look. 🟡 With a new install script on top: 🔴&lt;/li&gt;
&lt;li&gt;Provenance that verifies, but for another version or another tarball? A replay. 🔴&lt;/li&gt;
&lt;li&gt;No provenance ever? Most packages. Not suspicious on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you should do today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check your installs:&lt;/strong&gt; &lt;code&gt;npm audit signatures&lt;/code&gt; verifies registry signatures and provenance for what's in your lockfile. It's already in npm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you publish:&lt;/strong&gt; use trusted publishing from CI (&lt;code&gt;npm trust github &amp;lt;pkg&amp;gt; --file release.yml --repo you/repo&lt;/code&gt;). No long-lived token to steal, provenance for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pack once, publish what you tested.&lt;/strong&gt; Don't let &lt;code&gt;npm publish&lt;/code&gt; repack after your tests ran.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read provenance as "where", never as "safe".&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If an AI agent installs packages for you:&lt;/strong&gt; put a gate in front of it. croak is one: &lt;code&gt;npm i -g @vola-trebla/croak&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thirteen years ago I thought that diploma was the end of elliptic curves for me. Turns out they were just waiting for &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;croak is open source and early: &lt;a href="https://github.com/vola-trebla/croak" rel="noopener noreferrer"&gt;github.com/vola-trebla/croak&lt;/a&gt;. Issues, and attacks it misses, are welcome.&lt;/p&gt;

&lt;p&gt;🐸&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>npm</category>
      <category>cryptography</category>
    </item>
    <item>
      <title>OpenTelemetry JS 3.0 just shipped. I tested both lines side by side. Here's what broke.</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:31:34 +0000</pubDate>
      <link>https://dev.to/vola-trebla/opentelemetry-js-30-just-shipped-i-tested-both-lines-side-by-side-heres-what-broke-4o44</link>
      <guid>https://dev.to/vola-trebla/opentelemetry-js-30-just-shipped-i-tested-both-lines-side-by-side-heres-what-broke-4o44</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; - On October 20, OpenTelemetry JS shipped &lt;code&gt;@opentelemetry/api&lt;/code&gt; 1.10 and SDK 3.0. I was building a library that has to work with &lt;em&gt;whatever&lt;/em&gt; OTel setup an app already has, so I ran the same test suite against both lines. I found three breaking changes, a peer range split that one dependency can't span, an app quietly running &lt;strong&gt;two copies of the OTel API&lt;/strong&gt;, and a Node 24 behavior that contradicts the Node 24 docs. Everything below is reproduced in code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why I had to care about both lines
&lt;/h2&gt;

&lt;p&gt;I'm building a test library for AI agents. It records OpenTelemetry &lt;code&gt;gen_ai.*&lt;/code&gt; spans while a test runs and asserts on what the agent did. The hard requirement: &lt;strong&gt;never break the app's own telemetry.&lt;/strong&gt; No replaced providers, no fighting over globals, and the app should export exactly the same spans with or without my library.&lt;/p&gt;

&lt;p&gt;That means I don't get to pick an OTel version. The app picks it. And as of this week, apps are split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;SDK&lt;/th&gt;
&lt;th&gt;&lt;code&gt;sdk-node&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Legacy&lt;/td&gt;
&lt;td&gt;≤ 1.9&lt;/td&gt;
&lt;td&gt;2.x (2.12)&lt;/td&gt;
&lt;td&gt;0.2xx (0.223)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;0.300&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I wrote a spike that runs the same 27 scenarios on both lines, with Vercel AI SDK 7 as the instrumentation. Here's what came out.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Three breaking changes in SDK 3.0
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;SimpleSpanProcessor&lt;/code&gt; takes an options object
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SDK 2.x&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// SDK 3.0&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;exporter&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript catches this. Plain JavaScript doesn't. With the old call shape on 3.0, nothing throws at construction. Spans just never arrive, and the only trace of the problem is a diag log entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'export')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;forceFlush()&lt;/code&gt; then fails with the same error for &lt;code&gt;forceFlush&lt;/code&gt;. If your diag logger is off, your traces silently vanish.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;sdk-trace-base&lt;/code&gt; became &lt;code&gt;sdk-trace&lt;/code&gt;, and the provider was renamed
&lt;/h3&gt;

&lt;p&gt;The 3.0 tracing SDK lives in a new package, &lt;code&gt;@opentelemetry/sdk-trace&lt;/code&gt;. The old &lt;code&gt;@opentelemetry/sdk-trace-base&lt;/code&gt; tops out at 2.12, which is already a thin shim re-exporting from the new package. In 3.0, &lt;code&gt;BasicTracerProvider&lt;/code&gt; is gone. The class is now &lt;code&gt;TracerProvider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SDK 2.x&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BasicTracerProvider&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="s1"&gt;@opentelemetry/sdk-trace-base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// SDK 3.0&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TracerProvider&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="s1"&gt;@opentelemetry/sdk-trace&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;h3&gt;
  
  
  &lt;code&gt;sdk-node&lt;/code&gt; dropped its re-exports
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sdk-node&lt;/code&gt; 0.2xx re-exported the whole SDK as namespaces, so a lot of setup code looks like 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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodeSDK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tracing&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="s1"&gt;@opentelemetry/sdk-node&lt;/span&gt;&lt;span class="dl"&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;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InMemorySpanExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 0.300, the package exports exactly two things: &lt;code&gt;NodeSDK&lt;/code&gt; and &lt;code&gt;startNodeSDK&lt;/code&gt;. &lt;code&gt;tracing&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, so the line above throws &lt;code&gt;Cannot read properties of undefined (reading 'InMemorySpanExporter')&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. One dependency can't span both lines
&lt;/h2&gt;

&lt;p&gt;These are the peer ranges as published:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;@opentelemetry/api&lt;/code&gt; peer range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sdk-trace-base&lt;/code&gt; 2.12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.3.0 &amp;lt;1.10.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sdk-node&lt;/code&gt; 0.223&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.3.0 &amp;lt;1.10.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sdk-trace&lt;/code&gt; 3.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.10.0 &amp;lt;1.11.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sdk-node&lt;/code&gt; 0.300&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.10.0 &amp;lt;1.11.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;context-async-hooks&lt;/code&gt; 3.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=1.0.0 &amp;lt;1.11.0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ranges don't overlap. If a library depends on &lt;em&gt;any&lt;/em&gt; SDK package, it supports exactly one line, and every app on the other line gets peer warnings or a second SDK.&lt;/p&gt;

&lt;p&gt;My fix was to depend on no SDK at all. The library ships a small in-memory &lt;code&gt;Tracer&lt;/code&gt; written against &lt;code&gt;@opentelemetry/api&lt;/code&gt; only, and takes the API as a &lt;strong&gt;peer&lt;/strong&gt; dependency, so it always shares the app's copy. That last part turned out to matter more than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your app may be running two copies of the OTel API
&lt;/h2&gt;

&lt;p&gt;Vercel's &lt;code&gt;@ai-sdk/otel&lt;/code&gt; 1.0.150 lists the API as a regular dependency, pinned to an exact version:&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;"dependencies"&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;"@opentelemetry/api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.9.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;Put that in an app on the modern line and you get two copies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/@opentelemetry/api                                   → 1.10.0  (the app)
node_modules/.pnpm/@ai-sdk+otel@…/node_modules/@opentelemetry/api → 1.9.1   (@ai-sdk/otel)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It still works, and the reason is worth knowing. OTel keeps its globals (tracer provider, context manager, propagator) on a shared global object, and every API copy checks the registered version before using it. Within a major version, the rule in &lt;code&gt;api/src/internal/semver.ts&lt;/code&gt; boils down to:&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;// A copy may use a global only if the global was registered by the same or a NEWER minor.&lt;/span&gt;
&lt;span class="nx"&gt;ownVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minor&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;globalVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an older copy can read globals registered by a newer one, but not the other way around. I checked both directions with two API copies side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Global registered by&lt;/th&gt;
&lt;th&gt;Read by&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API 1.10&lt;/td&gt;
&lt;td&gt;API 1.9&lt;/td&gt;
&lt;td&gt;✅ sees the registered context manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 1.9&lt;/td&gt;
&lt;td&gt;API 1.10&lt;/td&gt;
&lt;td&gt;❌ gets a &lt;strong&gt;no-op&lt;/strong&gt; manager, the value is silently lost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The modern line plus &lt;code&gt;@ai-sdk/otel&lt;/code&gt; is the safe direction: the app registers with 1.10, and the SDK reads with 1.9. Now flip it. An app on the legacy line registers with 1.9, and any library that brings its own 1.10 copy gets no-ops. It throws no error and logs no warning. Its spans and context just aren't there.&lt;/p&gt;

&lt;p&gt;Check what you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm why @opentelemetry/api   &lt;span class="c"&gt;# or: npm ls @opentelemetry/api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you maintain a library: &lt;strong&gt;make &lt;code&gt;@opentelemetry/api&lt;/code&gt; a peer dependency, never a direct one.&lt;/strong&gt; That's how you guarantee one copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Node 24: &lt;code&gt;AsyncLocalStorage.disable()&lt;/code&gt; doesn't do what the docs say
&lt;/h2&gt;

&lt;p&gt;OTel's context manager for Node is a thin wrapper around &lt;code&gt;AsyncLocalStorage&lt;/code&gt;, and its &lt;code&gt;disable()&lt;/code&gt; calls &lt;code&gt;AsyncLocalStorage.disable()&lt;/code&gt;. I wrote my own small manager to drop the &lt;code&gt;context-async-hooks&lt;/code&gt; dependency (its peer range caps the API below 1.11). CI went green on Node 22 and red on Node 24.&lt;/p&gt;

&lt;p&gt;Here's a minimal reproduction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&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="s1"&gt;node:async_hooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;sleep&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="s1"&gt;node:timers/promises&lt;/span&gt;&lt;span class="dl"&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;als&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AsyncLocalStorage&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;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;als&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ctx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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;als&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&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="nx"&gt;als&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node 22.23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 24.22&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'ctx'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 24.22 with &lt;code&gt;--no-async-context-frame&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Node 24 docs say that after &lt;code&gt;disable()&lt;/code&gt;, "all current contexts linked to the instance will be exited." On Node 24, an operation that's already in flight keeps its store anyway. The cause is &lt;code&gt;AsyncContextFrame&lt;/code&gt;, which became the default &lt;code&gt;AsyncLocalStorage&lt;/code&gt; implementation in Node 24. Turn it off and you get the documented behavior back.&lt;/p&gt;

&lt;p&gt;Who cares? Mostly test suites. A common pattern is &lt;code&gt;afterEach(() =&amp;gt; context.disable())&lt;/code&gt;. On Node 24, async work that's still running from the previous test can keep its context. My manager now keeps its own &lt;code&gt;disabled&lt;/code&gt; flag, so &lt;code&gt;active()&lt;/code&gt; returns &lt;code&gt;ROOT_CONTEXT&lt;/code&gt; after &lt;code&gt;disable()&lt;/code&gt; on every runtime, until the next &lt;code&gt;with()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NodeSDK&lt;/code&gt; + &lt;code&gt;SimpleSpanProcessor&lt;/code&gt; exports asynchronously.&lt;/strong&gt; It waits for async resource detection before exporting. A test that reads an &lt;code&gt;InMemorySpanExporter&lt;/code&gt; right after a span ends sees zero spans. Call &lt;code&gt;processor.forceFlush()&lt;/code&gt; first. I lost a while on this one: my "app exports the same spans" check reported a baseline of 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm 12 won't install anything published less than a day ago.&lt;/strong&gt; &lt;code&gt;ai@7.0.153&lt;/code&gt; came out the same afternoon I needed it, and I got &lt;code&gt;ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION&lt;/code&gt;. Don't bypass it; pin the previous release. That's the supply-chain policy working as intended.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run &lt;code&gt;pnpm why @opentelemetry/api&lt;/code&gt; and confirm a single copy, or at least that the &lt;strong&gt;app's&lt;/strong&gt; copy is the newest one.&lt;/li&gt;
&lt;li&gt;[ ] If you write a library, make the OTel API a peer dependency and don't depend on an SDK package unless you must.&lt;/li&gt;
&lt;li&gt;[ ] Moving to SDK 3.0: switch to &lt;code&gt;new SimpleSpanProcessor({ exporter })&lt;/code&gt;, &lt;code&gt;@opentelemetry/sdk-trace&lt;/code&gt; and &lt;code&gt;TracerProvider&lt;/code&gt;, and stop importing &lt;code&gt;tracing&lt;/code&gt; from &lt;code&gt;sdk-node&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Turn on the diag logger in tests (&lt;code&gt;diag.setLogger&lt;/code&gt;). Silent export failures only show up there.&lt;/li&gt;
&lt;li&gt;[ ] On Node 24, don't rely on &lt;code&gt;AsyncLocalStorage.disable()&lt;/code&gt; to cut off in-flight async work.&lt;/li&gt;
&lt;li&gt;[ ] Compare the &lt;strong&gt;shape&lt;/strong&gt; of exported spans (names, parents, attributes), not just their names. Name-only checks let a changed parent slip through. I learned that from my own code review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;All of this came out of the first spike for &lt;strong&gt;toadwright&lt;/strong&gt; 🐸, a Playwright-style test library for AI agents. You assert on the agent's trajectory (tools called, order, arguments, cost), and every test runs N times, because one green run proves nothing. It's early. The spike, the matrix that runs both OTel lines in CI, and the design decision behind it are public: &lt;a href="https://github.com/vola-trebla/toadwright/blob/main/docs/adr/0001-span-collection-and-isolation.md" rel="noopener noreferrer"&gt;ADR-001&lt;/a&gt; · &lt;a href="https://github.com/vola-trebla/toadwright" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next post will be about the part that actually matters: what happens when you run the same agent test ten times.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you hit the two-copies problem in a real app? I'd love to hear how it showed up.&lt;/em&gt; 🐸&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>javascript</category>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My security tool aced its own exam. Then I ran it on real repos</title>
      <dc:creator>Albert Alov</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:25:31 +0000</pubDate>
      <link>https://dev.to/vola-trebla/my-security-tool-aced-its-own-exam-then-i-ran-it-on-real-repos-28c</link>
      <guid>https://dev.to/vola-trebla/my-security-tool-aced-its-own-exam-then-i-ran-it-on-real-repos-28c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; - I'm building &lt;strong&gt;croak&lt;/strong&gt; 🐸, a tool that checks what a repo or a package install would do &lt;em&gt;before&lt;/em&gt; any of its code runs. On my own eval it caught every attack with zero false alarms. On its first run against 13 popular open-source repos, it flagged &lt;strong&gt;zod&lt;/strong&gt; and &lt;strong&gt;esbuild&lt;/strong&gt; as malicious. This post is about why a self-written eval wasn't enough, and the two changes that real code forced.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem croak is for
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; doesn't just download code. It &lt;strong&gt;runs&lt;/strong&gt; it: &lt;code&gt;postinstall&lt;/code&gt; scripts, native builds, whatever a package decides to do. Opening a folder in VS Code can run a task too. And AI coding agents now do both all day, usually without reading anything first.&lt;/p&gt;

&lt;p&gt;So I'm building a gate that inspects a repo as plain data and gives a verdict before anything executes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;allow&lt;/strong&gt;: nothing suspicious found (not a guarantee)&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;warn&lt;/strong&gt;: behavior that needs your attention (never "safe")&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;block&lt;/strong&gt;: strong evidence of an attack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's early-stage. Seven detectors so far: install scripts, VS Code autorun tasks, hidden code, obfuscation, dangerous git config, committed &lt;code&gt;node_modules&lt;/code&gt;, and files too large to scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eval first
&lt;/h2&gt;

&lt;p&gt;I wrote the eval before the first detector. Every case is a small fixture repo with an expected verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"d-postinstall-curl-pipe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"threat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;// fake "test assignment" repos&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expect"&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&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="s2"&gt;"repo/lifecycle-script"&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;Payloads are inert: they point at reserved &lt;code&gt;.invalid&lt;/code&gt; domains or just &lt;code&gt;echo&lt;/code&gt;. Two rules turned out to matter a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Every attack gets a benign twin.&lt;/strong&gt; A &lt;code&gt;postinstall&lt;/code&gt; that pipes &lt;code&gt;curl&lt;/code&gt; into &lt;code&gt;sh&lt;/code&gt; sits next to one that runs &lt;code&gt;prisma generate&lt;/code&gt;. A 413-character line hiding &lt;code&gt;eval&lt;/code&gt; after 320 spaces sits next to an honest 2,875-character Tailwind safelist. Without twins, a detector that just flags "has a postinstall" or "has a long line" looks perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You can't let the eval be gamed.&lt;/strong&gt; My review agent wrote a detector that matched only the &lt;code&gt;SYNTHETIC&lt;/code&gt; marker comment in my fixtures. It scored &lt;strong&gt;4/4 with zero false alarms&lt;/strong&gt;, because only the malicious fixtures had the marker. Now every fixture, benign or malicious, carries it (a test enforces that), and a lint rule forbids detector code from mentioning it.&lt;/p&gt;

&lt;p&gt;After a few iterations the eval looked great:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;croak eval — 32 cases (16 malicious, 16 benign)
  flag recall   16/16  100%
  false alarms   0/16    0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote every one of those 32 cases myself. That's the catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real run
&lt;/h2&gt;

&lt;p&gt;I pinned 13 popular repositories by commit (axios, express, zod, hono, ky, husky, chalk, create-t3-app, trpc, esbuild, sharp, vscode-extension-samples, vue core) and scanned them. 7,455 files took about one second.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Repos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟢&lt;/td&gt;
&lt;td&gt;express, hono, husky, chalk, create-t3-app, sharp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟡&lt;/td&gt;
&lt;td&gt;axios, ky, trpc, vscode-extension-samples, vue core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;zod, esbuild&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;zod and esbuild are not malware. Two false blocks on the first real run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: "near" means near
&lt;/h2&gt;

&lt;p&gt;Both blocks came from the obfuscation detector:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nx"&gt;zod&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;zod&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;v3&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;types&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="mi"&gt;607&lt;/span&gt;  &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;obfuscation&lt;/span&gt;
        &lt;span class="nx"&gt;obfuscated&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;escapes &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;607&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;decoding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 607 of zod is a Unicode regex: dense &lt;code&gt;\u…&lt;/code&gt; escapes, a &lt;em&gt;weak&lt;/em&gt; signal I deliberately don't treat as obfuscation on its own. But the file also calls &lt;code&gt;atob()&lt;/code&gt; for base64 validation, thousands of lines away. My rule said "weak signal + eval/decoding in the file → block". The file was the wrong unit.&lt;/p&gt;

&lt;p&gt;The fix: a weak signal only escalates when eval or decoding is within three lines of it. &lt;code&gt;const font = "…base64…"; eval(atob(font))&lt;/code&gt; on adjacent lines still blocks. A regex and an unrelated &lt;code&gt;atob()&lt;/code&gt; in different corners of a big file don't. Strong signals (&lt;code&gt;_0x&lt;/code&gt; identifiers, string-array rotation, calls through computed globals) keep the file-wide rule.&lt;/p&gt;

&lt;p&gt;My synthetic fixtures were all small files. That bug couldn't exist in my eval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: a warning everyone ignores is worse than none
&lt;/h2&gt;

&lt;p&gt;The second finding wasn't a bug. Seven of the 13 repos got 🟡 because their install scripts ran things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare: husky
prepare: npm run build      → tsc
postinstall: prisma generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically true: code runs on install. In practice it's noise, and noise trains people to skip the yellow line, which defeats the tool.&lt;/p&gt;

&lt;p&gt;The new rule is strict on purpose. A script is only an info note when &lt;strong&gt;every&lt;/strong&gt; command is a known tool, or an &lt;code&gt;npm run&lt;/code&gt; call to a script that is itself routine, and there is &lt;strong&gt;nothing else&lt;/strong&gt;: no network, no local files, no inline code, no suspicious code signals. Anything unknown stays 🟡. So &lt;code&gt;cd client &amp;amp;&amp;amp; npm install&lt;/code&gt; (a nested install runs other packages' scripts) is still a warning, and &lt;code&gt;prisma generate &amp;amp;&amp;amp; curl … | bash&lt;/code&gt; is still a block. Recognizing a known tool never launders the rest of the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Real-world corpus&lt;/th&gt;
&lt;th&gt;First run&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🔴 blocks&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repos at 🟡&lt;/td&gt;
&lt;td&gt;7 / 13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 / 13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warnings&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 26 warnings that remain are all explainable: nested installs, an unknown tool that downloads files at install time, install scripts that run local code. That's exactly what 🟡 is for.&lt;/p&gt;

&lt;p&gt;The synthetic eval didn't move: still 100% flagged, zero false alarms. That's the point. Both kinds of runs have to pass at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself a week ago
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A synthetic eval proves your detectors match your imagination.&lt;/strong&gt; It's necessary, but on its own it isn't enough. Pin a few real repos and run them on every significant change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the benign twin first.&lt;/strong&gt; If you can't describe what honest code looks like, you can't tell your detector what not to flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume your eval can be gamed,&lt;/strong&gt; and check whether it can, ideally with a reviewer that has no stake in the score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat noise as a defect,&lt;/strong&gt; not a cost of being careful.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What croak doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;It doesn't follow &lt;code&gt;require&lt;/code&gt; chains past the file a script runs directly. It doesn't check package installs yet (that's next), it isn't an antivirus, and there's no AI layer yet. When one comes, it will only be allowed to &lt;em&gt;raise&lt;/em&gt; a verdict, never lower one. All of that is written down in the repo, because a security tool that oversells itself is its own kind of vulnerability.&lt;/p&gt;

&lt;p&gt;croak is TypeScript with zero runtime dependencies, and early-stage. The repo is going public soon: [link]. I'll write about the install check and the cryptography chapter (it starts with my 2013 thesis) in later posts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading. If you've built evals for detectors or LLM features, I'd love to hear how you keep them honest.&lt;/em&gt; 🐸&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <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>
  </channel>
</rss>
