<?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: Pierre- Laurent Medori</title>
    <description>The latest articles on DEV Community by Pierre- Laurent Medori (@pierrelaurentmedori).</description>
    <link>https://dev.to/pierrelaurentmedori</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%2F4022967%2F06fd4098-6c66-4406-b44d-43d0e63b179f.png</url>
      <title>DEV Community: Pierre- Laurent Medori</title>
      <link>https://dev.to/pierrelaurentmedori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pierrelaurentmedori"/>
    <language>en</language>
    <item>
      <title>Your agent's memory is a liability: track state, not history</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:04:57 +0000</pubDate>
      <link>https://dev.to/pierrelaurentmedori/your-agents-memory-is-a-liability-track-state-not-history-le7</link>
      <guid>https://dev.to/pierrelaurentmedori/your-agents-memory-is-a-liability-track-state-not-history-le7</guid>
      <description>&lt;p&gt;There is a French draft in one of my test apps that I keep like a fossil. Article 96924661: a title, a slug, zero paragraphs. A June translation run created it; I remember that run as green. I cannot check it: the n8n execution history from June is purged. What survives is a screenshot and the likeliest path, read off the canvas and never reproduced: the article is created before its paragraphs, so if the split step yields nothing, "Create FR paragraph" runs without error on nothing and the report node declares success. Green all the way down. The draft is still empty.&lt;/p&gt;

&lt;p&gt;As I wrote in June, in the &lt;a href="https://www.goodbarber.com/blog/how-to-automate-your-goodbarber-app-with-n8n-and-mcp-a1523/" rel="noopener noreferrer"&gt;piece on automating an app with n8n and MCP&lt;/a&gt;, "our runs burned 40,000 to 77,000 input tokens per minute, past the rate limits of an entry-tier API account, whatever the model." The survivor was not an agent: deterministic plumbing, plus one model call per article. One line of context so you know where I stand: I run engineering at GoodBarber, an app platform, and we run &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;a production MCP server&lt;/a&gt; everything below runs on systems I operate myself.&lt;/p&gt;

&lt;p&gt;Then a preprint crossed my feed at the end of August with a headline number: 16 times fewer tokens, 94 percent once you do the subtraction, if your agent keeps state instead of history. I had lived the mechanism before the paper, so I read it back. Here is the cell the number comes from, my June job replayed five ways, what our server learned the hard way, and the rule the paper does not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number is real. The cell matters more.
&lt;/h2&gt;

&lt;p&gt;The preprint is SKILL.state, &lt;a href="https://arxiv.org/abs/2608.26263" rel="noopener noreferrer"&gt;arXiv 2608.26263&lt;/a&gt;, v1 posted on August 26, 2026, by Sanket Badhe and Priyanka Tiwari at Google LLC with Jonghyun Chung at Purdue. At every step the model receives an immutable skill specification, the current structured state as JSON, and the last observation; the reasoning trace is discarded once the runtime has validated its state patch. The prompt stays roughly constant, so cumulative cost is O(T) instead of O(T²).&lt;/p&gt;

&lt;p&gt;The 94 percent is exact and it is one cell. Table 1, the authors' synthetic Warehouse environment, Gemini-3-Flash, horizon T = 100: 65,408 cumulative tokens against 1,062,387 for the stateful baseline, a 16.2x gap. Same column at T = 10: 43 percent. On the public benchmarks, InterCode CTF and τ-Bench, the reduction lands between 11 and 66 percent depending on the baseline. The gain is a horizon effect.&lt;/p&gt;

&lt;p&gt;The result that matters is elsewhere. At equal budget, roughly 1,800 prompt tokens for every runtime at T = 100, accuracy was 0.94 for the structured state, 0.52 for a capped summary, 0.22 for ReAct plus LLMLingua, 0.18 for a truncated window. The saving is a side effect. The structure is the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prefix is the bill
&lt;/h2&gt;

&lt;p&gt;On September 3, 2026, I counted our tool inventories with the count_tokens endpoint against claude-haiku-4-5. My n8n test app, 62 tools: 17,763 tokens. The larger test app, 77 tools: 23,174. The seven tools a translation needs: 2,416. One default page of &lt;code&gt;cms_list_articles&lt;/code&gt; on the small app: 16,987. The paper's mean prompt per step, all three inputs included: 1,905.&lt;/p&gt;

&lt;p&gt;A ReAct loop on that app opens every turn with nine times the paper's whole prompt in schemas alone, and each list it calls adds nine more that history never forgets. D's six-field state object counted 56 tokens; a create-and-read-back envelope, 112.&lt;/p&gt;

&lt;h2&gt;
  
  
  I replayed June in five arms
&lt;/h2&gt;

&lt;p&gt;Same job, same six English articles, same test app, September 3, 2026, claude-haiku-4-5, prepaid credits; the table is run 1, except D, the best of its six versions. Every French draft was created as &lt;code&gt;status: draft&lt;/code&gt; with a marker, then deleted. Verification is deterministic and outside the model: the five checks in the code block further down.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;what&lt;/th&gt;
&lt;th&gt;verified&lt;/th&gt;
&lt;th&gt;model calls&lt;/th&gt;
&lt;th&gt;input tokens&lt;/th&gt;
&lt;th&gt;cache read&lt;/th&gt;
&lt;th&gt;list price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;ReAct, full history, 62 tools&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;1,159,084&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$1.267&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;ReAct, full history, 7 tools&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;893,341&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$1.002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;script plumbing, one call per article&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;7,135&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$0.054&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;explicit state, best of six runtime versions&lt;/td&gt;
&lt;td&gt;4/6&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;155,249&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$0.348&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;arm A with prompt caching&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;1,080,099&lt;/td&gt;
&lt;td&gt;$0.306&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A does C's job with 160 times the input tokens. B, with 55 fewer tools, still needs 125 times: trimming the inventory does not fix a loop that re-reads its own transcript. A peaked at 694,434 prompt tokens in a 60-second window and never saw a 429; the account is not where it was in June, so I will not quote a June limit. C is the June survivor, a stdlib script now that the n8n workflow no longer runs: O(1) per article, and its prompt was the paper's triplet before anyone named it.&lt;/p&gt;

&lt;p&gt;A second pass the same day reproduced A within 0.1 percent (1,158,626 input tokens) and B 13 percent lower (773,767); C used the same 7,135 tokens and E came out at the same $0.306.&lt;/p&gt;

&lt;p&gt;E, arm A with cache breakpoints: 123 uncached input tokens, 73,114 written to cache, 1,080,099 read from it, 93 percent of the prompt served from cache, a quarter of A's price. Caching makes the transcript cheap. It does not make it small, or a sufficient statistic of anything.&lt;/p&gt;

&lt;p&gt;And D, my own SKILL.state, never finished. Six runtime versions in one afternoon, 80 to 100 steps each, prompt flat between 1,400 and 2,500 tokens per step, best result 4 of 6. Each version failed somewhere new. The first schema carried ids and statuses but not the translation, so the model re-read the source at every step, 33 reads for 6 articles, and the runtime filed one French id under two sources. With the translation in state, the model invented tool arguments, &lt;code&gt;author_id&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, and the server refused 74 creates in a row. With arguments filtered against the tool schema, it re-read and re-created: 52 reads, 17 drafts for 6 sources. With the runtime owning the state machine and naming the next expected action, it created 70 paragraphs on the same drafts: after each write the runtime read the article first and the paragraph list second, so the list came from the 60-second GET cache, empty, for about 25 cycles, then 131,991 bytes at once. My commit rule was right. My read path was the cached one. With the read-back taken off the cache, 4 of 6, then a patch rejected 52 times for a string where an integer was expected. The paper reports that on small open-weight models 68 percent of failures are premature overwrites of the state; on claude-haiku-4-5 mine were the runtime's, and every one of them was a validation the runtime did not do yet. The state was constant. Sufficient is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own transcripts are not O(T) either
&lt;/h2&gt;

&lt;p&gt;On September 3, 2026, I scanned my local Claude Code transcripts, counters only, no content: 264 files, 175 sessions with at least 30 assistant turns, 32,607 turns. Prompt per turn (input, cache creation and cache reads) climbs until a compaction, then drops: 37 drops of more than half, a median cost of 2.2 times a constant-size state, a median cache read share of 0.968. The longest session ran 2,444 turns and 1,071,111,255 prompt tokens, 6.89 times a constant state, 97 percent of it cache reads. That is &lt;a href="https://codex.danielvaughan.com/2026/08/29/skill-state-ot-agent-memory-structured-execution-state-codex-cli-long-horizon/" rel="noopener noreferrer"&gt;Daniel Vaughan's point about Codex CLI&lt;/a&gt;: O(T²/K) by chunks, not O(T). I have never noticed a decision lost to compaction and will not claim one. I can only vouch for the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server did it to itself first
&lt;/h2&gt;

&lt;p&gt;Our MCP server stopped keeping history on August 10, 2026. Since then the transport is stateless: no &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, no session cap. What the server keeps per agent, keyed by the sha256 of the access token: the last ten tool names for one hour, enough to push the read that follows a write past the cache, and since August 11 a sliding counter of 1,000 requests per minute that answers 429 with &lt;code&gt;Retry-After&lt;/code&gt;. Ten names and a number.&lt;/p&gt;

&lt;p&gt;Measured on September 3, 2026, 50 cycles on a test app. A get straight after a delete: 3 stale reads out of 50, the August fix holds, not perfectly. Delete, list, get: the deleted object came back in 50 of 50 cycles and stayed a median 61.1 seconds, maximum 61.2, the 60-second GET cache doing what it was told. The bypass covers only the read right after the write; a list resets it. Three limits I read in the code rather than measured: another token never gets the bypass, nothing invalidates on write, and on an upstream failure the server serves stale cache on purpose. Read-back through a cache lies in both directions, as I wrote in the write-safety piece. It still does.&lt;/p&gt;

&lt;h2&gt;
  
  
  State is what was verified, not what the model believes
&lt;/h2&gt;

&lt;p&gt;The rule I want, and the one the paper does not have: an agent's state is not what the model reports. It is what the world confirmed. Every write tool on our server returns &lt;code&gt;_mcp_policy.verification_required: true&lt;/code&gt;, except six fire-and-forget push tools. D's runtime turned that policy into a commit rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit rule, enforced by the script, never by the model:
  1. the model proposes a patch: {"set": {...}, "delete": [...]}
  2. the runtime validates it: an unknown key, a wrong type or any touch
     of verified_fr, and the patch is rejected, state untouched
  3. an id enters verified_fr only after cms_get_article passes every check:
     exists, status draft, at least one paragraph, no residual delimiter,
     body length within 0.6 to 1.8 of the EN body
  4. a failed read-back commits nothing; the failure is the next observation
  a 200 on cms_create_article is not a commit. The read-back is.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On one article, 20 identical calls at temperature 0 on September 3 gave one distinct output; the eight offline checks I run on a translation, a different set from the five runtime checks above, caught all six faults I injected, and a status code would have caught none. Deterministic checks, not a 200.&lt;/p&gt;

&lt;p&gt;Running that rule against our own server hit the same holes I had logged in August, and the September 3 cycles confirmed them. Ticket B: a delete returns only the policy envelope, no &lt;code&gt;deleted&lt;/code&gt;, no &lt;code&gt;id&lt;/code&gt;, no &lt;code&gt;status&lt;/code&gt;. Ticket C: &lt;code&gt;cms_create_article&lt;/code&gt; defaults to &lt;code&gt;status: published&lt;/code&gt;, unchanged since May 13. Ticket D: a create on September 3 returned &lt;code&gt;cms_get_article&lt;/code&gt;, &lt;code&gt;cms_get_article_paragraph&lt;/code&gt; and &lt;code&gt;cms_get_event&lt;/code&gt; as verification tools, the first three read tools in alphabetical order, while the tool plan points its verification call at &lt;code&gt;cms_list_cms_sections&lt;/code&gt;. Follow the plan to the letter and you verify an article by listing sections; follow the envelope and you may end up reading an event.&lt;/p&gt;

&lt;p&gt;The June draft reads differently now: an invalid state patch, created without verified, accepted by a run whose only witness is my memory. IDs, not vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  My scheduled tasks never had a memory
&lt;/h2&gt;

&lt;p&gt;My scheduled Claude Code tasks had the triplet before I read the paper. Each starts from a fresh context: a daily ops review that reads Nagios and BuildBot, daily code reviews of two workspaces, a Monday glossary sentinel. From local transcript counters over August 1 to September 2, 2026: the ops review ran 30 times, median 25 turns, 49 uncached input tokens and 1,529,904 cache-read tokens per run, about 14 tool calls a run. The two code reviews, 23 and 29 runs, read 14,689,395 and 10,637,775 cache tokens per median run. The sentinel, 3 runs, 820,312.&lt;/p&gt;

&lt;p&gt;A few dozen to a few hundred uncached tokens per run, and the cached prefix is the rest of the prompt: spec, then observation. Where is the state? In Nagios, in git, in the CMS: the system of record, never the agent. The paper's architecture, state pushed one layer down, no validator anywhere.&lt;/p&gt;

&lt;p&gt;The sentinel is the closest thing I have to a validator. A cloud routine created on August 12 ran twice, both green; I replaced it on August 17 on a design argument, not an observed drift: a routine that rewrites its script each week is a moving specification. The replacement is a frozen script with 22 negative test cases, under version control; its four reports, August 17, 25, 31 and September 2, were all green on 170 pages in 89, 139, 116 and 166 seconds. The state is the sitemap. The validator is the test file.&lt;/p&gt;

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

&lt;p&gt;The paper's third limitation is the one I hit last month: when the task is the trajectory itself, throwing the history away destroys the work. My llms.txt forensic needed 121 days of logs and 1,321 requests to find 26 AI-labeled hits; the facts existed only in the sequence, and no crawler state schema would have held them. There, the log is not the memory. The log is the deliverable.&lt;/p&gt;

&lt;p&gt;What the paper is not: synthetic-first, a strong horizon effect, a stateful baseline that pads the transcript with a state block rather than using LangGraph idiomatically, single-agent only. What our server has not shipped: any of it. No validator in front of any state file, no idempotency keys, no version-bound writes, no plan-then-commit object, no semantic span attributes, no token fingerprint column in the usage log. Tickets B, C and D were still open at our August 28 commit. D never finished.&lt;/p&gt;

