<?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: Hiroki Nomura</title>
    <description>The latest articles on DEV Community by Hiroki Nomura (@nomunomu0504).</description>
    <link>https://dev.to/nomunomu0504</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%2F4170064%2F2801507a-070b-4222-ac24-f5edd64501db.png</url>
      <title>DEV Community: Hiroki Nomura</title>
      <link>https://dev.to/nomunomu0504</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nomunomu0504"/>
    <language>en</language>
    <item>
      <title>Funded key with limits, or approve each payment? Putting a spend policy between an AI agent and HTTP 402</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Fri, 30 Oct 2026 06:36:46 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/funded-key-with-limits-or-approve-each-payment-putting-a-spend-policy-between-an-ai-agent-and-1nfl</link>
      <guid>https://dev.to/nomunomu0504/funded-key-with-limits-or-approve-each-payment-putting-a-spend-policy-between-an-ai-agent-and-1nfl</guid>
      <description>&lt;p&gt;Agents can pay for APIs now. With x402 and MPP a server answers &lt;code&gt;402 Payment Required&lt;/code&gt; with a price, the client signs a stablecoin authorization, retries, and gets the data. No API key, no invoice, no account. It works, and it is the part people demo.&lt;/p&gt;

&lt;p&gt;The part nobody demos is &lt;em&gt;who decides&lt;/em&gt;. Two patterns keep coming up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A funded key with a cap.&lt;/strong&gt; Fast. The model can also burn the whole cap in a retry loop and you find out from the balance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve every payment.&lt;/strong&gt; Safe. Nobody wants to approve two hundred $0.002 calls, so after a day the approval becomes a rubber stamp or the agent gets the key anyway.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both put the money decision inside the agent's reach: in a prompt, in a tool argument, in an environment variable the model can read. I wanted the decision outside the model, in something a human writes once and can audit later. That became &lt;strong&gt;spendctl&lt;/strong&gt;, a local sidecar. This post is about the design, including the one state that turned out to matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;The agent never holds a key. It sends its HTTP request through the sidecar instead: as a gateway URL, through &lt;code&gt;HTTP_PROXY&lt;/code&gt;, with a &lt;code&gt;wrapFetch&lt;/code&gt; in the SDK, or as an MCP &lt;code&gt;pay&lt;/code&gt; tool. When the seller answers 402, the sidecar does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evaluates a policy file&lt;/strong&gt; written by a human: per-agent budgets (hourly, daily, weekly, monthly), a per-request cap, allowlisted hosts, pinned recipients and networks, a first-payment cap for new recipients, a price-jump check, and a threshold above which a human has to approve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signs and pays&lt;/strong&gt; with a wallet the agent cannot see, retries the request with the payment, and passes the response back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes one ledger row&lt;/strong&gt; per decision, with the reasons. Refusals come back as readable text the model can act on ("&lt;code&gt;$0.40&lt;/code&gt; would exceed the daily budget of &lt;code&gt;$0.25&lt;/code&gt; for agent &lt;code&gt;researcher&lt;/code&gt;, &lt;code&gt;$0.21&lt;/code&gt; already spent"), not as a stack trace.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;researcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;perRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.05"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;daily&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.25"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.example.com"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;requireApprovalAbove&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.02"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model sees a budget, a refusal or an approval id. It never sees a private key, and it cannot change the policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approvals that fit how agents run
&lt;/h2&gt;

&lt;p&gt;A payment that crosses the threshold needs a human. Two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hold.&lt;/strong&gt; The agent's request stays open until someone decides, up to four minutes by default. Good for CLIs and scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry.&lt;/strong&gt; The sidecar answers at once with an approval id. After a human approves, the agent retries with &lt;code&gt;X-Spendctl-Approval: &amp;lt;id&amp;gt;&lt;/code&gt;. One approval pays for exactly one retry, and it is bound to what the human saw: recipient, network, asset, amount. If the seller's next 402 asks for anything else, the retry is refused and nothing is paid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Humans approve from the CLI, a local dashboard, or a review link in a Slack or Discord webhook. Agents cannot approve; the admin token lives in a file the agent does not read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state nobody models
&lt;/h2&gt;

&lt;p&gt;Someone on r/x402 asked the question that every payment system eventually has to answer: &lt;em&gt;the payment settles, but the paid API times out before the agent gets the result. Does the ledger know the difference?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most agent payment demos do not model this at all. A retry is a second payment. spendctl has an explicit state for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once a signed payment has left the sidecar, the outcome is &lt;code&gt;settled&lt;/code&gt; (the seller answered 2xx with a settlement receipt), &lt;code&gt;failed&lt;/code&gt; (the seller rejected it), or &lt;strong&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/strong&gt;: the request timed out or errored after the payment was sent, or the seller returned an error after taking it, or the settlement header says "pending".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unknown&lt;/code&gt; &lt;strong&gt;counts against the budget&lt;/strong&gt; until a human reconciles it. An unconfirmed payment is never free spend.&lt;/li&gt;
&lt;li&gt;The sidecar &lt;strong&gt;never retries a sent payment&lt;/strong&gt;. The agent gets a 502 with the ledger id and a sentence saying so. If the agent resends with the same &lt;code&gt;Idempotency-Key&lt;/code&gt; inside the window, the sidecar refuses and points at the original row, so paying twice takes deliberate effort.&lt;/li&gt;
&lt;li&gt;A human closes it with &lt;code&gt;spendctl resolve &amp;lt;id&amp;gt; --settled --tx 0x…&lt;/code&gt; or &lt;code&gt;--failed&lt;/code&gt; after checking the chain or the seller's dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not clever. It is the boring bookkeeping that is missing from most "let the agent pay" code, and it is where the money actually leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without installing anything
&lt;/h2&gt;

&lt;p&gt;There is a hosted sandbox: a real sidecar running against mock paid APIs, exposed as a remote MCP server at &lt;code&gt;https://mcp.spendctl.dev/mcp&lt;/code&gt;. Add it to Claude, Claude Code, Cursor or VS Code, then ask the model to create a sandbox, make a paid call, blow the budget, hold a payment for approval and approve it. Throwaway wallet, mock facilitator, no outbound network, nothing real. The tools are &lt;code&gt;sandbox_create&lt;/code&gt;, &lt;code&gt;sandbox_pay&lt;/code&gt;, &lt;code&gt;check_budget&lt;/code&gt;, &lt;code&gt;list_receipts&lt;/code&gt;, &lt;code&gt;get_policy&lt;/code&gt; and &lt;code&gt;approve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx spendctl init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx spendctl start   &lt;span class="c"&gt;# sidecar on :8402&lt;/span&gt;
npx spendctl mock                          &lt;span class="c"&gt;# mock facilitator + two paid APIs&lt;/span&gt;
npx spendctl fetch &lt;span class="s2"&gt;"http://localhost:4022/search?q=x402"&lt;/span&gt; &lt;span class="nt"&gt;--agent&lt;/span&gt; researcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it is not, yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The sidecar is a commercial product with a free tier (two agents, a monthly cap on governed payments). The client SDK and the editor plugins are Apache-2.0 on GitHub.&lt;/li&gt;
&lt;li&gt;Reconciliation of &lt;code&gt;unknown&lt;/code&gt; is manual today. Asking the facilitator or the chain automatically is under consideration.&lt;/li&gt;
&lt;li&gt;The sandbox cannot simulate a seller that settles and then times out; that path is only testable locally.&lt;/li&gt;
&lt;li&gt;MCP clients cut tool calls at about 60 seconds, which is shorter than hold mode. For MCP agents, retry mode is the right setting today. A dedicated &lt;code&gt;await_approval&lt;/code&gt; tool that long-polls under the client timeout is under consideration, after a good suggestion in the r/mcp thread.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are putting agents in front of paid APIs, I would like to hear which ledger fields you would want to see, and what your clients actually do when a payment is held. Docs are at spendctl.dev/docs; the sandbox needs no account.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>payments</category>
    </item>
    <item>
      <title>We scored 1,274 MCP tool descriptions, then checked whether the scores predict what models do</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/we-scored-1274-mcp-tool-descriptions-then-checked-whether-the-scores-predict-what-models-do-27bk</link>
      <guid>https://dev.to/nomunomu0504/we-scored-1274-mcp-tool-descriptions-then-checked-whether-the-scores-predict-what-models-do-27bk</guid>
      <description>&lt;p&gt;We published the scores of 55 public MCP servers, 1,274 tools in all, at &lt;a href="https://forecall.dev/en/servers" rel="noopener noreferrer"&gt;forecall.dev/en/servers&lt;/a&gt;. Each tool description is scored out of 100 on whether it says what the tool is for, when to use it, its parameters, what it returns, its constraints and an example.&lt;/p&gt;

