Skip to content
Chapter 7 of 8 · Tools over MCPDescriptions and tool selection

14 min read

Descriptions and tool selection

On this page

Selection is the hop nobody owns and everybody blames. The server owns the listing, the host owns the rendering, and the choice itself happens inside a model on somebody else's infrastructure — which makes it the natural home for theories, because none of the three parties can see the other two without deliberately capturing something.

The capture is cheap, and it splits the problem in two. Either your tool is present in the tools array of the request the host sent the provider, or it is not. If it is not, the diagnosis is plumbing and the wording is irrelevant. If it is, everything the protocol was responsible for happened correctly, and what remains is a comparison between your text and the text of whatever sits beside it.

Why the plumbing theory is the first one reached for is worth naming, because it is not laziness. Every other component in this path has a knob on it. A server can be redeployed, a transport can be swapped, a host has settings, and an index somewhere can nearly always be told to prefer something. A description has no knob. It is a paragraph, and "rewrite the paragraph" does not feel like an engineering action in the way that "check whether the host is truncating the list" does. So the search goes to the places that look adjustable, and it goes there before anybody has established that the tool reached the model at all. The capture reverses that order at the cost of one command.

The artefact that settles it

Here is what such a capture looks like when a runbook-search tool is losing to a general web search exposed by a different server on the same connection panel. Both entries as they arrived, next to each other, which is how the model encountered them:

json
{
  "tools": [
    {
      "name": "mcp__runbooks__search_runbooks",
      "description": "Searches internal runbooks.",
      "input_schema": {
        "type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"]
      }
    },
    {
      "name": "mcp__websearch__web_search",
      "description": "Search the web for current information on any topic. Returns ranked results with titles, snippets and URLs. Use for questions about current events, documentation, error messages, product behaviour, or anything you are not certain about from memory.",
      "input_schema": {
        "type": "object",
        "properties": {
          "query": {"type": "string", "description": "Natural-language search query."},
          "max_results": {"type": "integer", "description": "Default 5, maximum 20."}
        },
        "required": ["query"]
      }
    }
  ]
}

The protocol did everything it was asked to do. What remains is a comparison between three words and forty-six, on a question the shorter entry does not address: when should the model use this rather than the other one. The web search entry says it explicitly, including the clause "anything you are not certain about from memory", which fits every internal-procedure question a user is likely to type.

The comparison is not really about length, and it repays looking at what each entry answers. Both say what the tool searches. Only one says when to use it. Only one says what a result will look like. And only one addresses the case where the model already believes it knows the answer — which is the case every internal-procedure question falls into, since a model has read a great deal about Kubernetes in general and nothing whatever about this company's failover runbook. Three words can answer the first of those questions and no others; the longer entry answers four. So the model is not weighing two options of unequal quality. It is weighing one option it has been told when to use against one it has not.

Not every host will hand you that request. When there is no debug mode and no request log, the substitute is to reproduce the comparison yourself: connect a bare client to both servers, take the two listings, build the provider request by hand with the same tool array, and send a dozen real user questions through it. That is not identical to what the host sends, since it omits the host's system prompt and any renaming it does, and it answers the question that matters — whether these two descriptions, side by side, lead the model to yours.

Selection is a reading, not a ranking

There is no retrieval step here and no relevance score. The model is given both descriptions in the same request and picks based on the text, the same way it picks between two functions in a file it has been asked to edit. That means the mechanisms engineers reach for by analogy with search do not exist: there is nothing to boost, no index to tune, no signal from how often a tool succeeded last week. Improving selection means improving the text, and the text has to do its work in the presence of whatever else happens to be connected.

What follows from that is a list of instruments that do not exist here, and the list is worth having, because every item on it is something an engineer will go looking for. There is no relevance score, so nothing can be boosted. There is no index, so nothing can be tuned. There is no feedback from outcomes, so a tool that succeeded on every call last week carries no advantage into this one — nobody told the model how last week went, and the request in front of it contains descriptions and nothing else. There is not even a stable ordering to exploit, since the array is assembled by the host and the host may reorder it. Everything an engineer would reach for by analogy with search is absent, and what remains is the text and the company it is keeping.

