<?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>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>
    <item>
      <title>Privacy by design at the binary level: no ghost SDK in your build</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:15:20 +0000</pubDate>
      <link>https://dev.to/goodbarber/privacy-by-design-at-the-binary-level-no-ghost-sdk-in-your-build-1bl0</link>
      <guid>https://dev.to/goodbarber/privacy-by-design-at-the-binary-level-no-ghost-sdk-in-your-build-1bl0</guid>
      <description>&lt;p&gt;Most apps ship with passengers nobody invited: trackers, analytics and ad SDKs baked into the binary by default. We took the problem the other way around: nothing ships unless the app's owner explicitly turned it on. Here's the engineering, down to the compilation flags.&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 and the platform compiles real native builds, Swift for iOS and Kotlin for Android, plus a PWA. The people who own these apps will never open Xcode. Which makes the question sharper: who decides what code ends up in their binary?&lt;/p&gt;

&lt;h2&gt;
  
  
  The ghost SDK problem
&lt;/h2&gt;

&lt;p&gt;A ghost SDK is any library that lives in an app's binary without the app's maker having chosen it. The pattern is everywhere, no-code or not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Template runtimes.&lt;/strong&gt; Many app factories compile every app from the same master project. Every feature the platform supports, and every SDK behind it, ships in every binary, used or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transitive passengers.&lt;/strong&gt; An SDK pulls its own dependencies. You added a video player; you also got its analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default-on integrations.&lt;/strong&gt; Analytics and crash reporting wired in "for your own good", opt-out at best.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why it matters, concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data can move without anyone deciding it.&lt;/strong&gt; An embedded SDK is code that runs with your users, and some of it phones home whether or not the feature is used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attack surface.&lt;/strong&gt; Every library is code you now answer for, including its CVEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weight.&lt;/strong&gt; Dead SDKs are dead bytes on every install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't answer the stores honestly.&lt;/strong&gt; Apple's privacy manifests and Google Play's Data safety form ask what your app collects. If you don't know what's inside your binary, you're guessing on a compliance document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The industry default is opt-out. We think a binary should be opt-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  A library ships only if the feature is on
&lt;/h2&gt;

&lt;p&gt;We call the model &lt;strong&gt;on-demand embedded code&lt;/strong&gt;, and the principle fits in one sentence: a library is embedded in the binary only if the corresponding feature is activated in the app.&lt;/p&gt;

&lt;p&gt;How it actually works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every app has a feature graph.&lt;/strong&gt; Sections, extensions, monetization, push, integrations: the back office knows exactly what an app uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The graph drives conditional compilation.&lt;/strong&gt; At build time, the feature graph toggles compilation flags (pragmas) that include or exclude whole modules and third-party SDKs from the project. Off doesn't mean disabled or dormant. It means the code is not compiled in at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Privacy Center is the control tower.&lt;/strong&gt; One place where the app's owner sees what runs in their app and decides what is allowed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds happen on demand.&lt;/strong&gt; We don't recompile on every toggle. A binary is generated when it's needed, typically when an update is about to be submitted to the stores, with exactly what the app's current configuration requires. For the owner this is transparent: they configure, we tailor the build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On iOS, for example: AdMob is in the binary only if ads are enabled. The Facebook SDK, only if that integration is on. Even the in-app purchase stack (our StoreKit integration) stays out of the build unless in-app purchases are part of the app.&lt;/p&gt;

&lt;p&gt;The consequence is easy to state and rare in practice: the app doesn't just &lt;em&gt;declare&lt;/em&gt; less, it &lt;em&gt;contains&lt;/em&gt; less.&lt;/p&gt;

&lt;p&gt;And it's measurable. An &lt;strong&gt;Apps for Kids&lt;/strong&gt; build, which by construction embeds none of these third-party SDKs, weighs about &lt;strong&gt;a third of a full-featured build&lt;/strong&gt; (from our build data, July 2026).&lt;/p&gt;

&lt;h2&gt;
  
  
  The data question: hosted by us, in Europe
&lt;/h2&gt;

&lt;p&gt;A clean binary is half the story. The other half is where the data lives.&lt;/p&gt;

