# MCP tool discovery: 3 new apps, 0 changes to the client code

URL: https://systemdesignsimple.com/how-ai-learns-new-apps-tool-discovery/
Published: 2026-11-05
Updated: 2026-11-07
Category: AI Systems
Publisher: System Design Simple (https://systemdesignsimple.com)

## Short answer

MCP tool discovery lets a client request tool names, descriptions and input schemas at runtime. In our in-process simulation, unchanged selected client code used three apps. A keyword matcher chose correctly on 55/60 requests; vague to-do descriptions reduced correct choices from 17/20 to 8/20.

## Key facts

- **Lab setup:** 3 in-process apps (notes, to-do, unit converter) with 4 + 4 + 3 = 11 tools, one fixed client, a minimal JSON-RPC subset with MCP message shapes, seed 1313, one Mac (M2 Pro, Node 22)
- **Client code:** the first 16 hexadecimal characters of the selected-source SHA-256 were 863e0236cb2be1ab at baseline and after each of 4 checkpoints
- **Per-app servers:** 50, 53 and 40 non-blank lines, plus a 24-line shared helper standing in for an SDK
- **Handshake:** 3 client messages and 2 server responses per app: initialize, notifications/initialized, tools/list
- **Tool choice:** right tool 18/20, 17/20 and 20/20, 55 of 60 in total; right result 52 of 60
- **Vague descriptions:** with every to-do description replaced by 'Works with an item.', right picks fell from 17/20 (85%) to 8/20 (40%)
- **Model:** a deterministic keyword matcher standing in for a model, not an LLM; its accuracy says nothing about real model accuracy

MCP tool discovery lets an AI client start using a new app without a code change: the client asks the app's server for its tools, gets back each tool's name, description and input schema, and lets the model choose. In our in-process simulation one client used three apps with its selected code unchanged, and replacing the to-do descriptions reduced this keyword matcher's correct picks from 17/20 to 8/20.

## Prerequisites {#prerequisites}

A model cannot run code. It writes a structured tool call, and the app runs the function and returns the result, as in how an AI tells the time with no clock (AI 5). Before any of that can happen, the client has to learn which tools exist.

In a backend, a client looks up where a service lives in a registry instead of hard-coding addresses (service discovery, Part 20). Tool discovery is the same idea one level up: the client looks up what an app can do. Our lab wired every server in by hand.

## Definitions {#definitions}

> Definition: **Tool**: a function an app exposes to a model, described by a name, a natural-language description and a JSON Schema for its arguments.

> Definition: **Tool discovery**: the client asking a server at run time which tools it offers (MCP method `tools/list`), instead of having them compiled in.

> Definition: **MCP (Model Context Protocol)**: an open protocol built on JSON-RPC 2.0 that standardizes how AI clients connect to servers exposing tools, resources and prompts.

## How does MCP tool discovery work? {#how-it-works}

In the 2025-06-18 specification, initialization negotiates version and capabilities. After <code>notifications/initialized</code>, the client can request <code>tools/list</code>, following <code>nextCursor</code> until all pages are fetched. Our lab captured the exchange for each app:

```text
# one handshake, as captured for the notes app (5 wire messages, 1,735 bytes)
client -> server  initialize                 {"protocolVersion":"2025-06-18","capabilities":{}, ...}
server -> client  result                     {"capabilities":{"tools":{"listChanged":false}}, ...}
client -> server  notifications/initialized  (a notification, no reply)
client -> server  tools/list
server -> client  result                     {"tools":[{"name":"create_note","description":"...","inputSchema":{...}}, ...]}
```

The <code>tools</code> capability in the initialize result tells the client that <code>tools/list</code> is supported. Our servers return every tool in one page. Each message is serialized to a JSON string and parsed back, so client and server share nothing by reference. After the exchange the client holds 4 notes tools, 4 to-do tools and 3 converter tools, 11 in all.

[Image: Three app menus with 4, 4 and 3 tool pills and a tally reading 11 tools | Each server hands over a menu. The client asks the same question of every app.]

Nothing in the client names an app. The lab hashes the client's selected source (the client class, the matcher and its helpers) before any app connects and after each of four checkpoints. The first 16 hexadecimal characters of all five SHA-256 hashes are <code>863e0236cb2be1ab</code>, and a scan finds no app words in that code. The hash covers that selected source only, not the transport or the harness. Each new app needs a compatible server: ours were 50, 53 and 40 non-blank lines, plus a 24-line shared helper that stands in for an SDK.

## How does the model pick a tool from the menu? {#choice}

A real client would put the tool list in the model's context. To keep the run reproducible, our lab uses a deterministic keyword matcher in place of the model. It works from the request, the discovered metadata and a fixed unit-alias table. Before scoring, it removes quoted text, drops stop words and stems the remaining words into a set. It then gives 2 points per request word found in the tool name, 1 per word found in the description and 1 per match with a schema enum value. Enum matches use the unit-alias table. A seeded random number below 0.9 breaks ties.

For the request <code>Add "buy milk" to my list</code> the scores are:

| Tool | Score |
|---|---|
| add_task | 4 |
| list_tasks | 3 |
| delete_task | 1 |
| complete_task | 0 |

<code>add_task</code> wins. The client then extracts <code>{"text": "buy milk"}</code>, validates it against the tool's input schema and sends <code>tools/call</code>; the to-do app replies <code>Added "buy milk". 5 open.</code> Servers must validate tool inputs even when clients validate them.

Over 20 fixed requests per app (9 of the 60 ambiguous on purpose), the matcher picked the right tool 18, 17 and 20 times, 55 of 60, though right results were lower. The converter got 20 of 20 tools but only 17 right results, because "Convert 20 degrees to fahrenheit" never says degrees of what, and the schema check stopped the call.

[Image: Large 55/60 on an orange background, labelled right tool, our sum | 55 of 60 right tools across the three apps. A keyword matcher, not an LLM.]

Then the lab reran the to-do app with every tool description replaced by "Works with an item." and the names, schemas and handlers left as they were. The keyword matcher's correct picks fell from 17/20 to 8/20 (85% to 40%). For the milk request the scores became a tie, <code>add_task</code> 2 and <code>list_tasks</code> 2, so the seeded tie-break decided.

[Image: A bar falling from 85 percent to 40 percent next to the text Works with an item | With only the descriptions made vaguer, right picks went from 85% to 40% in our simulation.]

## Worked example {#worked-example}

```text
apps            notes 4 tools + to-do 4 + converter 3        = 11 tools
servers         50 + 53 + 40 lines, + 24-line shared helper  (three app adapters plus one shared helper)
client hash     863e0236cb2be1ab x 5 checks                  = unchanged
right tool      18 + 17 + 20 of 20 each                      = 55/60 = 91.7%
right result    18 + 17 + 17                                 = 52/60 = 86.7%
vague to-do     17/20 -> 8/20                                = 85% -> 40%
```

## Code {#code}

```sh
# labs/ai-13 (Node 22, standard library only)
node run.mjs      # handshakes, scores, every miss with its reason, RESULT json
node scores.mjs   # the matcher's integer scores for request 1, clear and vague
```

In the miss list, the clear-description misses are ambiguous requests ("Forget about fix bike"), while the vague rerun also misses plain ones ("Mark pay rent as done").

## Failure modes and edge cases {#failure-modes}

- <code>complete_task</code> and <code>delete_task</code> both take something off the open list. Say in each description when to use which.
- The right tool with an unfillable schema still fails. Mark required fields and let the client ask back.
- A server advertising <code>tools.listChanged: true</code> should send <code>notifications/tools/list_changed</code> when its tools change; the client then refreshes its cached list.
- Every description costs context. With hundreds of tools, choice gets harder, so group or filter them.
- Tool text goes into the model's context, so a hostile server can steer it, as in prompt injection (AI 6). Review servers like dependencies and require confirmation for risky calls, as in agent permissions (AI 8).

## Trade-offs {#trade-offs}

| Option | Good for | Costs you | Use when |
| --- | --- | --- | --- |
| Tools hard-coded in the client | Full control, no handshake | A client release per new app | One or two fixed integrations |
| Discovery over a shared protocol | New apps without client changes | A server per app, description quality matters | Many apps, many clients |
| Discovery plus a registry | Finding servers you did not wire in | Trust and versioning of the registry | Open ecosystems |

Our take: discovery moves the integration work from the client to the server, and much of the quality risk into the descriptions. If you write a server, spend your time on each tool's description and input checks; in our lab, changing only the to-do descriptions cut correct picks by more than half.

## Common mistakes {#common-mistakes}

- Expecting a shared protocol to remove all code. Budget a small server per app; the client is what stays fixed.

- Writing one-line descriptions like "Works with an item." Say what the tool does and when to pick it over its neighbours.

- Measuring only the tool choice. Check arguments and results too; 55 right tools became 52 right results.

- Trusting every server's tool text. Treat descriptions as untrusted input to the model.

## Practice {#practice}

**Q (Beginner): What three things describe a tool?**
A name, a natural-language description and an input schema.

**Q (Intermediate): Why is notifications/initialized not answered?**
It is a JSON-RPC notification: it has no id, so no response is sent.

**Q (Intermediate): The client hash stayed the same. What did the work for each new app?**
A small per-app server (40 to 53 lines in the lab) plus a shared helper.

**Q (Intermediate): Why did the converter score 20/20 tools but 17/20 results?**
Three requests lacked an argument the schema requires, such as the source unit.

**Q (Advanced): Why did vague descriptions hurt even with the same names?**
Most requests do not repeat a name word, so without description words several tools tie and the tie-break decides.

**Q (Advanced): What does listChanged protect against?**
A stale cached list after the server adds or removes tools.

## Cheat sheet {#cheat-sheet}

| Term | Meaning |
| --- | --- |
| initialize | Version and capability negotiation, first request |
| tools/list | Returns names, descriptions and input schemas, paged with nextCursor |
| tools/call | Runs one tool with arguments |
| inputSchema | JSON Schema the arguments must pass |
| Right tool vs right result | Choice vs a correct call; our lab 55/60 vs 52/60 |

## Further reading {#further-reading}

- [MCP lifecycle specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle): the initialize exchange and capability negotiation.
- [MCP tools specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools): tools/list, tools/call, input schemas and list changes.
- [MCP announcement](https://www.anthropic.com/news/model-context-protocol): why the protocol was introduced.
- [JSON-RPC 2.0](https://www.jsonrpc.org/specification): requests, responses and notifications.
- [JSON Schema](https://json-schema.org/learn/getting-started-step-by-step): how input schemas constrain arguments.

Our lab is a small simulation: a hand-written subset of MCP message shapes connected in-process, not the official SDK and not a test of interoperability with real MCP clients or servers. The tool chooser is a keyword matcher, not a language model, and the 60 requests are our own. The numbers show the direction and rough size of the effect in this setup; they do not measure production accuracy.

**Read next:** the same idea from the other side, a short description that decides when an agent loads instructions, in how to create an AI agent skill.

## Frequently asked questions

**How does an AI learn to use a new app without new code?**
A compatible server returns tool metadata through tools/list. The client supplies it to the model and routes the selected call.

**Does MCP mean zero code?**
Each app needs an adapter. Our lab used 40 to 53 non-blank lines per app and one shared 24-line helper.

**Why do tool descriptions matter so much?**
Descriptions supply selection cues. Replacing them reduced our keyword matcher's correct to-do picks from 17/20 to 8/20; this does not measure LLM accuracy.

## Sources

- [Model Context Protocol specification 2025-06-18: Lifecycle (initialize, initialized)](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle)
- [Model Context Protocol specification 2025-06-18: Tools (tools/list, tools/call, inputSchema)](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
- [Introducing the Model Context Protocol (announcement, Nov 2024)](https://www.anthropic.com/news/model-context-protocol)
- [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification)
- [JSON Schema: getting started](https://json-schema.org/learn/getting-started-step-by-step)