Which is the second thing the side-by-side shows. Your tool is not evaluated against a standard; it is evaluated against its neighbours in that particular request, and its neighbours change per host and per user. A description that read fine when your server was the only one connected is competing, in that user's session, against a tool written by a vendor whose entire product is that tool and who spent a week on the wording.

What each clause is doing

The rewrite that fixed this is one paragraph, and every clause in it was added to close a specific gap the original left open.

python
description=(
    "Search this company's internal runbooks, incident postmortems and on-call "
    "procedures. The corpus covers our own services only: deploy and rollback "
    "steps, failover procedures, alert response, escalation paths, and known "
    "issues with named internal systems (payments-api, ledger, edge-proxy). "
    "Use this for any question about how we operate our systems, including "
    "'how do I', 'what do I do when', and questions naming an internal service "
    "or alert. Prefer this over a general web search whenever the question "
    "concerns this company's own infrastructure, because public sources do not "
    "contain our procedures and will give plausible but wrong answers. Do not "
    "use it for general technology questions with no internal component. "
    "Returns up to 8 passages with the runbook title, section heading and "
    "last-reviewed date; an empty result means no runbook covers the topic."
)
ClauseGap it closes
"internal runbooks, incident postmortems and on-call procedures"The original left the searchable material undefined; the model could not tell whether a postmortem question qualified
"our own services only" plus named systemsGives the model concrete strings to match a user's question against
"how do I", "what do I do when"States the trigger in the phrasing users actually type, which is what the model is matching
"Prefer this over a general web search whenever…"Addresses the neighbour directly, which is the comparison being lost
"because public sources … plausible but wrong"Supplies the reason, which survives paraphrase better than a bare instruction
"Do not use it for general technology questions"Bounds the tool, so the added assertiveness does not turn it into the default for everything
"Returns up to 8 passages … last-reviewed date"Lets the model predict the result's shape, which affects whether calling seems worth a turn
"an empty result means no runbook covers the topic"Pre-empts the loop where an empty result reads as a failure and gets retried

The two clauses that carried most of the improvement, tested by removing them one at a time in an offline evaluation — the same recorded questions replayed against the same model with nothing changed except the description under test — were the explicit comparison against web search and the list of named internal systems. The return-shape sentence made almost no difference to selection and noticeably reduced retries on empty results, which is a different win and worth keeping for its own reasons.

Removing one clause at a time is the right instrument here for a reason worth naming, because a description rewrite is otherwise a change with a dozen variables in it. Each run holds the model, the questions, the neighbouring tools and the rest of the description fixed, and moves exactly one clause, so a difference in the outcome has exactly one candidate cause. What the method cannot see is two clauses doing one job between them: drop either half of a redundant pair and the number barely moves, which reads as both being useless when the truth is that either one alone would have done. The signature of that is a set of individually unimportant clauses whose removal together is expensive, and it is worth checking for once rather than assuming the single-clause results add up.

Naming, and the limit of a comparison clause

Naming matters less than the description but is not free. search_runbooks is a good name because it says what it searches; query_metrics is worse than search_dashboards_and_timeseries for the same reason, and a name like execute or run on a server whose name is stripped or aliased by the host is close to unusable. The name is also the string the model repeats when it explains itself to the user, so a name that reads as a sentence fragment shows up in transcripts.

The reason a name matters at all, given that the description carries the argument, is that it is read first and read every time. It sits at the top of the entry, it is short enough that nothing about it can be skimmed past, and it is the one part of the entry the host may rewrite — prefixed with a server alias, or truncated — so it has to survive being seen out of context. search_runbooks states its subject and its action in two words and loses nothing when a host prefixes it. execute states neither, and once the server name is stripped or aliased there is nothing left in the string at all. The test is whether the name still communicates when it is the only part of the entry a reader has looked at, because for the tools that get skipped, it is.

There is a limit to how far the comparison clause can be pushed, and it is worth seeing before the tactic gets over-applied. If every server author writes "prefer this over other tools", the request fills with mutually contradictory imperatives and the model resolves them on the same basis it resolves everything else, which is the surrounding text. The clause above works because it is specific and gives a reason a reader can check against the question in front of them, so it applies to internal-infrastructure questions and stays quiet elsewhere. A bare "always use this tool first" competes with every other tool's claim to primacy and tends to produce the opposite of what its author wanted, in the form of calls on questions the tool cannot answer.

