One Tool — the Description Matters More Than the Code

5 min
GoalSecond in the mcp_server course. One tool gets registered. It is four pieces — name, description, input schema, handler — and three of them are read not by us but by whoever calls. So writing the description well is worth more than writing the handler well.

The server from part 1 could do nothing. Attach one tool.

Four pieces

server.addTool(
  name: 'desk.count',
  description: 'How many people are waiting at the desk right now',
  inputSchema: const {'type': 'object', 'properties': {}},
  handler: (args) async =>
      CallToolResult(content: [TextContent(text: '{"waiting":3}')]),
);

And declare the capability on.

capabilities: ServerCapabilities(tools: ToolsCapability(listChanged: true))

Three of them we never read

Of the four, only handler is read by our code. The other three are all read by whoever calls.

Ask tools/list and this goes out.

{
 "tools": [
  {
   "name": "desk.count",
   "description": "How many people are waiting at the desk right now",
   "inputSchema": {
    "type": "object",
    "properties": {}
   }
  }
 ]
}

A human-written client draws a button from this list. A model decides which tool to call from it. The basis of that decision is one line of description.

So that line is not a comment. It is running code.

Naming

Use dotted names like desk.count. What it is about, then what it does.

desk.count      counts who is waiting
desk.admit      admits people from the queue
camera.settings reads the camera's settings

With three tools it looks like it does not matter. At twenty, the absence of a rule shows. And a name cannot be changed once it ships — it is embedded in the caller's code.

What happens without a good description

Say two tools read like this.

desk.count   "count"
desk.admit   "admit"

Nothing says what is counted or who is admitted. A model picks either. And picking wrong raises no error — the tool runs fine, only the answer is beside the point.

Written this way, it can choose.

desk.count   "How many people are waiting at the desk right now"
desk.admit   "Admit a number of people from the queue"

The plant piece put "these steps are set by the plant engineer and must not be paraphrased" into a checklist tool's description for the same reason. That sentence is the only instruction that reaches the model.

Verification

Step 2's check is one line.

ask step2 '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' > captures/s2.txt
grep -q 'desk.count' captures/s2.txt || die "step2: tool not listed"

Registering and appearing in the list are two different events. Call addTool without turning the capability on and the list goes out empty.

Run it yourself

dart run bin/step2.dart
bash verify.sh

What to take

  1. The four pieces of addTool — three of them are read from outside
  2. Dotted names — decide at three tools or you cannot fix it at twenty
  3. Description as interface — it is what a model chooses from

Next

Take arguments. And refuse them when they are wrong.

Run the sample

cd course-server
dart pub get
dart run bin/step2.dart