<?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: Prakhar Gupta</title>
    <description>The latest articles on DEV Community by Prakhar Gupta (@guptaprakhariitr).</description>
    <link>https://dev.to/guptaprakhariitr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3979316%2F4d452510-d5cb-4837-8030-713448c23d51.jpeg</url>
      <title>DEV Community: Prakhar Gupta</title>
      <link>https://dev.to/guptaprakhariitr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guptaprakhariitr"/>
    <language>en</language>
    <item>
      <title>How AI agents become your customers — lessons from shipping 17 paid MCP servers</title>
      <dc:creator>Prakhar Gupta</dc:creator>
      <pubDate>Thu, 11 Jun 2026 10:25:53 +0000</pubDate>
      <link>https://dev.to/guptaprakhariitr/how-ai-agents-become-your-customers-lessons-from-shipping-17-paid-mcp-servers-56pd</link>
      <guid>https://dev.to/guptaprakhariitr/how-ai-agents-become-your-customers-lessons-from-shipping-17-paid-mcp-servers-56pd</guid>
      <description>&lt;p&gt;&lt;em&gt;Cross-post to dev.to, Hashnode, Medium.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover image suggestion: split-screen — left side a human customer support ticket, right side an AI agent API call. Title overlay.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The premise
&lt;/h2&gt;

&lt;p&gt;For most of SaaS history, the buyer was a human. They visited a marketing page, signed up, entered a credit card. The product was built for human ergonomics — pretty dashboards, helpful onboarding emails, account managers for the bigger checks.&lt;/p&gt;

&lt;p&gt;That assumption is breaking. Increasingly the entity calling your API isn't a developer integrating your product into their app. It's an autonomous agent (Claude, GPT, an internal LLM) being asked to &lt;em&gt;use&lt;/em&gt; your tool mid-conversation, often by an end user the LLM has never seen and whose company has never heard of yours.&lt;/p&gt;

&lt;p&gt;I shipped 17 MCP (Model Context Protocol) servers over the last few weeks. MCPs are tool servers that AI agents call to fetch data or take actions. By definition, the consumer is not a human — it's a language model. After weeks of designing those tool surfaces and watching usage trickle in, here's what's different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when your customer is an agent
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The agent reads your docs before every call
&lt;/h3&gt;

&lt;p&gt;Humans skim docs once, screenshot the SDK example, never come back. Agents re-read your tool descriptions on every conversation — because that's literally what the MCP spec does. Every connection includes a &lt;code&gt;tools/list&lt;/code&gt; call that returns each tool's name, description, and parameter schema.&lt;/p&gt;

&lt;p&gt;This means &lt;strong&gt;your tool description is the user manual&lt;/strong&gt; every single time. There's no "the user read the README." There's only the description you handed to the agent at session start.&lt;/p&gt;

&lt;p&gt;Practical implication: spend disproportionate effort on tool descriptions. The right description tells the LLM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the tool does (1 sentence)&lt;/li&gt;
&lt;li&gt;When to use it vs other tools (this is the critical part — LLMs route badly when descriptions overlap)&lt;/li&gt;
&lt;li&gt;What the parameters mean in plain English&lt;/li&gt;
&lt;li&gt;What the return shape will look like&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I rewrote SEC EDGAR's &lt;code&gt;edgar_read_filing&lt;/code&gt; description four times. Version 1: "Reads an SEC filing." Useless. Version 4: "Returns the full text of a specific SEC filing identified by its accession number. Use after &lt;code&gt;edgar_search_filings&lt;/code&gt; returns candidates. For 8-K item-specific extraction, use &lt;code&gt;edgar_get_8k&lt;/code&gt; instead. Pass &lt;code&gt;section='risk_factors'&lt;/code&gt; to extract just risk factor disclosures from 10-Ks."&lt;/p&gt;

&lt;p&gt;The fourth version reduced misrouted calls (LLM calling read_filing when it should have called get_8k) by roughly half in my own test traces.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Errors must be actionable to the model, not just the human
&lt;/h3&gt;

&lt;p&gt;When a human gets a 429 error, they read the docs and back off. When an agent gets a 429, it's about to retry with the exact same payload unless your error tells it not to.&lt;/p&gt;