The mechanism behind that is worth following, because it explains why the two clauses are not the same tactic at different volumes. An unconditional claim is true of every question, so it gives the model no way to decide which question it applies to; put two such claims in one request and they conflict on every input, and the model resolves the conflict from the rest of the text, which is where it would have been looking anyway. A conditional claim is true of some questions and false of others, so it partitions the input rather than competing over all of it — and two conditional claims with different conditions do not conflict at all. That is why "prefer this whenever the question concerns this company's own infrastructure" survives being sat next to a rival's comparison clause, and "always use this tool first" does not. The clause has to name a condition the reader can evaluate against the question in front of them, or it contributes nothing that distinguishes anything.

Selection also moves when the model does. The 247-out-of-291 figure below was measured on one model version, and a provider upgrade that changes how aggressively the model prefers acting over answering will move it in either direction without anybody touching the server. That makes the number worth re-measuring on a model change rather than treating as a property of the description, and it is one reason to keep the evaluation set around after a rewrite ships.

When the wording is not the problem

Rewriting descriptions is cheap enough that it becomes the reflex, and there are several signatures that mean you should stop.

The tool is absent from the provider request. Then the diagnosis genuinely is plumbing: the host may cap the number of tools it forwards, may load servers lazily, may have failed to connect and shown the failure only in a log file. This is the case the theory above imagined, and it is worth ruling out first precisely because everything after it assumes the tool was on the table.

The tool is chosen but called with wrong arguments. That is a schema problem, and no amount of description will fix a query property with no description and no examples. Property-level descriptions and an enum where the domain is closed do more here than another paragraph at the top. Or the tool is chosen, the arguments are right, and the results are unhelpful — then selection is working and the retrieval behind it is not, which is a search-quality project with nothing MCP-shaped about it.

Two full rewrites produce no measurable movement. At that point the constraint is probably not the text of this tool but the environment it is being read in — most often the number of near-neighbours it sits among, which is the next chapter, and occasionally a host system prompt that instructs the model to prefer its own built-in capabilities. Both are visible in the same captured request, which is why capturing it is the first move rather than the last.

The last signature is the awkward one: the tool is being selected correctly and the team's impression is that it is not. Three selections in 400 sessions is a number; "it never uses our tool" is a feeling, and the two disagree often enough that the count is worth having before the rewrite rather than after.

Those four signatures form a decision procedure rather than a list, and running them in order is what stops the rewrite reflex. Is the tool in the captured request at all? If not, stop: nothing about the wording is relevant yet. If it is, is it being chosen? If it is being chosen and the arguments are wrong, that is the schema, and another paragraph at the top of the description will not reach it. If it is being chosen with the right arguments and the results are poor, that is retrieval quality and has nothing to do with any of this. Only what survives all three of those is a wording problem, and each step is answered by an artefact rather than by an opinion — which is the same discipline the first chapter of this topic applied to hops, arriving here at the one hop that has no log of its own.

Length, annotations and scale

None of this is a case for writing longer descriptions in general. The winning entry above is 96 words against the neighbour's 46, and length was incidental — each clause was answering a question the model has to resolve at selection time. Padding that does not answer one of those questions costs tokens on every turn, as the second chapter's arithmetic showed, and dilutes the clauses that do work.

It is also not something annotations can help with. readOnlyHint: true on search_runbooks is correct and irrelevant here, since most hosts consume annotations in their own permission layer rather than forwarding them to the model. A safety property expressed as a flag does not participate in selection at all.

A tool that lists and is never chosen is therefore a wording problem until the captured request says otherwise, and the wording that wins is the wording that answers the question the model is actually resolving: what is this for, when does it apply, when does it not, and what will come back. Three of those four are missing from most descriptions, and the fourth is missing from almost all of them.

The tactic does not scale on its own, and the reason is visible in the rewrite. It worked because search_runbooks had one serious competitor and could address it by name. A server whose tools compete mainly with each other cannot do that, because there is no single neighbour to distinguish yourself from and the burden lands on every description simultaneously.

Tools over MCP — read the current version at https://daviddominguez.dev/learn/en/tools-over-mcp/descriptions-and-tool-selection

Back to top