
Eval Mcp
- 168 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
eval-mcp: A skill for development. This provides functionality for development workflows.
Key points
- eval-mcp
Eval Mcp by the numbers
- 168 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,275 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill eval-mcpAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 168 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use eval-mcp for development tasks?
Use eval-mcp for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with eval-mcp.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use eval-mcp for development tasks, or when eval-mcp: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to eval-mcp: eval-mcp.
Files
Evaluate MCP Tools
Tool descriptions are prompt engineering — they land directly in Claude's context window and determine whether Claude picks the right tool with the right arguments. This skill makes tool quality measurable and improvable instead of guesswork.
Three levels of testing, each building on the last: 1. Static Analysis — deterministic schema quality checks (no Claude calls) 2. Selection Testing — does Claude pick the right tool for each intent? 3. Description Optimization — iterative improvement based on confusion patterns
When to Apply
- User wants to check if their MCP tool schemas are well-designed
- User wants to test whether Claude selects the right tools for user intents
- User is debugging tool confusion (Claude picks the wrong tool)
- User wants to optimize tool descriptions for better selection accuracy
- User has finished scaffolding with
build-mcp-serverand wants to validate quality
Workflow Overview
Phase 1: Connect → Phase 2: Static Analysis → Phase 3: Selection Testing → Phase 4: Optimize
↑__________________________|Phase 4 loops back: apply rewrites → refetch schemas → retest → compare accuracy.
Prerequisites
- Node.js >= 18 — required for the MCP Inspector CLI (
npx) - jq — required for schema analysis scripts
- A running MCP server — the server must respond to
tools/list. Usebuild-mcp-server/scripts/test-server.shto verify connectivity first.
---
Phase 1 — Connect & Inventory
Connect to the user's MCP server and fetch the tool schemas.
1a: Get connection details
Ask the user how to reach their server:
- HTTP/SSE: URL (e.g.,
http://localhost:3000/mcp) - stdio: spawn command (e.g.,
node dist/server.js)
1b: Fetch tool schemas
bash scripts/fetch-tools.sh <url-or-command> <transport> <workspace>/tools.jsonThis calls tools/list via the MCP Inspector CLI and saves the schemas.
1c: Display inventory
Show a summary table:
| # | Tool | Description (preview) | Params | Annotations |
|---|------|-----------------------|--------|-------------|
| 1 | search_issues | Search issues by keyword... | 3 | readOnlyHint |
| 2 | create_issue | Create a new issue... | 4 | — |Flag tool count: 1-15 optimal, 15-30 warning, 30+ excessive (consider search+execute pattern).
1d: Create workspace
Create workspace at {server-name}-eval/ adjacent to the skill directory or in the user's project:
{server-name}-eval/
├── tools.json
├── evals/
│ └── evals.json
└── iteration-N/---
Phase 2 — Static Analysis
Run deterministic quality checks — no Claude calls needed. This gives immediate feedback during development.
2a: Run analysis
bash scripts/analyze-schemas.sh <workspace>/tools.json <workspace>/iteration-N/static-analysis.json2b: Display results
Show per-tool quality scores. Read `references/quality-checklist.md` for the criteria being checked.
| Tool | Desc | Params | Schema | Annotations | Overall | Issues |
|------|------|--------|--------|-------------|---------|--------|
| search_issues | 3/3 | 3/3 | 2/3 | 2/3 | 2.5 | No negation |
| create_issue | 1/3 | 1/3 | 0/3 | 0/3 | 0.5 | 4 issues |2c: Flag sibling pairs
If the analysis found tools with high description overlap, highlight them as confusion risks:
### Sibling Pairs (confusion risk)
| Tool A | Tool B | Overlap | Risk |
|--------|--------|---------|------|
| search_issues | list_issues | 52% | HIGH |2d: Decision point
If critical issues exist (missing descriptions, zero annotations), recommend fixing them before Phase 3. Static issues create noise in selection testing — fix the obvious problems first, then measure the subtle ones.
If all tools score well, proceed to Phase 3.
---
Phase 3 — Selection Testing
Test whether Claude picks the right tool for each user intent. This is the core eval.
3a: Generate test intents
Read `references/eval-patterns.md` for intent generation patterns.
For each tool, generate:
- 3 should-trigger intents — direct, implicit, and casual phrasings
- 2 should-not-trigger intents — near-miss and keyword overlap
For each sibling pair flagged in Phase 2:
- 1 disambiguation intent per tool — tests whether Claude picks the RIGHT sibling
Present all intents to the user for review. Ask if any should be added, removed, or modified.
3b: Save intents
Save to {workspace}/evals/evals.json:
{
"server_name": "my-server",
"generated_from": "tools.json",
"intents": [
{
"id": 1,
"intent": "Are there any open bugs related to checkout?",
"expected_tool": "search_issues",
"type": "should_trigger",
"target_tool": "search_issues",
"notes": "Implicit intent — doesn't name the action"
}
]
}3c: Run selection tests
For each intent, spawn a subagent that receives: 1. The full tool schemas from tools.json (formatted as they'd appear in Claude's context) 2. The user intent text 3. Instructions to select exactly one tool and provide arguments, or decline if no tool fits
The subagent prompt:
You have access to the following MCP tools:
{tool schemas as JSON}
A user sends this message:
"{intent text}"
Which tool would you call? Respond with JSON:
{
"selected_tool": "tool_name" or null,
"arguments": { ... } or {},
"reasoning": "One sentence explaining your choice"
}
If no tool fits the user's request, set selected_tool to null.
Select exactly ONE tool. Do not suggest calling multiple tools.Save each result to {workspace}/iteration-N/selection/intent-{ID}/result.json.
Launch all selection tests in parallel for efficiency.
3d: Grade results
bash scripts/grade-selection.sh \
<workspace>/iteration-N/selection \
<workspace>/evals/evals.json \
<workspace>/iteration-N/benchmark.json3e: Display results
## Selection Results — Iteration N
**Accuracy:** 82% (41/50 correct)
| Metric | Count |
|--------|-------|
| Correct | 41 |
| Wrong tool | 5 |
| False accept | 2 |
| False reject | 2 |
### Per-Tool Accuracy
| Tool | Precision | Recall |
|------|-----------|--------|
| search_issues | 0.90 | 0.85 |
| create_issue | 1.00 | 1.00 |
### Worst Confusions
| Expected | Selected Instead | Times |
|----------|-----------------|-------|
| list_issues | search_issues | 3 |
| get_user | find_user_by_email | 2 |---
Phase 4 — Optimize & Iterate
Analyze confusion patterns and suggest description improvements. Read `references/optimization.md` for rewrite patterns.
4a: Analyze confusions
For each confused pair (from worst_confusions): 1. Read both tools' current descriptions 2. Identify why they're confusing (missing negation, overlapping scope, no cross-reference) 3. Draft a specific rewrite following the disambiguation patterns in optimization.md
4b: Present suggestions
## Suggested Improvements
### search_issues ↔ list_issues (confused 3 times)
**search_issues — Before:**
> Search issues by keyword.
**search_issues — After:**
> Search issues by keyword across title and body. Returns up to `limit` results ranked by relevance. Does NOT filter by status, assignee, or date — use list_issues for structured filtering.
**Reason:** Adding scope boundary and cross-reference to disambiguate from list_issues.Save to {workspace}/iteration-N/suggestions.json (format defined in optimization.md).
4c: Apply and retest
After the user applies the rewrites to their server code:
1. Restart the server 2. Re-run Phase 1 to refetch tools.json (descriptions may have changed) 3. Re-run Phase 2 for updated static analysis 4. Re-run Phase 3 into iteration-N+1 using the same evals.json 5. Compare accuracy:
## Iteration Comparison
| Metric | Iteration 1 | Iteration 2 | Delta |
|--------|------------|------------|-------|
| Accuracy | 82% | 94% | +12% |
| search↔list confusion | 3 | 0 | -3 |4d: Iteration guidance
- Change one sibling pair per iteration so you can attribute improvements
- If accuracy plateaus, the remaining confusions may need architectural changes (merging tools, renaming, or restructuring the tool surface)
- Stop when accuracy exceeds 90% or when remaining confusions are in ambiguous edge cases that humans would also struggle with
---
Reference Files
Read these when you reach the relevant phase — not upfront:
- `references/quality-checklist.md` — Testable quality criteria for tool schemas (Phase 2)
- `references/eval-patterns.md` — How to write tool selection test intents (Phase 3)
- `references/optimization.md` — How to improve descriptions from eval results (Phase 4)
Related Skills
build-mcp-server— Design and scaffold MCP servers (run this first, then eval-mcp to validate)build-mcp-app— MCP servers with interactive UI widgets
Gotchas
No known gotchas yet. Append entries as they're discovered during use.
{
"version": "1.0.3",
"organization": "pproenca",
"technology": "Model Context Protocol",
"discipline": "composition",
"type": "verification",
"date": "March 2026",
"abstract": "MCP tool evaluation skill — tests whether Claude uses your tools correctly by analyzing schema quality, measuring tool selection accuracy, and iteratively optimizing descriptions.",
"references": [
"https://modelcontextprotocol.io/specification",
"https://github.com/modelcontextprotocol/typescript-sdk"
]
}
Tool Selection Eval Patterns
How to generate test intents that measure whether Claude picks the right tool. Read this before Phase 3 of the eval workflow.
---
Should-Trigger Intents
Intents that should cause a specific tool to be selected. Generate 3 per tool, varying formality and directness.
Direct Request
Name the action explicitly. Easiest to get right — tests basic description matching.
Tool: create_issue
Intent: "Create a new issue titled 'Login timeout on mobile'"Implicit Intent
Describe the need without naming the action. Tests whether the description captures the use case, not just keywords.
Tool: search_issues
Intent: "Are there any open bugs related to the checkout flow?"Casual / Domain Jargon
Use informal language or domain-specific terms. Tests description robustness beyond formal phrasing.
Tool: update_issue
Intent: "Can you bump issue 42 to high priority? It's blocking the release."Key rule
Never use the tool's exact name in the intent. You're testing whether the description drives selection, not whether Claude can pattern-match on the tool name.
---
Should-NOT-Trigger Intents
Intents that should cause Claude to decline (select no tool). Generate 2 per tool, testing different failure modes.
Near-Miss (Adjacent Capability)
Share vocabulary with a real tool but request something it can't do.
Available: search_issues (searches issues by keyword)
Intent: "Search the pull requests for mentions of the auth refactor"
Why decline: No PR search tool exists. The keyword "search" overlaps but the domain is wrong.Keyword Overlap (Wrong Semantics)
Use the same verbs/nouns but for a fundamentally different operation.
Available: create_issue, add_comment
Intent: "Create a new Slack channel for the team"
Why decline: "Create" overlaps but the target system is completely different.Beyond Capability
Request something no tool in the set can do.
Available: [issue tracker tools]
Intent: "Deploy the latest build to staging"
Why decline: No deployment tools exist in this server.---
Disambiguation Intents
For tool pairs flagged as siblings (high description overlap), generate intents that test whether Claude picks the RIGHT sibling. These are the highest-value tests.
Input-Type Disambiguation
Tools: get_user (by ID), find_user_by_email (by email)
Intent: "Look up the user with email john@example.com"
Expected: find_user_by_email (not get_user)
Why: Tests whether descriptions clarify which input type each accepts.Scope Disambiguation
Tools: search_issues (keyword search), list_issues (browse with filters)
Intent: "Show me all critical issues from this week"
Expected: list_issues (filter by severity + date, not keyword search)
Why: Tests whether "search" vs "list/filter" distinction is clear.Action Disambiguation
Tools: update_issue (modify fields), add_comment (append note)
Intent: "Add a note to issue 42 saying the fix is deployed"
Expected: add_comment (not update_issue)
Why: Tests whether "add a note" maps to comment, not field update.---
Edge Cases
Include 2-3 edge cases per eval set.
Empty / Meaningless Intent
Intent: ""
Expected: none (decline)Multi-Tool Intent
Intent: "Find the issue about login bugs and add a comment saying it's fixed"
Expected: search_issues (first tool in the sequence)
Note: Selection tests pick ONE tool. Multi-step sequences are out of scope — the test validates the first selection.Negated Intent
Intent: "Don't create a new issue, just search for existing ones about login"
Expected: search_issues (not create_issue)
Why: Tests whether Claude parses negation correctly and doesn't keyword-match on "create."---
Intent Writing Guidelines
1. Natural language only. Write as a real user would — include context, vary formality, allow typos if natural. 2. Never use the tool name. Test description quality, not name matching. "Run the search_issues tool" is useless. 3. One intent, one expected tool. Each intent maps to exactly one tool (or none). Multi-tool intents test the first step. 4. Include domain context. "Find the login bug" is generic; "Check if there's already a ticket for the 504 errors on /api/checkout" is realistic. 5. Vary across tools. Don't cluster all intents on the most obvious tool. Less-used tools often have the worst descriptions.
---
How Many Intents?
| Tool Count | Should-Trigger | Should-Not | Disambiguation | Total |
|---|---|---|---|---|
| 3-5 tools | 3 per tool | 2 per tool | 1 per sibling pair | ~20-30 |
| 6-10 tools | 3 per tool | 2 per tool | 1 per sibling pair | ~35-60 |
| 11-15 tools | 2 per tool | 1 per tool | 1 per sibling pair | ~40-60 |
Keep total under 60 for practical cost. Each intent is one Claude call.
Tool Description Optimization
How to improve MCP tool descriptions based on eval results. Read this during Phase 4 when analyzing confusion patterns and drafting rewrites.
---
The Optimization Loop
Confusion matrix → Identify worst pairs → Rewrite descriptions → Retest → CompareEach iteration should improve accuracy on the confused pairs without regressing on tools that already work. If a rewrite fixes one confusion but creates another, the descriptions need a different framing.
---
Disambiguation Patterns
Cross-Reference Siblings
When two tools get confused, each description should say when to use the OTHER:
Before (confusing):
get_user — Fetch a user from the database.
find_user — Look up a user in the system.After (disambiguated):
get_user — Fetch a user by their ID (e.g., usr_abc123). If you only have an email address, use find_user_by_email instead.
find_user_by_email — Look up a user by email address. Returns null if not found. If you already have the user's ID, use get_user.The key move: each description tells Claude exactly when this tool is wrong and which tool is right instead.
Input-Type Routing
When tools accept different identifier types, make the input type the first distinguishing feature:
get_order — Fetch an order by its order ID (format: ORD-XXXXXX). Returns full order details including line items and shipping status.
lookup_order_by_email — Find all orders for a customer email address. Returns a list of order summaries (ID, date, total) — use get_order for full details.Scope Boundaries
When tools operate on different scopes of the same entity, define the boundary:
search_issues — Full-text keyword search across issue title and body. Returns up to `limit` results ranked by relevance. Does NOT filter by status, assignee, or date — use list_issues for structured filtering.
list_issues — Browse issues with structured filters (status, assignee, priority, date range). Returns a paginated list sorted by date. For keyword/text search, use search_issues.---
Negation Patterns
Explicit Exclusion
State what the tool does NOT do when an adjacent capability exists:
search_issues — Search issues by keyword. Does NOT search comments or pull requests — use search_comments / search_prs for those.Capability Boundary
Define the edge of what the tool can do:
get_issue — Fetch issue metadata (title, status, assignee, labels). Does NOT include the comment thread — use list_comments for that.System Boundary
When a tool only works within one system:
create_issue — Create a new issue in the project tracker. Does NOT create tickets in Jira or Slack threads — this only operates on the internal tracker.---
Return Shape Documentation
State the Shape
Tell Claude exactly what comes back:
search_items — Returns a JSON array of {id, title, score} objects, up to `limit` results.State Truncation
When results can be large, describe the truncation behavior:
Returns up to 50 results. If more exist, the response includes `hasMore: true` and a `cursor` for pagination.State What's NOT Returned
Prevent follow-up confusion:
Returns issue metadata only (title, status, labels). Does not include the full body or comments — use get_issue for the body and list_comments for the thread.---
Recovery Hint Patterns
Next-Step Hints
When a tool call fails, the error should suggest what to do next:
if (!item) {
return {
isError: true,
content: [{
type: "text",
text: "Item 'xyz' not found. Use search_items to find valid IDs."
}]
};
}Alternative Suggestion
When the user's intent doesn't match this tool:
"This tool only searches by keyword. For filtering by date or status, use list_items with the appropriate filters."---
The suggestions.json Format
Phase 4 outputs improvement suggestions in this format:
{
"iteration": 1,
"suggestions": [
{
"tool": "search_issues",
"field": "description",
"before": "Search issues by keyword.",
"after": "Search issues by keyword across title and body. Returns up to `limit` results ranked by relevance. Does NOT filter by status or assignee — use list_issues for structured filtering.",
"reason": "Confused with list_issues 40% of the time. Adding scope boundary and cross-reference should disambiguate.",
"confused_with": "list_issues",
"confusion_rate": 0.4
}
]
}Fields:
tool: The tool to modifyfield: Which field to change (descriptionor a param description likeparam:status)before: Current textafter: Proposed replacementreason: Why this change should help (linked to eval data)confused_with: The sibling tool causing confusion (if applicable)confusion_rate: How often this tool was selected when the sibling was expected
---
Applying Rewrites
1. The suggestions.json file contains before/after text for each tool 2. Find the tool registration in your server source code 3. Replace the description string with the after text 4. Restart the server 5. Re-run Phase 1-3 of eval-mcp into a new iteration 6. Compare accuracy: the confusion rate for the modified pair should drop
Do not apply all suggestions at once. Change one sibling pair per iteration so you can attribute accuracy changes to specific rewrites. If you change everything at once, you can't tell which rewrites helped and which hurt.
MCP Tool Quality Checklist
Testable criteria for MCP tool schemas. Each check has a pass/fail threshold used by scripts/analyze-schemas.sh. Source of truth for these criteria is build-mcp-server/references/tool-design.md.
---
Description Quality
DQ-1: Description Length
Pass: >= 20 characters Fail: Missing or under 20 characters Why: Short descriptions like "Searches for issues" give Claude nothing to disambiguate. Descriptions must say what the tool does, what it returns, and what it doesn't do.
DQ-2: States What the Tool Does
Pass: Description contains an action verb describing the operation Fail: Description is a noun phrase or lacks a clear action Why: Claude reads descriptions as contracts. "Issue searcher" is ambiguous; "Search issues by keyword across title and body" is actionable.
DQ-3: States What the Tool Returns
Pass: Description mentions return/output format (contains "return", "output", "result", "produce", "respond") Fail: No mention of what comes back Why: Claude needs to know whether a tool returns a list, a single item, a confirmation, or structured data to use it correctly in multi-step workflows.
DQ-4: Disambiguates from Siblings
Pass: Description mentions what the tool does NOT do, or when to use a different tool Fail: No negation or cross-reference Why: When two tools overlap, each description should say when to use the OTHER one. Without this, Claude picks based on keyword similarity and frequently confuses siblings.
---
Parameter Schema Quality
PS-1: All Parameters Have Descriptions
Pass: 100% of parameters have a description field Fail: Any parameter lacks a description Why: The .describe() text shows up in the schema Claude sees. Omitting it forces Claude to guess what the parameter means from the name alone.
PS-2: String Parameters Use Constraints
Pass: String parameters use enum, pattern (regex), or format where applicable Fail: Bare string type for a constrained value (IDs, statuses, categories) Why: Tight schemas prevent bad calls. z.string() for an ID lets Claude pass anything; z.string().regex(/^usr_[a-z0-9]{12}$/) validates at the schema level.
PS-3: Number Parameters Have Bounds
Pass: Number parameters have minimum, maximum, or default Fail: Unbounded number with no guidance Why: Without bounds, Claude may pass 0, -1, or 999999. Bounds prevent nonsensical values and communicate intent.
PS-4: Optional Parameters Document Defaults
Pass: Optional parameters describe their default behavior Fail: Optional parameter with no hint about what happens when omitted Why: Claude decides whether to include optional params based on the description. "Defaults to the caller's workspace" tells Claude it can usually skip this param.
---
Annotation Coverage
AC-1: Tool Has At Least One Annotation
Pass: annotations object exists with at least one hint Fail: No annotations at all Why: Annotations drive host UX (auto-approve for readonly, confirm for destructive). Missing annotations means the host assumes worst case.
AC-2: Read-Only Tools Marked
Pass: Tools that don't modify state have readOnlyHint: true Fail: Read-only tool without the annotation Why: Hosts may auto-approve read-only calls, reducing user friction. Missing annotation means unnecessary confirmation prompts.
AC-3: Destructive Tools Marked
Pass: Tools that delete or overwrite have destructiveHint: true Fail: Destructive tool without the annotation Why: Hosts show confirmation dialogs for destructive tools. Missing annotation means a delete could execute without warning.
---
Tool Set Quality
TS-1: Tool Count in Range
Pass: 1-15 tools (optimal), 15-30 (acceptable with warning) Fail: 0 tools or 30+ tools Why: Every tool schema consumes tokens in Claude's context window. 30 tools with rich schemas can eat 3-5k tokens before the conversation starts. Over 30, switch to search+execute.
TS-2: No High Sibling Overlap
Pass: No two tools share > 50% of non-stopword description tokens Fail: A pair exceeds the overlap threshold Why: High overlap means Claude will confuse the pair. Either merge them, rename them, or add disambiguation to both descriptions.
TS-3: Similar Tools Cross-Reference
Pass: Tools flagged as siblings in TS-2 reference each other in descriptions Fail: Similar tools exist without cross-references Why: The pattern from tool-design.md: get_user — Fetch by ID. If you only have an email, use find_user_by_email.
---
Error Handling
EH-1: Descriptions Mention Error Cases
Pass: Description or return docs mention at least one error condition Fail: No mention of failure modes Why: Tools that only describe the happy path leave Claude guessing when things fail. "Returns null if not found" is more useful than silence.
EH-2: Error Returns Include Recovery Hints
Pass: Error responses use isError: true with a next-step suggestion Fail: Bare error message or transport exception Why: The hint turns a dead end into a next step: "Item not found. Use search_items to find valid IDs."
Note: EH-2 is only testable at runtime, not via static schema analysis.
#!/usr/bin/env bash
# analyze-schemas.sh — Static quality checks on MCP tool schemas
set -euo pipefail
command -v jq >/dev/null 2>&1 || {
echo "jq not found. Install jq: https://jqlang.github.io/jq/download/"
exit 1
}
TOOLS_FILE="${1:-}"
OUTPUT="${2:-}"
if [[ -z "$TOOLS_FILE" ]]; then
echo "Usage: analyze-schemas.sh <tools-json> [output-file]"
echo ""
echo " tools-json Path to tools.json (output of fetch-tools.sh)"
echo " output-file Write analysis to file instead of stdout"
echo ""
echo "Runs static quality checks against tool schemas."
exit 1
fi
if [[ ! -f "$TOOLS_FILE" ]]; then
echo "File not found: $TOOLS_FILE"
exit 1
fi
# Stopwords for sibling similarity
STOPWORDS='["the","a","an","and","or","is","are","was","were","be","been","being","in","on","at","to","for","of","with","by","from","as","it","its","this","that","these","those","has","have","had","do","does","did","will","would","can","could","may","might","shall","should","not","no","but","if","then","else","when","where","which","who","what","how","all","each","every","both","few","more","most","other","some","such","than","too","very","just","also","into","over","after","before","between","under","about","up","out","off","down","only","own","same","so"]'
RESULT=$(jq --argjson stopwords "$STOPWORDS" '
# Helper: score 0-3 from value
def score_length:
if . == null or . == "" then 0
elif (. | length) < 20 then 1
elif (. | length) < 50 then 2
else 3
end;
# Helper: check if string contains any of the patterns (case-insensitive)
def contains_any(patterns):
. as $s | [patterns[] | select($s | ascii_downcase | test(.))] | length > 0;
# Helper: tokenize and remove stopwords
def meaningful_tokens:
ascii_downcase | gsub("[^a-z0-9 ]"; " ") | split(" ") | map(select(length > 2)) | map(select(. as $w | $stopwords | index($w) | not));
# Tool count check
.tool_count as $count |
(if $count <= 15 then "optimal"
elif $count <= 30 then "warning"
else "excessive" end) as $count_grade |
# Per-tool analysis
[.tools[] | . as $tool |
# Description checks
($tool.description // "") as $desc |
($desc | score_length) as $desc_length_score |
# DQ-2: has action verb (does something)
($desc | length > 10) as $has_action |
# DQ-3: mentions returns
([$desc | ascii_downcase | test("return|output|result|produce|respond|yield|give")] | .[0] // false) as $mentions_returns |
# DQ-4: disambiguation / negation
([$desc | ascii_downcase | test("not |don.t|does not|instead|rather than|use .+ for|if you")] | .[0] // false) as $has_negation |
# Description pattern score (0-3)
([($has_action | if . then 1 else 0 end), ($mentions_returns | if . then 1 else 0 end), ($has_negation | if . then 1 else 0 end)] | add) as $desc_pattern_score |
# Parameter analysis
($tool.inputSchema.properties // {} | to_entries) as $params |
($params | length) as $param_count |
(if $param_count == 0 then 3
else
([$params[] | select(.value.description != null and (.value.description | length) > 0)] | length) as $described |
(if $param_count > 0 then ($described * 100 / $param_count) else 100 end) as $pct |
(if $pct >= 100 then 3 elif $pct >= 50 then 2 elif $pct > 0 then 1 else 0 end)
end) as $param_desc_score |
# Param description coverage percentage
(if $param_count == 0 then 100
else
([$params[] | select(.value.description != null and (.value.description | length) > 0)] | length) as $d |
($d * 100 / $param_count)
end) as $param_desc_pct |
# Schema specificity: params with constraints beyond base type
(if $param_count == 0 then 3
else
([$params[] | select(
.value.enum != null or
.value.pattern != null or
.value.minimum != null or
.value.maximum != null or
.value.minLength != null or
.value.maxLength != null or
.value.default != null or
.value.format != null
)] | length) as $constrained |
(if $param_count > 0 then ($constrained * 100 / $param_count) else 100 end) as $cpct |
(if $cpct >= 75 then 3 elif $cpct >= 50 then 2 elif $cpct > 0 then 1 else 0 end)
end) as $schema_spec_score |
# Annotation coverage
($tool.annotations // {}) as $anns |
($anns | keys | length) as $ann_count |
(if $ann_count >= 3 then 3
elif ($anns | has("readOnlyHint") or has("destructiveHint")) then 2
elif $ann_count >= 1 then 1
else 0
end) as $ann_score |
# Overall score (average of all subscores, 0-3)
(([$desc_length_score, $desc_pattern_score, $param_desc_score, $schema_spec_score, $ann_score] | add) / 5) as $overall |
# Collect issues
([
(if $desc_length_score < 2 then "Description too short (< 20 chars)" else empty end),
(if $has_action | not then "Description lacks action verb" else empty end),
(if $mentions_returns | not then "Description does not mention return value" else empty end),
(if $has_negation | not then "Description has no disambiguation/negation" else empty end),
(if $param_desc_score < 3 and $param_count > 0 then "Some parameters missing .describe()" else empty end),
(if $schema_spec_score < 2 and $param_count > 0 then "Parameters lack constraints (enum, pattern, bounds)" else empty end),
(if $ann_score == 0 then "No tool annotations set" else empty end)
]) as $issues |
{
name: $tool.name,
description_preview: ($desc | if length > 80 then .[:80] + "..." else . end),
scores: {
descriptionLength: { score: $desc_length_score, chars: ($desc | length) },
descriptionPattern: {
score: $desc_pattern_score,
has: ([
(if $has_action then "action" else empty end),
(if $mentions_returns then "returns" else empty end),
(if $has_negation then "negation" else empty end)
])
},
paramDescribeCoverage: { score: $param_desc_score, described_pct: $param_desc_pct, param_count: $param_count },
schemaSpecificity: { score: $schema_spec_score },
annotationCoverage: { score: $ann_score, annotations: ($anns | keys) },
overall: ($overall * 100 | round / 100)
},
issues: $issues
}
] as $tool_results |
# Sibling similarity detection
[.tools | to_entries | . as $tools |
$tools[] | . as $a |
$tools[] | . as $b |
select($a.key < $b.key) |
($a.value.description // "" | meaningful_tokens) as $a_tokens |
($b.value.description // "" | meaningful_tokens) as $b_tokens |
([$a_tokens[] | select(. as $t | $b_tokens | index($t) != null)]) as $shared |
($a_tokens | length) as $a_len |
($b_tokens | length) as $b_len |
(if ($a_len + $b_len) > 0 then (($shared | length) * 2 * 100 / ($a_len + $b_len)) else 0 end) as $overlap_pct |
select($overlap_pct > 30) |
{
tool1: $a.value.name,
tool2: $b.value.name,
shared_tokens: $shared,
overlap_pct: ($overlap_pct | round),
risk: (if $overlap_pct > 50 then "high" elif $overlap_pct > 30 then "medium" else "low" end)
}
] as $sibling_pairs |
# Summary
($tool_results | map(.scores.overall) | add / length * 100 | round / 100) as $avg_score |
($tool_results | map(.issues) | add | length) as $total_issues |
([$tool_results[] | select(.issues | length > 0)] | length) as $tools_with_issues |
{
tool_count: { count: $count, grade: $count_grade },
tools: $tool_results,
sibling_pairs: $sibling_pairs,
summary: {
avg_score: $avg_score,
tools_with_issues: $tools_with_issues,
total_issues: $total_issues,
critical_issues: ([$tool_results[] | .issues[] | select(startswith("Description too short") or startswith("No tool annotations"))] | length)
}
}
' "$TOOLS_FILE")
if [[ -n "$OUTPUT" ]]; then
echo "$RESULT" > "$OUTPUT"
echo "Analysis saved to $OUTPUT" >&2
else
echo "$RESULT"
fi
# Summary to stderr
TOOL_COUNT=$(echo "$RESULT" | jq '.tool_count.count')
AVG_SCORE=$(echo "$RESULT" | jq '.summary.avg_score')
ISSUES=$(echo "$RESULT" | jq '.summary.total_issues')
SIBLINGS=$(echo "$RESULT" | jq '.sibling_pairs | length')
echo "" >&2
echo "Analyzed $TOOL_COUNT tools. Average quality score: $AVG_SCORE/3.0" >&2
echo "Issues found: $ISSUES | Sibling pairs: $SIBLINGS" >&2
if [[ "$ISSUES" -gt 0 ]]; then
echo "" >&2
echo "Top issues:" >&2
echo "$RESULT" | jq -r '.tools[] | select(.issues | length > 0) | " \(.name): \(.issues | join(", "))"' >&2
fi
#!/usr/bin/env bash
# fetch-tools.sh — Fetch tool schemas from a running MCP server via Inspector CLI
set -euo pipefail
command -v npx >/dev/null 2>&1 || {
echo "npx not found. Install Node.js (>= 18): https://nodejs.org"
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "jq not found. Install jq: https://jqlang.github.io/jq/download/"
exit 1
}
URL="${1:-}"
TRANSPORT="${2:-http}"
OUTPUT="${3:-}"
if [[ -z "$URL" ]]; then
echo "Usage: fetch-tools.sh <server-url> [transport] [output-file]"
echo ""
echo " server-url MCP server endpoint (e.g. http://localhost:3000/mcp)"
echo " transport 'http' (default), 'sse', or 'stdio'"
echo " output-file Write JSON to file instead of stdout"
echo ""
echo "Fetches tools/list from the server and outputs the tool schemas as JSON."
exit 1
fi
echo "Fetching tools from $URL (transport: $TRANSPORT)..." >&2
RAW_OUTPUT=$(npx @modelcontextprotocol/inspector --cli "$URL" \
--transport "$TRANSPORT" --method tools/list 2>/dev/null) || {
echo "Could not connect to MCP server at $URL." >&2
echo "Verify the server is running. Try:" >&2
echo " npx @modelcontextprotocol/inspector --cli $URL --transport $TRANSPORT --method tools/list" >&2
exit 1
}
# Extract tools array from the response
# Inspector CLI outputs the JSON-RPC result directly
TOOLS=$(echo "$RAW_OUTPUT" | jq -e '.tools // empty' 2>/dev/null) || {
# Try extracting from nested result structure
TOOLS=$(echo "$RAW_OUTPUT" | jq -e '.result.tools // empty' 2>/dev/null) || {
echo "Could not parse tool schemas from Inspector output." >&2
echo "Raw output:" >&2
echo "$RAW_OUTPUT" >&2
exit 1
}
}
TOOL_COUNT=$(echo "$TOOLS" | jq 'length')
if [[ "$TOOL_COUNT" -eq 0 ]]; then
echo "Server returned zero tools. Check that tools are registered in server init." >&2
exit 2
fi
echo "Found $TOOL_COUNT tool(s)." >&2
# Build output with metadata
RESULT=$(jq -n \
--argjson tools "$TOOLS" \
--arg url "$URL" \
--arg transport "$TRANSPORT" \
--arg fetched_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
server_url: $url,
transport: $transport,
fetched_at: $fetched_at,
tool_count: ($tools | length),
tools: $tools
}')
if [[ -n "$OUTPUT" ]]; then
echo "$RESULT" > "$OUTPUT"
echo "Saved to $OUTPUT" >&2
else
echo "$RESULT"
fi
#!/usr/bin/env bash
# grade-selection.sh — Compare actual vs expected tool selections, build confusion matrix
set -euo pipefail
command -v jq >/dev/null 2>&1 || {
echo "jq not found. Install jq: https://jqlang.github.io/jq/download/"
exit 1
}
RESULTS_DIR="${1:-}"
EVALS_FILE="${2:-}"
OUTPUT="${3:-}"
if [[ -z "$RESULTS_DIR" ]] || [[ -z "$EVALS_FILE" ]]; then
echo "Usage: grade-selection.sh <results-dir> <evals-json> [output-file]"
echo ""
echo " results-dir Directory containing intent-N/result.json files"
echo " evals-json Path to evals.json with expected tool selections"
echo " output-file Write grading JSON to file instead of stdout"
echo ""
echo "Compares actual tool selections against expected, builds confusion matrix."
exit 1
fi
if [[ ! -d "$RESULTS_DIR" ]]; then
echo "Results directory not found: $RESULTS_DIR"
exit 1
fi
if [[ ! -f "$EVALS_FILE" ]]; then
echo "Evals file not found: $EVALS_FILE"
exit 1
fi
CORRECT=0
WRONG_TOOL=0
FALSE_ACCEPT=0
FALSE_REJECT=0
TOTAL=0
GRADES="[]"
CONFUSION="{}"
# Iterate over each intent in evals.json
INTENT_COUNT=$(jq '.intents | length' "$EVALS_FILE")
for ((i = 0; i < INTENT_COUNT; i++)); do
INTENT_ID=$(jq -r ".intents[$i].id" "$EVALS_FILE")
EXPECTED=$(jq -r ".intents[$i].expected_tool" "$EVALS_FILE")
INTENT_TEXT=$(jq -r ".intents[$i].intent" "$EVALS_FILE")
INTENT_TYPE=$(jq -r ".intents[$i].type" "$EVALS_FILE")
RESULT_FILE="$RESULTS_DIR/intent-${INTENT_ID}/result.json"
if [[ ! -f "$RESULT_FILE" ]]; then
echo "Warning: Missing result for intent $INTENT_ID, skipping" >&2
continue
fi
ACTUAL=$(jq -r '.selected_tool // "null"' "$RESULT_FILE")
[[ "$ACTUAL" == "null" ]] && ACTUAL="none"
[[ "$EXPECTED" == "null" ]] && EXPECTED="none"
TOTAL=$((TOTAL + 1))
# Classify the result
if [[ "$ACTUAL" == "$EXPECTED" ]]; then
VERDICT="correct"
PASS=true
CORRECT=$((CORRECT + 1))
elif [[ "$EXPECTED" == "none" ]] && [[ "$ACTUAL" != "none" ]]; then
VERDICT="false_accept"
PASS=false
FALSE_ACCEPT=$((FALSE_ACCEPT + 1))
elif [[ "$EXPECTED" != "none" ]] && [[ "$ACTUAL" == "none" ]]; then
VERDICT="false_reject"
PASS=false
FALSE_REJECT=$((FALSE_REJECT + 1))
else
VERDICT="wrong_tool"
PASS=false
WRONG_TOOL=$((WRONG_TOOL + 1))
fi
# Add to grades array
GRADE=$(jq -n \
--argjson id "$INTENT_ID" \
--arg intent "$INTENT_TEXT" \
--arg expected "$EXPECTED" \
--arg actual "$ACTUAL" \
--arg verdict "$VERDICT" \
--argjson pass "$PASS" \
--arg type "$INTENT_TYPE" \
'{intent_id: $id, intent: $intent, expected: $expected, actual: $actual, verdict: $verdict, pass: $pass, type: $type}')
GRADES=$(echo "$GRADES" | jq --argjson g "$GRADE" '. + [$g]')
# Update confusion matrix
CONFUSION=$(echo "$CONFUSION" | jq \
--arg exp "$EXPECTED" \
--arg act "$ACTUAL" \
'.[$exp] = ((.[$exp] // {}) | .[$act] = ((.[$act] // 0) + 1))')
done
# Compute per-tool precision and recall
TOOL_NAMES=$(jq -r '.intents[].expected_tool // "none"' "$EVALS_FILE" | sort -u)
PER_TOOL="{}"
for TOOL in $TOOL_NAMES; do
[[ "$TOOL" == "null" ]] && TOOL="none"
# Recall: correct for this tool / times this tool was expected
EXPECTED_COUNT=$(echo "$GRADES" | jq --arg t "$TOOL" '[.[] | select(.expected == $t)] | length')
CORRECT_FOR_TOOL=$(echo "$GRADES" | jq --arg t "$TOOL" '[.[] | select(.expected == $t and .pass == true)] | length')
# Precision: correct for this tool / times this tool was selected
SELECTED_COUNT=$(echo "$GRADES" | jq --arg t "$TOOL" '[.[] | select(.actual == $t)] | length')
CORRECT_SELECTED=$(echo "$GRADES" | jq --arg t "$TOOL" '[.[] | select(.actual == $t and .expected == $t)] | length')
if [[ "$EXPECTED_COUNT" -gt 0 ]]; then
RECALL=$(echo "scale=3; $CORRECT_FOR_TOOL / $EXPECTED_COUNT" | bc)
else
RECALL="1.000"
fi
if [[ "$SELECTED_COUNT" -gt 0 ]]; then
PRECISION=$(echo "scale=3; $CORRECT_SELECTED / $SELECTED_COUNT" | bc)
else
PRECISION="1.000"
fi
PER_TOOL=$(echo "$PER_TOOL" | jq \
--arg tool "$TOOL" \
--argjson prec "$PRECISION" \
--argjson rec "$RECALL" \
--argjson exp "$EXPECTED_COUNT" \
--argjson sel "$SELECTED_COUNT" \
'.[$tool] = {precision: $prec, recall: $rec, expected_count: $exp, selected_count: $sel}')
done
# Find worst confusions (wrong_tool pairs sorted by count)
WORST=$(echo "$GRADES" | jq '[.[] | select(.verdict == "wrong_tool")] | group_by([.expected, .actual]) | map({expected: .[0].expected, actual: .[0].actual, count: length}) | sort_by(-.count)')
# Compute accuracy
if [[ "$TOTAL" -gt 0 ]]; then
ACCURACY=$(echo "scale=3; $CORRECT / $TOTAL" | bc)
else
ACCURACY="0.000"
fi
# Build final output
RESULT=$(jq -n \
--argjson accuracy "$ACCURACY" \
--argjson total "$TOTAL" \
--argjson correct "$CORRECT" \
--argjson wrong_tool "$WRONG_TOOL" \
--argjson false_accept "$FALSE_ACCEPT" \
--argjson false_reject "$FALSE_REJECT" \
--argjson grades "$GRADES" \
--argjson confusion "$CONFUSION" \
--argjson per_tool "$PER_TOOL" \
--argjson worst "$WORST" \
'{
accuracy: $accuracy,
total: $total,
correct: $correct,
wrong_tool: $wrong_tool,
false_accept: $false_accept,
false_reject: $false_reject,
per_tool: $per_tool,
confusion_matrix: $confusion,
worst_confusions: $worst,
grades: $grades
}')
if [[ -n "$OUTPUT" ]]; then
echo "$RESULT" > "$OUTPUT"
echo "Grading saved to $OUTPUT" >&2
else
echo "$RESULT"
fi
# Summary to stderr
echo "" >&2
echo "Selection accuracy: ${ACCURACY} ($CORRECT/$TOTAL correct)" >&2
echo " Wrong tool: $WRONG_TOOL | False accept: $FALSE_ACCEPT | False reject: $FALSE_REJECT" >&2
if [[ $(echo "$WORST" | jq 'length') -gt 0 ]]; then
echo "" >&2
echo "Worst confusions:" >&2
echo "$WORST" | jq -r '.[:3][] | " \(.expected) → \(.actual) (\(.count)x)"' >&2
fi
[[ "$CORRECT" -eq "$TOTAL" ]] && exit 0 || exit 1
Related skills
FAQ
What does eval-mcp do?
eval-mcp: A skill for development. This provides functionality for development workflows.
When should I use eval-mcp?
When you need to use eval-mcp for development tasks, or when eval-mcp: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
eval-mcp.