How the WingMan Tool Directory Stays Honest (No Inflated Counts)

· 3 min read · Platform · WingMan Protocol

Every agent-tools directory publishes a number. Fewer of them can tell you exactly how that number was produced, or defend it against the obvious question: is every one of those actually callable right now, or is some fraction of it aspirational — a roadmap item with a card already made for it? WingMan's answer isn't a policy or a promise; it's three separate pieces of code that make an inflated count structurally hard to ship by accident.

The catalog is synced from the live server, not hand-maintained

scripts/sync_mcp_tools.py builds fulfillment/mcp_tools_map.json by calling the actual running gateway: POST /mcp with a JSON-RPC tools/list request gets the full catalog with input schemas, straight from the server that would answer a real agent's call. That's the opposite of a spreadsheet someone updates when a feature ships — the map file is a cache of what tools/list already says, re-derived from the live endpoint rather than authored by hand. If a tool is removed from the server, the next sync removes it from the map too; there's no path for a stale entry to survive a re-sync.

That also means you don't have to trust the map file at all. The same request it's built from is one any agent can make directly:

curl -s https://wingmanprotocol.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Count the array that comes back. That's the honest number, and it's the same mechanism the directory pages use to render theirs — not a separately-maintained figure that could quietly drift from what the endpoint actually serves.

Templates can't reference a tool that doesn't exist

The agent-templates module (agent_gateway/templates.py) takes this further than a sync script: it asserts its own consistency at import time. _self_check() walks every one of the 15 templates and confirms every steps[].tool is both a real kebab-slug in mcp_tools_map.json and backed by a live calculation engine on disk (api_marketplace/calc_<name>.py). If either check fails, the module raises and the application doesn't start. That's a materially stronger guarantee than "we tested this once" — it means a template referencing a renamed or removed tool is a deploy-blocking error, not a silent broken link a user finds later.

Pricing and counts are computed, not typed in

The pattern repeats at the page-rendering layer. /pricing and /pay don't have a hardcoded price range in their source — they compute min() and max() over tool_registry.TOOLS.values() on every request, so if a tool's price changes, the pages reflect it on the next load with no one having to remember to update a number in three places. The /mcp/tools/ hub page is the same: its headline count is len(live) — the length of whatever the map actually contains after filtering to status == "live" — not a string someone typed into a template and forgot to revisit.

The dedupe and validation layer

Because the map is synced automatically rather than hand-edited, the failure mode to guard against isn't a fabricated count — it's an accidental duplicate, two slugs resolving to the same underlying tool after a rename. scripts/dedupe_tools_map.py and scripts/validate_tools_map.py exist to catch exactly that class of error before it reaches a published count, checking cross-references and flagging anything that doesn't resolve cleanly.

The point isn't the number — it's that you don't have to take it on faith

None of this is about landing on any particular figure. It's about the count being a computed property of what's actually running, checked by code that fails loudly if it's wrong, rather than a marketing figure maintained separately from the thing it describes. If you want to verify it yourself rather than take the directory's word for it, discover_tools and get_tool_schema are built for exactly that — ask for what's live, get back what's live, no step in between where the number could have been rounded up.

platformtransparencymcptooling

Tools used in this post