&lt;p&gt;GoodBarber is its own hosting provider. Customer and end-user data is hosted exclusively on our servers in Europe, and it does not leave the EU. No hyperscaler region to audit, no transfer mechanism to squint at: sovereignty here is one company, one jurisdiction. That baseline is what makes GDPR (and Germany's stricter BDSG) tractable for the small teams who build on the platform.&lt;/p&gt;

&lt;p&gt;Consent gets the same treatment. For apps that do turn on ads or analytics, the platform ships an integrated CMP supporting the IAB Transparency &amp;amp; Consent Framework v2, plus Google Funding Choices for ad-monetized apps. So what is in the binary is disclosed, and consented to, per user and per purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes at store review
&lt;/h2&gt;

&lt;p&gt;Apple's privacy manifests and Play's Data safety form stop being archaeology. Our privacy declarations are fed by the same source of truth as the build itself: the feature graph. What we declare is what we compiled, because both derive from the same place. There is no "wait, what does that SDK actually collect?" moment, because an SDK nobody chose is not in the build to begin with.&lt;/p&gt;

&lt;p&gt;The extreme case proves the model: &lt;strong&gt;Apps for Kids&lt;/strong&gt;. One declaration in the back office marks an app as a kids' title (Apple Kids Category, Google Play Designed for Families), and the platform disables every feature incompatible with those programs: third-party analytics, behavioral ads, intrusive permissions. The build that comes out is compliant by construction, because compliance is enforced where it's real: in what gets compiled.&lt;/p&gt;

&lt;p&gt;Now the honest limits, because a claim like "nothing you didn't invite" deserves its footnotes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The iOS push stack is always there.&lt;/strong&gt; Our pipeline has never been wired to produce an iOS binary without push. Nobody ever needed that build, so that path doesn't exist. If you never use push, its code is still in your binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Clean" means no uninvited third parties, not zero code.&lt;/strong&gt; GoodBarber's own runtime is in every build. The difference: we own it, we answer for it, and it is one vendor you already chose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We don't make your app compliant by magic.&lt;/strong&gt; What owners collect through their own content (forms, orders, accounts) is their responsibility. We ship a clean binary, honest declarations and the consent tooling; the rest is theirs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you build a platform that compiles other people's apps, the transferable pattern is this: derive everything from one feature graph. Binary contents, privacy declarations, consent configuration, all from the same source of truth. The moment two of those are maintained by hand, they drift, and drift in privacy is how ghost SDKs are born.&lt;/p&gt;

&lt;p&gt;More on the platform's privacy stance: &lt;a href="https://www.goodbarber.com/privacy-compliance/" rel="noopener noreferrer"&gt;goodbarber.com/privacy-compliance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions in the comments. I'll answer with build-level details where I can.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>nocode</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Building a production MCP server: How we made GoodBarber agent-ready (without the glue code)</title>
      <dc:creator>Pierre- Laurent Medori</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:10:40 +0000</pubDate>
      <link>https://dev.to/goodbarber/building-a-production-mcp-server-how-we-made-goodbarber-agent-ready-without-the-glue-code-4co6</link>
      <guid>https://dev.to/goodbarber/building-a-production-mcp-server-how-we-made-goodbarber-agent-ready-without-the-glue-code-4co6</guid>
      <description>&lt;p&gt;We wanted an AI agent to manage a real production app (its content, its catalog, its push notifications) natively, not through screenshots and simulated clicks. So we built an MCP server. Here's the whole thing: why, how, a real session, and where it still falls short.&lt;/p&gt;

&lt;p&gt;One line of context so the rest makes sense: 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. The question we set out to answer: can an AI agent &lt;a href="https://dev.to/goodbarber/the-first-interface-you-dont-have-to-learn-1f9b"&gt;operate one of those apps, end to end, the way its owner does&lt;/a&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  We already had APIs. That wasn't the point.
&lt;/h2&gt;

&lt;p&gt;The platform has had APIs for years. Exposing endpoints was never the problem. The problem was that every "connect your assistant" integration is custom glue: tool definitions written for one vendor, an auth flow, retry logic, docs written for a model to read. Then you do it all again for the next assistant. The classic N×M integration mess: N assistants × M platforms.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (introduced by Anthropic in November 2024, donated to the Linux Foundation in late 2025) standardizes exactly the parts we kept rewriting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; The client asks the server what tools exist; no hardcoded catalog on the agent side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typing.&lt;/strong&gt; Every tool ships a JSON Schema; the model knows what arguments are legal before it calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth.&lt;/strong&gt; OAuth is part of the protocol conversation, not a bespoke setup doc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical difference is blunt: with a classic API, you write an integration for each agent. With an MCP server, any MCP client (Claude, ChatGPT, Claude Code, Cursor, n8n) connects and figures out how to drive the app on its own. We shipped one server and got every current and future MCP client for free.&lt;/p&gt;

&lt;p&gt;So the goal was never "another API". It was: make the app itself drivable by an agent. Here's how that's wired.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude · ChatGPT · Cursor · n8n        (any MCP client)
      │      natural language → typed tool calls
      ▼
GoodBarber MCP server (hosted)         https://mcp.goodbarber.dev/mcp/sse
      │      OAuth 2 sign-in · scoped to *your* app
      ▼
Platform APIs                          CMS · push · shop · orders · customers · stats
      ▼
Your live app                          iOS · Android · PWA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be clear up front: &lt;strong&gt;the server itself is a hosted GoodBarber service, not an open-source package you install.&lt;/strong&gt; What's open source is the Skills layer (below). The reproducible part for you as a reader is the connection: plug the endpoint into an MCP client, sign in, and an agent is operating a live app a couple of minutes later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool design.&lt;/strong&gt; Tools are namespaced by domain (&lt;code&gt;cms_*&lt;/code&gt;, &lt;code&gt;shop_*&lt;/code&gt;, &lt;code&gt;classic_*&lt;/code&gt;) and map one-to-one onto what the back office can do. The full machine-readable inventory is public in the &lt;a href="https://mcp.goodbarber.dev/.well-known/mcp/server-card.json" rel="noopener noreferrer"&gt;server card&lt;/a&gt;: 150 tools at the time of writing (July 2026). That card is the contract: when the platform grows, the card grows, and connected agents pick up the new tools automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scoping.&lt;/strong&gt; Sign-in is OAuth 2, and every session is scoped to the authenticated customer's app. An agent connected to app A cannot see or touch app B. Agencies running several client apps connect each one separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side guardrails.&lt;/strong&gt; Two design decisions we'd defend in any review:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forced read-back on writes.&lt;/strong&gt; Every write returns &lt;code&gt;_mcp_policy.verification_required: true&lt;/code&gt;. The agent is expected to re-read the object it just created or changed and confirm the result. Hallucinated success is the failure mode we fear most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature gating.&lt;/strong&gt; An app that doesn't have push configured doesn't expose push tools at all. Agents can't call what isn't there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The open-source layer: 44 Claude Skills.&lt;/strong&gt; On top of the server we publish &lt;a href="https://github.com/goodbarber/goodbarber-skills" rel="noopener noreferrer"&gt;&lt;code&gt;goodbarber-skills&lt;/code&gt;&lt;/a&gt;: 44 open-source Claude Skills, plain markdown recipes that wrap common workflows (create a product with variants, schedule a push campaign, refund an order…). Two distinct artifacts: the server is the live connection; the Skills are curated entry points that call it. The repo is deliberately white-label-friendly, so agencies can rebrand and redistribute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real session: an agent managing an e-commerce catalog
&lt;/h2&gt;

&lt;p&gt;Enough theory. A real task, run from Claude with the server connected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a black "Storm" T-shirt at €29 in S, M and L, put it in the Summer 2026 collection, and schedule a push for 6 p.m. announcing the drop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What the agent actually does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shop_list_collections      → resolves "Summer 2026" to a collection id
shop_create_product        → creates the product (name, €29, collection)
                             ← verification_required: true
shop_get_product           → read-back: confirms the product exists as intended
shop_create_variant  ×3    → S, M, L
shop_create_push_broadcast → drafts the push, scheduled for 18:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it stops and shows me the push copy before anything is scheduled: interactive clients like Claude ask before executing writes. The agent does the plumbing (id resolution, argument formatting, sequencing, verification); I approve what goes out. In the back office this task is a dozen screens; by conversation it's one prompt and one approval.&lt;/p&gt;

&lt;p&gt;Nothing in the session is Claude-specific: the same flow works from ChatGPT connectors, Claude Code, or Cursor. And because the tools are typed, the failure mode is honest: pass a malformed price and you get a schema error back, not a silently broken product.&lt;/p&gt;

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

&lt;p&gt;The honest list, because this is the part most write-ups skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No design tools, on purpose.&lt;/strong&gt; The server covers operations: content, catalog, campaigns, orders, stats. Layout and design stay in the builder. We don't believe pushing visual design through text-shaped tools produces good apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-app tool subsets surprise agents.&lt;/strong&gt; Feature gating means two apps expose different tool lists. Correct behavior, but an agent following a generic recipe can look for a tool that isn't there. The Skills handle this; raw prompting sometimes doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes are chatty by design.&lt;/strong&gt; Forced read-back costs tokens and latency on every mutation. We accept the trade: verified writes beat fast fiction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing we chose &lt;strong&gt;not&lt;/strong&gt; to build into the server: a mandatory approval flow. How much autonomy an agent gets is the operator's decision, made in the client. Interactive assistants like Claude ask before they act; the same tools, &lt;a href="https://www.goodbarber.com/blog/how-to-automate-your-goodbarber-app-with-n8n-and-mcp-no-code-required-a1523/" rel="noopener noreferrer"&gt;wired into n8n&lt;/a&gt;, run fully unattended. The server's job is to keep both modes safe, with per-app OAuth scope, typed tools, and verified writes.&lt;/p&gt;

&lt;p&gt;We fully intend to take this further. The list above is just where the honest line sits today.&lt;/p&gt;

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

&lt;p&gt;If you have a GoodBarber app: add a custom connector in Claude (or any MCP client), point it at &lt;code&gt;https://mcp.goodbarber.dev/mcp/sse&lt;/code&gt;, sign in with your account, and ask for something small. "Fix the typo in my latest article" is a good first test. The &lt;a href="https://www.goodbarber.com/mcp-complete-guide/" rel="noopener noreferrer"&gt;complete MCP guide&lt;/a&gt; covers per-client setup.&lt;/p&gt;

&lt;p&gt;If you're building an MCP server for your own platform: the pattern that held up for us in production is boring and worth stealing. Typed tools namespaced by domain, per-tenant OAuth scoping, feature-gated tool exposure, and forced read-back on every write.&lt;/p&gt;

&lt;p&gt;Questions welcome in the comments. I'll answer with real payloads where I can.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>nocode</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
