<?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: kyb8801</title>
    <description>The latest articles on DEV Community by kyb8801 (@kyb8801).</description>
    <link>https://dev.to/kyb8801</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3903877%2F554a226b-6f2e-4b7a-8745-6ce0bc83c820.png</url>
      <title>DEV Community: kyb8801</title>
      <link>https://dev.to/kyb8801</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kyb8801"/>
    <language>en</language>
    <item>
      <title>24h after my 5-posts-per-day experiment on dev.to: 11 views total. The platform was right.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Fri, 22 May 2026 08:24:32 +0000</pubDate>
      <link>https://dev.to/kyb8801/24h-after-my-5-posts-per-day-experiment-on-devto-11-views-total-the-platform-was-right-3abk</link>
      <guid>https://dev.to/kyb8801/24h-after-my-5-posts-per-day-experiment-on-devto-11-views-total-the-platform-was-right-3abk</guid>
      <description>&lt;h1&gt;
  
  
  24h after the 5-posts-per-day experiment: every post is at zero. The platform was right.
&lt;/h1&gt;

&lt;p&gt;Yesterday I posted five articles to dev.to in a 24-hour window to test what "consistency wins" actually means. Public results, 24 hours after the first post went up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Views&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Shipped: first MCP server for ISO 10012:2026&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;I Put 7 Products on Gumroad. 4 of Them Had No Files.&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;I Did Not Write New Code This Week. (README rewrite)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;What an MCP server actually returns: CD-SEM 45 nm&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;sympy.parse_expr will run os.system if you let it&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;11 views total. One post got distributed. The other four did not exist as far as the dev.to algorithm was concerned.&lt;/p&gt;

&lt;p&gt;I also published a meta-post the same day analyzing the multi-post-per-day dampening. That meta-post is also at zero views.&lt;/p&gt;

&lt;p&gt;The platform has been telling me one thing for years: post once a day, consistently, and let it cook. I had assumed that meant "post often." It meant the opposite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "consistency wins" actually means on dev.to
&lt;/h2&gt;

&lt;p&gt;Each post you publish goes into an algorithmic surfacing queue. The queue has a per-author budget. Empirically, that budget appears to be roughly one post per day. If you submit five posts in 24 hours, four of them get processed but never surfaced — they go into your feed and into the public list ordered by published_at, but they do not enter the home-page recommendation rotation.&lt;/p&gt;

&lt;p&gt;This is the same pattern Twitter, Instagram, and YouTube run. Multi-post-per-day is treated as a low-quality signal and depressed accordingly. dev.to is more lenient than YouTube but still penalizes the pattern hard.&lt;/p&gt;

&lt;p&gt;What I would have done if I had read the dev.to forem source code more carefully:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One post per day, no exceptions&lt;/li&gt;
&lt;li&gt;Schedule batches via the API's &lt;code&gt;published_at&lt;/code&gt; future date rather than publishing all at once&lt;/li&gt;
&lt;li&gt;Tag combinations that cluster around one main tag rather than spraying across &lt;code&gt;#showdev&lt;/code&gt;, &lt;code&gt;#mcp&lt;/code&gt;, &lt;code&gt;#anthropic&lt;/code&gt;, &lt;code&gt;#metrology&lt;/code&gt;, &lt;code&gt;#indiehackers&lt;/code&gt;, &lt;code&gt;#gumroad&lt;/code&gt;, &lt;code&gt;#sideprojects&lt;/code&gt;, &lt;code&gt;#python&lt;/code&gt;, &lt;code&gt;#writing&lt;/code&gt;, &lt;code&gt;#security&lt;/code&gt; (this was the actual tag set I used across the 5 posts — way too scattered)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What works in my data so far
&lt;/h2&gt;