&lt;p&gt;What is cheap, and what I am doing next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☐ Weigh your tools/list with count_tokens. Compare with 1,905.&lt;/li&gt;
&lt;li&gt;☐ Log the four usage counters per turn.&lt;/li&gt;
&lt;li&gt;☐ Schema and validator in front of the state file.&lt;/li&gt;
&lt;li&gt;☐ Commit state after read-back; deterministic checks, not a status code.&lt;/li&gt;
&lt;li&gt;☐ Freeze and version the spec.&lt;/li&gt;
&lt;li&gt;☐ When the trajectory is the deliverable, keep a ledger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does your prompt look like at step 50, and who decided what goes in it? If you run a scheduled agent: where does its state live between runs, what validates it before the next run reads it, and how big is it in tokens? I am genuinely curious whether anyone has a schema in front of that file, or whether we are all editing JSON by hand and hoping. I will answer with token counts where I can.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>There is no free lunch, especially in app publishing</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:54:27 +0000</pubDate>
      <link>https://dev.to/goodbarber/there-is-no-free-lunch-especially-in-app-publishing-2ec8</link>
      <guid>https://dev.to/goodbarber/there-is-no-free-lunch-especially-in-app-publishing-2ec8</guid>
      <description>&lt;p&gt;2009, mobile networks class. We were studying how TCP handles packets lost over 3G: the network detects the loss, replays the packet, and the transfer survives a radio link that drops things all the time. Someone asked the obvious question: why not just make the radio link reliable in the first place? The professor's answer became the soundtrack of that semester: there is no free lunch. Reliability over 3G is not free, it is paid for in retransmissions, latency and battery. The cost never disappears. It just moves to wherever you are not looking.&lt;/p&gt;

&lt;p&gt;I now run engineering at a no-code app builder. Every pricing decision I have seen from the inside since, his sentence explains better than any business book.&lt;/p&gt;

&lt;p&gt;Here is the version of it nobody puts on a landing page: &lt;strong&gt;you can build an app for free anywhere, and you can ship one for free nowhere.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The asterisk every vendor knows by heart
&lt;/h2&gt;

&lt;p&gt;Search "free app builder". Every result promises an app for free. Every vendor behind those pages knows exactly where their free tier ends: right before the App Store and Google Play. Build for free, preview for free, then the store gates close and the pricing table appears.&lt;/p&gt;

&lt;p&gt;I will not do the pricing comparison here. We published a dated, sourced table of what each major free tier actually includes on the company blog: &lt;a href="https://www.goodbarber.com/blog/can-you-really-create-an-app-for-free-what-free-app-builders-actually-include-a1606/" rel="noopener noreferrer"&gt;Can you really create an app for free?&lt;/a&gt;. Spoiler: the pattern is identical across the market, and one competitor even admits in its own comparison that store publishing always requires a paid plan.&lt;/p&gt;

&lt;p&gt;This post is about the side of the counter you cannot see from a landing page: what the word "published" costs the vendor. Because that is where the free lunch dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "published" costs us, forever
&lt;/h2&gt;

&lt;p&gt;A built app is a file. A published app is a commitment. From our machine room, that commitment looks like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The store treadmill never stops.&lt;/strong&gt; Google Play raises its target API requirement every year. Apple killed UIWebView in 2020. The legacy push protocol died in March 2021. The Play Data safety form arrived in July 2022, Apple privacy manifests in May 2024. Each event means rebuilding and resubmitting fleets of apps through two review pipelines. Skip one deadline and apps disappear from stores. This work is invisible, recurring, and nobody can bill it to "free".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime is real hardware.&lt;/strong&gt; Our customers' apps call a backend every day. We run our own machines in racks in European datacenters. The CDN moves terabytes per day. Push delivery counts in millions of notifications per week. An engineer is on call all year; the status page is public (goodbarberstatus.com). None of this pauses when a customer pays zero.&lt;/p&gt;

&lt;p&gt;A free plan that included native publishing would mean carrying all of the above, indefinitely, for free. Nobody in this market does it. That is not stinginess. That is arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  So who pays for your free plan?
&lt;/h2&gt;

&lt;p&gt;The lunch is never free; the bill just moves. On a typical free app builder tier, you pay with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branding.&lt;/strong&gt; Your app advertises its builder. Removing the badge is a paid feature, everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wall.&lt;/strong&gt; The upgrade prompt lands at maximum investment, minimum leverage. That timing is not an accident, it is the business model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A link instead of a listing.&lt;/strong&gt; Free "apps" are often web links. The store fees never trigger, and neither does the store presence you came for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your time.&lt;/strong&gt; The expensive one. Rebuilding elsewhere because platform one could not ship is the worst deal in no-code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And two line items belong to nobody's free plan because they are not the builder's to give: Apple charges 99 $ a year for a developer account, Google Play 25 $ once. A page that promises store publishing "for free" without those numbers is describing a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  In defense of free tiers
&lt;/h2&gt;

&lt;p&gt;Said plainly, because my employer's competitors deserve fairness: free tiers are a great deal when your goal matches their content. Testing an idea on a real phone screen. Learning how a no-code editor thinks. Shipping a link to five colleagues. For all of that, use a free plan and pay nobody, including us.&lt;/p&gt;

&lt;p&gt;The problem is never the free tier. It is the landing page that lets you believe the lunch stays free all the way to the App Store.&lt;/p&gt;

&lt;h2&gt;
  
  
  The menu with prices on it
&lt;/h2&gt;

&lt;p&gt;GoodBarber has no free plan, and now you know the reason is structural, not commercial. Our trade is different: the full platform, free for 30 days, no credit card, including a PWA you can actually publish during the trial (&lt;a href="https://www.goodbarber.com/free-app-builder/" rel="noopener noreferrer"&gt;what free really includes&lt;/a&gt;). When it ends, nothing is charged, because we never took a card. You have seen the real product and the real prices, and you decide.&lt;/p&gt;

&lt;p&gt;No free lunch. But a menu with prices on it, and a table where you eat for 30 days before ordering.&lt;/p&gt;

&lt;p&gt;Same lesson as those 3G retransmissions: the cost never disappears, so honest design shows you where it sits. You know what to expect, and the price is not hidden. In 2009 we would have called that a well-designed protocol.&lt;/p&gt;

</description>
      <category>nocode</category>
      <category>mobile</category>
      <category>infrastructure</category>
      <category>startup</category>
    </item>
    <item>
      <title>Determinism Is a Feature</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:31:29 +0000</pubDate>
      <link>https://dev.to/pierrelaurentmedori/determinism-is-a-feature-3kb9</link>
      <guid>https://dev.to/pierrelaurentmedori/determinism-is-a-feature-3kb9</guid>
      <description>&lt;p&gt;We spent seventy years building machines that do exactly the same thing every time. It took about three years of AI hype to start describing that property as a limitation.&lt;/p&gt;

&lt;p&gt;I keep running into the framing in threads, in pitch decks, in hallway conversations: deterministic systems as the old world, rigid, unimaginative, waiting to be disrupted by something that improvises. And every time, I want to push back with the least fashionable opinion I hold: determinism is not the boring baseline that AI liberates us from. It is the single most valuable property a production system can have, and 2026 is the year we are most eagerly trading it away.&lt;/p&gt;

&lt;p&gt;Let me be precise about what this post is not. It is not an anti-AI post. I run an engineering team, and we run agents in production: they draft, they call tools, they touch real systems every day. I like this work. The argument here is not "don't use models". The argument is that the more probabilistic components you add, the more load-bearing the deterministic parts around them become, and that almost nobody is hiring, promoting, or celebrating for the second half of that sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What determinism actually buys you
&lt;/h2&gt;

&lt;p&gt;Reproducibility is not an aesthetic preference. It is purchasing power. Walk through what "same input, same output" quietly funds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reproducible bug is a bug that is already half fixed.&lt;/strong&gt; You can trap it, shrink it, watch it happen in a debugger. A non-reproducible bug is not a bug, it is a haunting. You don't fix hauntings, you appease them: add a retry, widen a timeout, sacrifice a sleep(500) and hope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failing test means something.&lt;/strong&gt; In a deterministic system, red means broken. The moment flakiness enters, red means "run it again", and the entire signal collapses. A suite that people rerun until it passes is not a test suite, it is a slot machine with CI minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;git bisect works.&lt;/strong&gt; Bisect is a miracle we stopped noticing: binary-search across history and the machine tells you which commit broke the world. The miracle rests entirely on replay giving the same answer twice. Bisect over a component that answers differently each run is not debugging, it is astrology with version control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A diff tells the truth.&lt;/strong&gt; Code review works because a change in behavior is legible from a change in text. The whole social contract of "I read your PR, therefore I know what it does" assumes the text determines the behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incidents can be reconstructed.&lt;/strong&gt; The 3 a.m. question is always "what exactly happened, in what order?" Every answer you can give assumes the system will tell the same story twice.&lt;/p&gt;

&lt;p&gt;Notice the shape of that list. Testing, review, bisect, rollback, audit: essentially every practice we call "software engineering" is a child of determinism. It is not one nice property among many. It is the property the rest of the discipline is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck moved
&lt;/h2&gt;

&lt;p&gt;Here is the economic argument, because the craft argument never convinces anyone in a planning meeting.&lt;/p&gt;

&lt;p&gt;For most of my career, producing the artifact was the expensive part. Writing the code, the config, the migration, the docs. So that is where the effort, the tooling, and the prestige went.&lt;/p&gt;

&lt;p&gt;That cost just fell off a cliff. Code, tests, configs, entire services: generating a plausible version of almost anything is now nearly free. And when the cost of production approaches zero, the remaining cost of software is the cost of verification. Checking that the plausible thing is actually true is no longer a phase of the project. It is the project.&lt;/p&gt;

&lt;p&gt;The asymmetry is brutal. Generated output scales with compute, which gets cheaper every quarter. Verification scales with human attention, which does not scale at all. Every team I talk to is discovering the same imbalance: the pipeline that produces changes got a jet engine, and the pipeline that validates them still runs on the same few pairs of eyes it had in 2019.&lt;/p&gt;

&lt;p&gt;So the scarce skill inverted. The valuable engineer of 2020 was the one who could produce faster. The valuable engineer of 2026 is the one who can look at something plausible and determine, efficiently and reliably, whether it is correct. Plausible is the commodity now. Correct is still artisanal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the non-determinism where wrong is cheap
&lt;/h2&gt;

&lt;p&gt;None of this means banning models from production. It means placing them deliberately, the way you place any component with a known failure mode.&lt;/p&gt;

&lt;p&gt;A probabilistic component belongs where a wrong answer is cheap and checkable: a draft, a suggestion, a summary, a first pass that a cheaper process can validate. It does not belong holding your state, moving your money, or standing alone in front of an irreversible action.&lt;/p&gt;

&lt;p&gt;The pattern that works is deterministic rails around a probabilistic core. And the rails are built from the least glamorous toolbox in the profession: schemas validated at the boundary, state machines whose enums make illegal transitions unrepresentable, idempotency keys so a confused retry collapses into one effect instead of two, version-bound writes so decisions made on stale data get rejected instead of applied, a plan-then-commit gate in front of anything with blast radius, and an append-only log so "what did the system do?" never depends on anyone's memory.&lt;/p&gt;

&lt;p&gt;I wrote a whole article on the how of those rails at one specific boundary, &lt;a href="https://dev.toURL-MCP-WRITE-ARTICLE"&gt;agents writing to production systems&lt;/a&gt;: where each mechanism goes, what each one catches, what the server contract looks like. Consider this piece the why. Nothing in that toolbox was invented this decade, and that is exactly the point: it is decades of transactional common sense, redeployed at a new boundary, the line between a model and everything you care about. The model gets to be creative precisely because the rails are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it back before you believe it
&lt;/h2&gt;

&lt;p&gt;I have a rule I apply to everything now, and it started long before agents: a system's report about itself is a claim, not a fact. Read it back before you believe it.&lt;/p&gt;

&lt;p&gt;The agent says it published the article? Fetch the article through the same path a reader would, and look at it. The migration says it ran? Count the rows. The dashboard is green? Green means "the things we chose to measure are within the thresholds we chose to set", which is a much smaller statement than "everything is fine". Your own code from last year? Same treatment. The author being you buys it nothing.&lt;/p&gt;

&lt;p&gt;For years this was a habit. Recently I understood it is actually a design requirement, and this is where it connects back to determinism: verifiability is a property you build in, not a virtue you exercise afterwards.&lt;/p&gt;

&lt;p&gt;A deterministic system is cheap to verify. One run is proof; the test you wrote once keeps testifying forever. A probabilistic system can be made verifiable too, but not for free: you have to design the read-back paths, the invariants, the reconciliation jobs, the alert that fires when the report and the reality disagree. Skip that work and you have not built a system, you have built a story the system tells you about itself.&lt;/p&gt;

&lt;p&gt;The question that changed how I build is not "does it work?" It is "how will I know it is still working when I am not watching?" A system that cannot answer that question is not finished, no matter what the demo looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring is a career strategy
&lt;/h2&gt;

&lt;p&gt;Ten years ago, Dan McKinley told us to choose boring technology: spend your innovation tokens on your actual problem, not on your stack. The 2026 version of that advice is one level deeper: choose boring properties. Determinism. Idempotency. Reproducibility. Auditability. The stack will change under you; the properties transfer.&lt;/p&gt;

&lt;p&gt;Because here is the career math nobody puts on a slide. When everyone on the team can generate code, generating code stops being what distinguishes anyone. The floor rose; the ceiling did not move. What is scarce in a room full of people who can produce plausible artifacts is the person who can say "this is correct, and here is how I know", and have the second half of that sentence be load-bearing.&lt;/p&gt;

&lt;p&gt;That skill compounds, too. The prompt tricks of last spring are already quaint. The ability to design an invariant, bound a blast radius, or smell a non-reproducible test has been appreciating since the seventies and shows no sign of stopping.&lt;/p&gt;

&lt;p&gt;Nobody demos idempotency keys. Demos are generation; production is verification. The distance between the two is where reputations are quietly being made right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the boring thing
&lt;/h2&gt;

&lt;p&gt;The systems I trust all have the same personality: they do the same thing every time, and they can prove it. When I add a model to one of them, and I do, I add it where being wrong is cheap, with rails around it that never improvise.&lt;/p&gt;

&lt;p&gt;Determinism is a feature. Some years I would have said an underrated one. This year I will go further: it is the feature, the one that makes all the others checkable.&lt;/p&gt;

&lt;p&gt;Ship it. And read it back before you believe it.&lt;/p&gt;

