Skip to content

Paste an MCP tools/list answer and get an exact report: two tools described the same way, names that collide once case is ignored, one enum spelled two ways, plus ten per-tool schema checks. Runs in your browser, nothing is uploaded.

Your input is processed in this browser. It is not uploaded. A file or pasted rows go nowhere. A URL is fetched by your browser directly from the address you give it. Sharing creates a link containing the data you choose to include.

What it does

A tool schema can be faultless on its own and still break the server it belongs to. Two tools whose descriptions say the same thing leave a model guessing which one to call. Two names that differ only by an underscore look like one name to anything that matches loosely. An enum spelled open in one tool and OPEN in the next teaches a model one spelling and fails the calls that expected the other.

This page reads a tools/list answer, JSON-RPC envelope and all, and reports in two layers: what is wrong between tools first, then each tool on its own under the same ten rules as the tool-schema linter.

How this differs from the official MCP Inspector

The official Inspector is a runtime client. It connects to your server, lists what it exposes, and lets you call a tool and see what comes back. It answers “does this server work”.

This page never connects to anything. It reads the answer you already have and compares the tools in it with each other, which is the one thing a client looking at a server cannot do for you. It answers “can a model choose between these tools”. Use both. The Inspector is also the easiest way to get the input for this page.

How to get a tools/list answer

From the official Inspector, in one command

The Inspector ships a CLI alongside its web UI. One command connects, asks for the tool list, prints it and exits.

npx @modelcontextprotocol/inspector --cli node build/index.js \
  --method tools/list --format json

For a server that is already running somewhere, point at its URL instead:

npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp \
  --transport http --method tools/list --format json

--format json prints a single JSON object with no banners around it. Paste the whole thing. That is the shape this page reads first.

From a remote server with curl

Three requests, because tools/list only answers inside a session. The first one hands back a session id in a response header, so keep the headers.

URL=https://api.example.com/mcp
JSON='Content-Type: application/json'
ACCEPT='Accept: application/json, text/event-stream'

curl -s -D headers.txt -o init.txt "$URL" -H "$JSON" -H "$ACCEPT" -d '{
  "jsonrpc":"2.0","id":1,"method":"initialize",
  "params":{"protocolVersion":"2025-06-18","capabilities":{},
            "clientInfo":{"name":"curl","version":"1"}}}'

SESSION=$(awk 'tolower($1)=="mcp-session-id:"{print $2}' headers.txt | tr -d '\r')

curl -s "$URL" -H "$JSON" -H "$ACCEPT" \
  -H "Mcp-Session-Id: $SESSION" -H 'MCP-Protocol-Version: 2025-06-18' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

curl -s "$URL" -H "$JSON" -H "$ACCEPT" \
  -H "Mcp-Session-Id: $SESSION" -H 'MCP-Protocol-Version: 2025-06-18' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | sed -n 's/^data: //p'

Three things bite here. The Accept header has to list both application/json and text/event-stream, or the server is entitled to refuse. The answer usually arrives as a server-sent event rather than a plain body, which is why the last line pulls the JSON off the data: line. And a session id, once the server has given you one, belongs on every request after it.

If the reply is truncated or arrives with a trailing comma, the JSON repairer will close it and say what it changed.

From the SDKs

TypeScript, where the result is already in wire shape:

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