&lt;p&gt;The error responses an agent sees should answer: "What should I do &lt;em&gt;now&lt;/em&gt; to make this succeed?" Examples that work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Rate limit reached. The free tier allows 100 calls/month per IP. To continue, get an API key at https://sec-edgar-mcp.atlasword.workers.dev/upgrade and pass it as Authorization: Bearer &amp;lt;key&amp;gt;."&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Filing not found. The accession number must be in format 0001193125-23-123456. Use edgar_search_filings to find valid accession numbers first."&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Unauthorized"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Bad request"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Quota exceeded"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good versions are longer. That's fine. The agent is reading them with full attention; the human will never see them unless they're debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pricing must be transparent to the model, not just the website
&lt;/h3&gt;

&lt;p&gt;If your pricing is "$9/mo" on a marketing page, the agent doesn't know that. The agent knows what you returned in the last &lt;code&gt;tools/list&lt;/code&gt; response. So you have to encode the relevant facts about pricing into the tool metadata or into the error responses.&lt;/p&gt;

&lt;p&gt;I include a tool called &lt;code&gt;&amp;lt;product&amp;gt;_get_pricing&lt;/code&gt; on most of my MCPs. It returns the current tier structure as JSON. The agent calls it once and now it knows what to recommend to the end user when they hit the free tier limit. Without that, the agent will hallucinate a price or just say "you should upgrade" with no actionable detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The conversion funnel collapses to one step
&lt;/h3&gt;

&lt;p&gt;For human SaaS, the funnel is: visit site → sign up → enter card → use product → maybe upgrade.&lt;/p&gt;

&lt;p&gt;For agent SaaS, the funnel is: end user asks a question → agent calls your tool → either succeeds within free tier, or returns an upgrade error with a direct upgrade URL.&lt;/p&gt;

&lt;p&gt;There's no "sign up first" step. There can't be — the agent has no email, no payment method, no concept of an account. The only way conversion happens is if your upgrade flow is callable as a single URL the agent shows the human.&lt;/p&gt;

&lt;p&gt;Concretely: my upgrade URL takes a Stripe Checkout that returns a usable API key the moment payment clears. No account creation step. No email verification. The buyer is the &lt;em&gt;human supervising the agent&lt;/em&gt;, and that human just wants the agent to keep working. Adding any friction kills conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Your value prop is now LLM-shaped
&lt;/h3&gt;

&lt;p&gt;When the buyer was a human developer, "we have the best Python SDK" was a value prop. When the buyer is an LLM, it isn't. The LLM doesn't care about your SDK.&lt;/p&gt;

&lt;p&gt;What the LLM cares about (per my error-trace analysis):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency. The agent will time out at ~10s. Anything that takes &amp;gt;5s gets retried, which hurts your rate limits and the user's experience.&lt;/li&gt;
&lt;li&gt;Determinism. Tools that return slightly different data for the same inputs cause the agent to lose confidence in the result and re-ask.&lt;/li&gt;
&lt;li&gt;Composability. Tools that pipe cleanly into other tools (output type matches input type of the next likely tool) get used more. Tools that require the agent to do data shape conversion get used less.&lt;/li&gt;
&lt;li&gt;Description quality (see point 1).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a different value prop than "great DX for developers." Your README is now an artifact for SEO and human onboarding. Your tool description is the actual product surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The "growth loop" is agent-to-agent
&lt;/h3&gt;

&lt;p&gt;Here's the unexpected one. Once an agent (let's say Claude) has used your tool successfully in a conversation, the end user is more likely to use Claude for that kind of question in the future. Claude's value to that user has gone up &lt;em&gt;because of your tool&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This means the marketing flywheel is partly: "agents that have used your tool deliver better answers, which makes the agent platform stickier, which makes the platform invest more in MCP infrastructure, which makes more agents discover your tool."&lt;/p&gt;

&lt;p&gt;You're not just selling to humans through agents. You're indirectly improving the agent's market position, and the agent platforms (Anthropic, OpenAI, Cursor) have an incentive to surface tools that make their agents look good.&lt;/p&gt;

&lt;p&gt;I'm watching for this empirically. Smithery, Glama, mcp.so — these are agent-shopping surfaces. They're not for humans to browse. They're for agent platforms to scrape and for agents themselves to discover tools at session-init time.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Pricing can be much lower than human SaaS
&lt;/h3&gt;

&lt;p&gt;Human SaaS minimum viable price is roughly $9-19/mo because the human's attention to evaluate, sign up, and integrate is the dominant cost. Below that, the time isn't worth it.&lt;/p&gt;

&lt;p&gt;Agent SaaS has no such floor. The agent has no attention cost. Conversion happens in one second when the human clicks "upgrade." This means $5/mo or even $1/mo can be viable for high-frequency low-value tools.&lt;/p&gt;