&lt;p&gt;What is the most boring thing in your stack you would defend to the death? Genuinely curious.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>career</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Cows Don't Send Alerts</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:45:06 +0000</pubDate>
      <link>https://dev.to/pierrelaurentmedori/the-cows-dont-send-alerts-2n3f</link>
      <guid>https://dev.to/pierrelaurentmedori/the-cows-dont-send-alerts-2n3f</guid>
      <description>&lt;p&gt;My neighbor spent yesterday morning clearing a patch of maquis with a bulldozer. I watched him from the fence for a while, coffee in hand, contributing nothing. Then I walked down to check that the cows had water, because it's August in Corsica and that is the one job on this farm that cannot wait until later.&lt;/p&gt;

&lt;p&gt;The rest of the year I do engineering at GoodBarber, a mobile app platform. This month, my production system is a herd of cows and a water trough.&lt;/p&gt;

&lt;p&gt;This is not a burnout story. I'm fine, the team is fine, nothing is on fire. This is about something I re-learn every summer at my parents' farm and manage to forget again by October: disconnecting is not the opposite of engineering. It's where my engineering comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job I closed the laptop on
&lt;/h2&gt;

&lt;p&gt;For the past few months my work has looked like this: agents that draft, publish and verify things through MCP servers, pipelines that touch production, and one rule I repeat so often my team can probably recite it in their sleep: grep and eyes. When an agent says it did something, you go read the actual output, with your actual eyes, before you believe it. And a human stays in the loop for anything that ships.&lt;/p&gt;

&lt;p&gt;I like this work. A lot. Which is exactly why closing the laptop for two weeks felt harder than it should have. When you love the loop, you stop noticing you're inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The farm
&lt;/h2&gt;

&lt;p&gt;The heat arrives before you finish your coffee. By nine the cicadas are at full volume and they won't stop until dark; after a day you stop hearing them, a wall of sound your brain files under silence.&lt;/p&gt;

&lt;p&gt;The maquis smells like it's cooking. Immortelle, myrtle, hot dust. When the bulldozer bites into it the smell turns sharp, crushed green and diesel, and my father comments on the neighbor's technique from three fields away.&lt;/p&gt;

&lt;p&gt;The trough is a twenty-minute walk, morning and evening. You put your hand in the water, because looking is not enough: the surface can shine while the float valve sits jammed. The cows watch you do this with monumental indifference.The soft tick of the electric fence. On the way back you kick the same pine cone the whole length of the path, because there is nothing else to do. It takes a few days to understand that nothing else to do is the entire point.&lt;/p&gt;

&lt;p&gt;My phone lives in the kitchen. Half the property has no signal anyway. Evenings are long: tomatoes that actually taste of tomato, a card game nobody remembers the full rules of, chairs on the terrace facing the dark. When the cicadas finally stop, the silence has a texture. The silence in my apartment is just a machine that hasn't beeped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distance gives you back your judgment
&lt;/h2&gt;

&lt;p&gt;From the hill behind the house you can see the whole farm at once: which field burned yellow, where the fence sags, where the water runs and where it doesn't. None of that is visible from inside the barn.&lt;/p&gt;

&lt;p&gt;For months my days were a hundred items deep. A flaky deploy. A graph doing something odd. An agent behaving strangely at 2 a.m. Every one of those items legitimate, none of them wrong to look at, and together they formed a wall between me and the only question that actually matters: what shape is the system, and where is it sagging?&lt;/p&gt;

&lt;p&gt;Two weeks of distance, and the shape comes back on its own. Without opening a laptop, I can tell you the two things that genuinely worry me for the autumn. In June I couldn't have, and in June I had all the dashboards.&lt;/p&gt;

&lt;p&gt;Judgment turns out to be a renewable resource with a single supplier: distance from the thing you're judging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard bugs die in boredom
&lt;/h2&gt;

&lt;p&gt;Every engineer carries one around: the bug that survives every fix. You fix it, it comes back wearing a different stack trace. Mine came on vacation with me, uninvited. They always do.&lt;/p&gt;

&lt;p&gt;The answer did not arrive at a desk. It arrived on day six, somewhere between the trough and the gate, wet hands, no phone. I didn't write a line of code. I just finally saw it: the whole mechanism at once, including the part every previous fix had been politely stepping around.&lt;/p&gt;

&lt;p&gt;Boredom without notifications is not empty time. It's the only state in which the complete model of a hard problem fits in your head, because nothing is evicting it every ninety seconds. A notification doesn't need to be answered to cost you. It only needs to arrive.&lt;/p&gt;

&lt;p&gt;We talk about deep work as if it were a scheduling technique. I'd call it something simpler: it's what your brain does by default when nothing is allowed to interrupt it. The farm doesn't teach you to focus. It just stops preventing you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Urgent is a word that means something here
&lt;/h2&gt;

&lt;p&gt;On a farm in August, urgent is unambiguous. Cows without water is urgent. The smell of smoke on the wind is urgent. Everything else waits, and waiting costs nothing.&lt;/p&gt;

&lt;p&gt;The cows don't send alerts, by the way. You find out how they're doing by showing up, twice a day, whether anything is wrong or not. There is probably a monitoring philosophy hiding in that sentence, but I promised myself I wouldn't write it on vacation.&lt;/p&gt;

&lt;p&gt;Before leaving I skimmed the threads marked urgent across my inboxes. Dozens. I already know what I'll find when I get back, because it's the same every year: most of them will have resolved themselves, gone stale, or turned out to be someone else's adrenaline. What remains is the real work, and it was there all along, hidden in plain sight under the pile.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable part: I can't tell the two apart while I'm in the pile. Nobody can. Up close, all urgency produces the same cortisol. Telling signal from noise takes distance, and you cannot acquire distance while you're busy responding. The 90% of noise was never a triage failure. It was the absence of the one tool that makes triage possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grep and eyes, with actual eyes
&lt;/h2&gt;

&lt;p&gt;Here is the thing I actually came to say.&lt;/p&gt;

&lt;p&gt;Checking the trough with your hand because the surface can lie to you: that is grep and eyes. Walking the fence line instead of assuming it held: that is verification before belief. The rule I repeat to my team all year was not invented at a whiteboard. It was installed here, a long time ago, by people who never wrote a line of code and would never trust a report about anything they could go and check themselves.&lt;/p&gt;

&lt;p&gt;The farm and the job run on the same values. Look with your own eyes. Keep a human in the loop. Know the difference between urgent and loud. I don't disconnect to get away from engineering. I disconnect to visit the place my engineering came from, and I come back with the judgment, the unstuck bug, and a short list of the things that actually matter.&lt;/p&gt;

&lt;p&gt;The neighbor finished his patch of maquis yesterday. Where there was scrub there is now clean ground, ready for whatever comes next. Two more weeks and it's my turn.&lt;/p&gt;

&lt;p&gt;The first thing I'll do back at work is not open a dashboard. I'll go look at the system with my own eyes.&lt;/p&gt;

</description>
      <category>career</category>
      <category>wellbeing</category>
      <category>devlife</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Our MCP server is now a ChatGPT plugin</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:06:29 +0000</pubDate>
      <link>https://dev.to/goodbarber/our-mcp-server-is-now-a-chatgpt-plugin-2pjm</link>
      <guid>https://dev.to/goodbarber/our-mcp-server-is-now-a-chatgpt-plugin-2pjm</guid>
      <description>&lt;p&gt;GoodBarber is now listed in the &lt;a href="https://chatgpt.com/plugins/plugin_asdk_app_6a16d2ac52508191887344ea891be616?q=goodbarber" rel="noopener noreferrer"&gt;ChatGPT plugin directory&lt;/a&gt;. Find GoodBarber in the directory, connect your app, and run it from the chat: content, catalog, members, push notifications. This post extends &lt;a href="https://www.goodbarber.com/blog/goodbarber-is-now-in-the-chatgpt-app-directory-a1598/" rel="noopener noreferrer"&gt;the announcement on our blog&lt;/a&gt; with the developer view, and it closes a loop we opened when we wrote up how we built that server.&lt;/p&gt;

&lt;p&gt;One line of context for new readers: GoodBarber is a no-code app builder (running since 2011). Customers configure an app in a web back office; the platform compiles native iOS and Android builds plus a PWA. We &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;built a public MCP server&lt;/a&gt; so that MCP clients (Claude, ChatGPT, Codex, Cursor, n8n and others) can operate those apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  One server, two ways in
&lt;/h2&gt;

&lt;p&gt;Nothing changed server-side. Both paths land on the MCP connection your app already has, scoped to that single app and authorized by you. What changed is the effort to get connected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The official app in the directory.&lt;/strong&gt; Find GoodBarber, connect, authorize access to your app. No Developer mode, no URL to paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full GoodBarber MCP server as a custom connector.&lt;/strong&gt; Enable ChatGPT's Developer mode and add &lt;code&gt;https://mcp.goodbarber.dev/mcp/sse&lt;/code&gt; (the &lt;code&gt;/sse&lt;/code&gt; is a route name; the server speaks Streamable HTTP). This is also the path for Codex: Settings, Integrations, MCP servers, leave the bearer token field empty and the save opens the OAuth flow in a browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The step-by-step for both lives on &lt;a href="https://www.goodbarber.com/connect-chatgpt-app/" rel="noopener noreferrer"&gt;our setup page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the connection actually is
&lt;/h2&gt;

&lt;p&gt;If you have read our write-up on building the server, you know the architecture. The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One connection = one app.&lt;/strong&gt; Each GoodBarber app has its own MCP URL with a unique app id; an agent authorized on app A cannot reach app B. You authorize the connection in a browser via OAuth; there is no API key to paste into the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes are policy-flagged.&lt;/strong&gt; Every write returns &lt;code&gt;_mcp_policy.verification_required: true&lt;/code&gt;, so a well-behaved client reads back what it just changed before claiming success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool surface is documented, not hardcoded.&lt;/strong&gt; The authoritative list is the &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;server card&lt;/a&gt;; what a given app exposes can be a subset, depending on its features and plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our own tests, including the “Send my users a push about the spring sale.” example that opens &lt;a href="https://www.goodbarber.com/blog/goodbarber-is-now-in-the-chatgpt-app-directory-a1598/" rel="noopener noreferrer"&gt;the announcement&lt;/a&gt;, ran from a free ChatGPT account through the MCP connection: we drafted the push, confirmed it, and it landed on the test phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the listing matters
&lt;/h2&gt;

&lt;p&gt;Building the server was engineering. Being found is distribution, and for an agent-facing product the distribution surface is inside the assistant, not on your marketing site. Until now, connecting GoodBarber to ChatGPT meant knowing the endpoint existed, enabling a developer toggle, and pasting a URL. That filters out almost everyone who would benefit. A directory listing replaces all of that with a search box: something a non-technical app owner can actually do, without reading a single setup doc.&lt;/p&gt;

&lt;p&gt;What we do not control: how the directory surfaces or ranks apps. That side is OpenAI's.&lt;/p&gt;

&lt;p&gt;For us, building the server turned out to be the easy half. The listing is what puts it in front of the people it was built for.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>openai</category>
      <category>chatgpt</category>
      <category>ai</category>
    </item>
    <item>
      <title>MCP 2026-07-28 from the server side: Codex already speaks it, Claude doesn't yet</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Thu, 13 Aug 2026 12:52:51 +0000</pubDate>
      <link>https://dev.to/goodbarber/mcp-2026-07-28-from-the-server-side-codex-already-speaks-it-claude-doesnt-yet-4fj</link>
      <guid>https://dev.to/goodbarber/mcp-2026-07-28-from-the-server-side-codex-already-speaks-it-claude-doesnt-yet-4fj</guid>
      <description>&lt;p&gt;On July 28, the Model Context Protocol project shipped a new spec revision, &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;2026-07-28&lt;/a&gt;. I run backend engineering at GoodBarber, and our &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;public MCP server&lt;/a&gt; is a live production surface: real apps, real content, real push notifications. So for us a new revision is not a changelog to skim on a Friday. It is a migration with our name on it.&lt;/p&gt;

&lt;p&gt;We have just brought the server up to the new revision. This post is three things: the operator's cut of what changed, what upgrading a public server actually involves, and the thing we found in our logs while checking the work. The last one is the reason I'm writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operator's cut of 2026-07-28
&lt;/h2&gt;

&lt;p&gt;The headline is the stateless core. MCP grew up as a stateful, bidirectional protocol: an &lt;code&gt;initialize&lt;/code&gt; handshake, a negotiated session, an &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header to carry it all. The new revision retires that entirely. Every request now self-describes in &lt;code&gt;_meta&lt;/code&gt;: protocol version, client identity, capabilities. The practical consequence is the one server operators have wanted since day one: you can put an MCP server behind a plain round-robin load balancer with no shared session storage. If you have ever kept session affinity alive with duct tape, you know exactly which muscle just relaxed.&lt;/p&gt;

&lt;p&gt;The rest, fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method and tool names now also travel in &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; HTTP headers, so gateways can route and meter without parsing JSON bodies.&lt;/li&gt;
&lt;li&gt;Multi Round-Trip Requests: a call can come back with &lt;code&gt;resultType: "input_required"&lt;/code&gt; and continue over stateless connections. Mid-call questions no longer need a held-open stream.&lt;/li&gt;
&lt;li&gt;List results (tools, prompts, resources) carry &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt;, so clients can finally cache your inventory honestly instead of guessing.&lt;/li&gt;
&lt;li&gt;Authorization hardening: RFC 9207 issuer validation, and Client ID Metadata Documents replacing Dynamic Client Registration.&lt;/li&gt;
&lt;li&gt;Tasks, MCP Apps, and Enterprise Managed Authorization become formal extensions instead of core features.&lt;/li&gt;
&lt;li&gt;Roots, Sampling, and Logging are deprecated, with a minimum twelve months of support before removal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four Tier 1 SDKs spoke the revision on launch day, which for a protocol this young is genuinely good release engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading without breaking anyone
&lt;/h2&gt;

&lt;p&gt;Here is the constraint that shapes everything when your server is public: you do not get to move your clients. Every agent that connects brings its own dialect, and you serve all of them or you break someone's production workflow.&lt;/p&gt;

&lt;p&gt;So "upgrading" really means "adding a dialect". The old &lt;code&gt;initialize&lt;/code&gt; path stays up for clients that expect it. The stateless path answers clients that self-describe. And one rule keeps the whole thing sane: the declared protocol version selects the behavior, never the client name. You would be surprised how tempting user-agent sniffing becomes during a migration. Resist it. The version declaration is the contract; the client name is trivia.&lt;/p&gt;