const client = new Client({ name: 'list-tools', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')));

console.log(JSON.stringify(await client.listTools(), null, 2));

Python, where it is not:

async with streamable_http_client("http://localhost:8000/mcp") as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()
        print(tools.model_dump_json(indent=2, by_alias=True, exclude_none=True))

by_alias=True is not optional. Without it the Python SDK serialises its own field names, so inputSchema comes out as input_schema. This page still reads that document and still reports on it, but it says so in a note and stands down the two checks about safety annotations, because nothing in the document looks like MCP any more.

From a stdio server

A stdio server speaks newline-delimited JSON-RPC on its standard input and output, so the handshake can be piped in. It is fiddly by hand, and the Inspector CLI at the top of this section drives exactly that transport for you. Use it unless you have a reason not to.

A note on paging

A server with many tools may return one page and a cursor. The Inspector CLI and both SDK clients follow the cursor and give you every tool. A single curl gets the first page only, and this page can only compare the tools it is given.

The checks, in the order the report runs them

Eighteen in total: eight that only exist between tools, and the linter’s ten underneath. A finding is either breaks, meaning a call fails or a provider rejects the schema, or degrades, meaning the model gets it wrong more often than it should.

Across the whole server

These are the ones no single-schema check can produce, because the fault is not inside any one tool.

  • breaks name_collides_folded. Two tool names are the same string once case and separators are ignored, as search_orders and searchOrders both fold to searchorders. Anything that matches tool names loosely, which includes some clients and most logs, cannot tell them apart.
  • breaks same_description_exact. Two tools carry exactly the same description. A model choosing between them has nothing to go on but the names.
  • breaks same_description_case. The same, once case and spacing are ignored.
  • breaks same_description_words. The same, once case, punctuation and small words such as “the” and “for” are ignored. The report prints what both descriptions reduce to, so you can read the pair and check the claim.
  • degrades enum_differs_across_tools. One property name carries different enum values in different tools. A model that learns one spelling uses it everywhere, and the calls that expected the other spelling fail.
  • degrades mixed_name_convention. The server names tools in snake_case and camelCase at once. One convention throughout is easier to remember and easier to complete.
  • degrades no_annotations. This tool declares neither readOnlyHint nor destructiveHint while others on the server do, so a client cannot tell whether calling it is safe.
  • degrades no_annotations_anywhere. No tool declares either. Annotations are optional in the MCP specification, so that is one decision to make rather than one fault per tool, and it is reported once.

One thing this page cannot check for you: the specification also says a client must treat annotations as untrusted unless the server is trusted. A readOnlyHint is a claim the server makes about itself. Reading it here tells you what the server says, not what it does.

Tool by tool

The same ten rules the tool-schema linter runs on a single schema, run here on every tool at once.

  • breaks tool_no_description. The tool has no description, so the model has no idea when to call it.
  • breaks param_no_type. A parameter has no usable type. Providers reject it and models free-type it.
  • breaks required_missing_property. required names something that is not a property. The provider rejects the schema or the model invents the field.
  • breaks duplicate_tool_name. A second tool has the same name. Which one is called is undefined.
  • breaks tool_name_invalid. The name is not ^[a-zA-Z0-9_-]{1,64}$. OpenAI and Anthropic reject it.
  • degrades param_no_description. A parameter has no description, so the model guesses from the name alone.
  • degrades enum_case_collision. Two enum values differ only by case or spacing. The model picks one at random.
  • degrades description_restates_name. The description of a parameter only restates its name. It adds nothing the model did not already have.
  • degrades nested_optional_required_children. An optional object has required children. The model either omits it or sends it half-filled.
  • degrades too_many_parameters. More than twenty parameters on one tool. This one is a heuristic and the report says so: accuracy falls off somewhere around there, and the fix is usually to split the tool.

A worked example

A four-tool order server. Every tool in it would pass a single-schema review, and it has a name collision, a pair of interchangeable descriptions, an enum spelled two ways, a tool that says nothing about itself and a parameter with no type. The “Load an example” button above loads this exact document.


{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "search_orders",
        "description": "Search for orders",
        "inputSchema": {
          "type": "object",
          "properties": {
            "customer_id": {
              "type": "string",
              "description": "The customer id"
            },
            "status": {
              "type": "string",
              "description": "Order status",
              "enum": [
                "open",
                "shipped"
              ]
            }
          },
          "required": [
            "customer_id"
          ]
        },
        "annotations": {
          "readOnlyHint": true
        }
      },
      {
        "name": "findOrders",
        "description": "Search orders.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "customerId": {
              "type": "string",
              "description": "Which customer to look under"
            },
            "status": {
              "type": "string",
              "description": "Order status",
              "enum": [
                "OPEN",
                "SHIPPED"
              ]
            },
            "limit": {
              "type": "integer"
            }
          },
          "required": [
            "customerId"
          ]
        },
        "annotations": {
          "readOnlyHint": true
        }
      },
      {
        "name": "searchOrders",
        "description": "Look up orders by customer.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "customerId": {
              "type": "string",
              "description": "Which customer to look under"
            }
          },
          "required": [
            "customerId"
          ]
        },
        "annotations": {
          "readOnlyHint": true
        }
      },
      {
        "name": "refund_order",
        "description": "",
        "inputSchema": {
          "type": "object",
          "properties": {
            "order_id": {
              "description": "The order to refund"
            },
            "amount": {
              "type": "number",
              "description": "How much to refund, in the order currency"
            }
          },
          "required": [
            "order_id"
          ]
        }
      }
    ]
  }
}