&lt;p&gt;My unit-converter MCP is $5/mo for the same reason a vending machine is $1 a coke and not $19/mo. The friction is zero, so the price clears at a level that would be impossible for a human-evaluated SaaS.&lt;/p&gt;

&lt;p&gt;Whether $1-5/mo MCPs are a real market is testable now. My bet is yes, for utility surfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. What I still don't know
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How do you market to agents? You don't, directly. You market to agent platforms (get listed in directories) and to the humans who choose which tools their agents have access to. But the relative weight of those is unclear.&lt;/li&gt;
&lt;li&gt;How do you handle abuse? An agent doesn't have a credit history. If a bad actor uses an agent to hammer your free tier, the agent's reputation isn't on the line; only the underlying IP is. IP blocking helps but isn't sufficient.&lt;/li&gt;
&lt;li&gt;How do you build retention? With humans, you send re-engagement emails. With agents, there's no email. The agent uses your tool again only if the user asks a question for which your tool is the best fit. So retention is really "stay top-of-mind in the tool descriptions of the directories agents read."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone shipping for AI agents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat your tool description like it's the product page.&lt;/strong&gt; Because for the agent, it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make errors actionable.&lt;/strong&gt; The agent reads every byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-URL upgrade. No account creation.&lt;/strong&gt; Friction kills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize for latency and determinism, not feature surface.&lt;/strong&gt; The LLM cares about reliability, not bells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship a pricing tool.&lt;/strong&gt; Let the agent introspect what to recommend on upgrade prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List on every directory.&lt;/strong&gt; Discovery is agent-platform-driven, and listings are crawled by agents at session start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price lower than you'd dare for humans.&lt;/strong&gt; $1-5/mo is a real wedge for utility tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 17 MCP servers I built: &lt;a href="https://mcp-hub.atlasword.workers.dev/" rel="noopener noreferrer"&gt;https://mcp-hub.atlasword.workers.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source for all of them (MIT): &lt;a href="https://github.com/guptaprakhariitr" rel="noopener noreferrer"&gt;https://github.com/guptaprakhariitr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol spec: &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Honest disclaimer: zero customers as of this writing. Everything above is from designing the surface and watching test traces, not from years of MRR data. If you ship MCP-based products yourself and your data disagrees with any of this, I'd love to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>mcp</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Building an 18-product MCP portfolio in a few weeks</title>
      <dc:creator>Prakhar Gupta</dc:creator>
      <pubDate>Thu, 11 Jun 2026 10:25:51 +0000</pubDate>
      <link>https://dev.to/guptaprakhariitr/building-an-18-product-mcp-portfolio-in-a-few-weeks-5d83</link>
      <guid>https://dev.to/guptaprakhariitr/building-an-18-product-mcp-portfolio-in-a-few-weeks-5d83</guid>
      <description>&lt;p&gt;&lt;em&gt;Cross-post to dev.to, Hashnode, Medium. Recommended canonical URL: your personal blog if you have one, otherwise dev.to.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover image suggestion: a screenshot of the hub homepage with the 17 product cards visible.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Over a few weeks I built 17 customer-facing Model Context Protocol (MCP) servers plus an analytics service and a master hub. Each one is a thin Cloudflare Worker that wraps a free public data source into a tool surface AI agents can call. Source is MIT, hosted on Cloudflare's free tier, with an anonymous free tier on every product. Hub: &lt;a href="https://mcp-hub.atlasword.workers.dev/" rel="noopener noreferrer"&gt;https://mcp-hub.atlasword.workers.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is about the architecture, the economics, and the parts that are harder than they look.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a portfolio of 17 instead of one polished product
&lt;/h2&gt;

&lt;p&gt;The conventional indie-hacker advice is: pick one wedge, go deep, become the best-in-class for that one thing. I deliberately did the opposite. Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The marginal cost of an additional MCP is small.&lt;/strong&gt; Once you have the template — Worker entrypoint, KV cache, quota counter, Stripe webhook, OpenAPI generator, smithery.yaml, server.json, npm wrapper — the only product-specific code is the upstream API client and the tool definitions. For most public APIs that's 200-500 lines of TypeScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The marginal cost of an additional listing is also small.&lt;/strong&gt; Submitting to Smithery, Glama, mcp.so, the official registry, awesome-mcp lists, etc., is per-product effort. But you can template the listing content too. So 17 listings on 8 directories is 136 submissions — but the marginal one is two minutes once the script is written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. I genuinely don't know which buyer will show up first.&lt;/strong&gt; A portfolio is a hedge against being wrong about the buyer. If SEC EDGAR doesn't take off but GST validator does, fine — I follow the signal. Building one wedge would have locked me into a guess about the buyer.&lt;/p&gt;

