Tool selection
Steer agents to the right tool with docstrings and declared input alone.
An agent picking the wrong tool, or the right tool with invented arguments, is a schema problem, not a model problem - the docstring and the declared input are everything the model has to go on. The two deliberately similar services below share one MCP gateway - their schemas are what the agent sees, their docstrings are why it chooses correctly, and argument validation refuses the calls it guesses wrong.
Prerequisites. An MCP gateway with validate_input on and both services below assigned - the secured APIs example shows a full gateway configuration.
Two services that could be confused
Both are about invoices. What separates them is exactly what tool schemas are made from - the docstring and the declared input:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class GetInvoice(Service):
""" Returns one specific invoice when its exact ID is already known.
Do not use this to search - it requires the precise invoice ID.
"""
name = 'example.billing.get-invoice'
input = 'invoice_id'
output = 'invoice_id', 'amount', 'status'
def handle(self):
invoice_id = self.request.input.invoice_id
self.response.payload.invoice_id = invoice_id
self.response.payload.amount = 1490.50
self.response.payload.status = 'paid'
class FindInvoices(Service):
""" Searches for invoices of one customer, optionally within a date range.
Use this when the invoice ID is not known yet.
"""
name = 'example.billing.find-invoices'
input = 'customer_id', '-date_from', '-date_to'
output = 'invoice_id_list'
def handle(self):
customer_id = self.request.input.customer_id
self.response.payload.invoice_id_list = ['INV-2001', 'INV-2002']
Each docstring says what the tool does, when to use it and when not to - written for the model, which is the reader that decides.
The advertised tools
tools/list advertises exactly what the services declare - abridged to the parts the model weighs:
{
"tools": [
{
"name": "example.billing.get-invoice",
"description": "Returns one specific invoice when its exact ID is already known.\nDo not use this to search - it requires the precise invoice ID.",
"inputSchema": {
"type": "object",
"properties": {"invoice_id": {"type": "string"}},
"required": ["invoice_id"]
}
},
{
"name": "example.billing.find-invoices",
"description": "Searches for invoices of one customer, optionally within a date range.\nUse this when the invoice ID is not known yet.",
"inputSchema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"date_from": {"type": "string"},
"date_to": {"type": "string"}
},
"required": ["customer_id"]
}
}
]
}
Asked "what did customer ACME pay in July?", the agent has no invoice ID - the first docstring rules itself out, the second invites the call, and required tells the agent that customer_id alone suffices. That is the whole selection mechanism, and it lives in the service.
Refused calls
With validation on, a guessed argument never reaches the service. An agent that invents a status filter for the search:
The error names the field, the agent drops it and retries - agents correct themselves when told precisely what was wrong. A call to get-invoice without the ID is refused the same way, with Missing required parameter: `invoice_id`, and with the audit log on, every refusal is recorded, so an agent that keeps guessing is visible in the dashboard rather than silently retrying.
The docstring pattern
If the agent still picks the wrong tool, the fix is in the text, not in the model. The pattern that works in the docstrings above:
- First sentence - what the tool returns.
- Second sentence - when to use it or when not to, naming the boundary with its sibling.
Hot-deploying a reworded docstring updates tools/list live, making docstring iteration a seconds-long loop.
See also
| Feature | What it does |
|---|---|
| Tool schemas | How docstrings and declared I/O become the advertised schemas |
| Argument validation | The checks that refuse guessed arguments |
| Secured APIs as tools | A full gateway configuration for the services here |