The report:

4 tools, 4 breaking, 5 degrading

Across the whole server

  • breaks search_orders, searchOrders: The names “search_orders” and “searchOrders” are the same string once case and separators are ignored, both being “searchorders”. Anything matching tool names loosely, which includes some clients and most logs, cannot tell these two apart.
  • breaks search_orders, findOrders: Tools “search_orders” and “findOrders” have the same description once case, punctuation and small words such as “the” and “for” are ignored: both reduce to “search orders”. Read the two side by side. If you cannot say which one to call, neither can a model.
  • degrades search_orders, findOrders: The property “status” carries different enum values in different tools on this server. A model that learns one spelling will use it everywhere, and the calls that expected the other spelling fail.
  • degrades search_orders, refund_order, findOrders, searchOrders: This server names tools two ways: search_orders, refund_order in snake_case, and findOrders, searchOrders in camelCase. One convention throughout is easier to remember and easier to complete.
  • degrades refund_order: This tool declares neither readOnlyHint nor destructiveHint, while others on this server do, so a client cannot tell whether calling it is safe. Annotations are optional in the MCP specification; what this reports is the inconsistency.

Tool by tool

  • breaks refund_order at description: The tool has no description, so the model has no idea when to call it.
  • breaks refund_order at parameters.properties.order_id: Parameter “order_id” has no usable type; providers reject it and models free-type it.
  • degrades search_orders at parameters.properties.customer_id: The description of “customer_id” only restates its name; it adds nothing the model did not already have.
  • degrades findOrders at parameters.properties.limit: Parameter “limit” has no description; the model guesses from the name alone.

Two of those are worth reading twice. search_orders and searchOrders are different tools that a log line shows as one name. And refund_order is the tool that moves money: it says nothing about itself, and it is the one tool here that will not say whether calling it is safe.

What it does not do

It never scores how similar two descriptions are. Every description finding is an exact match after a stated amount of normalising, and the sentence says what was ignored, so you can read the pair and check the claim yourself. Safety annotations are optional in the MCP specification, so a server that declares none gets one note rather than one fault per tool. A clean run says what was checked and that nothing was found, which is a different and more honest claim than saying the server is fine.

Nothing is uploaded. The document is parsed in the page and never leaves the browser, which is also why the URL option only works for endpoints that allow cross-origin reads. If you want to share a report from a server whose tool descriptions carry anything sensitive, run it through the trace redactor first.

Who this is for

Four kinds of people, and the cross-server layer matters most to the last two.

  • Developers building an MCP server, checking what it exposes before wiring it into Claude Desktop, Claude Code, Cursor or VS Code.
  • Integration engineers adding somebody else’s server, who would like to read what it offers before trusting it.
  • Anyone reviewing tool descriptions for prompt-injection and tool-poisoning patterns, where the descriptions are the attack surface and reading all of them together is the job.
  • Platform and registry teams curating many servers at once. Per-tool checks pass on every server in a catalogue that is collectively unusable, because the collisions are between servers, not inside them.

If you are building the server itself, that is the work Rockstone does for clients. The other seven tools are on the tools index.

Built and maintained by Mats Bergsten, Rockstone AI Development. Last updated 16 September 2026.