&lt;p&gt;Then we tested whether a low score means models pick the wrong tool. For one-step choices, it didn't: two frontier models picked the right tool 91–99% of the time, low scores or not. This post covers both: what the descriptions leave out, and what that does and doesn't tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 1,274 tools came from
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;46 servers are Smithery's most-used servers, copied from its registry API on October 7, 2026 (UTC). The registry keeps each tool's name, description and input schema, but drops output schemas, annotations and server instructions.&lt;/li&gt;
&lt;li&gt;9 servers were taken from the running server with &lt;code&gt;forecall dump&lt;/code&gt;: Notion, Playwright, Chrome DevTools, Context7, Filesystem, Memory, Firecrawl, DebugBase and Bhived.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All were scored with the same rules (Forecall's scoring rules v2). Each server's page says which source it came from and when. The pages show scores, issues and tool names, not the descriptions themselves. If you own one of these servers and want it taken down, email &lt;a href="mailto:support@forecall.dev"&gt;support@forecall.dev&lt;/a&gt; and it's removed within 7 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the descriptions leave out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;885 of 1,274 tools (69%) never say when to use them.&lt;/strong&gt; The share is the same in both groups: 69% of the registry copies and 71% of the dumped tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;87 of the 156 dumped tools (56%) don't say what they return&lt;/strong&gt;, neither in an output schema nor in the description. (I'm leaving the registry copies out of this one, because the registry drops output schemas.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;18 of the 55 servers have at least one pair of near-identical descriptions&lt;/strong&gt;, 129 pairs in all. Two servers account for most of them: Notion (57 pairs across 24 tools) and Paper Search (29 across 25).&lt;/p&gt;

&lt;p&gt;Server averages range from 19.9 to 82, with a median of 44. Across all tools, 497 score under 40 and only 18 score 80 or more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does a low score mean models choose wrong? We measured it
&lt;/h2&gt;

&lt;p&gt;Forecall also runs evaluations: it writes requests for each tool and asks models which tool they'd call. We used that to calibrate the static score on 4 servers with 88 tools (Memory, Notion, Playwright and Chrome DevTools), 8 one-step requests per tool, with Claude Sonnet 5.5 and GPT-6.1 Sol:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Average score&lt;/th&gt;
&lt;th&gt;Right tool (Sonnet / Sol)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;43.8&lt;/td&gt;
&lt;td&gt;97% / 99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;33.0&lt;/td&gt;
&lt;td&gt;96% / 96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;41.9&lt;/td&gt;
&lt;td&gt;93% / 94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome DevTools&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;45.1&lt;/td&gt;
&lt;td&gt;99% / 98%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The correlation between a tool's score and how often models picked it correctly was 0.04 (Spearman). Tools scoring under 40 were picked correctly 95.5% of the time; tools scoring 60–79, 100%.&lt;/p&gt;

&lt;p&gt;We then put 25–55 tools from other servers next to each one, to make the choice harder. None of the requests that needed a tool went to the other servers' tools, and accuracy stayed at 91–98%.&lt;/p&gt;

&lt;p&gt;So, for frontier models choosing in one step, a short description wasn't the problem: the tool names and schemas carried enough. The misses that did happen were mostly the models doing something sensible first. When a request didn't include a page number, they called &lt;code&gt;list_pages&lt;/code&gt; before acting, in 77–87% of those requests in an earlier run. For &lt;code&gt;browser_fill_form&lt;/code&gt;, they took a page snapshot first, because the field references come from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the score is for, then
&lt;/h2&gt;

&lt;p&gt;The score says what a description puts in words, not how a model will choose. We changed the site's wording to match: what we used to call "pairs easy to mix up" are now "near-identical descriptions", and every result says what static scoring can't tell.&lt;/p&gt;

&lt;p&gt;What's missing from the text may still matter where this test doesn't reach: when the user doesn't have the ID a tool needs, over several steps, for smaller models, and for the people reading your docs. Just don't assume a longer description is better. In an earlier test, rewrites that raised scores (one tool went from 31 to 74) made two models worse (&lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/3026#issuecomment-6072359950" rel="noopener noreferrer"&gt;details here&lt;/a&gt;). Check rewrites against the models your users run.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Look it up: &lt;a href="https://forecall.dev/en/servers" rel="noopener noreferrer"&gt;forecall.dev/en/servers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Not listed? If it's a public Streamable HTTP server that needs no sign-in, paste its URL at &lt;a href="https://forecall.dev/en/lint" rel="noopener noreferrer"&gt;forecall.dev/en/lint&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Locally, with nothing sent anywhere: &lt;code&gt;npx forecall dump -o tools.json -- &amp;lt;command that starts your server&amp;gt;&lt;/code&gt;, then &lt;code&gt;npx forecall lint tools.json&lt;/code&gt;. The CLI also reads endpoint lists and search results.&lt;/li&gt;
&lt;li&gt;To see how models actually pick your tools, Forecall's evaluations (Pro and Team plans) report the right-tool rate, look-ups first, argument validity and false calls per model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm the author of Forecall. The linter is open source: &lt;a href="https://github.com/forecall/forecall-cli" rel="noopener noreferrer"&gt;github.com/forecall/forecall-cli&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>llm</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Recording what the Firestore emulator sends to Cloud Functions, and replaying it in Rust</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Tue, 13 Oct 2026 00:48:32 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/recording-what-the-firestore-emulator-sends-to-cloud-functions-and-replaying-it-in-rust-2pn8</link>
      <guid>https://dev.to/nomunomu0504/recording-what-the-firestore-emulator-sends-to-cloud-functions-and-replaying-it-in-rust-2pn8</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/hidane-dev/hidane" rel="noopener noreferrer"&gt;hidane&lt;/a&gt; is a reimplementation of the Cloud Firestore emulator as a single Rust binary: no JDK, the same gRPC, REST and WebChannel wire protocols, and it runs under firebase-tools with &lt;code&gt;hidane exec -- firebase emulators:start&lt;/code&gt;. Its rule is that every claim of compatibility has a test behind it: scripts record how the official emulator answers, and hidane's test suite replays every recording.&lt;/p&gt;

&lt;p&gt;Until now, one thing was missing under &lt;code&gt;emulators:start&lt;/code&gt;: Firestore-triggered Cloud Functions. Since 0.2.3 hidane accepted the Functions emulator's trigger registrations, so functions loaded, but no event ever reached them. 0.4.0 delivers those events. This post is about how.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the two emulators talk
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;firebase emulators:start&lt;/code&gt; with Firestore and Functions, firebase-tools starts the Firestore emulator with &lt;code&gt;--functions_emulator &amp;lt;host:port&amp;gt;&lt;/code&gt;. The Functions emulator then registers each Firestore trigger with the Firestore emulator. After a commit, the Firestore emulator sends an event for each document the commit changed to every trigger that watches it, as &lt;code&gt;POST /functions/projects/{p}/triggers/{key}&lt;/code&gt; on the Functions emulator.&lt;/p&gt;

&lt;p&gt;None of this is a documented API. It is a conversation between two local processes, and the only reliable description of it is what the official emulator actually sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording it from the outside
&lt;/h2&gt;

&lt;p&gt;hidane treats the official emulator as a black box: its jar is never decompiled, and everything below was observed on the wire (&lt;a href="https://github.com/hidane-dev/hidane/tree/main/docs/clean-room" rel="noopener noreferrer"&gt;docs/clean-room&lt;/a&gt; describes the policy).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tools/oracle/function_events.py&lt;/code&gt; starts the official emulator (v1.22.0, the version firebase-tools pins) three times, under an English locale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;with &lt;code&gt;--functions_emulator&lt;/code&gt; pointing at a small server of the script's own, which records every request it receives and answers &lt;code&gt;200&lt;/code&gt;, or &lt;code&gt;500&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt; or a late answer depending on the trigger's key;&lt;/li&gt;
&lt;li&gt;without the flag;&lt;/li&gt;
&lt;li&gt;with the flag pointing at a port nothing listens on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Against each, it registers triggers the way firebase-tools does, writes over REST, gRPC Commit and the Write stream, and records for every step: the answer, every request the Functions emulator received (in order, every header, the body as sent), and the &lt;code&gt;WARNING&lt;/code&gt; lines the emulator printed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it sends
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v1 triggers&lt;/strong&gt; (&lt;code&gt;providers/cloud.firestore/eventTypes/document.*&lt;/code&gt;) get a compact JSON body:&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="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"oldValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"updateMask"&lt;/span&gt;&lt;span class="p"&gt;:{}},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&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;"resource"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"firestore.googleapis.com"&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;A few details only a recording shows: the v1 triggers of one change share the same &lt;code&gt;eventId&lt;/code&gt;; &lt;code&gt;updateMask&lt;/code&gt; is always empty; the timestamp is the commit time; and v1 triggers only ever see the &lt;code&gt;(default)&lt;/code&gt; database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v2 triggers&lt;/strong&gt; (&lt;code&gt;google.cloud.firestore.document.v1.*&lt;/code&gt;, including &lt;code&gt;withAuthContext&lt;/code&gt;) get a binary-mode CloudEvent. The attributes travel as &lt;code&gt;ce-*&lt;/code&gt; headers (&lt;code&gt;ce-type&lt;/code&gt;, &lt;code&gt;ce-source&lt;/code&gt;, &lt;code&gt;ce-id&lt;/code&gt;, &lt;code&gt;ce-subject&lt;/code&gt;, &lt;code&gt;ce-time&lt;/code&gt;, &lt;code&gt;ce-database&lt;/code&gt;, &lt;code&gt;ce-document&lt;/code&gt;, and so on), and the body is a &lt;code&gt;google.events.cloud.firestore.v1.DocumentEventData&lt;/code&gt; protobuf, base64-encoded, which firebase-tools decodes. For &lt;code&gt;withAuthContext&lt;/code&gt;, the emulator sends &lt;code&gt;ce-authtype: unknown&lt;/code&gt; and the same placeholder &lt;code&gt;ce-authid&lt;/code&gt; whoever wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order: a Java HashMap
&lt;/h2&gt;

&lt;p&gt;Ordering looked simple at first: all v1 events of a commit, then the v2 ones, document by document in the order the writes name them. But when several triggers watch the same document, they did not come in registration order, nor alphabetically.&lt;/p&gt;

&lt;p&gt;They come in the iteration order of a Java &lt;code&gt;HashMap&lt;/code&gt; sized for the project's triggers of that version. The model was fitted on 116 observations, and hidane reproduces it in &lt;code&gt;presized_hash_order&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;presized_hash_order&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;AsRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// `(int) Math.ceil(n / 0.75)`, then `tableSizeFor`.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;wanted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.div_ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wanted&lt;/span&gt;&lt;span class="nf"&gt;.next_power_of_two&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;spread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="nf"&gt;.sort_by_key&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&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;&lt;code&gt;string_hash&lt;/code&gt; is Java's &lt;code&gt;String.hashCode&lt;/code&gt; (base 31 over UTF-16 code units), &lt;code&gt;spread&lt;/code&gt; folds the high half into the low one, and the table is the smallest power of two that holds n / 0.75 entries. Keys come out by bucket, and by insertion within a bucket. Functions that depend on the order in which their siblings fire are fragile, but a test suite that passes on the official emulator should not start failing because the order changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timing and failures
&lt;/h2&gt;

&lt;p&gt;Events go out after the commit, from one background queue, so a write never waits for a function. Each event waits for the answer to the previous one, for up to 20 seconds. hidane does the same, one event at a time from a task of its own.&lt;/p&gt;

&lt;p&gt;When delivery fails (an error status, a refused connection, a timeout), the official emulator logs a &lt;code&gt;WARNING&lt;/code&gt; and does not retry, for example &lt;code&gt;Unsuccessful HTTP response when publishing event for …&lt;/code&gt; or &lt;code&gt;Failed to publish event to {host}: Connection refused&lt;/code&gt;. Without &lt;code&gt;--functions_emulator&lt;/code&gt;, every event is a &lt;code&gt;WARNING&lt;/code&gt; that the Functions emulator host/port is not configured. hidane prints the same wording.&lt;/p&gt;

&lt;p&gt;Batches, transactions, &lt;code&gt;:batchWrite&lt;/code&gt;, gRPC Commit, the Write stream, deleted paths and cleared databases send events; imports and &lt;code&gt;POST /reset&lt;/code&gt; do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replaying it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;crates/hidane/tests/function_events.rs&lt;/code&gt; replays the 133 steps of the three runs against hidane and compares the status of each answer, every request the Functions emulator receives (v1 bodies byte for byte, v2 bodies decoded, with times and IDs compared as labels), and every &lt;code&gt;WARNING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recording also turned up something unrelated: Java prints the smallest double as &lt;code&gt;4.9E-324&lt;/code&gt;, and hidane printed &lt;code&gt;5.0E-324&lt;/code&gt;, over REST and WebChannel too. That is fixed now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What hidane does not copy
&lt;/h2&gt;

&lt;p&gt;Matching the official emulator is the goal, but not its bugs. Each of these is listed in &lt;code&gt;docs/parity-exceptions.md&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A v2 event's &lt;code&gt;ce-source&lt;/code&gt; (a function's &lt;code&gt;event.source&lt;/code&gt;) names the project twice: &lt;code&gt;//firestore.googleapis.com/projects/projects/{p}/databases/{d}&lt;/code&gt;. hidane names it once, as production does.&lt;/li&gt;
&lt;li&gt;A write that changes nothing sends v2 &lt;code&gt;updated&lt;/code&gt; and &lt;code&gt;written&lt;/code&gt; events, and deleting a missing document sends v2 &lt;code&gt;created&lt;/code&gt; and &lt;code&gt;written&lt;/code&gt;; v1 triggers get neither. hidane sends no event for either, as production does.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /reset&lt;/code&gt; makes the official emulator forget every registered trigger, so functions stop firing, silently, until they reload. hidane keeps them.&lt;/li&gt;
&lt;li&gt;A v2 trigger registered without a database makes every write to the project answer &lt;code&gt;500&lt;/code&gt;. hidane has it watch &lt;code&gt;(default)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  End to end
&lt;/h2&gt;

&lt;p&gt;Recordings check the wire; the last check is whether functions actually receive the same data. &lt;code&gt;tools/oracle/functions/&lt;/code&gt; is a functions project (firebase-functions 6.6.0) with v1 &lt;code&gt;onCreate&lt;/code&gt; / &lt;code&gt;onUpdate&lt;/code&gt; / &lt;code&gt;onDelete&lt;/code&gt; / &lt;code&gt;onWrite&lt;/code&gt;, v2 &lt;code&gt;onDocumentCreated&lt;/code&gt; / &lt;code&gt;Updated&lt;/code&gt; / &lt;code&gt;Deleted&lt;/code&gt; / &lt;code&gt;Written&lt;/code&gt;, &lt;code&gt;onDocumentWrittenWithAuthContext&lt;/code&gt;, a subcollection and a named database. It runs under &lt;code&gt;firebase emulators:exec --only functions,firestore&lt;/code&gt; (firebase-tools 15.33.0), once with the official emulator and once under &lt;code&gt;hidane exec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same 37 events reach the same functions with the same data. The official run has 6 more, the v2 events for the no-op write and the missing delete, and &lt;code&gt;projects/projects/&lt;/code&gt; in &lt;code&gt;event.source&lt;/code&gt;; both transcripts are in &lt;code&gt;results/functions-triggers-*.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in 0.4.0
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A commit's problems are reported in the official emulator's order.&lt;/li&gt;
&lt;li&gt;The Go, Python and Java server SDKs are verified against both emulators: documents, queries, aggregations, vector search, transactions and listeners.&lt;/li&gt;
&lt;li&gt;A weekly CI job starts the official emulator, re-records a set of recordings and fails if one differs from the committed fixture.&lt;/li&gt;
&lt;li&gt;The parity table now counts 20,224 recorded answers that the test suite replays.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://hidane.dev/install.sh | sh   &lt;span class="c"&gt;# or Homebrew, npm, cargo, pub, Docker&lt;/span&gt;
hidane &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; firebase emulators:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other ways to install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;hidane-dev/tap/hidane
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; hidane
cargo binstall hidane
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 ghcr.io/hidane-dev/hidane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://hidane.dev" rel="noopener noreferrer"&gt;https://hidane.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/hidane-dev/hidane" rel="noopener noreferrer"&gt;https://github.com/hidane-dev/hidane&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release notes: &lt;a href="https://github.com/hidane-dev/hidane/releases/tag/v0.4.0" rel="noopener noreferrer"&gt;https://github.com/hidane-dev/hidane/releases/tag/v0.4.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;hidane is also listed on DevHunt this week: &lt;a href="https://devhunt.org/tool/hidane" rel="noopener noreferrer"&gt;https://devhunt.org/tool/hidane&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your tests get a different answer from hidane than from the official emulator, please open an issue with the &lt;strong&gt;Parity gap&lt;/strong&gt; template. That is the most useful feedback.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Unofficial; not affiliated with Google. Firebase and Cloud Firestore are trademarks of Google LLC.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>firebase</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I scored the tool descriptions of Chrome DevTools MCP and Playwright MCP. Both rarely say when to use a tool</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/i-scored-the-tool-descriptions-of-chrome-devtools-mcp-and-playwright-mcp-both-rarely-say-when-to-2mbi</link>
      <guid>https://dev.to/nomunomu0504/i-scored-the-tool-descriptions-of-chrome-devtools-mcp-and-playwright-mcp-both-rarely-say-when-to-2mbi</guid>
      <description>&lt;p&gt;I scored the tool descriptions of two browser-automation MCP servers: Google's Chrome DevTools MCP and Microsoft's Playwright MCP. They averaged 45.1 and 41.9 out of 100. In Chrome DevTools MCP, 24 of 30 tools never say when to use them; in Playwright MCP, 23 of 25. And 27 and 21 tools respectively don't say what they return.&lt;/p&gt;

&lt;p&gt;The scoring was done with Forecall, a static linter for MCP tool descriptions that I build. This post shows the results, what separates the low-scoring descriptions from the high-scoring ones, what happened when I added three sentences to one of them, and what real models then did with both versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent picks a tool from its name, description and input schema, and nothing else
&lt;/h2&gt;

&lt;p&gt;An AI agent reads the list of tools an MCP server returns (the result of &lt;code&gt;tools/list&lt;/code&gt;) and decides which tool to call with which arguments. All it has to go on are three things: the tool's name, its description and its input schema.&lt;/p&gt;

&lt;p&gt;If the description only restates the name, the name and the schema are all the model has, and nothing tells it what a call returns until it makes one. Whether that is enough can only be measured with real models; I did that later in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I scored them: get tools/list, then score six parts out of 100
&lt;/h2&gt;

&lt;p&gt;On October 7, 2026, I started these two versions locally and saved their &lt;code&gt;tools/list&lt;/code&gt; results as JSON. No tools were called.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Package and version&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome DevTools MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chrome-devtools-mcp@1.10.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@playwright/mcp@0.0.83&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I scored the JSON with Forecall's CLI (&lt;code&gt;npx forecall lint&lt;/code&gt;). It scores each tool on six parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;What it looks for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;What the tool does, beyond restating its name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When to use&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;When to use it, and how to choose between similar tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arguments&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Each argument described, with its type and whether it's required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return value&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;What comes back, in an output schema or the description&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraints and side effects&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Preconditions and side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;An example of a call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scoring is rule-based and reads only the text of the descriptions and schemas. It doesn't measure whether a real model picks the right tool; I come back to that limit at the end. (I re-scored both servers on October 10 with the current rules, v2, and got the same numbers.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: both average in the 40s, and they're weak in the same places
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Chrome DevTools MCP&lt;/th&gt;
&lt;th&gt;Playwright MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average score&lt;/td&gt;
&lt;td&gt;45.1&lt;/td&gt;
&lt;td&gt;41.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No "when to use"&lt;/td&gt;
&lt;td&gt;24 / 30&lt;/td&gt;
&lt;td&gt;23 / 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No "what it returns"&lt;/td&gt;
&lt;td&gt;27 / 30&lt;/td&gt;
&lt;td&gt;21 / 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description restates the name&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairs with near-identical descriptions&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;In both, most of the lost points come from "when to use" and "return value".&lt;/strong&gt; Both describe their arguments well, and neither has a pair of tools with near-identical descriptions; their tool names are clearly distinct.&lt;/p&gt;

&lt;p&gt;Where they differ is how many descriptions only restate the name: 14 of Playwright MCP's 25 tools get that finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The low scorers end with one sentence that restates the name
&lt;/h2&gt;

&lt;p&gt;The lowest scores were 31 in Chrome DevTools MCP (drag, hover, upload_file) and 33 in Playwright MCP (browser_handle_dialog, browser_navigate, browser_type and others). Their descriptions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;drag&lt;/td&gt;
&lt;td&gt;Drag an element onto another element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hover&lt;/td&gt;
&lt;td&gt;Hover over the provided element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;browser_handle_dialog&lt;/td&gt;
&lt;td&gt;Handle a dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;browser_navigate&lt;/td&gt;
&lt;td&gt;Navigate to a URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They don't say when to call the tool, what's needed before calling it, or what comes back. browser_handle_dialog, for example, only makes sense while a dialog is open, but nothing in its description says so. That is what the linter measures: text that is missing. It doesn't show that a model will choose wrong, and for one-step choices, the test further down found that it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The high scorers say when to use the tool, with an example
&lt;/h2&gt;

&lt;p&gt;The same servers also have tools that score well. Chrome DevTools MCP's best is fill_form, at 72:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fill out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS prefer this tool over multiple individual 'fill' or 'click' calls when interacting with forms. It is significantly faster, more reliable, and reduces turn count. Example: Fill username, password, and check "Remember Me" in one call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It says how to choose between it and fill or click, and gives a concrete example. The only thing missing is the return value.&lt;/p&gt;

&lt;p&gt;Playwright MCP's best is browser_find, at 66. In one paragraph it explains what it searches for, what it returns, and why that's cheaper than taking a full snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three sentences took drag's score from 31 to 74
&lt;/h2&gt;

&lt;p&gt;I rewrote drag's description using only facts from its schema. I added three things: when to use it (and how it differs from upload_file), what to do before calling it, and what it returns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Drag one element and drop it onto another inside the page. Use it for drag-and-drop UIs such as sortable lists, kanban boards and sliders; to send files from the computer, use upload_file instead. Take a snapshot first and pass the uids of both elements as from_uid and to_uid. Returns a snapshot of the page after the drop when includeSnapshot is true.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Original&lt;/th&gt;
&lt;th&gt;Rewritten&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When to use&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arguments&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return value&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraints and side effects&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 43 points. The sentence on when to use it did the most: "when to use" went from 0 to the full 20.&lt;/p&gt;

&lt;p&gt;The rewrite also produced a new finding: the tool may be destructive but has no &lt;code&gt;destructiveHint&lt;/code&gt;. It reacted to the word "drop". drag doesn't destroy anything on the page, so this finding is wrong. Setting &lt;code&gt;destructiveHint: false&lt;/code&gt; in the tool's annotations makes it go away (the score stays at 74), and it also tells clients that the tool isn't destructive.&lt;/p&gt;

&lt;p&gt;A higher score, though, is not the same as better picks. The next section checks that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I tested real models: the short text was enough, and my rewrite made two models worse
&lt;/h2&gt;

&lt;p&gt;A Chrome DevTools MCP maintainer asked for a concrete case where an agent is confused by the short descriptions. So I tried to produce one. A stand-in server returned chrome-devtools-mcp 1.10.1's &lt;code&gt;tools/list&lt;/code&gt;, either as it is or with drag, hover and upload_file rewritten, and models got eight one-step tasks whose right tool is one of those three: a kanban card, a hover menu, a tooltip, an "Add photos" button, an avatar area that takes a dropped image, and so on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With the descriptions as they are, no model picked a wrong tool: Claude Code with Fable 5.1, Sonnet 5.5 and Haiku 4.5 (24 of 24), Codex CLI with gpt-6-astra (11 of 11), and one-turn API calls to Claude Opus 5.5, Claude Sonnet 5.5, gpt-6-astra and gpt-6.1-sol (32 of 32). The only wrong picks, &lt;code&gt;click&lt;/code&gt; instead of &lt;code&gt;hover&lt;/code&gt; from two smaller Codex models, came from a task whose text was ambiguous.&lt;/li&gt;
&lt;li&gt;My rewrites made two models worse. gpt-5.6-luna read "Use it for drag-and-drop UIs" against the avatar area that "accepts a dropped image", and called drag instead of upload_file in 2 of 4 runs (0 of 4 with the original). gpt-6-astra said it couldn't identify the tool for the "Add photos" task in 4 of 10 runs with my upload_file text (0 of 4 with the original).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for a one-step choice, the one-line description plus the schema (&lt;code&gt;from_uid&lt;/code&gt;/&lt;code&gt;to_uid&lt;/code&gt;, &lt;code&gt;filePaths&lt;/code&gt;) was enough, and the text that scores 74 cost accuracy on two models. The full table is in &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/3026#issuecomment-6072359950" rel="noopener noreferrer"&gt;the issue thread&lt;/a&gt;. What the descriptions still don't say, what a call returns and that uids come from the latest snapshot, may matter in longer runs, which this test didn't cover.&lt;/p&gt;

&lt;p&gt;A later, larger run with Forecall's own evaluation points the same way. Across all 30 Chrome DevTools MCP tools (8 requests per tool), Claude Sonnet 5.5 and GPT-6.1 Sol picked the right tool 99% and 98% of the time. And when a request didn't include a page number, they mostly called &lt;code&gt;list_pages&lt;/code&gt; first (77–87% of such requests), which is the right move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own server
&lt;/h2&gt;

&lt;p&gt;Get your server's &lt;code&gt;tools/list&lt;/code&gt; and score it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forecall dump &lt;span class="nt"&gt;-o&lt;/span&gt; tools.json &lt;span class="nt"&gt;--&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;command &lt;/span&gt;that starts your server&amp;gt;
npx forecall lint tools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both run on your machine, and the results aren't sent anywhere. Add &lt;code&gt;--fail-under 60&lt;/code&gt; to fail a CI job when the average drops below 60. To try it in the browser instead, paste the same JSON at &lt;a href="https://forecall.dev/en/lint" rel="noopener noreferrer"&gt;forecall.dev&lt;/a&gt;; no sign-up needed. For a public server that needs no sign-in, you can paste its URL there instead of JSON. Both servers' full results are at &lt;a href="https://forecall.dev/en/servers/chrome-devtools" rel="noopener noreferrer"&gt;forecall.dev/en/servers/chrome-devtools&lt;/a&gt; and &lt;a href="https://forecall.dev/en/servers/playwright" rel="noopener noreferrer"&gt;forecall.dev/en/servers/playwright&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One caveat. The score comes from reading the text alone, so a tool like browser_close, short but obvious from its name, scores low even when models use it correctly. Treat the score as a guide to where there's room to write more. Whether real models pick the right tools has to be measured another way, and as the test above shows, a higher score doesn't mean better picks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the score to find gaps, then test any rewrite with real models
&lt;/h2&gt;

&lt;p&gt;Google's and Microsoft's browser-automation servers describe their arguments carefully, but rarely say when to use a tool or what it returns. The linter finds those gaps quickly. It doesn't tell you that a model will choose wrong: for these three tools, the models I tested chose right with the one-line text, and my longer text made two of them worse.&lt;/p&gt;

&lt;p&gt;For your own server, start with the lowest-scoring tools and fill in what's missing, such as what a call returns. Then check the new text with the models your users run before you ship it. Words like "drop", or a list of example UIs, can pull a model toward the wrong tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I suggested rewrites upstream in &lt;a href="https://github.com/microsoft/playwright-mcp/issues/1788" rel="noopener noreferrer"&gt;microsoft/playwright-mcp#1788&lt;/a&gt; and &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/3026" rel="noopener noreferrer"&gt;ChromeDevTools/chrome-devtools-mcp#3026&lt;/a&gt;. A Playwright maintainer closed that one, asking for a reproducible trajectory against a frontier model rather than a score. In #3026, after the test above, I withdrew the claim that the short descriptions confuse models.&lt;/p&gt;

&lt;p&gt;I'm the author of Forecall. The linter is open source: &lt;a href="https://github.com/forecall/forecall-cli" rel="noopener noreferrer"&gt;github.com/forecall/forecall-cli&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>playwright</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>hidane 0.1.0: a Firestore emulator without Java, tested against the official one</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:05:08 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/hidane-010-a-firestore-emulator-without-java-tested-against-the-official-one-1j48</link>
      <guid>https://dev.to/nomunomu0504/hidane-010-a-firestore-emulator-without-java-tested-against-the-official-one-1j48</guid>
      <description>&lt;p&gt;&lt;strong&gt;hidane&lt;/strong&gt; (火種, &lt;em&gt;the seed of fire&lt;/em&gt;) 0.1.0 is out. It replaces the official Cloud Firestore emulator with a single Rust binary that speaks the same three wire protocols: gRPC, REST, and the WebChannel transport the browser SDK uses. It runs under firebase-tools, with no JDK installed. This post covers how to use it, why I built it, and how I check that it behaves like the official emulator.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/hidane-dev/hidane" rel="noopener noreferrer"&gt;https://github.com/hidane-dev/hidane&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://hidane.dev" rel="noopener noreferrer"&gt;https://hidane.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/hidane-dev/hidane/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;https://github.com/hidane-dev/hidane/releases/tag/v0.1.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;hidane is an independent open-source project, not affiliated with or endorsed by Google LLC. Firebase and Cloud Firestore are trademarks of Google LLC.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (2026-10-13):&lt;/strong&gt; hidane 0.2 enforces Security Rules. Since 0.2.0, &lt;code&gt;firestore.rules&lt;/code&gt; decides reads, writes, listeners and queries, with the official emulator's denial messages, &lt;code&gt;:ruleCoverage&lt;/code&gt; and &lt;code&gt;debug()&lt;/code&gt; output; 0.2.1 feeds the Emulator UI's Requests tab, and 0.2.2 checks a transaction's &lt;code&gt;verify&lt;/code&gt; writes against the rules. On those points, the "not yet" list below is out of date. 0.2.3 fixes &lt;code&gt;firebase emulators:start&lt;/code&gt; stopping when the Functions emulator loads a Firestore-triggered function, and verifies &lt;code&gt;@firebase/rules-unit-testing&lt;/code&gt; under Security Rules in Node and in the browser. 0.3.0 completes Web support: firebase-js-sdk's own browser test suite passes on hidane as on the official emulator (1,183 tests), and a parity table counts what the tests compare with the official emulator: &lt;a href="https://hidane.dev/#parity" rel="noopener noreferrer"&gt;https://hidane.dev/#parity&lt;/a&gt;. 0.4.0 delivers Firestore events to the Functions emulator: Firestore-triggered functions, v1 and v2, fire under &lt;code&gt;firebase emulators:start&lt;/code&gt; with the same data as with the official emulator, and the Go, Python and Java server SDKs are verified against both emulators. Latest release: &lt;a href="https://github.com/hidane-dev/hidane/releases" rel="noopener noreferrer"&gt;https://github.com/hidane-dev/hidane/releases&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Install it, then put &lt;code&gt;hidane exec --&lt;/code&gt; in front of the firebase-tools command you already use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7sx9vkf5w3pmpgkxkle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7sx9vkf5w3pmpgkxkle.png" alt="Installing hidane and running it under firebase-tools" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://hidane.dev/install.sh | sh
hidane &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; firebase emulators:start &lt;span class="nt"&gt;--only&lt;/span&gt; firestore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that one command, &lt;code&gt;hidane exec&lt;/code&gt; makes hidane stand in for &lt;code&gt;java&lt;/code&gt; and the official jar. Your firebase.json and your tests stay as they are, and &lt;code&gt;emulators:exec&lt;/code&gt; and the Emulator UI work too.&lt;/p&gt;

&lt;p&gt;Every channel installs the same release binaries, for macOS, Linux and Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;hidane-dev/tap/hidane
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; hidane
cargo binstall hidane            &lt;span class="c"&gt;# or: cargo install hidane&lt;/span&gt;
dart pub global activate hidane
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 ghcr.io/hidane-dev/hidane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also run it on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hidane &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FIRESTORE_EMULATOR_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In web code, use &lt;code&gt;connectFirestoreEmulator(db, '127.0.0.1', 8080)&lt;/code&gt; as usual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;The official emulator is a closed-source Java application shipped as a jar. Three things about it hurt in local development and CI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Java 21 is required.&lt;/strong&gt; firebase-tools 15.0.0 dropped support for older Java versions, so you need a JDK just to run a local database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup and memory.&lt;/strong&gt; It takes 0.74 s to open its port (2.5 s through &lt;code&gt;firebase emulators:start&lt;/code&gt;), idles at 95 MiB, and sits at 456 MiB after 1,000 documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes slow down while a listener is attached.&lt;/strong&gt; With &lt;code&gt;onSnapshot&lt;/code&gt; on a collection, each batch takes longer the more documents are already stored. Writing 100,000 documents in batches of 500 took 121 s, 11× the time without the listener (&lt;a href="https://github.com/firebase/firebase-tools/issues/3477" rel="noopener noreferrer"&gt;firebase-tools#3477&lt;/a&gt;). The Emulator UI keeps listeners open, which matches the "it's only slow when the UI is open" reports.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The same measurements on hidane
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0l7354rnwxwi8bu4zy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0l7354rnwxwi8bu4zy4.png" alt="The official emulator and hidane, measured" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Official emulator v1.22.0&lt;/th&gt;
&lt;th&gt;hidane&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time until the port accepts&lt;/td&gt;
&lt;td&gt;0.74 s (2.5 s via the CLI)&lt;/td&gt;
&lt;td&gt;5.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory, idle / after 1,000 documents&lt;/td&gt;
&lt;td&gt;95 MiB / 456 MiB&lt;/td&gt;
&lt;td&gt;7.4 MiB / 21 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100k documents with a listener&lt;/td&gt;
&lt;td&gt;121 s, growing per batch&lt;/td&gt;
&lt;td&gt;4.9 s, flat per batch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One caveat: these were not measured side by side. The official numbers come from early research on a loaded workstation; hidane's were taken later on a quiet M1 Max. A same-machine comparison is planned.&lt;/p&gt;

&lt;p&gt;The flat write cost is a design goal: a commit should cost what it changes, not what is stored. With a listener on the whole collection, hidane stays at about 10.5 ms per batch from the first batch to the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parity: the official emulator is the oracle
&lt;/h2&gt;

&lt;p&gt;An emulator is only useful if it behaves like the one your tests were written against. If error messages, the order of snapshot events, or which edge cases count as errors differ, tests break the day you switch.&lt;/p&gt;

&lt;p&gt;So hidane treats the &lt;strong&gt;official emulator as the oracle&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65mtjc9cm3574yjsegbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65mtjc9cm3574yjsegbi.png" alt="How the official emulator is used as the oracle" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scripts in &lt;code&gt;tools/oracle/&lt;/code&gt; send the official emulator many requests, edge cases included.&lt;/li&gt;
&lt;li&gt;Its answers are recorded as fixtures (17 so far).&lt;/li&gt;
&lt;li&gt;hidane's test suite replays every recording against hidane and compares.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For scale: 146 cases for how the &lt;code&gt;Authorization&lt;/code&gt; header is read, 87 for vector search (&lt;code&gt;find_nearest&lt;/code&gt;), and 57 for REST answers (compared byte for byte). On top of that, the Admin SDK, the Web SDK in Node, the Lite SDK, the Web SDK in a real browser, and &lt;code&gt;@firebase/rules-unit-testing&lt;/code&gt; run against both emulators, and their transcripts are compared (they are published in &lt;code&gt;results/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Recording the official emulator turned up plenty of details you would never guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A page of results that is exactly full still gets a next-page token; the last page is empty.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;explain_options&lt;/code&gt; is accepted and ignored, while &lt;code&gt;find_nearest&lt;/code&gt; is fully implemented.&lt;/li&gt;
&lt;li&gt;REST bodies are limited to 16 MiB, gRPC messages to 100 MiB.&lt;/li&gt;
&lt;li&gt;WebChannel frames carry their length in UTF-16 code units, not bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The WebChannel protocol isn't documented, so I put a recording proxy between Chromium and the official emulator and wrote the server from the capture (&lt;a href="https://github.com/hidane-dev/hidane/blob/main/docs/webchannel.md" rel="noopener noreferrer"&gt;docs/webchannel.md&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Exports in the official format
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthj8pbyfnqd3hjz8mbyr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthj8pbyfnqd3hjz8mbyr.png" alt="Exports read back in both directions" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;firebase emulators:start --import&lt;/code&gt; and &lt;code&gt;--export-on-exit&lt;/code&gt; work with the official emulator's export format: LevelDB logs of App Engine entities. Tests check hidane's files against the official emulator's, entity by entity, byte for byte. Through firebase-tools, an export written by either emulator reads back the same in the other, in all four combinations. Switching keeps your seed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs are not reproduced
&lt;/h2&gt;

&lt;p&gt;Parity has limits. Where the official emulator is clearly wrong, hidane does not copy it, and every such difference is listed in &lt;a href="https://github.com/hidane-dev/hidane/blob/main/docs/parity-exceptions.md" rel="noopener noreferrer"&gt;docs/parity-exceptions.md&lt;/a&gt;. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the listener slowdown above;&lt;/li&gt;
&lt;li&gt;after &lt;code&gt;POST /reset&lt;/code&gt;, attached listeners receive nothing, not even later writes;&lt;/li&gt;
&lt;li&gt;a client that sends an HTTP/2 ping every 10 s gets &lt;code&gt;GOAWAY too_many_pings&lt;/code&gt; after 30 s, which is likely what iOS users see as a disconnect after 90 s;&lt;/li&gt;
&lt;li&gt;WebChannel sessions are never released;&lt;/li&gt;
&lt;li&gt;vectors in nested fields cannot be searched with &lt;code&gt;find_nearest&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What 0.1.0 does, and what it doesn't yet
&lt;/h2&gt;

&lt;p&gt;Working, and checked against the official emulator:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;documents, queries (every operator, cursors, collection groups), aggregations (count / sum / avg), vector search;&lt;/li&gt;
&lt;li&gt;transactions, with the official emulator's locking;&lt;/li&gt;
&lt;li&gt;listeners with incremental updates and resume;&lt;/li&gt;
&lt;li&gt;gRPC, REST and WebChannel on one port;&lt;/li&gt;
&lt;li&gt;running under firebase-tools, the Emulator UI (browse, create, delete, clear all data), export and import;&lt;/li&gt;
&lt;li&gt;the Admin SDK, the Web SDK in Node, in the browser and Lite, and rules-unit-testing without rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security Rules&lt;/strong&gt;, coming in v0.2. Every request is currently allowed.&lt;/li&gt;
&lt;li&gt;The Emulator UI's request monitor, and events for the Functions emulator.&lt;/li&gt;
&lt;li&gt;Verification with the Go, Python, Java, iOS, Android and Flutter clients. They speak the same protocols, but nobody has run them against hidane yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For tests and development that don't depend on rules, hidane can replace the official emulator today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribution
&lt;/h2&gt;

&lt;p&gt;Each GitHub Release archive comes with a SHA-256 checksum and a build provenance attestation (&lt;code&gt;gh attestation verify&lt;/code&gt;). crates.io, npm and pub.dev are published from GitHub Actions through trusted publishing (OIDC), so no registry token lives in the repository. npm versions are staged and released only after a maintainer approves them with 2FA. License: MIT OR Apache-2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Next up is the Security Rules evaluator for v0.2. The plan is in the &lt;a href="https://github.com/hidane-dev/hidane/blob/main/docs/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try hidane with your own test suite and an answer differs from the official emulator's, please open an issue with the &lt;a href="https://github.com/hidane-dev/hidane/issues/new/choose" rel="noopener noreferrer"&gt;parity gap template&lt;/a&gt;. Reports from the Go, Python, Java or mobile SDKs, whether they worked or not, are very welcome. Questions go to &lt;a href="https://github.com/hidane-dev/hidane/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>firestore</category>
      <category>rust</category>
      <category>testing</category>
    </item>
    <item>
      <title>I open-sourced a linter for MCP tool descriptions</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Thu, 08 Oct 2026 03:54:27 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/i-open-sourced-a-linter-for-mcp-tool-descriptions-12nj</link>
      <guid>https://dev.to/nomunomu0504/i-open-sourced-a-linter-for-mcp-tool-descriptions-12nj</guid>
      <description>&lt;p&gt;AI agents choose an MCP tool by reading three things: its name, its description and its input schema. Many servers write those descriptions like docstrings, for a human who already knows why they opened the file. An agent with thirty tools in its context doesn't know that. It needs to know when to use a tool, what comes back, and which lookalike to use instead.&lt;/p&gt;

&lt;p&gt;Forecall's linter checks exactly that, and its code is now public under Apache-2.0: &lt;a href="https://github.com/forecall/forecall-cli" rel="noopener noreferrer"&gt;github.com/forecall/forecall-cli&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on a real server
&lt;/h2&gt;

&lt;p&gt;Two commands. The first starts a server and saves its tools/list; the second scores it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forecall dump &lt;span class="nt"&gt;-o&lt;/span&gt; memory.json &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @modelcontextprotocol/server-memory
npx forecall lint memory.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is part of the report for the official memory server (0.6.3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Average score     43.8 / 100
Tools             9
Confusable pairs  1

Across the server
  Major     confusable_pair  delete_entities and delete_relations are easy to mix up.
                             Description similarity 0.62 · Name similarity 0.33

  39 / 100  delete_relations
    Purpose 6/20 · When to use 0/20 · Arguments 18/25 · Return value 8/15 · Constraints and side effects 7/10 · Examples 0/10
    Major     restates_name     The description only restates the name (7 words).
    Major     too_short         The description is too short (7 words).
    Major     no_usage_context  Says nothing about when to use it, or when not to.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two delete tools with similar descriptions, and neither says when to use it. That's the kind of pair where an agent deletes the wrong thing. I've suggested rewrites for them in &lt;a href="https://github.com/modelcontextprotocol/servers/issues/5071" rel="noopener noreferrer"&gt;an issue on the servers repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a remote server, pass its URL instead, with &lt;code&gt;--header "Authorization: Bearer …"&lt;/code&gt; if it needs a key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forecall dump https://example.com/mcp &lt;span class="nt"&gt;-o&lt;/span&gt; tools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;p&gt;Each tool gets a score out of 100, in six parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;What it looks for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Purpose&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;What the tool does, beyond restating its name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When to use&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;When to use it, and which similar tool to use instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arguments&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Each argument described, typed and marked required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return value&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;What comes back, in an output schema or the description&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constraints and side effects&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Preconditions, side effects, auth, rate limits, annotations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;An example of a call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across the server, it flags too many tools, tools that are easy to mix up, and descriptions that share a long paragraph. Every issue has a code and a severity, so you can work through them one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it in CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--fail-under 60&lt;/code&gt; exits with 1 when the average is below 60, and &lt;code&gt;--json&lt;/code&gt; prints the report as JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx forecall dump -o tools.json -- node dist/server.js&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx forecall lint tools.json --fail-under &lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Nothing leaves your machine
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lint&lt;/code&gt; works offline. The scoring lives in &lt;code&gt;@forecall/lint&lt;/code&gt;, a package with no dependencies and no network code. &lt;code&gt;dump&lt;/code&gt; is the only command that talks to the network, and only to the server you name; the JSON it writes records where the tools came from, never headers or environment values.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't tell you
&lt;/h2&gt;

&lt;p&gt;The score comes from rules that read the text and the schema. A short description like &lt;code&gt;browser_close&lt;/code&gt; can score low even when every model uses it correctly, and a high score doesn't prove that a model will pick the right tool. Use the score to find where to write more. (Forecall's hosted Evaluations run real models against your tools for that. They're paid, and not in this repository.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found with it
&lt;/h2&gt;

&lt;p&gt;We ran it over the 46 most-used servers on Smithery: 769 of 1,118 tools (69%) never say when to use them. The full write-up is &lt;a href="https://dev.to/nomunomu0504/we-checked-46-popular-mcp-servers-on-smithery-7-in-10-tools-never-say-when-to-use-them-4gb9"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;The rules are at v1, and I'd like to hear where they're wrong. A reader of that write-up already suggested one: when two tools are flagged as lookalikes, check that each description names the other. It's now &lt;a href="https://github.com/forecall/forecall-cli/issues/7" rel="noopener noreferrer"&gt;issue #7&lt;/a&gt;. Issues and pull requests are welcome.&lt;/p&gt;

&lt;p&gt;I'm the author of Forecall.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>We checked 46 popular MCP servers on Smithery: 7 in 10 tools never say when to use them</title>
      <dc:creator>Hiroki Nomura</dc:creator>
      <pubDate>Thu, 08 Oct 2026 02:55:15 +0000</pubDate>
      <link>https://dev.to/nomunomu0504/we-checked-46-popular-mcp-servers-on-smithery-7-in-10-tools-never-say-when-to-use-them-4gb9</link>
      <guid>https://dev.to/nomunomu0504/we-checked-46-popular-mcp-servers-on-smithery-7-in-10-tools-never-say-when-to-use-them-4gb9</guid>
      <description>&lt;p&gt;We took 46 of the most-used MCP servers on Smithery and checked the descriptions of all 1,118 of their tools. &lt;strong&gt;769 tools (69%) never say when to use them.&lt;/strong&gt; 507 (45%) never say in their description what they return. And 15 servers have tools whose descriptions are so alike that an agent can mix them up: 90 such pairs in all.&lt;/p&gt;

&lt;p&gt;The 69% is exactly what we found last time, when we checked 8 public servers with 126 tools and 87 of them lacked it. Nine times the sample, the same share.&lt;/p&gt;

&lt;p&gt;The checks were done with Forecall, a static linter for MCP tool descriptions that I build.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we checked: the top 50 servers on Smithery, judged by their text only
&lt;/h2&gt;

&lt;p&gt;On October 8, 2026, we took the first 500 servers in Smithery's public registry (in the order its API returns), removed duplicates, picked the 50 with the most uses, and collected their tool definitions. We scored them with Forecall's CLI (&lt;code&gt;forecall&lt;/code&gt; 0.3.2). Four of the 50 have more than 200 tools, the CLI's limit, so we left them out: 46 servers and 1,118 tools remain.&lt;/p&gt;

&lt;p&gt;One caveat. The tool definitions Smithery's API returns leave out the input schemas' &lt;code&gt;required&lt;/code&gt; lists and any output schemas, so Forecall's overall scores come out lower than they should. &lt;strong&gt;This article does not use the overall scores. It counts only the findings that depend on the description text alone&lt;/strong&gt;: whether a tool says when to use it, whether its description says what it returns, whether the description is too short, and pairs of tools whose descriptions are alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  7 in 10 tools never say when to use them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No "when to use"&lt;/td&gt;
&lt;td&gt;769&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Description doesn't say what it returns&lt;/td&gt;
&lt;td&gt;507&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Too short, or restates the name&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By server it is starker. In 16 of the 46 servers, &lt;strong&gt;not one&lt;/strong&gt; tool says when to use it. Count the servers where 80% or more lack it, and you have half of them: 23.&lt;/p&gt;

&lt;p&gt;"When to use" is what decides between lookalike tools. Without it, a model picks by the tools' names and the feel of their descriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same notice on every tool makes the tools indistinguishable
&lt;/h2&gt;

&lt;p&gt;PubMed has the most lookalike pairs. It has only 7 tools, yet &lt;strong&gt;all 21 pairs&lt;/strong&gt; are flagged as easy to mix up. Among the 12 most similar pairs, the descriptions share 73% to 94% of their words.&lt;/p&gt;

&lt;p&gt;The cause is a long notice pasted into all 7 descriptions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT - PubMed Database Scope: This server provides access to PubMed, which ONLY indexes biomedical and life sciences literature including: …&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What sets each tool apart, its first sentence ("search articles", "get metadata", "find related articles" and so on), is buried in the shared paragraph.&lt;/p&gt;

&lt;p&gt;A note that applies to the whole server belongs once in the server's instructions, which the client receives at initialization, not in every tool's description. A tool's description should say what only that tool does.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Send a reply" and "draft a reply" are written almost the same way
&lt;/h2&gt;

&lt;p&gt;Gmail has 7 lookalike pairs. The closest two share 92% of their words:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Start of its description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gmail_ReplyToEmail&lt;/td&gt;
&lt;td&gt;Send a reply to an email message, optionally with one or more file attachments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail_WriteDraftReplyEmail&lt;/td&gt;
&lt;td&gt;Compose a draft reply to an email message, optionally with one or more file attachments.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After that, both go on with the same paragraph about attaching files. The difference is a few words: "Send" against "Compose a draft".&lt;/p&gt;

&lt;p&gt;Mix these two up, and an agent meant to write a draft sends the email. Tools that do something you can't undo, above all, should say in one sentence how to choose: "To prepare a reply without sending it, use Gmail_WriteDraftReplyEmail."&lt;/p&gt;

&lt;h2&gt;
  
  
  A good example: Brave Search says how its tools connect
&lt;/h2&gt;

&lt;p&gt;Brave Search's descriptions, by contrast, spell out how the tools relate. Here is brave_web_search:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performs web searches using the Brave Search API and returns comprehensive search results with rich metadata. To chain into local-POI enrichment, pass &lt;code&gt;result_filter=locations&lt;/code&gt; and feed the resulting &lt;code&gt;locations.results[].id&lt;/code&gt; values into &lt;code&gt;brave_local_search&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It says what comes back and which value to pass to which tool next, so an agent can use several tools in sequence. All 8 of Brave Search's tools say in their descriptions what they return, and it has no lookalike pairs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it: one sentence each for three things
&lt;/h2&gt;

&lt;p&gt;From these results, this is the order to fix descriptions in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Say when to use the tool.&lt;/strong&gt; "Use this when …", and if there is a lookalike, "for …, use X instead."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move shared notes to the server's instructions.&lt;/strong&gt; The same paragraph on every tool buries what tells them apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the difference between lookalikes into words.&lt;/strong&gt; Send or draft? Delete or remove? Say it plainly, especially for actions you can't undo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To check your own server, run these two commands. The first gets the tools/list from your server, the second scores it. Both run on your machine and send the results nowhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forecall dump &lt;span class="nt"&gt;-o&lt;/span&gt; tools.json &lt;span class="nt"&gt;--&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;command &lt;/span&gt;that starts your server&amp;gt;
npx forecall lint tools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Streamable HTTP server, pass its URL instead: &lt;code&gt;npx forecall dump https://example.com/mcp &amp;gt; tools.json&lt;/code&gt;. The CLI is open source: &lt;a href="https://github.com/forecall/forecall-cli" rel="noopener noreferrer"&gt;https://github.com/forecall/forecall-cli&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The weakness doesn't shrink with scale
&lt;/h2&gt;

&lt;p&gt;With 8 servers or 46, 7 in 10 tools never said when to use them, popular servers included. The fix is small: one sentence on when to use the tool and one on how it differs from its lookalikes leave an agent far less room to pick the wrong one.&lt;/p&gt;

&lt;p&gt;We'll keep this study going with new data. Next, we'll score the remote servers in the official MCP Registry, schemas included.&lt;/p&gt;

&lt;p&gt;The same study is also on Qiita, in Japanese: &lt;a href="https://qiita.com/nomunomu0504/items/b2eab1960a7aad38b59d" rel="noopener noreferrer"&gt;https://qiita.com/nomunomu0504/items/b2eab1960a7aad38b59d&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>llm</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
