mcp tools/list: The Field-Level Reference [288 Tools] | MCP Hunter
MCP Hunter

mcp tools/list: The Field-Level Reference [288 Tools]

Every field a tools/list response returns, what the spec requires, and how often each one actually appears across 288 tools on 14 live MCP servers.

7 min read

tools/list is the JSON-RPC method an MCP client calls to discover what a server can do. It returns an array of tool objects, each carrying a name, an inputSchema, and a set of optional fields. Only two of those are load-bearing in practice, and the gap between what the spec permits and what servers actually send is wide.

This page documents both. Every field is checked against the 2025-11-25 specification [1], and every frequency figure comes from a single run against 14 live public MCP servers on 5 August 2026, which returned 288 tools between them.

TL;DR:

  • name and inputSchema are the only fields you can rely on. Both appeared on 288 of 288 tools in our sample.
  • title is nearly absent in the wild: 10% of tools carried one, despite being the field designed for display.
  • annotations is far more common than you would guess at 91%, and the spec says clients "MUST consider tool annotations to be untrusted unless they come from trusted servers" [1].
  • Nothing in our sample paginated. Zero of 14 servers returned a nextCursor, which is exactly why the pagination bug is easy to ship and hard to notice.

The request

A tools/list request is an ordinary JSON-RPC call. The cursor parameter is optional and omitted on the first page.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

Over Streamable HTTP this is a POST to the server's MCP endpoint, and it must carry the protocol version negotiated during initialize, plus the session id if the server issued one:

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25
Mcp-Session-Id: 1868a90c...

The response, field by field

The result is an object with a tools array and an optional nextCursor. Each entry describes one tool.

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "title": "Weather Information Provider",
        "description": "Get current weather information for a location",
        "inputSchema": {
          "type": "object",
          "properties": {
            "location": { "type": "string", "description": "City name or zip code" }
          },
          "required": ["location"]
        }
      }
    ],
    "nextCursor": "next-page-cursor"
  }
}

Here is every field the spec defines, next to how often it actually turned up across our 288 tools:

Field Spec status Present in 288 tools What it is
name required 100% Unique identifier for the tool
inputSchema required, MUST be a valid JSON Schema object (not null) 100% JSON Schema defining expected parameters
annotations optional 91% Properties describing tool behaviour. Untrusted by default
description optional in the schema, effectively mandatory in practice see below Human-readable description of functionality
title optional 10% Human-readable display name
outputSchema optional 6% JSON Schema for structured results
icons optional not observed Array of icons for display
execution.taskSupport optional, defaults to "forbidden" not observed Whether the tool supports task-augmented execution

Two things stand out. title is the field most servers skip, even though it exists purely to give humans something better than a snake_case identifier to read. And annotations is on nine tools in ten, which matters more than its optional status suggests, because the spec attaches an explicit warning to it: clients "MUST consider tool annotations to be untrusted unless they come from trusted servers" [1]. A tool that annotates itself as read-only is making a claim, not a guarantee.

On description

The spec lists description as a plain data field with no MUST attached, but it is the field that decides whether a tool is usable by a model at all. In our sample, every one of the 14 connected servers had at least one tool carrying a description. That is the same bar the submission gate applies: a server exposing entirely undocumented tools is not ready to list, because a tool nobody can describe is a tool no model can pick.

What do real tool names look like?

Short, snake_case, and well inside the spec's limits. The specification says names "SHOULD be between 1 and 128 characters", "SHOULD be considered case-sensitive", and that the only allowed characters should be ASCII letters, digits, underscore, hyphen and dot [1].

Across 288 tools:

Convention Share
snake_case 83%
single word or other 11%
kebab-case 5%
camelCase 0%

Name lengths ran from 3 to 36 characters, median 15. Nothing came close to the 128-character ceiling, and not one tool used camelCase despite the spec explicitly listing getUser as a valid example. If you are naming tools and want to match what clients and models have already seen most of, snake_case at roughly 15 characters is the centre of the distribution.

The pagination trap

tools/list supports pagination. When more tools remain, the result carries a nextCursor, and the client is expected to send it back to fetch the next page:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/list",
  "params": { "cursor": "next-page-cursor" }
}

Zero of the 14 servers in our sample paginated. That is precisely what makes this dangerous. A client that reads only the first page works perfectly against every server you are likely to test against, then silently under-reports the moment it meets a large one. The failure is invisible: you do not get an error, you get a smaller number.

For anything that publishes a tool count, that undercount is not a rounding error, it is a wrong fact printed next to someone else's product. Our own tester follows nextCursor to the end and aborts with an explicit error rather than truncating if a server paginates past a configured page limit:

do {
    if (++$pages > $maxPages) {
        throw new McpTransportException('The server paginated its tool list past the page limit.');
    }

    $page = $client->post($url, [
        'jsonrpc' => '2.0',
        'id' => $requestId,
        'method' => 'tools/list',
        'params' => $cursor === null ? (object) [] : ['cursor' => $cursor],
    ], $headers, awaitId: $requestId, deadline: $deadline);

    // ... collect $pageResult['tools'] ...
} while ($cursor !== null);

Loud failure beats a quiet undercount. A number we cannot stand behind does not get published.

Staying in sync after the first call

A server that declares the listChanged capability tells you its tool list can change while you are connected:

{ "capabilities": { "tools": { "listChanged": true } } }

When it does, the server "SHOULD send a notification" of method notifications/tools/list_changed [1], and the client is expected to call tools/list again. If you cache a tool list, this notification is the invalidation signal.

What we keep from a tools/list response

MCP Hunter records exactly what came back and nothing derived from it: whether the connection succeeded, the tool count, the tool names, whether any tool carried a description, one round trip in milliseconds, the negotiated protocol version, and the raw payload itself. Empty strings are normalised to null rather than treated as content, so a server that sends "description": "" is recorded as documenting nothing, which is what it did.

The raw response is stored deliberately. When a maker disputes a result, the payload settles it without anyone re-running a test whose answer may have changed since. That single stored exchange is what every listing on the weekly board is built from, and it is the reason a listing can only ever make a dated, past-tense claim about a server. We tested once, on the date shown. We do not re-test on a schedule, so nothing here describes a server's present state, and we say so plainly rather than implying otherwise with a badge.

Sources

  1. Model Context Protocol specification 2025-11-25, Tools
  2. Model Context Protocol specification 2025-11-25, Transports