<?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: Tal Mogendorff</title>
    <description>The latest articles on DEV Community by Tal Mogendorff (@tal_mogendorff_2ef21db81f).</description>
    <link>https://dev.to/tal_mogendorff_2ef21db81f</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%2F4036375%2F575cc658-d8b0-42a5-9558-ed9a076342b4.jpg</url>
      <title>DEV Community: Tal Mogendorff</title>
      <link>https://dev.to/tal_mogendorff_2ef21db81f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tal_mogendorff_2ef21db81f"/>
    <language>en</language>
    <item>
      <title>I built an ad layer that can never break a tool call — monetizing MCP servers</title>
      <dc:creator>Tal Mogendorff</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:03:13 +0000</pubDate>
      <link>https://dev.to/tal_mogendorff_2ef21db81f/i-built-an-ad-layer-that-can-never-break-a-tool-call-monetizing-mcp-servers-3h2n</link>
      <guid>https://dev.to/tal_mogendorff_2ef21db81f/i-built-an-ad-layer-that-can-never-break-a-tool-call-monetizing-mcp-servers-3h2n</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm the founder of &lt;a href="https://getlulu.dev" rel="noopener noreferrer"&gt;Lulu Ads&lt;/a&gt;. This is the engineering story — the interesting parts are the constraints, and I'd rather you tear the design apart than clap.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody is paid to solve
&lt;/h2&gt;

&lt;p&gt;There are thousands of MCP servers right now — the tools that AI agents like Claude call to search, fetch, book, compute. Nearly all of them are free, run by individual developers, with real hosting bills and zero revenue. The tool layer of the agent economy has no business model. That's the gap I've been working on.&lt;/p&gt;

&lt;p&gt;The obvious answer — "put ads in it" — is also the obviously terrifying one. Ads near LLM output is how you inherit every mistake ad tech ever made, at higher speed. So this post is about the constraint list, because the constraints came before the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invariants
&lt;/h2&gt;

&lt;p&gt;Before writing code, I wrote five rules the SDK must never violate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An ad must never break a tool call.&lt;/li&gt;
&lt;li&gt;An ad must never instruct the model.&lt;/li&gt;
&lt;li&gt;An ad must never be undisclosed.&lt;/li&gt;
&lt;li&gt;An ad must never appear on a failure.&lt;/li&gt;
&lt;li&gt;An irrelevant ad must cost the advertiser, never the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else in the architecture falls out of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariant 1: fail-open, 150ms, no exceptions
&lt;/h2&gt;

&lt;p&gt;The SDK wraps your MCP tool response. The ad fetch gets a hard 150ms budget. Timeout, network error, malformed response, backend down — doesn't matter: your response goes out exactly as your server produced it. The failure mode is "no ad", never "no answer". If our whole backend disappears tomorrow, integrated servers notice nothing.&lt;/p&gt;

&lt;p&gt;This sounds trivial. It isn't — it means the ad path can never sit between your tool and your user. It's a side-channel annotation, not a proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariant 2: the model is the gatekeeper
&lt;/h2&gt;

&lt;p&gt;This is the design decision people push back on most, so to be precise: the sponsored unit is a &lt;strong&gt;data field&lt;/strong&gt;, labeled &lt;code&gt;sponsored&lt;/code&gt;, attached to the tool response. It is not a prompt. There are no display instructions anywhere in the SDK — not "show this", not "consider mentioning", nothing.&lt;/p&gt;

&lt;p&gt;The host model sees a clearly-labeled sponsored suggestion next to the actual tool result and decides on its own whether it's relevant enough to surface. If it's junk, the model ignores it and the user never sees it.&lt;/p&gt;

&lt;p&gt;That flips the usual ad-tech power dynamic: the publisher surface is smarter than the ad. A banner can't evaluate itself; a frontier model evaluating a labeled suggestion absolutely can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invariant 4: never on errors
&lt;/h2&gt;

&lt;p&gt;If your tool call failed, the user is having a bad moment. The SDK refuses to attach sponsored data to error responses, at the middleware level. Monetizing failure states is how products start hating their users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CPA and not impressions
&lt;/h2&gt;

&lt;p&gt;Impression-based pricing rewards spraying ads everywhere and praying. Cost-per-action means the advertiser pays only when someone actually converts — so the only ads worth running are genuinely relevant ones. Incentive alignment beats policy enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What integration actually looks like
&lt;/h2&gt;

&lt;p&gt;Python (FastMCP middleware — one line after setup):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;lulu_ads.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LuluAdsMiddleware&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LuluAdsMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;publisher_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LULU_ADS_PUBLISHER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LULU_ADS_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript (server wrapper):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withLuluAds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lulu-ads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;withLuluAds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install is &lt;code&gt;pip install lulu-ads&lt;/code&gt; or &lt;code&gt;npm install lulu-ads&lt;/code&gt;; you register once at &lt;a href="https://getlulu.dev/publishers" rel="noopener noreferrer"&gt;getlulu.dev/publishers&lt;/a&gt; to get credentials. 70% of revenue goes to the server owner. Adding it is free, and per invariant 1 it can't hurt your server even if it earns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm still not sure about
&lt;/h2&gt;

&lt;p&gt;Honest open questions, because pretending there are none would be marketing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Does model-as-gatekeeper hold up long-term?&lt;/strong&gt; Today, host models are conservative about surfacing sponsored content. If models ever get tuned for engagement the way feeds were, this safeguard weakens. I don't control that; disclosure at the protocol level is the backstop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context boundaries.&lt;/strong&gt; The SDK tells publishers to exclude sensitive user data from ad context. That's a rule, and rules need enforcement tooling — that's on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host policies.&lt;/strong&gt; Anthropic, OpenAI and friends will eventually have opinions about sponsored fields in tool responses. I think a disclosed, ignorable data field is the version that survives that conversation — it's why the design looks like this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run an MCP server, I'd genuinely like your take — especially if your take is "never touching this, because X". The repo is at &lt;a href="https://github.com/Lulu-The-Narwhal/lulu-ads" rel="noopener noreferrer"&gt;github.com/Lulu-The-Narwhal/lulu-ads&lt;/a&gt;, it's on the official MCP registry as &lt;code&gt;io.github.Lulu-The-Narwhal/lulu-ads&lt;/code&gt;, and the docs live at &lt;a href="https://getlulu.dev" rel="noopener noreferrer"&gt;getlulu.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