&lt;p&gt;That's the thesis. Whether it's right is testable, not arguable.&lt;/p&gt;

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

&lt;p&gt;Each product is a single Cloudflare Worker. The diagram is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude / Cursor / Cline / any MCP-compatible agent
                |
                | MCP-over-HTTP (JSON-RPC 2.0, POST /mcp)
                v
            Cloudflare Worker
               /          \
              /            \
       KV: usage         KV: cache
       (quota counter)   (upstream API responses)
              \            /
               \          /
                v        v
            Upstream public API
        (SEC EDGAR, openFDA, GDELT, ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/index.ts&lt;/code&gt; — MCP server, routes &lt;code&gt;/mcp&lt;/code&gt;, &lt;code&gt;/openapi.json&lt;/code&gt;, &lt;code&gt;/llms.txt&lt;/code&gt;, &lt;code&gt;/upgrade&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/client.ts&lt;/code&gt; — upstream API client with retries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/tools.ts&lt;/code&gt; — MCP tool definitions (the part that takes the longest)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/usage.ts&lt;/code&gt; — KV-backed quota counter (shared across all 17, copy-pasted)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/stripe.ts&lt;/code&gt; — Stripe webhook handler (shared across all 17, copy-pasted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total ~500-1500 LOC TypeScript per product. The shared code is roughly 300 LOC across all products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP-over-HTTP transport
&lt;/h2&gt;

&lt;p&gt;A lot of MCP servers in the wild are stdio-only npm packages. That works but it's clunky — every server you add to Claude Desktop spawns a child process at startup, every request requires JSON serialization over a pipe, and it's hard for non-desktop clients to use them.&lt;/p&gt;

&lt;p&gt;MCP-over-HTTP is just JSON-RPC 2.0 over POST. The endpoint accepts:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edgar_search_filings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&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;"cik"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0000320193"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10-K"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And returns:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Found 3 10-K filings for AAPL: ..."&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;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;You can &lt;code&gt;curl&lt;/code&gt; this. You can call it from any HTTP client. Claude Desktop's HTTP MCP support is recent; Cursor, Cline, and most newer agents speak it natively. For desktop fallback, I ship an npm wrapper (&lt;code&gt;@insnapsprakhar/&amp;lt;slug&amp;gt;-mcp&lt;/code&gt;) that bridges stdio → HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota metering on KV
&lt;/h2&gt;

&lt;p&gt;Workers KV is eventually consistent, which sounds scary for a counter but is fine in practice. Burst tolerance &amp;gt; strict correctness.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;incrementUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkQuota&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QuotaStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;`anon:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;month&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&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;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;incrementUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`usage:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;lookupTierLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;// anon free tier&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;used&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;limit&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;That's the whole metering surface. ~30 LOC. No D1, no Postgres, no Redis. The race condition during a burst is a few extra calls beyond the limit, which is a fine user experience for a free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest part: tool surface design
&lt;/h2&gt;

&lt;p&gt;The architecture took maybe 3 weeks of figuring out. The shipping took weeks. The hard part was — and still is — deciding what the right 5-10 MCP tools per data source are.&lt;/p&gt;

&lt;p&gt;A tool that's too generic (&lt;code&gt;edgar_query&lt;/code&gt;, takes any URL) gives the LLM no leverage; it has to construct the URL itself. A tool that's too narrow (&lt;code&gt;edgar_get_apple_10k&lt;/code&gt;, one company) doesn't compose. The right surface is somewhere in between, and depends on what kinds of questions the agent is being asked.&lt;/p&gt;

&lt;p&gt;For SEC EDGAR I settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;edgar_search_filings(cik?, form_type?, date_from?, date_to?, limit?)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgar_read_filing(accession_number, section?)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgar_get_facts(cik, concept?)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgar_get_8k(cik, item_number?, since?)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;edgar_get_company(query)&lt;/code&gt; — name/ticker → CIK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgar_get_insider_trades(cik|name, since?)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six tools. Each one composes with the others — you call &lt;code&gt;edgar_get_company&lt;/code&gt; to get a CIK, then &lt;code&gt;edgar_search_filings&lt;/code&gt;, then &lt;code&gt;edgar_read_filing&lt;/code&gt;. The tools intentionally don't pre-summarize because LLMs are better at that than I am.&lt;/p&gt;

&lt;p&gt;I am genuinely uncertain whether 6 is the right number for every product. Two of the simpler products (gst-validator, hsn-classifier) have 3 tools each and feel right. The more complex ones (indian-regulatory, indic-normalize) have 9-12 and feel like they're at the upper limit before the LLM starts misrouting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution: harder than the code
&lt;/h2&gt;

&lt;p&gt;There are at least 8 directories where you can list an MCP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Time per submission&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Official &lt;code&gt;modelcontextprotocol/registry&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mcp-publisher&lt;/code&gt; CLI + GitHub OIDC&lt;/td&gt;
&lt;td&gt;~2 min (after CI setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smithery&lt;/td&gt;
&lt;td&gt;smithery.yaml in repo, auto-crawled&lt;/td&gt;
&lt;td&gt;~0 min (passive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glama&lt;/td&gt;
&lt;td&gt;Auto-crawls awesome-mcp-servers&lt;/td&gt;
&lt;td&gt;~0 min (passive after PR merges)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp.so&lt;/td&gt;
&lt;td&gt;GitHub issue or web form&lt;/td&gt;
&lt;td&gt;~5 min per product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PulseMCP&lt;/td&gt;
&lt;td&gt;Web form&lt;/td&gt;
&lt;td&gt;~5 min per product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCPMarket&lt;/td&gt;
&lt;td&gt;Web form / OAuth&lt;/td&gt;
&lt;td&gt;~5 min per product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor MCP gallery&lt;/td&gt;
&lt;td&gt;PR to Cursor's repo&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;punkpeye/awesome-mcp-servers&lt;/td&gt;
&lt;td&gt;PR&lt;/td&gt;
&lt;td&gt;once for all 17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even with templating, 17 products × 8 directories = 136 submissions. Not all are programmatic. The directories that auto-crawl (Smithery, Glama) you get for free once your repo metadata is right. The ones that require manual submission are the bottleneck, and the way I've handled it is to script-generate the issue/PR bodies and submit in batches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economics
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers free tier: 100k requests/day, KV reads 100k/day free, writes 1k/day. The paid plan is $5/mo and would cover the entire 17-product directory at meaningful scale.&lt;/p&gt;

&lt;p&gt;Per product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier: ~100 calls/month/IP, anonymous&lt;/li&gt;
&lt;li&gt;$5-9/mo paid tier: 5k-10k calls/month&lt;/li&gt;
&lt;li&gt;$19-29/mo: premium tools + higher quota&lt;/li&gt;
&lt;li&gt;$79+ tiers: bulk endpoints, team accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Break-even per product is roughly one paying customer at $9/mo (covers shared Stripe + Worker bundle costs + my time at $50/hr against the ~6h spent). Across the portfolio, break-even is ~10 paying customers total. Today: zero. This is launch day, not a postmortem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build the first 3 hard, validate distribution, then template.&lt;/strong&gt; I built the first 8 in parallel before realizing the distribution flow needed work. Should have shipped 1-2-3 sequentially with full distribution per product, &lt;em&gt;then&lt;/em&gt; parallelized.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stripe on Day 0, not Day 5.&lt;/strong&gt; I delayed Stripe until product 5 thinking free tier would generate signups first. Free tier generates calls, not signups. Conversion happens via in-app upgrade prompts that need a payment surface from the start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick a sharper buyer for at least one product.&lt;/strong&gt; "Anyone who wants SEC data" is not a buyer. "Equity research analysts at funds under $500M AUM who pay $50+/mo for Bloomberg-alternative tools" is a buyer. I deliberately stayed generic on most products as a hedge; for at least one, I should have been narrow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;All 17 products are MIT, public on GitHub: &lt;a href="https://github.com/guptaprakhariitr" rel="noopener noreferrer"&gt;https://github.com/guptaprakhariitr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hub: &lt;a href="https://mcp-hub.atlasword.workers.dev/" rel="noopener noreferrer"&gt;https://mcp-hub.atlasword.workers.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to fork the pattern — it's the same Worker template across all of them, with the upstream client swapped — you can. The &lt;code&gt;_template&lt;/code&gt; directory under &lt;code&gt;no-grav/products/&lt;/code&gt; has the bones.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments — especially about Workers KV economics, the metering flow, or the tool-surface design choices.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, the easiest way to support is to try one of the 17 servers with your AI agent and tell me where the tool definitions are wrong. That's the part I'm least confident on.&lt;/em&gt;&lt;/p&gt;

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