# 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](https://zato.io/docs/ai/mcp/index.html) - their schemas are what the agent receives, their docstrings are why it chooses correctly, and [argument validation](https://zato.io/docs/ai/mcp/argument-validation.html) refuses the calls it guesses wrong.

**Prerequisites.** An MCP gateway with `validate_input` on and both services below assigned - the [secured APIs example](https://zato.io/docs/ai/examples/secured-apis-as-tools.html) shows a full gateway configuration.

## Two services that could be confused {#two-services-that-could-be-confused}

Both are about invoices. They differ in the two parts that [tool schemas](https://zato.io/docs/ai/mcp/tool-schemas.html) are built from - the docstring and the declared input:

```python
# -*- 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 selects a tool based on this text.

## The advertised tools {#the-advertised-tools}

`tools/list` advertises the tools as the services declare them - abridged to the parts the model weighs:

```json
{
  "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 {#refused-calls}

With validation on, a guessed argument never reaches the service. An agent that invents a `status` filter for the search:

```json
{
  "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 with the same error code and the message `` Missing required parameter: `invoice_id` ``, and with the [audit log](https://zato.io/docs/ai/mcp/audit-log.html) on, every refusal is recorded, so an agent that keeps guessing is visible in the dashboard rather than silently retrying.

## The docstring pattern {#the-docstring-pattern}

If the agent still picks the wrong tool, revise the docstrings. 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 the similar tool.

Hot-deploying a reworded docstring [updates `tools/list` live](https://zato.io/docs/ai/mcp/tool-schemas.html#live-updates), making docstring iteration a seconds-long loop.

## See also {#see-also}

- [Tool schemas](https://zato.io/docs/ai/mcp/tool-schemas.html) - How docstrings and declared I/O become the advertised schemas
- [Argument validation](https://zato.io/docs/ai/mcp/argument-validation.html) - The checks that refuse guessed arguments
- [Secured APIs as tools](https://zato.io/docs/ai/examples/secured-apis-as-tools.html) - A full gateway configuration for the services here
