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:

{
  "error": {
    "code": -32602,
    "message": "Unknown parameter: `status`"
  }
}

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

FeatureWhat it does
Tool schemasHow docstrings and declared I/O become the advertised schemas
Argument validationThe checks that refuse guessed arguments
Secured APIs as toolsA full gateway configuration for the services here