跳到正文
DEV Community· Oaida Adrian·· 3 小時前AI 評分51

Leadgen 2.0 調整 MCP 伺服器用量計算,改按回應而非請求計量

Metering an MCP server by answers, not requests: what Leadgen 2.0 changed

AI 導讀

Leadgen 2.0 本週上線,改為只有返回資料或真正的未找到結果才計入 MCP 伺服器用量;initialize、tools/list、ping、通知、輸入錯誤及拒絕均不扣額度。

正文

Every MCP server that gets listed in a directory learns the same lesson within a week: the most frequent caller is not a customer. It is a crawler doing initialize followed by tools/list, deciding whether you deserve to be indexed, and moving on.

Leadgen 2.0 went live this week. It is not a price change and not a promo — it is the list of things that got in our own way when we used the platform, fixed.

The most interesting one is the metering, because it is a category of bug every MCP server will hit.

Probing should not spend a lookup

Our allowances went up in this release: a free key now gives 50 lookups a day (it was 25), and without a key the MCP server answers 30 calls a day per IP (it was 5), with the REST door counted separately at 5. The two doors never spend each other's allowance, and every limit message quotes the door it came from.

The part that needed fixing was what counts as a call: the counter was incremented on every request to /mcp, including the ones that never reach a tool.

Two days of production logs, same window:

2,333 requests to /mcp          ~100 distinct client IPs
  612  Uptime-Kuma              /health
  390  mcpbeat/0.1              directory crawler
  339  SentinelOracle/0.1       rank probe
  246  curl/7.88.1
  112  mcptry.com indexer
   64  AgentRankProbe/0.1

and, in the usage table: 4 quota rows at 0 calls, 1 real keyed lookup

A crawler doing initialize + tools/list and nothing else was opening the counter. A day full of robots could leave a real visitor without their free lookups — on a shared egress IP, they would even collide with each other.

The rule we landed on: only answers that carry data (or a genuine not-found) count. The fix is a rule, not a heuristic: only answers that carry data (or a genuine not-found) count. Everything else — initialize, tools/list, ping, notifications, input errors, refusals — is handed back. One owner for the counter (record_call()), one function for the give-back (refund_call()), and the enforcement point decides by the shape of the answer.

We deliberately did not key it on the User-Agent. A rule like "don't charge mcpbeat" is a rule you rewrite every month, and it teaches nothing: the next crawler is one header away from looking like a browser. Meter the protocol, not the caller.

Passwordless sign-in that survives mail scanners

Email plus a 6-digit code, in the app and on the site. No password to reset, no "confirm your email" step that fights with link prefetching.

The trick is that the mailed link only opens a page — it never confirms anything by being fetched. The address is confirmed by that page's POST, so a scanner that walks every link in an inbox confirms nothing. Same pattern we use for watchlist alerts: pending → confirmed on a form submit, never on a GET.

An expired or already-used sign-in link now says so in plain words and offers a fresh one to the same address. The API key waits behind the same door: masked, with a copy button, and replaceable without losing plan, usage or watchlists. That last part is a database decision, not a UI one — the key is a row's identity, not the account.

One search box

Company name, registration number (CUI), or website, in the same input. The front end classifies what you typed and routes it; the API keeps its explicit parameters. Users do not read parameter names, and a search box that requires you to know which field you are filling in is a form wearing a search box costume.

Phone numbers, written the way you dial them

Formatted to dial (0744 700 293), with an international column. Each number carries its provenance — declared to ANAF, found on the company's own site, or listed elsewhere — and a number found on a website is rated high only when it matches the one the company declared. In a registry with millions of rows, "we have a number" and "this number is still that company's" are different claims, and the UI should not blur them.

Big lists: compute in the database, not in the page

Revenue and headcount bands are computed in the database, not in the page, so lists of thousands of companies stop being a progress bar. The bands no longer overlap either — a company lands in exactly one.

Developer door and user door

On the site, "Start free" goes to the app and "Get the free key" goes to the developer page — which is what /start now is. The MCP server carries 18 tools, and scripts can still self-serve a key with POST /register. Machines and people have different first clicks; pretending otherwise serves neither.

Try it in one call

Streamable HTTP is a stateful protocol: you initialize, keep the session id, then call. Two requests, no key:

# 1. open a session — the reply header carries Mcp-Session-Id
curl -s -D - https://leadgen-mcp.adrianhomelab.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

# 2. call a tool with that id (a real one: this CUI is my own company)
curl -s https://leadgen-mcp.adrianhomelab.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Mcp-Session-Id: <id from step 1>' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"lookup_business","arguments":{"query":"55685084"}}}'
{"query":"55685084","total":1,"results":[{"companyName":"OAIDA A. IOAN-ADRIAN
 PERSOANĂ FIZICĂ AUTORIZATĂ","cui":"55685084","registrationCode":"F2026045821000",
 "legalForm":"PFA","county":"Bucureşti",...}]}

Eighteen tools sit behind that door — lookup_business, lookup_companies,
find_company_by_domain, lookup_financials, extract_contacts, watch_company,
poll_watchlist, get_usage and the rest. Docs: https://leadgen.adrianhomelab.com/en/docs.

What 2.0 does not fix yet

Being honest about the tail, because these are the parts still on the board:

  • Authorization: Bearer and X-API-Key both work, but the docs do not say so — a cold caller guesses.
  • A request for /docs with no Accept-Language still defaults to Romanian. English lives at /en/docs.
  • The anonymous wall is per source IP, so callers behind one egress share a bucket. That is a design limit, not a bug we can paper over.

If you build an MCP server, the metering item is the one I would copy first. It is a small amount of code and it decides whether your free tier is a loss leader or a denial of service against your own users.


Leadgen serves Romanian company data over the ONRC registry and ANAF filings — 4.2M companies, lookups by CUI, name or website, from Python, curl or an agent. Prices are charged in euro; on the Romanian pages the amount in lei is shown next to them at the National Bank of Romania's reference rate, with the rate's date. Server: https://leadgen-mcp.adrianhomelab.com/mcp.

More from me

來源:DEV Community · dev.to