mcp-tool-selection-evaluator · v1.0.0 · 2026-10-10 · sha256 0113bd3ab951b0a5

mcp-tool-selection-evaluator v1.0.0A

Immutable. This exact content never changes and is served at /api/v1/blob/0113bd3ab951b0a5.

---
name: mcp-tool-selection-evaluator
description: >-
  Builds a golden question set and scoring harness that checks whether an LLM picks the
  right MCP tool with the right arguments, and reports tool-selection accuracy. Use when
  an engineer says "is the model calling the right tool", "evaluate tool selection for my
  MCP server", "build a golden set for these tools", or shares tools/list output with bad
  transcripts. Produces golden cases, a scorer, and an accuracy report draft the engineer
  runs against the real model.
license: MIT
metadata:
  author: TheTestingAcademy
  pack: mcp
  version: 1.0.0
---

# MCP Tool Selection Evaluator

You measure **whether the model chooses the right tool, with the right arguments, for real user
questions**, and turn every miss into a concrete fix to a tool name, schema, or description.

## When to use
- Tools were added or renamed and you need to know if selection got better or worse.
- Users report the agent calls `search` when it should call `get`, or invents arguments.
- You want a release gate for an MCP server that is consumed by an LLM.

## Workflow
1. **Freeze the tool surface.** Capture `tools/list` (Inspector CLI or `client.list_tools()`) and
   store a hash of names, descriptions, and schemas; evaluate exactly what the model sees.
2. **Write golden cases.** Per tool: 3-5 asks and paraphrases, argument extraction (IDs, enums,
   numbers), confusable pairs (search vs get), multi-step asks (expect the first call), and
   negatives where no tool fits. Fields: `question`, `expected_tool` (or null), `expected_args`.
3. **Get labels reviewed.** Domain owners confirm expected tools; mark ambiguous questions and
   ask instead of forcing a label. Start with 30-50 cases and keep a held-out slice.
4. **Run the harness.** Convert MCP tools to the provider's tool format (name, description,
   input schema), send each question, and record the tool calls without executing them.
5. **Score.** Tool accuracy, argument accuracy, no-call precision on negatives, and a confusion
   matrix of expected vs chosen tool. Repeat each case (e.g. 3 runs) to measure stability.
6. **Diagnose and iterate.** Group misses by cause (overlapping descriptions, vague params, enum
   mismatch); change one thing, re-run the full set, log model id, date, tools hash, and scores.
7. **List assumptions.** Model under test, provider settings, pass threshold (e.g. tool
   accuracy >= 0.90), and labels still awaiting review.

## Output shape
```python
# golden.jsonl, one case per line:
# {"id": "G01", "question": "Find high priority login tests", "expected_tool": "search_test_cases",
#  "expected_args": {"query": "login", "priority": "high"}, "match": {"query": "contains"}}
# {"id": "G02", "question": "Steps of TC-42?", "expected_tool": "get_test_case", "expected_args": {"case_id": "TC-42"}}
# {"id": "N01", "question": "What is a smoke test?", "expected_tool": null, "expected_args": {}}

def _arg_ok(actual, expected, mode: str) -> bool:
    if mode == "contains":
        return isinstance(actual, str) and str(expected).lower() in actual.lower()
    return actual == expected

def score_case(case: dict, calls: list[dict]) -> dict:
    """calls: [{"name": str, "arguments": dict}] produced by the model under test."""
    first = calls[0] if calls else None
    if case["expected_tool"] is None:
        return {"id": case["id"], "tool_ok": first is None, "args_ok": first is None}
    tool_ok = first is not None and first["name"] == case["expected_tool"]
    modes = case.get("match", {})
    args_ok = tool_ok and all(_arg_ok(first["arguments"].get(k), v, modes.get(k, "exact"))
                              for k, v in case["expected_args"].items())
    return {"id": case["id"], "tool_ok": tool_ok, "args_ok": args_ok}

def summarize(results: list[dict]) -> dict:
    n = len(results)
    return {"cases": n, "tool_accuracy": sum(r["tool_ok"] for r in results) / n,
            "arg_accuracy": sum(r["args_ok"] for r in results) / n}
```

## Guardrails
- Never fabricate scores or model outputs; report only real runs, with model id, date, and tools hash.
- Golden labels come from people who know the domain; flag ambiguous questions, do not force them.
- The harness records tool calls without executing writes against real systems.
- LLM output varies: report sample size and repeat runs; one run is not a verdict.
- This is a draft harness and golden set for the engineer to run and verify.