&lt;p&gt;The other rule is older than MCP: nothing gets removed the day the spec says "deprecated". The spec gives Roots, Sampling, and Logging at least twelve months. The real clock is not the spec calendar. It is in your own logs, which brings me to the fun part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we read the logs
&lt;/h2&gt;

&lt;p&gt;We still verify upgrades the old-school way at GoodBarber: a human reads the code, and a human reads the logs. No dashboard, no pipeline, just grep and eyes. &lt;a href="https://dev.to/pierrelaurentmedori/llmstxt-in-the-wild-1321-requests-and-not-one-ai-assistant-came-looking-3205"&gt;Last week that habit produced a whole article about llms.txt&lt;/a&gt;. It keeps paying for itself.&lt;/p&gt;

&lt;p&gt;The protocol version a client speaks sits in plain text: in the &lt;code&gt;initialize&lt;/code&gt; params for old-style clients, in the version declaration on each request for new-style ones. So during the development phase of the upgrade, we looked at who declares what on our traffic.&lt;/p&gt;

&lt;p&gt;What we saw surprised us. Requests from OpenAI's Codex were already declaring 2026-07-28. The revision was barely two weeks old. Meanwhile, Claude clients were still opening with &lt;code&gt;initialize&lt;/code&gt; on an earlier revision, exactly as they always had.&lt;/p&gt;

&lt;p&gt;Let me be precise about what this is not. It is not a scoreboard. Pinning a revision and upgrading deliberately is what a well-behaved client does. Serving several revisions at once is what a well-behaved server does. Both populations got exactly what they asked for, which is the entire point of version negotiation.&lt;/p&gt;

&lt;p&gt;But it is a data point you can only see from the server side, and it killed a lazy assumption I was carrying: that adoption of a brand-new revision would sit near zero for months, so supporting it "eventually" would be fine. In reality, part of your traffic may speak the newest revision before your migration branch is merged. And the split does not fall where intuition puts it: I would not have bet on which client family moved first.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run an MCP server
&lt;/h2&gt;

&lt;p&gt;Four suggestions, all cheap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log the declared protocol version.&lt;/strong&gt; One field per request. It is the cheapest telemetry you will ever add, and it is the only ground truth you have for deprecation decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test against real clients, plural.&lt;/strong&gt; If you validate against a single vendor's client, you will quietly extrapolate that client's dialect to the whole world. Our logs contradicted our intuition within days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan removals from your version distribution, not from the spec calendar.&lt;/strong&gt; Twelve months is a floor, not a schedule. If a revision still carries real traffic, it stays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key behavior on the declared version, never on the client name.&lt;/strong&gt; Negotiation is the contract you both signed. Honor it in both directions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The upgrade itself was the unglamorous kind of work that never makes a launch post. But the logs made it worth a story: protocol adoption is happening faster, and more unevenly, than the discourse suggests, and every server operator is sitting on the evidence.&lt;/p&gt;

&lt;p&gt;If you operate an MCP server: what does your version distribution look like? I am genuinely curious whether "Codex first" is universal or just our traffic.&lt;/p&gt;

&lt;p&gt;One line on where I'm writing from: I run engineering at &lt;a href="https://www.goodbarber.com" rel="noopener noreferrer"&gt;GoodBarber&lt;/a&gt;, an app platform. We run &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;a production MCP server&lt;/a&gt; and publish &lt;a href="https://github.com/goodbarber/goodbarber-skills" rel="noopener noreferrer"&gt;open-source Claude Skills&lt;/a&gt; — so the 2026-07-28 migration is a live problem for us, not a thought experiment.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>llms.txt in the wild: 1,321 requests, and not one AI assistant came looking</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:31:16 +0000</pubDate>
      <link>https://dev.to/pierrelaurentmedori/llmstxt-in-the-wild-1321-requests-and-not-one-ai-assistant-came-looking-3205</link>
      <guid>https://dev.to/pierrelaurentmedori/llmstxt-in-the-wild-1321-requests-and-not-one-ai-assistant-came-looking-3205</guid>
      <description>&lt;p&gt;We publish an llms.txt on our main domain and on every language portal. It is exactly what the proposal asks for: a curated markdown index of the site, at the root, written for AI systems to read. I pulled four months of nginx logs to answer one question: who actually reads it?&lt;/p&gt;

&lt;p&gt;From April 7 to August 5, 2026, our front-ends served 1,321 requests to &lt;code&gt;/llms.txt&lt;/code&gt; and &lt;code&gt;/llms-full.txt&lt;/code&gt; across eleven hostnames (plus a few stray subdomains). About eleven a day, 92 percent answered with a 200. That volume already tells you this is not a busy interface. But the volume is not the story. The story is who those requests came from, because I went through every one of them, checked the interesting ones against published IP ranges, and the answer says something uncomfortable about the file, and something genuinely funny about the ecosystem around it. Ahrefs and Evil Martians have already counted this traffic at scale, and I will point at both. What I wanted was per-request forensics: when a hit wears an AI badge, is it real, and who sent it?&lt;/p&gt;

&lt;p&gt;One line of context so you know where I stand: I run engineering at &lt;a href="https://www.goodbarber.com" rel="noopener noreferrer"&gt;GoodBarber&lt;/a&gt;, an app platform. We run &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;a production MCP server&lt;/a&gt;, we publish &lt;a href="https://github.com/goodbarber/goodbarber-skills" rel="noopener noreferrer"&gt;Claude Skills&lt;/a&gt;, we want the machine-readable web to work. This is not a takedown by someone who resents agents. It is a log review by someone who ships for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What llms.txt promised
&lt;/h2&gt;

&lt;p&gt;Quick recap, then the data. &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; was proposed by Jeremy Howard in September 2024: a markdown file at your site root "to provide information to help LLMs use a website at inference time". Same placement logic as robots.txt and sitemap.xml. Mintlify made it a default for every docs site it hosts in November 2024, Shopify quietly shipped it to storefronts in spring 2026, and &lt;a href="https://ppc.land/llms-txt-adoption-rises-8-8x-but-97-of-files-get-zero-ai-requests/" rel="noopener noreferrer"&gt;Originality.ai's tracker&lt;/a&gt; counted an 8.8x adoption jump in a year. On the publish side, the format is a success.&lt;/p&gt;

&lt;p&gt;The open question was always the other side: does anything read it? Not "does anything fetch any file on the internet", because scanners fetch everything. Does the intended audience, the assistants and their crawlers, actually consume it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually reads our llms.txt
&lt;/h2&gt;

&lt;p&gt;Here is the reader board for our 1,321 requests, by user-agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generic browser user-agents, HeadlessChrome, WebPageTest agents, assorted scripts&lt;/td&gt;
&lt;td&gt;~600&lt;/td&gt;
&lt;td&gt;tools that did not bother introducing themselves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;satoric-indexer/0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;116&lt;/td&gt;
&lt;td&gt;unknown indexer; even Ahrefs could not identify it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Googlebot&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;URL crawling, and Google says it ignores the file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;closing-web-clock/1.0 (+policy-surface observation)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;anonymous project observing "policy surfaces"; zero public footprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SemrushBot SiteAudit&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;SEO audit tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BuiltWith&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;tech profiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLMS-Txt-Scanner/1.0 (Research)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;anonymous scanner on Azure IPs, self-labeled research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AgentRadar-Research/1.0 (BCG Henderson Institute; agentic-web-research)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;identifies itself as a BCG crawler studying the agentic web; Evil Martians logged the same operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;statespace-indexer/0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;agentic-web indexer; its likely parent repo was archived on August 5, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prompt-injection-survey/1.0 (research)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;self-described security research scanning llms.txt as an attack surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SEO auditors, tech profilers, headless browsers, and a small zoo of research scanners. So far this matches what &lt;a href="https://ahrefs.com/blog/llmstxt-study/" rel="noopener noreferrer"&gt;Ahrefs found across 137,210 domains&lt;/a&gt; in June (97 percent of llms.txt files got zero requests; of the traffic that exists, SEO audit tools are the biggest reader at 21.7 percent, AI assistants 2.5 percent), and what &lt;a href="https://evilmartians.com/chronicles/which-ai-actually-reads-your-site-two-months-of-llm-traffic-measured" rel="noopener noreferrer"&gt;Evil Martians found in their own logs&lt;/a&gt; in July. Both are worth your time. Neither answered the question I actually had, which is why this post exists: when a request does carry an AI assistant's name, is it real, and did the assistant come on its own?&lt;/p&gt;

&lt;h2&gt;
  
  
  The 26 requests that wore an AI badge
&lt;/h2&gt;

&lt;p&gt;Out of 1,321 requests in four months, exactly 26 carried the user-agent of an AI assistant or its crawlers. I checked each one against the vendors' published IP ranges (&lt;a href="https://openai.com/gptbot.json" rel="noopener noreferrer"&gt;gptbot.json&lt;/a&gt;, &lt;a href="https://openai.com/chatgpt-user.json" rel="noopener noreferrer"&gt;chatgpt-user.json&lt;/a&gt;, &lt;a href="https://openai.com/searchbot.json" rel="noopener noreferrer"&gt;searchbot.json&lt;/a&gt;, &lt;a href="https://www.perplexity.ai/perplexitybot.json" rel="noopener noreferrer"&gt;perplexitybot.json&lt;/a&gt;, and Anthropic's known blocks). They decompose like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three were impostors.&lt;/strong&gt; A single DigitalOcean IP requested our llms.txt three times in the same second, presenting as &lt;code&gt;GPTBot/1.0&lt;/code&gt;, then &lt;code&gt;PerplexityBot/1.0&lt;/code&gt;, then &lt;code&gt;ClaudeBot/1.0&lt;/code&gt;. Bare version strings, none of the vendors' real UA formats, an IP in nobody's published ranges. Somebody's compliance checker cycling through bot costumes to see if we cloak (serve different content to bots than to humans). If you count assistants by user-agent alone, your llms.txt numbers include this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four were GPTBot, genuinely.&lt;/strong&gt; All four IPs validate against OpenAI's published gptbot.json ranges. This is the interesting part, because all four requests carried a &lt;code&gt;Referer&lt;/code&gt; header. Three referers were &lt;code&gt;https://agentready.md&lt;/code&gt;, an AI-readiness checker that validates llms.txt files and runs a public leaderboard of them. The fourth was &lt;code&gt;https://sitestatsdb.com&lt;/code&gt;, which I could not even inspect, because it now sits behind a bot-verification wall. GPTBot did not wake up wanting our llms.txt. It crawled third-party pages that link to our llms.txt, and followed the links like it follows any link. In four months and 215,484 GPTBot requests to our sites, the number of times it requested llms.txt without a third party pointing at it: zero.&lt;/p&gt;

&lt;p&gt;I think this detail matters beyond our logs. Ahrefs found GPTBot to be the single biggest AI fetcher of llms.txt files (4.51 percent of requests) and concluded, correctly, that "Zero requests came from AI bots for llms.txt files that don't exist. They never go looking." Our referer data shows the mechanism behind both facts: an ecosystem of llms.txt directories and audit tools now links out to these files, and a crawler that follows links will follow those too. The training crawler "reads" llms.txt the way it reads any URL it encounters: as content, not as protocol. The llms.txt promotion ecosystem is generating the llms.txt consumption statistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten were ChatGPT-User.&lt;/strong&gt; Azure IPs, half of them in the current published rotation (the list rotates, and these hits go back to April). ChatGPT-User is OpenAI's on-demand fetcher: it fires when a human asks ChatGPT something that requires opening a specific page. Ten requests in four months, each one meaning a human request was behind the fetch. Given what people use ChatGPT for these days, my honest guess is "check my site's llms.txt" prompts and GEO (generative engine optimization) audit tools, and I cannot rule out that some of those humans were us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two were ClaudeBot.&lt;/strong&gt; Anthropic's crawler, from its usual 216.73.216.x block. These are the only two requests in the entire window where a dedicated AI crawler fetched the file with no referer and no human in the loop. Two, out of 262,242 ClaudeBot requests to our sites in the same period. Credit where due, and note what it is: the training crawler stocking a corpus, not an assistant consulting an index on a user's behalf. No user-facing assistant ever came on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four were Claude-User and three were Claude Code.&lt;/strong&gt; Claude-User is Anthropic's on-demand fetcher, same story as ChatGPT-User: a human asked. The Claude Code hits are the most telling: &lt;code&gt;Claude-User (claude-code/2.1.x)&lt;/code&gt; requests coming from a residential ISP line in France and a French cloud box. That is a developer at a keyboard pointing Claude Code at the file, quite possibly someone on my own team. It is also the one documented use case that actually works: a coding agent reading a docs index because a human told it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meanwhile, in the same logs
&lt;/h2&gt;

