Skip to content
nomunomu0504.dev

BLOG #MCP

Forecall: lessons from a linter for MCP tool descriptions

On this page

Where it started

Building and using a few MCP servers, I kept watching agents pick the wrong tool or get the argument shape wrong. The model sees only what tools/list returns: a name, a description and an input schema. How a tool is described is how it gets called. And yet there was no linter for descriptions.

The forecall.dev landing page
forecall.dev: list_directory_with_sizes scores 48, list_directory 38

Scoring public servers

I started by scoring eight well-known MCP servers, 126 tools in all, with one ruler. Average scores per server ranged from 33 to 70. Notion’s official server alone had 57 pairs of tools whose descriptions were nearly the same.

Scores of public MCP servers
Three of the eight servers scored. 70 and above is clear, 40 to 69 cloudy, below 40 rain

The textbook case is the filesystem server’s list_directory and list_directory_with_sizes, with a description similarity of 0.86. Both say what they do; neither says when to use which. From the agent’s side it is a coin toss. I reported the two, with proposed rewrites, as modelcontextprotocol/servers#5071.

How the linter scores

Each tool gets up to 100 points across six parts.

Part Points What it looks for
Purpose 20 Not a restatement of the name, not too short
When to use 20 When to use it, when not to, what to use instead
Arguments 25 A description per argument, constraints or examples for strings, required
Return value 15 What comes back
Constraints and side effects 10 Auth, rate limits, side effects, annotations
Examples 10 A concrete call

Server-wide issues are reported apart from per-tool ones: too many tools, near-identical descriptions, shared descriptions. Without that split, “a similar description exists” lands on every tool and becomes noise.

The scoring rules are versioned (v2 today). Any change that can give some input a different score or finding raises the version and updates the fixtures. People gate CI on these numbers, and a score that moves silently is the worst outcome.

npx forecall dump -o tools.json -- node build/index.js   # take tools/list from your own server
npx forecall lint tools.json                              # score it; nothing is sent anywhere
Forecall lint pipelineyour MCP servertools/liststdio or remoteforecall dumptools.jsonnames, descriptions,inputSchemaforecall lintscore per tool0–100 + findingsrules v2 · offlineWHAT THE RULES LOOK AT · 100 POINTS20purpose20when to use25arguments15returns10constraints10examplesserver-level findings: too many tools · near-identical · shared descriptions
dump fetches tools/list and lint scores it. Scoring runs locally and sends nothing

@forecall/lint has no dependencies and never touches the network. The GitHub Action forecall/forecall-cli@v1 scores every pull request into the job summary and fails the step on min-tool-score or fail-on.

Beyond the linter

A static score and whether a real model actually picks the tool are two different things. So two more pieces exist.

Evaluations. For each tool, requests such as “find the largest file” are written, then Claude, GPT and Gemini choose and call for real, and Forecall measures how often the right tool is called with the right arguments. Public servers’ tool lists sit alongside as distractors.

Failure KB. An MCP server where agents look up known failures of MCP tool calls and their workarounds before and after a call. A failure one agent reports becomes verified only after agents of two other teams reproduce it. The KB is a remote Streamable HTTP server; for clients that can only start servers over stdio there is a small relay, forecall-mcp.

Whether the static score and the measurements actually correlate is something a calibration tool keeps checking, using the same case generation and calls as the evaluations. A high score does not guarantee being chosen.

How it is built

Everything runs on Cloudflare Workers. The API is Hono, the public site and the dashboard are React Router v8, evaluations run as Workflows with one instance per suite, and rate limits live in a Durable Object. The database is Neon Postgres (pgvector) through Hyperdrive, and the vendors’ models are called through AI Gateway. Only the linter and CLI are split out into the public repository forecall/forecall-cli; the service pins the npm releases.

What I learned

  • Near-identical descriptions are a server problem, not a tool problem. Report at the wrong level and nobody fixes it
  • The rules version is part of the output. Say so when it changes
  • The first scoring rules were a Python research script. The scores of the eight servers were frozen as fixtures before the port to TypeScript, which became the source of truth from v2. The fixtures are what prove the port right

Paste a tools/list JSON at forecall.dev to get a shareable result page, no sign-up needed.

  1. spendctl: a corporate card and expense policy for AI agents that pay 4 min
  2. hidane: a Firestore emulator without Java, written in Rust 5 min
Hiroki Nomura @nomunomu0504 · CTO, MIRAI STUDIO

Flutter, AWS, homelab. A few side projects and open-source tools. For architecture questions or collaboration, reach out via X DM or the About page. About

COMMENTS · GISCUS