&lt;p&gt;The only post that got distribution (the 11-view README rewrite story) had three things going for it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It was the longest&lt;/strong&gt; (~7 min read). dev.to weighs reading time as a quality proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was on a recognizable topic class&lt;/strong&gt; — README rewriting as a build-in-public lesson. The narrative arc is familiar to indie builders, who are dev.to's main audience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was a Sunday evening post&lt;/strong&gt; (~22:00 KST = ~13:00 UTC). Sunday traffic on dev.to is heavily indie/builder, which matches the content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four posts that got zero had one or more of these issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posted within 4 hours of each other (sequence dampening)&lt;/li&gt;
&lt;li&gt;Highly specific niche tags (&lt;code&gt;#metrology&lt;/code&gt;, &lt;code&gt;#anthropic&lt;/code&gt;) that filter audience&lt;/li&gt;
&lt;li&gt;Short read time (1-3 min) which the algorithm undervalues&lt;/li&gt;
&lt;li&gt;A Show-Off style post about an MCP server, which is interesting to a sub-1% of dev.to readers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I will do today, day 2
&lt;/h2&gt;

&lt;p&gt;One post. This one. Posted ~24 hours after the first of yesterday's batch, well outside the multi-post-per-day window. Then nothing tomorrow. Then one post on Friday.&lt;/p&gt;

&lt;p&gt;I will publish the next stats post in 7 days. If this post gets 10x the average view count of yesterday's batch (target: ≥100 views in 7 days), the one-per-day rule is confirmed working for this account. If it gets &amp;lt;50 views, the issue is either the topic class or my account standing — different fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  For other indie devs running into the same wall
&lt;/h2&gt;

&lt;p&gt;Public summary of the lesson, no caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-post-per-day on dev.to does not work.&lt;/strong&gt; Verified, 5 posts → 11 views.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The first post you publish each day is the only one the algorithm sees.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-post strategy&lt;/strong&gt; (Medium ↔ dev.to ↔ Hashnode) needs &lt;code&gt;canonical_url&lt;/code&gt; set on dev.to so SEO consolidates on the source-of-truth, not on the cross-post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;series&lt;/code&gt; field&lt;/strong&gt; on dev.to does internal link-building for free. Use it for any post group you would otherwise publish as a series of articles. I did not, and the four lost posts had no upstream funnel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are about to ship something and you think "I'll post 5 times to maximize reach," you will get exactly the opposite of what you hoped for.&lt;/p&gt;




&lt;p&gt;If you want to see the actual product the dev.to posts were about: github.com/kyb8801/measurement-uncertainty-mcp · MCP server for ISO 10012:2026 calibration uncertainty. Live at measurement-uncertainty.mcpize.run. Excel companion on Gumroad: kyb8801.gumroad.com/l/gum-toolkit.&lt;/p&gt;

&lt;p&gt;I'll publish next stats in 7 days.&lt;/p&gt;

</description>
      <category>devto</category>
      <category>writing</category>
      <category>buildinpublic</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I published 5 dev.to posts in 24 hours about my MCP server. Here's exactly what each one got.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Tue, 19 May 2026 02:59:47 +0000</pubDate>
      <link>https://dev.to/kyb8801/i-published-5-devto-posts-in-24-hours-about-my-mcp-server-heres-exactly-what-each-one-got-4g8g</link>
      <guid>https://dev.to/kyb8801/i-published-5-devto-posts-in-24-hours-about-my-mcp-server-heres-exactly-what-each-one-got-4g8g</guid>
      <description>&lt;h1&gt;
  
  
  I published 5 dev.to posts in 24 hours about my MCP server. Here's exactly what each one got.
&lt;/h1&gt;

&lt;p&gt;A test of the "consistency wins on dev.to" advice. Five posts in 24 hours about the same MCP server. Same author. Different angles. Public stats below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The raw numbers (May 19, 2026, ~24h after the first post)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Published&lt;/th&gt;
&lt;th&gt;Title (truncated)&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Views&lt;/th&gt;
&lt;th&gt;Reactions&lt;/th&gt;
&lt;th&gt;Comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2026-05-18&lt;/td&gt;
&lt;td&gt;Shipped: first MCP server for ISO 10012:2026&lt;/td&gt;
&lt;td&gt;1 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2026-05-18&lt;/td&gt;
&lt;td&gt;I Put 7 Products on Gumroad. 4 of Them Had No Files.&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2026-05-18&lt;/td&gt;
&lt;td&gt;I Did Not Write New Code This Week. (README rewrite)&lt;/td&gt;
&lt;td&gt;7 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2026-05-19&lt;/td&gt;
&lt;td&gt;What an MCP server actually returns: CD-SEM 45 nm&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2026-05-19&lt;/td&gt;
&lt;td&gt;sympy.parse_expr will run os.system if you let it&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: 11 views, 0 reactions, 0 comments. One post (the longest one, posted first) got 11 views. The other four got zero each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data says (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-post-per-day dampening is real.&lt;/strong&gt; Posts #1, #2, #3 went up on the same day with the same author. Only the first (or in this case the longest — posts went up in close succession) got any algorithmic distribution. Posts #2 and #3 received essentially zero algorithmic surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same pattern on day 2.&lt;/strong&gt; Posts #4 and #5 on May 19 are both still at zero a few hours after publish. Insufficient time to be sure, but the day-1 pattern strongly suggests dev.to caps the daily algorithm budget per author somewhere around "one post."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 7-minute post outperformed.&lt;/strong&gt; Longer reading time correlates with surfacing in this sample. n is too small to be a real signal but the post that got distribution was 7 minutes; the others were 1–4 minutes. Worth testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No tag was magic.&lt;/strong&gt; I tried &lt;code&gt;#showdev&lt;/code&gt;, &lt;code&gt;#mcp&lt;/code&gt;, &lt;code&gt;#anthropic&lt;/code&gt;, &lt;code&gt;#metrology&lt;/code&gt;, &lt;code&gt;#indiehackers&lt;/code&gt;, &lt;code&gt;#gumroad&lt;/code&gt;, &lt;code&gt;#sideprojects&lt;/code&gt;, &lt;code&gt;#python&lt;/code&gt;, &lt;code&gt;#writing&lt;/code&gt;, &lt;code&gt;#security&lt;/code&gt;. Combinations of 4 each. None of them produced organic discovery in 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will change next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One post per day, not three.&lt;/strong&gt; The data above is the experiment, and the experiment says don't do this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote on Mondays / Tuesdays.&lt;/strong&gt; Saturday/Sunday posts are a known weak window on dev.to. Posts #1–#3 went up on a Sunday (KST).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the canonical_url field.&lt;/strong&gt; All five posts were original-on-dev.to. Cross-posting from Medium with &lt;code&gt;canonical_url&lt;/code&gt; set still gets you onto dev.to algorithm surface area while consolidating SEO juice on the canonical. I did not do this and probably should have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start a series.&lt;/strong&gt; The Forem &lt;code&gt;series&lt;/code&gt; field groups related posts. Series posts appear linked at the top of each member, which is essentially free internal-link distribution. I will tag the next batch as a single series.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Five posts. Eleven views. Zero reactions. Zero comments. This is what build-in-public looks like for an indie MCP server in a regulated niche on day one, with no audience pre-built. If you are about to ship and you expected a soft landing because "dev.to is fair," recalibrate. The platform rewards consistency over time. Not five-on-one-day.&lt;/p&gt;

&lt;p&gt;If you are doing the same thing and want to compare notes, the underlying product is a measurement-uncertainty MCP for ISO/IEC 17025 calibration labs. Repo and live MCP below. I'll publish another stats post in 7 days.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;. Live MCP: &lt;code&gt;measurement-uncertainty.mcpize.run&lt;/code&gt;. Weekly build log: &lt;a href="https://yb-ai-hustle.beehiiv.com" rel="noopener noreferrer"&gt;YB's AI Hustle Weekly&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>writing</category>
      <category>showdev</category>
      <category>mcp</category>
    </item>
    <item>
      <title>sympy.parse_expr will run os.system if you let it. Here's the AST gate that stopped me from shipping the RCE.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Tue, 19 May 2026 02:56:02 +0000</pubDate>
      <link>https://dev.to/kyb8801/sympyparseexpr-will-run-ossystem-if-you-let-it-heres-the-ast-gate-that-stopped-me-from-1nbe</link>
      <guid>https://dev.to/kyb8801/sympyparseexpr-will-run-ossystem-if-you-let-it-heres-the-ast-gate-that-stopped-me-from-1nbe</guid>
      <description>&lt;h1&gt;
  
  
  sympy.parse_expr will run os.system if you let it. Here's the AST gate that stopped me from shipping the RCE.
&lt;/h1&gt;

&lt;p&gt;I was building an MCP server that accepts a measurement formula as a string from an LLM, parses it with sympy, and evaluates it via Monte Carlo. Five minutes of integration. Thirteen tests. Twelve passed.&lt;/p&gt;

&lt;p&gt;The thirteenth was a safety test. The formula it passed was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__import__(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;os&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).system(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo PWNED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test expected a &lt;code&gt;ValueError&lt;/code&gt;. Instead I got a test failure message &lt;strong&gt;and&lt;/strong&gt; the string &lt;code&gt;PWNED&lt;/code&gt; printed to my terminal.&lt;/p&gt;

&lt;p&gt;Let me spell that out. &lt;code&gt;sympy.parse_expr&lt;/code&gt;, with the default arguments I was using, &lt;strong&gt;actually invoked &lt;code&gt;os.system&lt;/code&gt; on my machine&lt;/strong&gt;. My own parser, running in my own test process, shelled out and echoed text into my terminal. If I had shipped this to production, any LLM user with a sufficiently creative prompt could have had that same shell-out happen on a Cloud Run instance under my billing account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sympy.parse_expr&lt;/code&gt; is implemented on top of Python's &lt;code&gt;eval()&lt;/code&gt;. When you don't explicitly lock down the global dictionary it evaluates in, &lt;code&gt;eval&lt;/code&gt; inherits the current module's &lt;code&gt;__builtins__&lt;/code&gt;, which includes &lt;code&gt;__import__&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;compile&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, &lt;code&gt;getattr&lt;/code&gt;, and friends. A string that looks like a math expression but references &lt;code&gt;__import__&lt;/code&gt; resolves like a Python expression — and runs.&lt;/p&gt;

&lt;p&gt;This is documented sympy behavior with warnings in the docs. Warnings do not save you when you are moving fast on launch day.&lt;/p&gt;

&lt;p&gt;The vulnerability surface is broader than &lt;code&gt;__import__&lt;/code&gt;. Any of these can give an attacker code execution depending on what is in scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;__import__('os').system(...)&lt;/code&gt; — direct shell out&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__builtins__.__dict__['eval'](...)&lt;/code&gt; — re-enter eval&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(0).__class__.__bases__[0].__subclasses__()&lt;/code&gt; — escape to arbitrary class instances&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getattr(__builtins__, 'open')('/etc/passwd').read()&lt;/code&gt; — file read&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;''.join.__globals__['__builtins__']['exec']('...')&lt;/code&gt; — exec via string method walk&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix that actually works: validate at the AST level &lt;strong&gt;before&lt;/strong&gt; sympy sees the string
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;

&lt;span class="n"&gt;_DANGEROUS_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__import__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__builtins__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__class__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__subclasses__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;globals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;getattr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;setattr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delattr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;_FORBIDDEN_AST_NODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# blocks "os.system" and "obj.__class__"
&lt;/span&gt;    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subscript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# blocks "x[0]" indexing into dunders
&lt;/span&gt;    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GeneratorExp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListComp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictComp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Starred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JoinedStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FormattedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NamedExpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_validate_formula_ast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_FORBIDDEN_AST_NODES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disallowed formula construct: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_DANGEROUS_NAMES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disallowed identifier: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dunder identifiers are not allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate runs before sympy ever sees the string. The whitelist permits exactly what a measurement model needs — arithmetic operators, unary minus, named inputs, and calls into sympy's own math namespace for &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt;. Anything else raises a clean &lt;code&gt;ValueError&lt;/code&gt; that names the offending construct.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ast.parse(..., mode="eval")&lt;/code&gt; is the standard library's free safety net here. It refuses to parse a statement (you cannot pass &lt;code&gt;import os&lt;/code&gt;), and it gives you a typed tree you can walk and filter without ever evaluating anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why blocking &lt;code&gt;ast.Attribute&lt;/code&gt; matters specifically
&lt;/h2&gt;

&lt;p&gt;The most common attack path through &lt;code&gt;sympy.parse_expr&lt;/code&gt; is &lt;strong&gt;attribute access&lt;/strong&gt;: &lt;code&gt;os.system&lt;/code&gt;, &lt;code&gt;().__class__.__bases__[0].__subclasses__()&lt;/code&gt;, and the string-method-walk family. If you block every &lt;code&gt;ast.Attribute&lt;/code&gt; node and every &lt;code&gt;ast.Subscript&lt;/code&gt; node at the parser stage, you cut off basically all known sandbox escapes through the eval-based path.&lt;/p&gt;

&lt;p&gt;The cost is that your accepted formula grammar becomes "arithmetic on bare names + calls to a whitelisted set of math functions." For a measurement uncertainty calculator, that is exactly the grammar you want. For more general DSLs, the whitelist gets longer but the pattern is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the gate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mc_blocks_import_trick&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__import__(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;os&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).system(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;echo PWNED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?i)disallowed|dunder|attribute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estimates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mc_blocks_attribute_walk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(0).__class__.__bases__[0].__subclasses__()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?i)disallowed|dunder|attribute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estimates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mc_allows_real_formula&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(V * R) / (V + I)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Should not raise. The downstream sympy call will lambdify it.
&lt;/span&gt;    &lt;span class="nf"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estimates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;R&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test asserts not only that the call raises, but that the error message contains "disallowed", "dunder", or "attribute" — because a downstream sympy error would indicate the gate failed. The safe version passed. The shell never ran. Test suite ended that evening at twenty-six green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, generalized
&lt;/h2&gt;

&lt;p&gt;Any time you accept a string-as-code from a source you do not trust — LLM output, web input, webhook payload, any of it — the pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse with &lt;code&gt;ast.parse(s, mode="eval")&lt;/code&gt; — refuses statements outright&lt;/li&gt;
&lt;li&gt;Walk the tree with &lt;code&gt;ast.walk(tree)&lt;/code&gt; and filter against a whitelist of allowed node types and names&lt;/li&gt;
&lt;li&gt;Reject explicitly with a typed error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt; hand the sanitized input to whatever permissive parser you actually wanted to use&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Defense in depth, with the permissive parser as the inner layer. This is what kept me from shipping the RCE to a hosted MCP endpoint that any LLM could call.&lt;/p&gt;

&lt;p&gt;If you are building an MCP server that takes a formula, a query, a filter expression, a JSONata path, or any other string-as-code from your LLM, please go check whether your parser is &lt;code&gt;eval&lt;/code&gt;-based and what its global dictionary contains by default. The bug is shipping today in more code than its authors realize.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;. The gate above lives in &lt;code&gt;math_kernel.py&lt;/code&gt;. Live MCP: &lt;code&gt;measurement-uncertainty.mcpize.run&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>showdev</category>
      <category>mcp</category>
    </item>
    <item>
      <title>What an MCP server for measurement uncertainty actually returns: a CD-SEM 45 nm worked example</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Tue, 19 May 2026 02:49:32 +0000</pubDate>
      <link>https://dev.to/kyb8801/what-an-mcp-server-for-measurement-uncertainty-actually-returns-a-cd-sem-45-nm-worked-example-2pbh</link>
      <guid>https://dev.to/kyb8801/what-an-mcp-server-for-measurement-uncertainty-actually-returns-a-cd-sem-45-nm-worked-example-2pbh</guid>
      <description>&lt;h1&gt;
  
  
  What an MCP server for measurement uncertainty actually returns: a CD-SEM 45 nm worked example
&lt;/h1&gt;

&lt;p&gt;A common reaction when I tell people about &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;measurement-uncertainty-mcp&lt;/a&gt; is: "OK but show me what it actually does."&lt;/p&gt;

&lt;p&gt;Fair. Here is one query, paste-ready for Claude Desktop, with the actual output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;I measured a 45 nm linewidth on a CD-SEM twenty times: 45.12, 45.08, 45.15, 45.11, 45.09, 45.13, 45.10, 45.14, 45.07, 45.12, 45.11, 45.09, 45.13, 45.10, 45.14, 45.08, 45.16, 45.10, 45.12, 45.11 nm. Combine with magnification calibration of ±0.5 nm at k=2 (50 dof) and line-edge roughness ±0.4 nm at k=2 (50 dof). Report u_c, effective ν via Welch-Satterthwaite, and expanded U at 95%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The tools the MCP calls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;type_a_uncertainty(samples=[...])&lt;/code&gt; — sample mean, Bessel std, u_A, ν_A&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type_b_normal(U=0.5, k=2, nu=50)&lt;/code&gt; — u_B1 magnification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type_b_normal(U=0.4, k=2, nu=50)&lt;/code&gt; — u_B2 line-edge roughness&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;combine_uncertainty(components=[u_A, u_B1, u_B2])&lt;/code&gt; — u_c via root-sum-of-squares&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;welch_satterthwaite(components=[...])&lt;/code&gt; — ν_eff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expanded_uncertainty(u_c, nu_eff, level=0.95)&lt;/code&gt; — k from Student-t, then U = k × u_c&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The actual numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean&lt;/td&gt;
&lt;td&gt;45.1125 nm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample std (Bessel)&lt;/td&gt;
&lt;td&gt;0.0245 nm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u_A (Type A)&lt;/td&gt;
&lt;td&gt;0.0055 nm (ν = 19)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u_B1 magnification&lt;/td&gt;
&lt;td&gt;0.2500 nm (ν = 50)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u_B2 line-edge roughness&lt;/td&gt;
&lt;td&gt;0.2000 nm (ν = 50)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;u_c combined&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.3202 nm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ν_eff (Welch-Satterthwaite)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;k @ 95%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.985&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;U @ 95%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.636 nm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Reported result&lt;/strong&gt;: 45.11 ± 0.64 nm at 95% confidence (k = 1.99, ν_eff = 95).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why each step matters (and where Excel gets it wrong)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;u_A&lt;/strong&gt;: the sample std must use the Bessel correction (n−1, not n). About one in ten Excel uncertainty templates I have audited use the population std formula by mistake. The MCP uses &lt;code&gt;numpy.std(ddof=1)&lt;/code&gt; — no choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;u_B from k-expanded inputs&lt;/strong&gt;: a vendor datasheet that says "±0.5 nm at k = 2" means the standard uncertainty is 0.5/2 = 0.25 nm. The tool exposes this conversion explicitly so it cannot be skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welch-Satterthwaite&lt;/strong&gt;: when you combine components with different degrees of freedom, the effective ν is not just the sum. The MCP uses the formula from JCGM 100:2008 §G.4.1 directly. Skipping this step and using k = 2 by default gives the wrong U whenever the dominant component has a small ν.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage factor k&lt;/strong&gt;: at ν&lt;em&gt;eff = 95.5 and confidence = 95%, k = &lt;code&gt;scipy.stats.t.ppf(0.975, 95.5)&lt;/code&gt; = 1.985. Note this is _not&lt;/em&gt; 2 — using k = 2 by reflex (the common shortcut) gives U = 0.640 nm instead of 0.636 nm. Small difference here, large difference when ν_eff is small.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this would take in Excel
&lt;/h2&gt;

&lt;p&gt;About 20 minutes per budget, and roughly 10% of attempted budgets have at least one numerical error in my experience auditing calibration certificates. The errors are always one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bessel skip (population std vs sample std)&lt;/li&gt;
&lt;li&gt;Forgetting to divide a k-expanded input by k&lt;/li&gt;
&lt;li&gt;Using k = 2 by default instead of Welch-Satterthwaite + Student-t&lt;/li&gt;
&lt;li&gt;Misapplying the sensitivity coefficient when the model is non-linear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MCP makes each of these into a tool call. You cannot skip Bessel because there is no &lt;code&gt;type_a_population_std&lt;/code&gt; tool. You cannot skip the k-divide because &lt;code&gt;type_b_normal&lt;/code&gt; requires both U and k. The whole point is that the wrong answer is not reachable without explicitly bypassing the typed interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is relevant right now
&lt;/h2&gt;

&lt;p&gt;ISO 10012:2026 was published in February — the first revision since 2003. It adds practical requirements around Test Uncertainty Ratio, decision rules, and guard-banding. Every calibration lab is currently updating their templates. The 10 MCP tools cover exactly the primitives the new standard requires.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http &lt;span class="s2"&gt;"Measurement Uncertainty"&lt;/span&gt; https://measurement-uncertainty.mcpize.run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier: 50 calls/month, no credit card.&lt;/p&gt;

&lt;p&gt;Paste the query above into Claude Desktop. The output you get should match the table above to four decimals.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;. Live MCP: &lt;code&gt;measurement-uncertainty.mcpize.run&lt;/code&gt;. Weekly solo-builder build log: &lt;a href="https://yb-ai-hustle.beehiiv.com" rel="noopener noreferrer"&gt;YB's AI Hustle Weekly&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>showdev</category>
      <category>python</category>
      <category>metrology</category>
    </item>
    <item>
      <title>I Did Not Write New Code This Week. I Rewrote Two Paragraphs and Ten Example Queries.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Mon, 18 May 2026 11:29:04 +0000</pubDate>
      <link>https://dev.to/kyb8801/i-did-not-write-new-code-this-week-i-rewrote-two-paragraphs-and-ten-example-queries-2ejb</link>
      <guid>https://dev.to/kyb8801/i-did-not-write-new-code-this-week-i-rewrote-two-paragraphs-and-ten-example-queries-2ejb</guid>
      <description>&lt;h1&gt;
  
  
  I Did Not Write New Code This Week. I Rewrote Two Paragraphs and Ten Example Queries.
&lt;/h1&gt;

&lt;p&gt;On Tuesday at 3:19 PM local time I committed the smallest, dumbest, highest-impact change I have made to my Model Context Protocol server since the day I made the repo public.&lt;/p&gt;

&lt;p&gt;I rewrote two paragraphs in &lt;code&gt;README.md&lt;/code&gt;. I added five concrete example queries. I removed one phrase. The diff was twenty-nine lines added, two lines removed. I did not touch a single Python file, no test was rerun, no version was bumped, no MCPize redeploy was triggered. The endpoint kept serving the same ten tools at the same Cloud Run instance. The deploy badge stayed green.&lt;/p&gt;

&lt;p&gt;Then I closed the laptop and went to bed because everything I had been doing wrong for six days was about to embarrass me, and I needed to think about it from a distance.&lt;/p&gt;

&lt;p&gt;This is the post about what changed and why I should have done it on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: A Live MCP Server With a Funnel That Did Not Funnel
&lt;/h2&gt;

&lt;p&gt;The product is &lt;code&gt;measurement-uncertainty-mcp&lt;/code&gt;. I shipped it the previous week — a Model Context Protocol server that implements the Guide to the Expression of Uncertainty in Measurement (GUM, JCGM 100:2008) plus the Monte Carlo supplement JCGM 101:2008. Ten tools, GitHub Actions tests, AST-whitelisted formula parser, MCPize Cloud Run live at &lt;code&gt;https://measurement-uncertainty.mcpize.run&lt;/code&gt;. I am proud of the technical work. I am embarrassed by the marketing.&lt;/p&gt;

&lt;p&gt;Here is what happened in the seven days after public launch.&lt;/p&gt;

&lt;p&gt;GitHub stars: 0. NPM downloads: 0. MCPize tool invocations: a handful, mostly mine. Aggregator submissions sat in three pending queues. A Show HN draft sat in a markdown file uncommitted because I kept second-guessing the title. The daily healthcheck task kept reporting "10 tools active, status Active." The infrastructure was healthy. The funnel was a corpse.&lt;/p&gt;

&lt;p&gt;I spent two days assuming the problem was reach. So I drafted a 5-channel coordinated launch script — Hacker News Show HN, r/LocalLLaMA, r/MachineLearning, r/Calibration, MCP Discord, LinkedIn — pre-wrote every post body, scheduled it for the following Saturday at 23:00 KST. I was about to drop my one shot at a coordinated public launch into a funnel that, if it had been working, would already have shown some signal from organic discovery.&lt;/p&gt;

&lt;p&gt;Sunday morning I sat down with the repo and read the README cold, pretending I was a stranger. That is when I noticed the sentence that was killing me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sentence That Was Killing Me
&lt;/h2&gt;

&lt;p&gt;The original second paragraph of the README, written six days earlier:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built for AI engineers, metrologists, and semiconductor equipment teams who need to compute Type A/B uncertainties, combined standard uncertainty, effective degrees of freedom, and expanded uncertainty directly from their LLM assistant — without leaving chat or switching to a spreadsheet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that sentence as if you are a calibration lab QA manager whose ISO 10012:2026 audit is in six months. You will bounce. You bounce on the first three words.&lt;/p&gt;

&lt;p&gt;"Built for &lt;strong&gt;AI engineers&lt;/strong&gt;" — you are not one of those.&lt;/p&gt;

&lt;p&gt;You skim the rest. You see "metrologists" and "semiconductor equipment teams" and conclude this tool is for people who do science at companies that buy ASML steppers, not for the eight-person calibration lab in Bucheon that calibrates pressure gauges for petrochemical plants. You close the tab. You go back to the Excel template you were updating because somebody at ILAC published new guidance on guard-banding and your decision rule needs a paragraph.&lt;/p&gt;

&lt;p&gt;That is the actual buyer of an uncertainty calculation tool. There are roughly ten thousand ISO/IEC 17025 accredited calibration labs globally. Each one has a QA manager, a senior metrologist, and a stack of Excel files. None of them describe themselves as "AI engineers." Most of them have heard of MCP only because their own kid mentioned it.&lt;/p&gt;

&lt;p&gt;I had written the sentence six days earlier without thinking, because the first audience I had imagined for the launch was the indie-hacker-developer crowd that lives on Hacker News and r/LocalLLaMA. That crowd will star a clever MCP. That crowd will not pay for an uncertainty calculation tool because they do not have an audit in six months.&lt;/p&gt;

&lt;p&gt;The sentence that needed to be there was a sentence that named the regulation, named the accreditation body, and used the buyer's own vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rewrite
&lt;/h2&gt;

&lt;p&gt;The first paragraph used to end with "GUM-compliant measurement uncertainty analysis." It now ends with: "GUM-compliant measurement uncertainty analysis. Built for ISO/IEC 17025 calibration labs, ISO 10012:2026 measurement management systems, and KOLAS / A2LA / UKAS accredited testing."&lt;/p&gt;

&lt;p&gt;That is one phrase. It does five things at once. It names the standard the audit will be measured against (ISO/IEC 17025). It names the new standard published in February that triggered every lab to update their templates this year (ISO 10012:2026). It names the three accreditation bodies a calibration lab in Korea, the United States, and the United Kingdom respectively reports to (KOLAS, A2LA, UKAS). It signals that I know the standards numbers. And it does all of this before the reader scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Example Queries
&lt;/h2&gt;

&lt;p&gt;The biggest change in conversion was probably the example query block.&lt;/p&gt;

&lt;p&gt;The repo previously had no concrete query examples. I replaced it with a section called "Five example queries (paste these into Claude Desktop)" and wrote five queries. Real queries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CMM length calibration at 10 mm nominal — uses the prebuilt template, computes u_c and U at 95%.&lt;/li&gt;
&lt;li&gt;CD-SEM 45 nm linewidth uncertainty budget — Type A from twenty replicate measurements, combined with magnification calibration and line-edge roughness, reports ν_eff via Welch-Satterthwaite.&lt;/li&gt;
&lt;li&gt;DMM 10 V calibration with TUR check — references a standard at ±20 µV, asks for u_c, expanded U, and the Test Uncertainty Ratio against a tolerance.&lt;/li&gt;
&lt;li&gt;Type-K thermocouple at 100 °C with a parameter override on the prebuilt template.&lt;/li&gt;
&lt;li&gt;Non-linear ratio model — Y = (V × R) / (V + I) — Monte Carlo with 200,000 trials, shortest 95% coverage interval per JCGM 101 §7.7.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three things make these examples convert that the previous "Use cases" paragraph did not.&lt;/p&gt;

&lt;p&gt;First, every query names a real measurement scenario from a real lab. CMM at 10 mm. CD-SEM at 45 nm. DMM at 10 V. Thermocouple at 100 °C. A reader who works in any of those scenarios sees their own day in the example.&lt;/p&gt;

&lt;p&gt;Second, every query produces an answer that the buyer needs to produce anyway. The example is not a demo. It is a job they have to do.&lt;/p&gt;

&lt;p&gt;Third, the queries use the buyer's vocabulary back at them. ν_eff via Welch-Satterthwaite. Coverage factor k. JCGM 101 §7.7. A QA manager who has been writing uncertainty budgets for ten years recognizes that vocabulary instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Lessons I Did Not Want to Learn
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One. The "Built for X, Y, Z" sentence kills your funnel before it starts.&lt;/strong&gt; If the X is wrong, the rest of the page does not get read. The sentence was a one-line buyer-persona declaration and I had declared the wrong persona. The fix — naming ISO/IEC 17025, ISO 10012:2026, KOLAS, A2LA, UKAS — took me twelve minutes once I knew what to write. The six days of zero conversion were the cost of not having known.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two. Example queries are not documentation. They are job listings for your tool.&lt;/strong&gt; Every example query you put in your README is a sentence that answers the question "what would I hire this tool to do for me today?" If you write generic examples — "compute uncertainty," "process this data" — you are listing a job nobody applies for. If you write "CD-SEM 45 nm linewidth uncertainty budget combining magnification calibration and line-edge roughness, report ν_eff via Welch-Satterthwaite" then somebody whose Tuesday looks like that recognizes their Tuesday in your README. They install. The cost of writing five concrete queries is one hour. The benefit is the entire conversion funnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three. In a regulated niche, standards-referenceable language is the buyer's trust currency, and you are spending counterfeit currency if you skip it.&lt;/strong&gt; A calibration lab QA manager has been burned at least once by a vendor whose product claimed compliance with a standard that the vendor had not actually read. Every time you cite a standard in your README — JCGM 100:2008, JCGM 101:2008, ISO 10012:2026, ISO 14253-1, ILAC G8 — you are putting a number on the page that the buyer can look up. In an unregulated developer niche this lesson does not apply, because developers do not look anything up. In a regulated niche it is the difference between getting installed and not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Will Not Do Again
&lt;/h2&gt;

&lt;p&gt;I will not write a "Built for X" sentence on a regulated-niche README without first writing down the three standards numbers that the buyer types into Google when they have a problem. If I cannot write down the three standards numbers, I do not understand the buyer well enough to be writing the README at all, and the right next action is to read the standards, not to write more code.&lt;/p&gt;

&lt;p&gt;I will not ship a README with no example queries. The example queries are the part the buyer reads. The rest is the part they skim while deciding whether to read the queries.&lt;/p&gt;

&lt;p&gt;I will not assume that an MCP server's discovery problem is a reach problem before I have read my own README cold, pretending I am the buyer. The reach problem is real. The wrong-buyer problem is more common.&lt;/p&gt;

&lt;p&gt;If you are running a solo MCP, a solo SaaS, or any indie product where the funnel feels broken and the build is fine, please go read your README cold. Pretend you are the buyer. Pretend you have an audit in six months. See how long it takes you to bounce.&lt;/p&gt;

&lt;p&gt;If it is fewer than three sentences, the build is not the problem.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;. Live MCP: &lt;code&gt;measurement-uncertainty.mcpize.run&lt;/code&gt;. Weekly solo-builder build log at &lt;a href="https://yb-ai-hustle.beehiiv.com" rel="noopener noreferrer"&gt;YB's AI Hustle Weekly&lt;/a&gt; — same operational logs, every Sunday morning KST.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>indiehackers</category>
      <category>writing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Put 7 Products on Gumroad. 4 of Them Had No Files.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Mon, 18 May 2026 11:27:18 +0000</pubDate>
      <link>https://dev.to/kyb8801/i-put-7-products-on-gumroad-4-of-them-had-no-files-4kkn</link>
      <guid>https://dev.to/kyb8801/i-put-7-products-on-gumroad-4-of-them-had-no-files-4kkn</guid>
      <description>&lt;h1&gt;
  
  
  I Put 7 Products on Gumroad. 4 of Them Had No Files.
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Gumroad Recovery Tour — Episode 1 of 4. 24 days of data, no filter.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;I published my first Gumroad store on March 24, 2026.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Seven products. Prices from $9 to $14. Pitched to an audience I had spent six weeks building — YouTube Shorts, X threads, a newsletter with a handful of loyal subscribers.&lt;/p&gt;

&lt;p&gt;Twenty-four days later, the dashboard looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Products listed&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Products with files uploaded&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Products with files missing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page views (all products combined)&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add-to-cart events&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed sales&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;Affiliate clicks from my Medium articles&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net revenue&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And here's the part nobody tells you about empty storefronts: &lt;strong&gt;four of my products had no product files attached.&lt;/strong&gt; If somebody had actually bought one of them, Gumroad would have sent them a receipt and then... nothing. No download link. No file. A refund request, best case. A chargeback at worst.&lt;/p&gt;

&lt;p&gt;I only noticed this on day 23 — while auditing my own pipeline for this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the missing files were
&lt;/h2&gt;

&lt;p&gt;Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Power Prompt Pack ($9)&lt;/strong&gt; — 100 prompts. Had the landing page copy written. Never rendered the PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python Data Analysis Scripts ($14)&lt;/strong&gt; — 8 scripts. Had an outline. Never zipped the folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest Tracker Pro ($12)&lt;/strong&gt; — a Notion + Excel template. Had neither.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Side Hustle Dashboard ($9)&lt;/strong&gt; — another Notion bundle. Same story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four products, four empty shells. A storefront that existed only as screenshots and promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happened (the honest answer)
&lt;/h2&gt;

&lt;p&gt;I'd been shipping YouTube Shorts for weeks. Built the pipeline, wrote the automation, set up cross-posting to X and TikTok. Had a newsletter in Beehiiv. A content machine with real outputs and real costs.&lt;/p&gt;

&lt;p&gt;And then, when it came time to &lt;strong&gt;actually package something somebody could buy&lt;/strong&gt;, I skipped the packaging step.&lt;/p&gt;

&lt;p&gt;I published the store &lt;em&gt;before&lt;/em&gt; the products existed.&lt;/p&gt;

&lt;p&gt;Why? Because publishing the store felt like "shipping." It had a URL. It looked done. The page said "Add to cart" and the cart worked. The storefront was a &lt;em&gt;demo&lt;/em&gt; of a storefront, and the demo was so convincing that I moved on.&lt;/p&gt;

&lt;p&gt;This is the content creator trap in 2026, and I walked directly into it: &lt;strong&gt;you get so used to making the wrapper — the Short, the tweet, the article, the thumbnail — that you forget the thing inside the wrapper.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Shorts get views. The tweets get likes. The newsletter gets subscribers. All of those feel like forward motion. None of them are money. The thing that would have been money — a finished PDF, a working Excel file, a zipped folder of Python scripts — required the one kind of work my pipeline couldn't do for me.&lt;/p&gt;

&lt;p&gt;It required me to &lt;em&gt;actually build the product&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost breakdown
&lt;/h2&gt;

&lt;p&gt;In the name of full transparency, here's what I spent to get to $0:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Cost over 24 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API (Claude scripts, newsletter generation)&lt;/td&gt;
&lt;td&gt;~$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google API (Gemini Flash-Lite, Veo B-roll)&lt;/td&gt;
&lt;td&gt;~$4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pexels API&lt;/td&gt;
&lt;td&gt;$0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beehiiv&lt;/td&gt;
&lt;td&gt;$0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gumroad&lt;/td&gt;
&lt;td&gt;$0 (fee-only model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium MPP&lt;/td&gt;
&lt;td&gt;$0 (free writer program)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain (optional)&lt;/td&gt;
&lt;td&gt;~$1 prorated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I spent sixteen dollars and roughly 60 hours of my own time to produce: a storefront, an automated Shorts pipeline running two videos a day, a Beehiiv newsletter, a cross-posting setup to X and TikTok, and a domain I've barely used.&lt;/p&gt;

&lt;p&gt;Sixteen dollars and zero sales is a cheap lesson in absolute terms. It's an expensive lesson in opportunity cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing about it (today)
&lt;/h2&gt;

&lt;p&gt;As of this publication:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;All four missing files now exist.&lt;/strong&gt; I spent yesterday generating them. Real PDF. Real zipped scripts. Real Excel with 1,055 working formulas (I checked). Real Notion-importable dashboard. Each one is in the store. Each one would actually download if somebody bought it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'm reducing to one Shorts per day&lt;/strong&gt;, not two. The two-per-day experiment ran for 35 days and produced the same subscriber growth as one-per-day. Content volume was never the bottleneck. Product readiness was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'm writing this series.&lt;/strong&gt; Four episodes. You're reading the first one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Honest question: if you've hit the same wall — you've built the wrapper and skipped the product inside — what was the thing that made you finally finish it?&lt;/p&gt;

&lt;p&gt;Reply or DM. I'll be writing the next episode as soon as the data from this one comes in.&lt;/p&gt;




&lt;p&gt;Building in public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store: &lt;a href="https://kyb8801.gumroad.com" rel="noopener noreferrer"&gt;https://kyb8801.gumroad.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP server (my next bet): &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;https://github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Weekly build log: &lt;a href="https://yb-ai-hustle.beehiiv.com" rel="noopener noreferrer"&gt;https://yb-ai-hustle.beehiiv.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full reflection on Medium: &lt;a href="https://medium.com/@kyb8801" rel="noopener noreferrer"&gt;https://medium.com/@kyb8801&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This article was edited by me. The outline and tables were drafted by Claude. The numbers and the embarrassment are both mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>gumroad</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Shipped: first MCP server for ISO 10012:2026 measurement uncertainty</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Mon, 18 May 2026 11:22:15 +0000</pubDate>
      <link>https://dev.to/kyb8801/shipped-first-mcp-server-for-iso-100122026-measurement-uncertainty-5350</link>
      <guid>https://dev.to/kyb8801/shipped-first-mcp-server-for-iso-100122026-measurement-uncertainty-5350</guid>
      <description>&lt;p&gt;Spent 48 hours building a Model Context Protocol server for ISO 10012:2026 measurement uncertainty.&lt;/p&gt;

&lt;p&gt;10 tools. JCGM 100:2008 plus 101:2008 compliant. Live on MCPize.&lt;/p&gt;

&lt;p&gt;Built for ISO/IEC 17025 calibration labs that spend hours in Excel on the same uncertainty math every month. The buyer is a QA manager whose audit is in six months, not an AI engineer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type A statistical uncertainty (Bessel-corrected, ν=n-1)&lt;/li&gt;
&lt;li&gt;Type B rectangular / triangular / k-expanded distributions&lt;/li&gt;
&lt;li&gt;Combined standard uncertainty u_c via root-sum-of-squares&lt;/li&gt;
&lt;li&gt;Welch-Satterthwaite effective degrees of freedom&lt;/li&gt;
&lt;li&gt;Expanded uncertainty U at target confidence level&lt;/li&gt;
&lt;li&gt;Monte Carlo propagation per JCGM 101:2008&lt;/li&gt;
&lt;li&gt;6 pre-built uncertainty-budget templates (CMM 10mm, CD-SEM 45nm, DMM 10V, OCD 50nm, thermocouple, balance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/kyb8801/measurement-uncertainty-mcp" rel="noopener noreferrer"&gt;https://github.com/kyb8801/measurement-uncertainty-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live MCP: &lt;a href="https://measurement-uncertainty.mcpize.run" rel="noopener noreferrer"&gt;https://measurement-uncertainty.mcpize.run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Free tier: 50 calls/month, no credit card&lt;/li&gt;
&lt;li&gt;Full build log (Medium): &lt;a href="https://medium.com/@kyb8801/i-did-not-write-new-code-this-week-i-rewrote-two-paragraphs-and-ten-example-queries-d5e7aa18ecfe" rel="noopener noreferrer"&gt;https://medium.com/@kyb8801/i-did-not-write-new-code-this-week-i-rewrote-two-paragraphs-and-ten-example-queries-d5e7aa18ecfe&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building in public. Weekly progress log: yb-ai-hustle.beehiiv.com (free).&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>anthropic</category>
      <category>metrology</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building DevSignal: AI signal-based outreach for solo devtool founders. 30-day public sprint.</title>
      <dc:creator>kyb8801</dc:creator>
      <pubDate>Thu, 30 Apr 2026 07:09:34 +0000</pubDate>
      <link>https://dev.to/kyb8801/building-devsignal-ai-signal-based-outreach-for-solo-devtool-founders-30-day-public-sprint-292o</link>
      <guid>https://dev.to/kyb8801/building-devsignal-ai-signal-based-outreach-for-solo-devtool-founders-30-day-public-sprint-292o</guid>
      <description>&lt;p&gt;Hey dev.to 👋&lt;/p&gt;

&lt;p&gt;I'm a solo SaaS founder. In the past 6 months I shipped 4 packages on NPM — and like everyone here, the hardest part wasn't building. It was getting anyone to &lt;em&gt;care&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I'm starting a 30-day public build of &lt;strong&gt;DevSignal&lt;/strong&gt; — a tool that turns GitHub stars, HN comments, Reddit threads, and Twitter mentions into warm leads + AI-drafted outreach. Built &lt;strong&gt;for&lt;/strong&gt; solo founders, &lt;strong&gt;by&lt;/strong&gt; a solo founder.&lt;/p&gt;

&lt;p&gt;Wait list: &lt;strong&gt;&lt;a href="https://devsignal.dev" rel="noopener noreferrer"&gt;https://devsignal.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm building it
&lt;/h2&gt;

&lt;p&gt;Three things I keep hitting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cold email blasts feel sleazy AND don't work anymore.&lt;/strong&gt; Open rates collapsed. Reply rates near zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apollo, Clay, Common Room are built for sales teams.&lt;/strong&gt; Clay starts at $185/mo. Common Room at $1,000+. I'm one person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The people who'd actually pay for my tool already exist.&lt;/strong&gt; They starred my competitor's repo. They complained on Reddit about the exact problem I solve. They hang out on HN. I just can't &lt;em&gt;see&lt;/em&gt; them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DevSignal is the tool I wish I had when I shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does (planned MVP)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect&lt;/strong&gt; — your repo, competitor repos, HN/Reddit/Twitter handles. 5 min.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We surface warm signal&lt;/strong&gt; — devs who starred you, devs complaining about a pain you solve, devs commenting on competitor threads. With context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You send 1-tap outreach&lt;/strong&gt; — AI drafts a context-aware first message. You approve. We track replies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No data brokers. No cold lists. No spam blasts. Signal-first.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 30-day plan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week 1&lt;/strong&gt; — Landing + 30 interview prospects + first conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 2&lt;/strong&gt; — 10–15 founder interviews. Refine value prop based on their words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 3&lt;/strong&gt; — MVP scope. Start building. Lock in 5 beta users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 4&lt;/strong&gt; — Ship MVP. Convert 3 paid pilots ($49 each).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Day 30 gate:&lt;/strong&gt; 50 wait list / 10 interviews / 5 betas / 3 paid → continue. Below that → pivot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack (cheap &amp;amp; boring)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tally for forms&lt;/li&gt;
&lt;li&gt;Carrd for landing&lt;/li&gt;
&lt;li&gt;Supabase + Clerk for auth/DB&lt;/li&gt;
&lt;li&gt;Vercel + Next.js for backend&lt;/li&gt;
&lt;li&gt;Claude API for messaging&lt;/li&gt;
&lt;li&gt;Resend for email send&lt;/li&gt;
&lt;li&gt;Stripe for payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infra: ~$50–200/mo at start. Scales.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd love from this community 🙏
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. If you've been a solo devtool founder and struggled with finding your first users&lt;/strong&gt; — would love a 30-min interview. I'll trade &lt;strong&gt;early access + lifetime 50% off&lt;/strong&gt; for your time. Reply or DM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Honest critique.&lt;/strong&gt; What kills this idea? What did Common Room/Clay &lt;em&gt;actually&lt;/em&gt; solve well that I'm missing? Tell me the unflattering truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Wait list:&lt;/strong&gt; &lt;a href="https://devsignal.dev" rel="noopener noreferrer"&gt;https://devsignal.dev&lt;/a&gt; — if you'd want this when it ships, drop your email. You'll get 14-day free trial + 50% off first 3 months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I think this works (or doesn't)
&lt;/h2&gt;

&lt;p&gt;✅ I've felt the pain personally. Currently dogfooding Phase 0.&lt;br&gt;
✅ Sub-niche is sharp: solo, devtool, signal-based (not list-based), $49–199/mo.&lt;br&gt;
✅ Competition (Common Room, Clay, Apollo) is real but sized for teams. Solo gap looks underserved.&lt;/p&gt;

&lt;p&gt;❌ "Yet another outreach tool." You should be skeptical.&lt;br&gt;
❌ NuReply, Coldreach.ai, PlusVibe exist at solo prices. Differentiation has to be sharp.&lt;br&gt;
❌ Solo founders are notoriously hard to acquire as customers.&lt;/p&gt;

&lt;p&gt;If this fails, I'll write a post-mortem here too. Either way, you'll see the journey.&lt;/p&gt;

&lt;p&gt;Let's go 🛠️&lt;/p&gt;




&lt;p&gt;&lt;a href="https://devsignal.dev" rel="noopener noreferrer"&gt;Wait list → devsignal.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>saas</category>
      <category>devtools</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