&lt;p&gt;Now the control group, because "assistants do not read llms.txt" could just mean "assistants do not read us". They read us plenty. Same window, same domains, AI bots only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User-agent&lt;/th&gt;
&lt;th&gt;Requests, all paths&lt;/th&gt;
&lt;th&gt;Requests to /llms*, unprompted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT-User&lt;/td&gt;
&lt;td&gt;472,985&lt;/td&gt;
&lt;td&gt;0 (10 user-triggered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta-ExternalAgent&lt;/td&gt;
&lt;td&gt;369,504&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;262,242&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;215,484&lt;/td&gt;
&lt;td&gt;0 (4 via directory links)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;171,121&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerplexityBot&lt;/td&gt;
&lt;td&gt;63,475&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity-User&lt;/td&gt;
&lt;td&gt;22,667&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude-User&lt;/td&gt;
&lt;td&gt;19,324&lt;/td&gt;
&lt;td&gt;0 (4 user-triggered)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude-SearchBot&lt;/td&gt;
&lt;td&gt;17,512&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckAssistBot&lt;/td&gt;
&lt;td&gt;6,511&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude-User (claude-code)&lt;/td&gt;
&lt;td&gt;4,513&lt;/td&gt;
&lt;td&gt;0 (3 user-triggered)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is 1.6 million requests from AI systems in four months. ChatGPT-User alone made 89,668 blog-article fetches, about 740 a day, triggered by real people asking real questions; our most-fetched pages are deep technical pieces on app store compliance that classic SEO metrics would call worthless. The assistants are not ignoring our content. They are devouring it, hundreds of article fetches a day. And in the same four months, the file that exists specifically for them was fetched unprompted exactly twice, both times by the training crawler. Every other AI-labeled visit had a human behind it or a link in front of it.&lt;/p&gt;

&lt;p&gt;And the ratio that settles it: Claude Code user-agents made 4,513 requests to our pages in four months. Documentation pages mostly, 2,264 hits on &lt;code&gt;/help/&lt;/code&gt; alone, plus 1,182 on the blog. Number of those requests that went to llms.txt: three. Even the archetypal llms.txt consumer, the coding agent that the strongest case for llms.txt is built on, overwhelmingly goes straight to the actual pages.&lt;/p&gt;

&lt;p&gt;This is consistent with everything the vendors themselves publish, by the way. &lt;a href="https://developers.openai.com/api/docs/bots" rel="noopener noreferrer"&gt;OpenAI's bot documentation&lt;/a&gt; explains robots.txt directives and never mentions reading llms.txt. Same for &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic's&lt;/a&gt; and &lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity's&lt;/a&gt;. Google wrote it into its &lt;a href="https://developers.google.com/search/docs/fundamentals/ai-optimization-guide#mythbusting" rel="noopener noreferrer"&gt;official AI documentation&lt;/a&gt; in June: "Google Search ignores them." Meanwhile all three AI vendors publish llms.txt files for their own docs sites, largely because their docs platforms generate one by default. Everyone is publishing the format. Nobody is committing to consume it.&lt;/p&gt;

&lt;p&gt;John Mueller said it plainly back in April 2025: none of the AI services have said they use it, "and you can tell when you look at your server logs that they don't even check for it." Fifteen months later, on our logs, still true.&lt;/p&gt;

&lt;h2&gt;
  
  
  An object of research before an object of use
&lt;/h2&gt;

&lt;p&gt;So who is left? Go back to the reader board. Once you remove the SEO auditors, the profilers, and the scripts, the most attentive readers of our llms.txt are research crawlers: an agent presenting itself as BCG Henderson Institute research on the agentic web, an anonymous &lt;code&gt;LLMS-Txt-Scanner&lt;/code&gt; running from Azure, a &lt;code&gt;closing-web-clock&lt;/code&gt; observing "policy surfaces", a &lt;code&gt;statespace-indexer&lt;/code&gt; whose likely operator archived its repo the day before I wrote this, and my personal favorite, &lt;code&gt;prompt-injection-survey/1.0 (research)&lt;/code&gt;: a self-described survey of llms.txt as a prompt injection attack surface, because a plaintext file that sites voluntarily write for LLMs to ingest is, of course, a lovely place to plant instructions. All of this assuming their user-agents tell the truth, which, as the DigitalOcean impostor demonstrated, is optional. Ahrefs found the same pattern at scale: 12 percent of all llms.txt requests come from tools auditing, scoring, validating, or studying llms.txt itself.&lt;/p&gt;

&lt;p&gt;The file has a thriving ecosystem of directories that list it, auditors that score it, scanners that catalog it, researchers that study it, and security people who probe it. Every layer of that stack is real and shipping. The one layer that does not exist is the consumer it was designed for.&lt;/p&gt;

&lt;p&gt;llms.txt has become an object of research before ever becoming an object of use.&lt;/p&gt;

&lt;p&gt;I want to be fair to the proposal here: nothing in the spec is wrong. It is a reasonable, well-designed answer to a real problem, context windows are finite and HTML is noisy. The assistants simply built around the problem instead: they fetch the real pages on demand, at scale, triggered by user questions. The protocol layer the file was supposed to occupy got filled by retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would actually do
&lt;/h2&gt;

&lt;p&gt;My recommendation, which is what we do ourselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the file if it costs you an hour.&lt;/strong&gt; Ours stays. It is one hour of maintenance, it is harmless, and if a platform someday announces real consumption, you are ready. Google's official position is literally "won't harm (nor help)".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it accurate, precisely because humans send agents to it.&lt;/strong&gt; Every genuine assistant-family read in our logs, the two ClaudeBot fetches aside, had a human behind it or a link in front of it. Those are the readers you are writing for: a developer pointing Claude Code at your docs, a prospect asking ChatGPT to look at your file. Stale content there is worse than no file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you have developer docs, that is the one real use case.&lt;/strong&gt; Coding agents directed at an llms.txt index do consume it (tooling like &lt;a href="https://github.com/langchain-ai/mcpdoc" rel="noopener noreferrer"&gt;mcpdoc&lt;/a&gt; is built on exactly this). If your product has no docs for developers, your llms.txt is decoration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never report it as an AI-visibility deliverable.&lt;/strong&gt; If someone bills you for "GEO optimization" and the deliverable is an llms.txt file, you now have four months of somebody else's server logs saying what that is worth. What actually got our content into assistants, per the same logs: deep, specific, technical articles that answer the questions people actually ask, plus a robots.txt that lets retrieval bots in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the file as code, not copy.&lt;/strong&gt; Version-control it, restrict who edits it, keep it to plain links and descriptions. The most motivated readers of the format right now include people studying it as an injection surface. Do not give them material.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own logs
&lt;/h2&gt;

&lt;p&gt;Methodology, so you can replicate: 121 days of nginx access logs (April 7 to August 5, 2026) across our production hostnames, every request matching &lt;code&gt;GET /llms&lt;/code&gt;, user-agents tallied, and every AI-labeled hit checked against the vendors' published IP ranges where they exist (OpenAI, Perplexity), and, for Anthropic, which publishes no list, against the blocks that carry its 262,000 other requests in the same logs. One caveat cuts both ways: user-agent strings are claims, not proofs. Our ~600 generic-browser requests surely hide a few interesting readers, and any study that counts assistants by UA alone, including this one before the IP checks, will overcount them. Could an assistant fetch llms.txt in stealth, hiding in those 600? In principle. But a vendor that cheerfully declares itself on 1.6 million fetches to the same servers has no evident reason to go incognito for this one file.&lt;/p&gt;

&lt;p&gt;The whole analysis is a couple of grep and awk commands. If you publish an llms.txt, run them. The base rate says you will find silence; if you find an AI assistant that came looking on its own, unprompted, no referer, IP in the vendor's published range, I genuinely want to see that log line. Comments are open.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your MCP write returned 200. Did the right thing actually happen?</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:09:00 +0000</pubDate>
      <link>https://dev.to/pierrelaurentmedori/your-mcp-write-returned-200-did-the-right-thing-actually-happen-38n0</link>
      <guid>https://dev.to/pierrelaurentmedori/your-mcp-write-returned-200-did-the-right-thing-actually-happen-38n0</guid>
      <description>&lt;p&gt;The failure mode that worries me most in agent tooling is not a crash. It looks like success.&lt;/p&gt;

&lt;p&gt;An operator, somewhere, asks their agent to put the summer collection on sale. The agent discovers the products, loops over the variants, fires four hundred write calls, collects four hundred 200s, and reports back, confident and polite: "Done! All prices are updated." And they are. On the wrong collection.&lt;/p&gt;

&lt;p&gt;Nothing in that transcript failed. The transport worked. The writes persisted. The agent did not hallucinate a single call. Yet the one thing the operator actually wanted, "the summer collection, on sale, nothing else", is not what happened. If you have shipped distributed systems you know this smell: every component is green and the system is wrong.&lt;/p&gt;

&lt;p&gt;I run engineering at GoodBarber, an app platform, and our MCP server exposes a live production surface: content, commerce, push notifications, real apps with real users. So this is not a thought experiment for me. And judging by the comments on my last piece and the threads landing on r/mcp lately, it is not a thought experiment for many of us anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three different things we call "success"
&lt;/h2&gt;

&lt;p&gt;When an agent reports "done", that word is doing a lot of undeclared work. Unpack it and there are three separate claims stacked on top of each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transport success.&lt;/strong&gt; The request arrived, the server answered, nothing threw. HTTP 200, no protocol error. This is the only one of the three the agent gets for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State success.&lt;/strong&gt; The write persisted: the article exists, the price changed, the push is scheduled. This is what a verified read-back proves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent success.&lt;/strong&gt; What persisted is what the operator meant, within the policies of the account. The right collection. The right segment. The right amount.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Language models are spectacular at conflating the three. A 200 becomes "done"; "done" becomes "what you wanted". Not because the model lies, but because nothing in the loop ever represented intent explicitly, so the model reports the only success it can observe.&lt;/p&gt;

&lt;p&gt;After my last piece, an engineer I traded notes with put the gap in one line I have not managed to improve on: given only transport success, the agent will "faithfully confirm the wrong thing." Confirmation is not correctness. A model that is perfectly honest about what it saw can still be wrong about what it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where verified read-back stops
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.goodbarber.com/blog/not-all-mcp-servers-are-equal-baas-mcp-vs-application-mcp-a1565/" rel="noopener noreferrer"&gt;my last piece&lt;/a&gt; I argued that an application MCP server should verify writes as a matter of server contract. On our server, every write result carries &lt;code&gt;_mcp_policy.verification_required: true&lt;/code&gt;: the server itself instructs the agent to read the object back through the public read path and confirm the result before declaring success. I still believe every word of that. Read-back closes the gap between transport and state, and it catches the most common agent failure in the wild, which is declaring success on a write that never landed.&lt;/p&gt;

&lt;p&gt;But I want to be honest about where it stops. Read-back proves the object changed the way the call said it would. It cannot prove the call was the right call. In the repricing story above, read-back passes four hundred times in a row. Wrong collection, verified thoroughly.&lt;/p&gt;

&lt;p&gt;Necessary, not sufficient. That exchange, and those threads, shaped the list below: the layers that close the rest of the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layers that close the gap
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Most of it is decades of transactional common sense applied at a new boundary: the line between a language model and your production system. Each layer kills one specific class of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Scope the credential, not the prompt
&lt;/h3&gt;

&lt;p&gt;"Please only touch the blog" is not a permission model. The prompt is reachable territory for anyone who can get text in front of your agent: a product review, a support ticket, a pasted document. If the prompt is the only thing standing between the agent and a tool family it should never use, you do not have scoping, you have a suggestion.&lt;/p&gt;

&lt;p&gt;Scoping has to live where the prompt cannot reach it: in the credential. On our server that takes two forms. Sessions are bound by OAuth to a single app, so an agent connected to app A cannot discover that app B exists. And the tool list is generated from what the app has enabled: no shop, no &lt;code&gt;shop_&lt;/code&gt; tools; push not configured, no push tools. The families the agent must not touch are not denied, they are absent from the inventory. Billing, design, and the build pipeline are not on the surface for anyone. The &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;server card&lt;/a&gt; is public if you want to see what a feature-gated inventory looks like.&lt;/p&gt;

&lt;p&gt;What this buys you: a compromised or confused session cannot escalate. There is nothing to escalate into.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Idempotency, because agents retry
&lt;/h3&gt;

&lt;p&gt;Agents retry. Clients time out and replay. Runtimes resume interrupted loops. In human API usage a duplicate submit is rare; in agent usage, retry-on-ambiguity is standard behavior, which means "the network blinked between the request and the response" must never be able to mean two pushes or two charges.&lt;/p&gt;

&lt;p&gt;The first half of the answer is declarative, and MCP already has the vocabulary for it: tool annotations. Every tool on our card declares &lt;code&gt;idempotentHint&lt;/code&gt; (along with &lt;code&gt;readOnlyHint&lt;/code&gt; and &lt;code&gt;destructiveHint&lt;/code&gt;): updates are marked safe to replay, creates and push are not, and the server-side failure policy the meta tool returns caps retries at exactly one. A client that respects the annotations will not loop a non-idempotent write on its own initiative.&lt;/p&gt;

&lt;p&gt;The second half is where most surfaces, ours included, still have work to do: true idempotency keys on the non-idempotent writes. The mechanism transfers unchanged from classic API design: the client sends a key with the write, the server stores the outcome under that key, and a replay returns the stored outcome instead of running the write again. Stripe drilled this into a generation of engineers. MCP write tools need it for a bigger reason: the thing doing the retrying now improvises.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bind the write to the version you read
&lt;/h3&gt;

&lt;p&gt;Between the agent's read and the agent's write, the ground can move: another agent, a colleague in the back office, a scheduled job. A write built on a stale read should be rejected, not applied. The mechanism is old and boring: the write carries the version (or etag, or updated-at) of the object as the agent read it, and the server refuses with a conflict if the object has moved since. The agent re-reads and re-decides, with current facts.&lt;/p&gt;

&lt;p&gt;There is a coarser flavor of precondition you can get for free at schema-design time: make illegal transitions unrepresentable. On our order tooling, the shipping-status enum only contains &lt;code&gt;FULFILLED&lt;/code&gt; and &lt;code&gt;DELIVERED&lt;/code&gt;, and the state machine runs one way. An agent cannot un-deliver an order, however confused it gets. If you do nothing else from this section, enum your state machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. High-impact writes get a plan, then a commit
&lt;/h3&gt;

&lt;p&gt;A draft article is a non-event; delete it and nothing happened. A broadcast push has no undo: our card marks it &lt;code&gt;destructiveHint: true, idempotentHint: false&lt;/code&gt;, which is the formal spelling of "no undo, no retry". Bulk repricing touches four hundred objects in one intent. Above a certain blast radius, one-shot writes are the wrong shape entirely.&lt;/p&gt;

&lt;p&gt;The shape that works is plan-before-commit. The first call returns a plan: what will change, how many objects, a sample of them, bound to the exact context it was computed in, with an expiry:&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;"plan_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;"pl_9f27"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reprice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"objects_matched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sample"&lt;/span&gt;&lt;span class="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;"Home Kit Hoodie 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Away Kit Hoodie 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"bound_to"&lt;/span&gt;&lt;span class="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;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-shop-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;"object_set_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:c41d..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool_schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"policy_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;14&lt;/span&gt;&lt;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;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-05T18:00:00Z"&lt;/span&gt;&lt;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;Commit is a second call that references &lt;code&gt;plan_id&lt;/code&gt; and fails if any binding moved: the object set changed, the schema changed, the policy changed, the plan expired. Between the two calls sits a human or an explicit policy, looking at "412 objects" and deciding whether that number matches the intent.&lt;/p&gt;

&lt;p&gt;We are partway there in production and heading the rest of the way. Every tool on our server publishes its recommended discover, call, verify sequence through a meta tool (&lt;code&gt;meta_get_tool_plan&lt;/code&gt;), and my own standing rule for push is &lt;code&gt;send: "at"&lt;/code&gt;, never &lt;code&gt;now&lt;/code&gt;: the agent schedules, which leaves a review window between the ask and the broadcast. The plan object is where this converges, because "a number a human saw before commit" is the single cheapest defense against the wrong-collection story.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Verified read-back stays
&lt;/h3&gt;

&lt;p&gt;Everything above is why read-back is not enough. None of it is a reason to drop read-back: it remains the floor. After every write, read the object back through the same read path anyone else would use, and compare against what was sent. It is the layer that catches the write that silently did not land, the truncation, the default that filled a field you did not send. Keep it, make it the server's contract rather than the client's memory, and stack the rest on top.&lt;/p&gt;

&lt;p&gt;One requirement I did not fully appreciate until I ran this article's checklist against our own server: the read path you verify through must be read-your-writes consistent. While testing, I hit the mirror image of the 200-that-failed: a delete succeeded, and the immediate read-back served a seconds-stale cached copy of the object, so the verification step swore the delete had not happened. An agent following the discipline to the letter would have retried a write that had already landed, which is exactly the failure class layer 2 exists for. Read-back through a cache lies in both directions. Verify against the source of truth, or invalidate on mutation for the writing session.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Make the 200-that-failed observable
&lt;/h3&gt;

&lt;p&gt;The nastiest property of an intent failure is that your telemetry approves of it. A standard span records transport: status code, latency, maybe payload size. It has no opinion about whether the business outcome matched the ask. On a default dashboard, the span for the wrong-collection repricing is indistinguishable from the span for the right one. All green.&lt;/p&gt;

&lt;p&gt;The signal has to be emitted on purpose, in two places. In the tool result, where the agent can see it: a server-side outcome flag, plus the read-back requirement, so "it worked" is a claim the server participates in. And on the span, where your on-call can see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http.status_code     = 200          # transport: the easy part
mcp.write.readback   = "match"      # state: persisted as sent
mcp.write.outcome    = "ok"         # intent: preconditions held, plan honored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then alert on the combination that matters: status 200 with an outcome that is not &lt;code&gt;ok&lt;/code&gt;. That alert is the entire point of this post compressed into one rule: the failures worth paging on are the ones that returned success.&lt;/p&gt;

&lt;h2&gt;
  
  
  You do not remove the human. You move them.
&lt;/h2&gt;

&lt;p&gt;It would be easy to read all this as "add friction everywhere", and that would be the wrong lesson. Draft states, scheduled sends, version conflicts and plan gates are all ways of sorting writes into two lanes: low-impact writes flow without ceremony, high-impact writes pause at exactly one place, the commit line.&lt;/p&gt;

&lt;p&gt;That is not less automation. It is oversight spent where it changes the outcome. The content agents I run create drafts all day without me. Nothing reaches a user's lock screen without a human having seen the plan. Both of those sentences are policy, and I can defend each one, which is more than I could say for "the agent seems careful."&lt;/p&gt;

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

&lt;p&gt;Before an agent gets write access to a production system, you should be able to answer yes six times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☐ &lt;strong&gt;Scoped&lt;/strong&gt;: can this credential reach only the tool families this agent needs, with everything else absent from the inventory, not just denied?&lt;/li&gt;
&lt;li&gt;☐ &lt;strong&gt;Idempotent&lt;/strong&gt;: does the same write, retried, produce one change instead of two?&lt;/li&gt;
&lt;li&gt;☐ &lt;strong&gt;Version-bound&lt;/strong&gt;: does a write built on a stale read get rejected instead of applied?&lt;/li&gt;
&lt;li&gt;☐ &lt;strong&gt;Previewable&lt;/strong&gt;: do high-blast-radius writes produce a plan that a human or a policy approves before commit?&lt;/li&gt;
&lt;li&gt;☐ &lt;strong&gt;Read-back verified&lt;/strong&gt;: is post-write verification part of the server contract, not client etiquette?&lt;/li&gt;
&lt;li&gt;☐ &lt;strong&gt;Semantically observable&lt;/strong&gt;: would a 200 that did the wrong thing show up on a dashboard, or only in a customer email?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six noes is a demo. Six yeses is something you can hand to a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Getting an agent to call your write tools and collect 200s is the easy 80 percent, and it is the part every MCP server demo shows. The remaining 20 percent, making sure the right thing happened and making it survivable when it did not, is unglamorous server work: scopes, keys, versions, plans, read-backs, span attributes. It is also, I think, where production MCP surfaces will actually differentiate from here on.&lt;/p&gt;

&lt;p&gt;The surface these notes come from is documented at &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;goodbarber.com/mcp&lt;/a&gt; if you want to see the choices in context.&lt;/p&gt;

&lt;p&gt;What is in your write-safety stack that I missed? Genuinely curious. The comments on the last piece made this one better.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I gave an AI agent the keys to a live production app: here's the MCP setup</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:21:47 +0000</pubDate>
      <link>https://dev.to/goodbarber/i-gave-an-ai-agent-the-keys-to-a-live-production-app-heres-the-mcp-setup-27e</link>
      <guid>https://dev.to/goodbarber/i-gave-an-ai-agent-the-keys-to-a-live-production-app-heres-the-mcp-setup-27e</guid>
      <description>&lt;p&gt;AI agents are good at writing code. What they mostly cannot do is operate the software you already run in production: publish the article, schedule the push, fulfill the order. Not because the models are incapable, but because most production apps expose no structured surface an agent can act on.&lt;/p&gt;

&lt;p&gt;MCP fixes exactly that. This post is the hands-on setup I use to let an agent operate a live mobile app: real endpoint, real tool calls, real payloads, and the gotchas I hit.&lt;/p&gt;

&lt;p&gt;Disclosure up front: I run engineering at GoodBarber, an app platform, so the production app in this post runs on our MCP server. The patterns transfer to any remote MCP server you point an agent at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect it
&lt;/h2&gt;

&lt;p&gt;The server is a hosted remote MCP server. No &lt;code&gt;npx&lt;/code&gt;, nothing local to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mcp.goodbarber.dev/mcp/sse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/sse&lt;/code&gt; path is historical; the server answers both SSE and Streamable HTTP, so every current client works.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; sse goodbarber https://mcp.goodbarber.dev/mcp/sse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or in &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"goodbarber"&lt;/span&gt;&lt;span class="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;"sse"&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;"https://mcp.goodbarber.dev/mcp/sse"&lt;/span&gt;&lt;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;Claude (claude.ai):&lt;/strong&gt; Settings, then Connectors, then add a custom connector with that URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT:&lt;/strong&gt; Settings, then Apps &amp;amp; Connectors, enable Developer mode, create a connector with the same URL. Write actions worked on a free account when I tested it; OpenAI's docs gate some of this by plan, so verify on yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex:&lt;/strong&gt; add a custom MCP server, transport Streamable HTTP, leave the bearer token field empty. Saving opens the OAuth flow.&lt;/p&gt;

&lt;p&gt;The first tool use triggers OAuth in the browser: you sign in with the app's account, and the session is scoped to that single app. No API key to paste anywhere. That scoping does a lot of security work later.&lt;/p&gt;

&lt;p&gt;After OAuth, the client pulls the tool list. What the agent sees is not "the API": it is an operations menu, namespaced by domain. The &lt;code&gt;cms_&lt;/code&gt; tools cover articles, events, and media. The &lt;code&gt;shop_&lt;/code&gt; tools cover products, variants, orders, and promo codes. The &lt;code&gt;classic_&lt;/code&gt; tools cover push, analytics, and memberships. The full inventory is public in the server card, which is the file to read before writing any client code:&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;https://mcp.goodbarber.dev/.well-known/mcp/server-card.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First real call: schedule a push
&lt;/h2&gt;

&lt;p&gt;Push is the scariest operation to hand an agent (a sent push has no undo), which makes it the best test of a server's design.&lt;/p&gt;

&lt;p&gt;Me, in the chat:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Schedule a push for 6 PM tonight: "Doors open at 7. First 50 people get the poster."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent calls &lt;code&gt;classic_create_push_broadcast&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doors open at 7. First 50 people get the poster."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"send_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-29T18:00+02:00"&lt;/span&gt;&lt;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;Three details in this schema are worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;message&lt;/code&gt; is capped at 255 characters server-side, so the agent gets a hard error instead of a silently truncated push.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send&lt;/code&gt; is an enum, &lt;code&gt;now&lt;/code&gt; or &lt;code&gt;at&lt;/code&gt;; scheduling requires the timezone-aware &lt;code&gt;send_at&lt;/code&gt;, and the tool handles the UTC conversion. The agent does no date math.&lt;/li&gt;
&lt;li&gt;The tap action is structured too: &lt;code&gt;action_type&lt;/code&gt; is one of &lt;code&gt;open_app&lt;/code&gt;, &lt;code&gt;external_link&lt;/code&gt;, or &lt;code&gt;section&lt;/code&gt;. "Open the tickets section" resolves to a real section id, not a guessed deeplink.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result comes back confirming the scheduled send, along with something rarer: a policy block. Every write on this server returns &lt;code&gt;_mcp_policy.verification_required: true&lt;/code&gt;. The server's own guidance tells the agent to read back what it just wrote before declaring success. There is even a meta tool for this: &lt;code&gt;meta_get_tool_plan&lt;/code&gt; takes a tool name and returns the recommended discover, call, verify sequence plus the failure policy. Agents follow instructions embedded in tool results remarkably well; putting that discipline server-side beats hoping every client prompt remembers it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operate the app: one call per domain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Publish an article (CMS).&lt;/strong&gt; &lt;code&gt;cms_create_article&lt;/code&gt; wants a title and category ids, and category ids come from &lt;code&gt;cms_list_cms_sections&lt;/code&gt; first. Discover, then write:&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Matchday guide: what to know before Saturday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&lt;/span&gt;&lt;span class="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;4821&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-01T08:00:00+02:00"&lt;/span&gt;&lt;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;code&gt;status&lt;/code&gt; is &lt;code&gt;published&lt;/code&gt;, &lt;code&gt;draft&lt;/code&gt;, or &lt;code&gt;stock&lt;/code&gt;; a future &lt;code&gt;publishedDate&lt;/code&gt; requires &lt;code&gt;stock&lt;/code&gt;, which is the scheduled state. Body content is its own resource: &lt;code&gt;cms_create_article_paragraph&lt;/code&gt;, one call per paragraph, with &lt;code&gt;cms_reorder_article_paragraphs&lt;/code&gt; when the agent restructures. There is also an &lt;code&gt;accessTier&lt;/code&gt; field (&lt;code&gt;free&lt;/code&gt; or &lt;code&gt;premium&lt;/code&gt;) that hooks straight into the app's paywall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a product and a variant (shop).&lt;/strong&gt; &lt;code&gt;shop_create_product&lt;/code&gt; first:&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Home Kit Hoodie 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DRAFT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collections"&lt;/span&gt;&lt;span class="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;312&lt;/span&gt;&lt;span class="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 &lt;code&gt;shop_create_variant&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;"product_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;88410&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"49.00000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HK26-M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"option_values"&lt;/span&gt;&lt;span class="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;"option_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;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M"&lt;/span&gt;&lt;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;Two things I like here. &lt;code&gt;price&lt;/code&gt; is a decimal string, not a float: whoever wrote this schema has met floating-point money. And the variant model is strict: all variants of a product must share the exact same set of &lt;code&gt;option_id&lt;/code&gt;s, so introducing a Size option on one variant forces you to define it on all of them. That is a real invariant of the commerce domain, enforced at the tool layer. A raw database connection would let your agent violate it silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look up and update an order (fulfillment).&lt;/strong&gt; &lt;code&gt;shop_list_orders&lt;/code&gt; filters by status and date range. &lt;code&gt;shop_update_order_shipping&lt;/code&gt; moves an order along a one-way state machine, PENDING → FULFILLED → DELIVERED, with optional tracking:&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;"order_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;55231&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FULFILLED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipping_tracking_num"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6A0301234567"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipping_tracking_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://tracking.example.com/6A0301234567"&lt;/span&gt;&lt;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 &lt;code&gt;status&lt;/code&gt; enum only contains &lt;code&gt;FULFILLED&lt;/code&gt; and &lt;code&gt;DELIVERED&lt;/code&gt;. You cannot un-deliver an order through this surface, however confused the agent gets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull the numbers (analytics).&lt;/strong&gt; &lt;code&gt;classic_list_downloads&lt;/code&gt; and &lt;code&gt;classic_list_page_views&lt;/code&gt; take plain ISO date ranges and return aggregates. The stats family is read-only by construction; there is nothing to break. My standing Monday ask is one sentence: last week's downloads and page views versus the previous week, flag anything odd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security model (the part that actually matters)
&lt;/h2&gt;

&lt;p&gt;This is what I would look at before connecting an agent to anything in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OAuth per app, not per account.&lt;/strong&gt; The session is scoped to a single app. Connect App A and the agent cannot tell App B exists. Agencies operating many client apps add one connection per app; there is a per-app URL form for that, on a white-label domain if you resell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature-gating shapes the tool list.&lt;/strong&gt; The tools exposed are a function of what the app has enabled. Connect an app with no shop and the &lt;code&gt;shop_&lt;/code&gt; namespace is simply absent; push not configured, no push tools. The agent cannot call what it cannot see. Corollary if you write client code: never hardcode the tool list, read it at connect time or from the server card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify-after-write is server policy,&lt;/strong&gt; not client etiquette. The &lt;code&gt;_mcp_policy&lt;/code&gt; block rides on every write result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No design surface.&lt;/strong&gt; Nothing in the inventory touches layout, navigation, or the build pipeline. The blast radius of a bad agent day is content, campaigns, and commerce state, all inspectable in the back office. Not the app itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Skills: the recipes layer
&lt;/h2&gt;

&lt;p&gt;On top of the server, there is an open-source repo of 44 Skills: markdown recipes in the Claude Skills format, one per workflow (create a product with variants, schedule a push campaign, process the morning's orders, and so on). They encode the discover-then-write sequences above so the agent does not rediscover them every session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/goodbarber/goodbarber-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terms allow rebranding and redistribution; they were written for resellers. The server itself is proprietary. The recipes are the open part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs and gotchas
&lt;/h2&gt;

&lt;p&gt;Things I would want to know before recommending this to another engineer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The tool list varies per app.&lt;/strong&gt; A tool name from a tutorial (or this post) may be absent on your app because the feature is off. Check the list, not the docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversational operations do not batch.&lt;/strong&gt; Three hundred products means three hundred &lt;code&gt;shop_create_product&lt;/code&gt; calls. Bulk import stays a back-office job; the agent shines on the daily delta, not the migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify-after-write costs round trips.&lt;/strong&gt; A multi-step ask (product, three variants, launch push) runs tens of seconds, not milliseconds. That is deliberate, and the right trade for writes on a live app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDs, not vibes.&lt;/strong&gt; Agents guess names when you let them. Make them discover first (&lt;code&gt;cms_list_cms_sections&lt;/code&gt;, &lt;code&gt;shop_list_products&lt;/code&gt;); &lt;code&gt;meta_get_tool_plan&lt;/code&gt; returns exactly that sequence per tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a human on send.&lt;/strong&gt; My own rule: the agent schedules pushes with &lt;code&gt;send: "at"&lt;/code&gt; rather than &lt;code&gt;now&lt;/code&gt;, so there is always a review window between the ask and the broadcast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One app per connection.&lt;/strong&gt; A multi-app morning means multiple connectors. Scoped beats convenient.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Everyone is racing to make agents build software. The quieter and, I think, more useful shift is agents operating the software you already have. Building is a one-time event. Operating is every day.&lt;/p&gt;

&lt;p&gt;If you want to poke at a live implementation: the &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;server card&lt;/a&gt; is public, the &lt;a href="https://github.com/goodbarber/goodbarber-skills" rel="noopener noreferrer"&gt;Skills repo&lt;/a&gt; is open, and the non-dev version of this story lives at &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;goodbarber.com/mcp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Not all MCP servers are equal: BaaS MCP vs application MCP</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:10:29 +0000</pubDate>
      <link>https://dev.to/goodbarber/not-all-mcp-servers-are-equal-baas-mcp-vs-application-mcp-11bj</link>
      <guid>https://dev.to/goodbarber/not-all-mcp-servers-are-equal-baas-mcp-vs-application-mcp-11bj</guid>
      <description>&lt;p&gt;&lt;em&gt;Every platform now advertises an MCP server, and the label tells you almost nothing. Two servers can carry the same three letters and hand an AI agent completely different powers: one gives it your database, the other gives it your live app. Here is the difference between a BaaS MCP and an application MCP, and why that altitude decides what an agent can actually do for you.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "Has an MCP server" is the wrong question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; An MCP server is only as useful as what it exposes. BaaS platforms such as Back4app and Supabase expose their backend: database tables, schemas, queries, cloud code. GoodBarber's MCP server exposes the operations of a live mobile app: publish an article, schedule a push, update the catalog, read the stats. 150 domain-typed tools at the time of writing, feature-gated, scoped to one app by OAuth, with a verified read-back on every write. Same protocol, very different altitude.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol has won fast. Introduced by Anthropic in November 2024 and donated to the Linux Foundation a year later, &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; is now the standard way to hand tools to an AI agent, with more than 9,400 public servers listed in the official MCP Registry in 2026. Which means the phrase "we have an MCP server" has quietly become a checkbox. Every platform can tick it, and the tick tells you nothing.&lt;/p&gt;

&lt;p&gt;The questions that matter sit one level deeper. What does the server let an agent see? What does it let an agent change? And when the agent writes, what stands between a well-phrased prompt and a broken production system? The answers depend far less on the protocol, which is the same for everyone, than on the altitude at which a platform plugs into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two altitudes: MCP for your database, MCP for your app
&lt;/h2&gt;

&lt;p&gt;Backend-as-a-Service platforms plug MCP into their infrastructure layer. Back4app's MCP server, as its &lt;a href="https://www.back4app.com/docs/mcp" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; describes it in July 2026, lets an agent create and manage Parse apps, define database schemas, query and modify objects through the Parse REST API, manage users and permissions, and deploy cloud code. Supabase's official MCP server points the same way: list tables, execute SQL, run migrations, manage branches and Edge Functions. These are real, useful capabilities. They are also unmistakably backend-shaped: what the agent reads and writes are rows, schemas and deployments. Call it MCP for your database.&lt;/p&gt;

&lt;p&gt;GoodBarber plugs MCP in at a different altitude: the application itself. GoodBarber's MCP server exposes the operations of a finished, published mobile app: publish an article, schedule a push notification, create a product with its variants, update an order, read the analytics. The agent never sees a table. It sees the same product-level actions the app's owner sees in the back office. Call it MCP for your app.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A BaaS MCP hands an agent the keys to your data. An application MCP lets an agent run your product, safely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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;Dimension&lt;/th&gt;
&lt;th&gt;BaaS MCP server&lt;/th&gt;
&lt;th&gt;Application MCP server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What the agent sees&lt;/td&gt;
&lt;td&gt;Tables, schemas, rows, cloud functions&lt;/td&gt;
&lt;td&gt;Articles, push campaigns, products, orders, stats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A typical tool&lt;/td&gt;
&lt;td&gt;Run a SQL query, create a database class&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cms_create_article&lt;/code&gt;, &lt;code&gt;classic_create_push_broadcast&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A write is&lt;/td&gt;
&lt;td&gt;A raw data mutation&lt;/td&gt;
&lt;td&gt;A product action, run through the application layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;Read-only modes, project scoping&lt;/td&gt;
&lt;td&gt;Feature gating, per-app OAuth, verified read-back on every write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built for&lt;/td&gt;
&lt;td&gt;Developers in AI coding tools&lt;/td&gt;
&lt;td&gt;Any operator, technical or not, in any MCP client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;Back4app, Supabase&lt;/td&gt;
&lt;td&gt;GoodBarber&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the altitude changes everything
&lt;/h2&gt;

&lt;p&gt;Same protocol, same JSON, same agents on the other end. Four things change completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantics: the agent knows what it is doing
&lt;/h3&gt;

&lt;p&gt;A backend tool speaks data. An application tool speaks intent. When an agent's tool is a raw SQL query, the agent knows it is inserting a row; whether that row makes sense as a product, a subscriber or a campaign is entirely the prompt's problem. When an agent calls &lt;code&gt;classic_create_push_broadcast&lt;/code&gt; on GoodBarber's server, the tool's name, its typed schema and its constraints already encode what a push campaign is. There is far less room to be confidently wrong, because the domain knowledge lives in the tool, not in the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety: where the guardrails live
&lt;/h3&gt;

&lt;p&gt;Good BaaS MCP servers do ship controls, and they matter: Supabase, for instance, offers a read-only mode and project scoping. But at database altitude, a permitted write is still a raw mutation. Nothing checks that the new row respects the invariants your application enforces everywhere else.&lt;/p&gt;

&lt;p&gt;GoodBarber's MCP server enforces its guardrails at the product level, on the server side. Feature gating: a tool only exists if the matching feature is active in the app, so an app without push configured exposes no push tools at all. Per-app OAuth scope: every session is bound to one authenticated app, an agent connected to app A cannot see or touch app B, and agencies connect each client app separately. Verified writes: every write returns a server-side flag requiring the agent to read the object back and confirm the result before moving on. Hallucinated success is the failure mode agents are most prone to; GoodBarber's answer is to make verification part of the server's contract rather than a best practice left to the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Completeness: a database is not a product
&lt;/h3&gt;

&lt;p&gt;An agent with full control of your backend still controls no product. The mobile app around that backend remains yours to design, build, connect, submit to the App Store and Google Play, and maintain: exactly the gap we mapped in &lt;a href="https://www.goodbarber.com/blog/ai-app-builders-can-build-an-app-can-they-run-one-a1560/" rel="noopener noreferrer"&gt;AI app builders can build an app. Can they run one?&lt;/a&gt; An application MCP starts on the other side of that gap. The app already exists: compiled native iOS and Android builds plus a PWA, with hosting, CMS, push infrastructure and payments included rather than assembled from separate subscriptions. The agent operates a live product from day one, and there is nothing left to build around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operators: who can actually use it
&lt;/h3&gt;

&lt;p&gt;Back4app's MCP documentation lists the clients it is built for: Cursor, Windsurf, VS Code, Claude Code. Developer tools, reasonably, because driving a backend safely requires a developer's judgment. An application MCP moves the interface up to plain language. A shop owner can ask Claude to reprice a product, a publisher can ask ChatGPT to publish the morning's article and schedule the push, a club manager can ask for last month's downloads, and none of them needs an IDE. GoodBarber built its MCP surface for that operator, the same person its no-code back office was built for, and it works from any MCP client, &lt;a href="https://www.goodbarber.com/blog/zapier-mcp-goodbarber-drive-your-app-with-an-ai-agent-a1457/" rel="noopener noreferrer"&gt;including automation platforms like Zapier&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GoodBarber's MCP server exposes
&lt;/h2&gt;

&lt;p&gt;GoodBarber runs a hosted, production MCP server: nothing to install, nothing to self-host. You plug the endpoint into your MCP client, sign in with OAuth 2, and the session is scoped to your app from the first call.The inventory is public and machine-readable. The &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;server card&lt;/a&gt; lists 150 domain-typed tools at the time of writing (July 2026), namespaced by what they operate: tools prefixed &lt;code&gt;cms_&lt;/code&gt; cover content (articles, events, maps, photos, videos, podcasts, including scheduled publication), &lt;code&gt;shop_&lt;/code&gt; tools cover commerce (products and variants, collections, orders, promo codes, customers), and &lt;code&gt;classic_&lt;/code&gt; tools cover the running of the app (push broadcasts, analytics, memberships). The card is the contract: when the platform grows, the card grows, and connected agents pick up the new tools automatically. On top of the server, GoodBarber publishes &lt;a href="https://github.com/goodbarber/goodbarber-skills" rel="noopener noreferrer"&gt;44 open-source Claude Skills&lt;/a&gt; that wrap common workflows as tested recipes, part of the same &lt;a href="https://www.goodbarber.com/blog/your-goodbarber-app-is-now-ai-agent-ready-44-skills-for-claude-code-cursor-and-any-mcp-client-a1520/" rel="noopener noreferrer"&gt;agent-ready platform&lt;/a&gt; push.&lt;/p&gt;

&lt;p&gt;Just as deliberate is what the server does not expose. Design and layout stay in the builder, where GoodBarber's design system can protect them; pushing visual design through text-shaped tools does not produce good apps. And agent ready does not mean the human left the room: you grant the scope, you set the policies, and the server verifies what the agent does. Details and per-client setup live on the &lt;a href="https://www.goodbarber.com/mcp/" rel="noopener noreferrer"&gt;MCP page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a BaaS MCP server is the right choice
&lt;/h2&gt;

&lt;p&gt;If you are a developer building custom software, with your own data model, your own business logic and your own frontend, a BaaS MCP server is exactly the right tool, and the good ones are genuinely good. Back4app's gives your coding agent a real Parse backend to build against; Supabase's does the same for Postgres, with scoping controls that show the category maturing. GoodBarber is not that tool and does not try to be: it will not host your custom backend, and it is built for content apps and mobile commerce, not for arbitrary software.&lt;/p&gt;

&lt;p&gt;These are two altitudes for two different jobs, not two competitors on one axis. The practical test: if your project needs an agent that can touch raw data structures, you want a BaaS MCP. If it needs an agent that can run a live mobile app, you want an application MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which MCP server does your project need?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You are building custom software and want an agent working on your schema, data and cloud code: choose a BaaS MCP server such as Back4app or Supabase.&lt;/li&gt;
&lt;li&gt;You want an agent to operate a real mobile app in production, across content, catalog, push notifications, orders and analytics: choose an application MCP server. That is what GoodBarber runs.&lt;/li&gt;
&lt;li&gt;The app's day-to-day operator does not code: an application MCP is the only altitude that works in plain language from mainstream clients like Claude and ChatGPT.&lt;/li&gt;
&lt;li&gt;You need both: some teams run them side by side, a BaaS MCP for the custom system a developer maintains, GoodBarber's MCP server for the mobile app the business operates. The protocol is the same; only the altitude differs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a BaaS MCP server and an application MCP server?&lt;/strong&gt;&lt;br&gt;
A BaaS MCP server exposes backend infrastructure to an agent: database tables, schemas, queries, cloud functions. An application MCP server exposes the operations of a finished product. GoodBarber's MCP server lets an agent publish content, schedule push notifications, manage a catalog and read analytics on a live mobile app, without ever touching raw data structures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does GoodBarber's MCP server give an agent access to my database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. GoodBarber's MCP server exposes product operations, not SQL. An agent works with articles, products, orders, push campaigns and stats through domain-typed tools, and every call runs through the same application layer as the back office, so business rules and validations apply. Raw table access is never on the menu.&lt;br&gt;
&lt;strong&gt;Is an MCP server on a backend enough to run a mobile app?&lt;/strong&gt;&lt;br&gt;
No. A backend MCP server operates the data layer, and the app around it still has to be designed, built, connected, submitted to the App Store and Google Play, and maintained. An application MCP server operates an app that already exists. That is the difference between managing rows and running a product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does GoodBarber keep agent writes safe?&lt;/strong&gt;&lt;br&gt;
Through three server-enforced layers. Feature gating: a tool only exists if the matching feature is active in the app. Per-app OAuth scope: an agent connected to one app cannot reach another. Verified writes: after every write, the server requires the agent to read the object back and confirm the result. Safety lives on the server, not in the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best MCP server for a no-code mobile app?&lt;/strong&gt;&lt;br&gt;
Judge any candidate on three criteria: tools that speak the app's language rather than raw SQL, authentication scoped to a single app, and server-enforced verification on writes. GoodBarber's MCP server checks all three, with 150 domain-typed tools at the time of writing and a public server card listing every one of them, so you can verify the inventory instead of taking the claim on faith.&lt;/p&gt;

&lt;p&gt;See the altitude difference for yourself. &lt;a href="https://www.goodbarber.com" rel="noopener noreferrer"&gt;Start a free trial&lt;/a&gt;, build your app, then plug its MCP endpoint into Claude, ChatGPT or any MCP client: connecting an agent to a live app takes about two minutes. The &lt;a href="https://www.goodbarber.com/mcp-complete-guide/" rel="noopener noreferrer"&gt;complete MCP guide&lt;/a&gt; covers setup client by client.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>In-app purchases without writing StoreKit or Play Billing: selling subscriptions inside your app</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:08:47 +0000</pubDate>
      <link>https://dev.to/goodbarber/in-app-purchases-without-writing-storekit-or-play-billing-selling-subscriptions-inside-your-app-71e</link>
      <guid>https://dev.to/goodbarber/in-app-purchases-without-writing-storekit-or-play-billing-selling-subscriptions-inside-your-app-71e</guid>
      <description>&lt;p&gt;Selling a subscription inside a mobile app is one of the most demanding things you can build: StoreKit on iOS, Google Play Billing on Android, server-side receipt validation, renewals, refunds. Or, on our platform, one toggle. This post explains what that toggle actually hides, and the honest trade-off that comes with it, because there is one and it is not small.&lt;/p&gt;

&lt;p&gt;Quick context: GoodBarber is a no-code app builder running since 2011; the platform compiles native iOS and Android apps for people who will never open Xcode. Some of those people live from their content. This is the machinery that lets them charge for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What in-app purchases are for (and not for)
&lt;/h2&gt;

&lt;p&gt;In-app purchases sell &lt;strong&gt;access to digital content inside the app&lt;/strong&gt;: premium articles, videos, podcasts, courses, members-only sections. On our platform that takes the form of subscriptions, monthly or annual, and it exists for Content Apps published as native iOS and Android builds.&lt;/p&gt;

&lt;p&gt;What in-app purchases are &lt;em&gt;not&lt;/em&gt; for: selling physical goods. That distinction is not a product choice, it is store law. Apple and Google &lt;strong&gt;require&lt;/strong&gt; in-app purchase for digital content sold inside an app, and &lt;strong&gt;forbid&lt;/strong&gt; it for physical goods and real-world services, which must go through a regular payment gateway. So an app that sells t-shirts checks out through Stripe or PayPal, and an app that sells premium podcasts checks out through the App Store and Google Play. Two different rails, two different cost structures, and no way to swap them.&lt;/p&gt;

&lt;p&gt;Keep that in mind for the rest of this post: everything below is about the digital-content rail. The e-commerce rail is a separate module with separate economics (no store cut, standard processor fees). Mixing the two up is the single most common confusion in this topic.&lt;/p&gt;

&lt;p&gt;When is the in-app rail the right call? When your audience is mobile-first, your product is content, and your growth depends on people subscribing at the exact moment they hit your paywall. Which is precisely the moment a payment form kills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the hardest checkout in mobile
&lt;/h2&gt;

&lt;p&gt;Here is what "selling a subscription in your app" means when you build it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two APIs, two lifecycles.&lt;/strong&gt; StoreKit on iOS and Play Billing on Android are separate systems with separate product models, separate testing sandboxes and separate failure modes. You implement everything twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server you now operate.&lt;/strong&gt; Purchases must be validated server-side: the store issues a receipt or purchase token, and your backend has to verify it, or your paywall can be bypassed. That means running a service, storing entitlements, and keeping it up for as long as you have subscribers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A state machine that runs for years.&lt;/strong&gt; A subscription is not one transaction. It renews, fails to renew (card expired), enters grace periods, gets cancelled, gets refunded, gets restored on a new phone. Every one of those events must reach your server and flip content access accordingly, or you have paying users locked out and free riders let in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store-specific edge cases.&lt;/strong&gt; Restore purchases is mandatory on iOS. Refund handling differs per store. Review teams check your subscription UX against their guidelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic for a payments team. All of it is a wall for a creator, and a serious multi-week project for a developer doing it once. It is the hardest checkout in mobile because it is not a checkout: it is a long-running billing system with two masters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens behind the toggle
&lt;/h2&gt;

&lt;p&gt;On our side, the creator's job is to define the offer in the back office: the subscription products, monthly or annual, and the price for each store, because the App Store and Google Play each have their own pricing grids and currencies. Then they turn the feature on.&lt;/p&gt;

&lt;p&gt;To be precise about what "we handle the rest" means: it is not one big automation. Behind the toggle there is software and there is a team, splitting the list from the previous section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The billing machinery is software.&lt;/strong&gt; Server-side receipt and purchase-token validation runs on infrastructure we already operate. Renewals, grace periods, cancellations, refunds and restores are processed and wired to content access: a subscriber sees the premium content, a lapsed one stops seeing it, with no code written by the creator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The store side is our app-store experts.&lt;/strong&gt; The creator's configuration drives the product setup with Apple and Google and the submission work around it, carried by the same team that publishes and maintains apps in the stores all day. Store-specific edge cases, and the drift of StoreKit and Play Billing over time, land on their desk, not the creator's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One toggle on the creator's side does not mean one script on ours; it means the complexity moved to people and systems whose job it is.&lt;/p&gt;

&lt;p&gt;For the end user, the entire experience is one tap on the store's native payment sheet. No form, no card number, no new account. The store already has their payment method on file.&lt;/p&gt;

&lt;p&gt;The fine print, because a toggle this clean has some:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is for &lt;strong&gt;Content Apps published as native builds&lt;/strong&gt;, not PWA-only plans, and not eCommerce Apps (different rail, as covered above);&lt;/li&gt;
&lt;li&gt;it sells &lt;strong&gt;subscriptions&lt;/strong&gt;, monthly or annual, not one-time unlocks or consumable credits;&lt;/li&gt;
&lt;li&gt;subscription access is &lt;strong&gt;binary&lt;/strong&gt;: subscribers see the premium content, everyone else sees the public content. It also replaces the standard login flow; an app uses one or the other, and the back office makes you switch deliberately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Very few no-code builders ship native in-app purchases at all; the usual answer is a plugin, a third-party billing service, and a validation server that becomes your problem. Shipping it as one toggle is rare, and it is one of the features we consider a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest trade-off: the store cut
&lt;/h2&gt;

&lt;p&gt;Apple and Google take &lt;strong&gt;15% to 30%&lt;/strong&gt; of every in-app subscription. Where you land in that range depends on their programs and your history: the small-business tiers, subscription retention past the first year. That cut is real money, and any article that shows you the toggle without the cut is selling you something.&lt;/p&gt;

&lt;p&gt;GoodBarber adds &lt;strong&gt;no commission on subscriber revenue&lt;/strong&gt;: the platform is flat-rate subscription pricing, and what your subscribers pay is yours, minus the store's share. But the store's share is structural. It is not our fee to waive; it is the cost of being on the rail.&lt;/p&gt;

&lt;p&gt;So why accept it? Because of what the rail buys: the highest-converting checkout that exists on mobile. The user is already authenticated with the store, their card is on file, and the purchase completes in one tap at the exact moment of intent. Compare that with sending a mobile user to a web page to type a card number: conversion drops sharply, and the drop routinely costs more than the cut. The store cut is not a tax on the feature; it is the price of the smoothest checkout in mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  In-app or web checkout: how to choose
&lt;/h2&gt;

&lt;p&gt;The alternative is legitimate: sell the subscription on your website, keep your full margin minus processing fees, and let subscribers sign in inside the app. Regulators and courts have also started forcing the stores to loosen their rules on external purchase links in some regions, so the legal ground is moving. The physics, so far, are not: a web checkout still asks a mobile user to leave the app and fill a form.&lt;/p&gt;

&lt;p&gt;A short decision grid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-app wins on volume.&lt;/strong&gt; Mobile-first audience, impulse subscriptions at the paywall moment, price points where 15-30% is absorbable: the conversion uplift usually outweighs the cut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web wins on margin.&lt;/strong&gt; High-ticket subscriptions, an audience you already convert on the web, thin margins where the cut genuinely hurts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most creators are not choosing a religion.&lt;/strong&gt; They are choosing a default for where their audience actually subscribes, and for a content app, that is usually inside the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are evaluating no-code platforms for premium content, here is the one question that sorts the field: &lt;strong&gt;who validates the receipts?&lt;/strong&gt; If the answer involves you, a plugin and a server, you now know what you are signing up for.&lt;/p&gt;

&lt;p&gt;Questions in the comments. happy to go deeper on the billing lifecycle or the store-specific quirks.&lt;/p&gt;

</description>
      <category>nocode</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>No-code at scale: Why the real engineering challenge is the treadmill, not the editor.</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:52:11 +0000</pubDate>
      <link>https://dev.to/goodbarber/no-code-at-scale-why-the-real-engineering-challenge-is-the-treadmill-not-the-editor-275o</link>
      <guid>https://dev.to/goodbarber/no-code-at-scale-why-the-real-engineering-challenge-is-the-treadmill-not-the-editor-275o</guid>
      <description>&lt;p&gt;"No-code" sounds effortless. Configure your app in a browser, hit publish, done. And that is exactly how it should feel, for one app, seen from the outside. Keeping thousands of no-code apps alive and published, for years, is another job entirely. This post opens the machine room: hosting, push delivery, store submissions, and the maintenance treadmill running underneath.&lt;/p&gt;

&lt;p&gt;Quick context: GoodBarber is a no-code app builder running since 2011. Customers configure an app in a web back office; the platform compiles native builds (Swift for iOS, Kotlin for Android) plus a PWA, and operates everything those apps need at runtime. That last clause is the subject here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "at scale" means for a published app
&lt;/h2&gt;

&lt;p&gt;The demo problem and the production problem are different problems. Any stack can produce one app that works today. A published app is a longer commitment: a binary sitting in two stores behind two review processes, a backend it calls every day, push credentials that expire, an OS release every year and store policies that move under your feet.&lt;/p&gt;

&lt;p&gt;Scale, in our case, as of mid-2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;thousands of live apps&lt;/strong&gt; in production;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;one download every 4 seconds&lt;/strong&gt; across the fleet, about 20,000 a day and 8 million a year (measured over 29 days in April 2026);&lt;/li&gt;
&lt;li&gt;paying customers in &lt;strong&gt;152 countries&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;a platform running &lt;strong&gt;since 2011&lt;/strong&gt;, so "long-term" is measured in OS generations, not in sprints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operative definition of scale is multiplication. With one app, a store policy change costs you an afternoon. With thousands, every external change becomes a fleet event: whatever Apple or Google decides this quarter, multiply it by every app in production. The entire architecture below exists to make that multiplication survivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure nobody sees
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hosting.&lt;/strong&gt; GoodBarber is its own hosting provider. We rent rack space in large European datacenters and run our own machines there: several racks of hardware, not a tenancy on someone's hyperscaler. The backend is multi-tenant: thousands of apps share the platform's services, each with its own content, configuration and API surface, and the CDN in front of it moves several terabytes a day. Running the hardware ourselves keeps the data question short (one company, one jurisdiction, servers in Europe) and puts capacity planning where it belongs: on us, once, instead of on thousands of individual app owners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push, at volume.&lt;/strong&gt; The platform delivers several million push notifications a week. The app owner's view of that sentence: write the message, pick the audience, send now or schedule. Everything else is ours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fan-out to APNs and FCM, with per-app credentials provisioned and rotated;&lt;/li&gt;
&lt;li&gt;device-token hygiene: tokens die constantly (uninstalls, device restores, OS reinstalls), so delivery responses have to be consumed and dead tokens pruned, or delivery rates rot silently;&lt;/li&gt;
&lt;li&gt;payload limits, throttling, retries, and the scheduling queue that turns thousands of "send at 9am" campaigns into synchronized bursts of individual deliveries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliable push is a data-hygiene problem at least as much as a throughput problem. The throughput is bursty and predictable; the token decay never stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store submissions, handled by people who do it all day.&lt;/strong&gt; A dedicated publication team submits and maintains apps in the App Store and Google Play. The baseline that team works against: Apple rejects approximately &lt;strong&gt;42% of first submissions&lt;/strong&gt; (measured across the submissions our team handled over the last 12 months; that is Apple's baseline, not our failure rate). The team recovers &lt;strong&gt;91%&lt;/strong&gt; of those first-submission rejections. On updates, upstream prevention work makes rejections rare in the first place. App review is not an API call that returns 200; it is a negotiation with guidelines that shift, and it goes measurably better when it is somebody's full-time job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The treadmill: what a platform absorbs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/goodbarber/what-breaks-when-nobody-touches-your-app-for-three-years-2dgm"&gt;An app left alone degrades.&lt;/a&gt; Not because its code changes, but because everything around it moves. A sample of what "around it" has meant, with dates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every year:&lt;/strong&gt; Google Play raises the required target API level. Miss the deadline and you cannot ship updates; fall further behind and the app is hidden from new users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;April to December 2020:&lt;/strong&gt; Apple stopped accepting new apps, then updates, still using UIWebView. Every app embedding web content had to move to WKWebView.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;March 2021:&lt;/strong&gt; APNs dropped its legacy binary protocol. Push senders had to migrate to the HTTP/2-based API or stop delivering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;July 2022:&lt;/strong&gt; Google Play's Data safety form became mandatory for every listing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 2024:&lt;/strong&gt; Apple began enforcing privacy manifests and "required reason" API declarations, including for third-party SDKs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the annual rhythm on top: new iOS and Android majors every fall, Xcode and Gradle toolchains that move, SDKs that deprecate.&lt;/p&gt;

&lt;p&gt;On a platform, each of these is absorbed exactly once. The fix lands in the shared codebase, the apps are rebuilt, and the fleet rolls forward through regular app updates. That is also why we advise owners to update regularly even when nothing visible changed: the update is the vehicle that carries those fixes into production, and the stores themselves are hostile to dormant apps (Apple periodically sweeps apps that have not been updated in years; Google buries apps targeting outdated API levels). An app that ships updates stays current, visible and installable. Most of our owners experienced the events above as a release note, if they noticed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill, itemized
&lt;/h2&gt;

&lt;p&gt;Strip the platform away and here is what one production app makes you own, permanently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one target API level raise per year, so at least one rebuild and resubmission per year;&lt;/li&gt;
&lt;li&gt;OS beta season: test every summer, fix before every fall;&lt;/li&gt;
&lt;li&gt;your dependencies' CVEs, deprecations and license changes;&lt;/li&gt;
&lt;li&gt;push infrastructure: credentials, token hygiene, delivery monitoring;&lt;/li&gt;
&lt;li&gt;store policy watch: new forms, new manifests, new review guidelines;&lt;/li&gt;
&lt;li&gt;monitoring, and someone reachable when it breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is hard in isolation. All of them are permanent, and they compound.&lt;br&gt;
What a no-code platform actually sells is not the editor; it is amortization.&lt;br&gt;
Which editor you came through — &lt;a href="https://dev.to/goodbarber/the-prompt-didnt-replace-drag-drop-1plb"&gt;drag &amp;amp; drop, prompt, or both&lt;/a&gt; —&lt;br&gt;
changes nothing to that list. One team absorbs it once, and thousands of apps&lt;br&gt;
inherit the result with their next update.&lt;/p&gt;

&lt;p&gt;Our own receipts are public: an engineer is on call year-round, and the platform status page is at &lt;a href="https://www.goodbarberstatus.com/" rel="noopener noreferrer"&gt;goodbarberstatus.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions in the comments. happy to go one layer deeper on any of this, from colocation choices to the push pipeline to review stories.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>infrastructure</category>
      <category>mobile</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
