mispick 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. mispick-0.1.0/.github/ISSUE_TEMPLATE/benchmark-rerun.md +27 -0
  2. mispick-0.1.0/.github/workflows/benchmark.yml +76 -0
  3. mispick-0.1.0/.github/workflows/ci.yml +72 -0
  4. mispick-0.1.0/.github/workflows/pages.yml +41 -0
  5. mispick-0.1.0/.github/workflows/release.yml +89 -0
  6. mispick-0.1.0/.gitignore +18 -0
  7. mispick-0.1.0/CHANGELOG.md +23 -0
  8. mispick-0.1.0/CLAUDE.md +29 -0
  9. mispick-0.1.0/CONTRIBUTING.md +60 -0
  10. mispick-0.1.0/LICENSE +21 -0
  11. mispick-0.1.0/PKG-INFO +217 -0
  12. mispick-0.1.0/README.md +190 -0
  13. mispick-0.1.0/SPEC.md +210 -0
  14. mispick-0.1.0/action.yml +181 -0
  15. mispick-0.1.0/benchmark/README.md +42 -0
  16. mispick-0.1.0/benchmark/build_site.py +185 -0
  17. mispick-0.1.0/benchmark/capture.py +116 -0
  18. mispick-0.1.0/benchmark/measure.py +175 -0
  19. mispick-0.1.0/benchmark/pick.py +142 -0
  20. mispick-0.1.0/benchmark/snapshots/README.md +19 -0
  21. mispick-0.1.0/docs/launch-checklist.md +103 -0
  22. mispick-0.1.0/docs/media/demo.svg +229 -0
  23. mispick-0.1.0/docs/research.md +549 -0
  24. mispick-0.1.0/examples/github-action/README.md +86 -0
  25. mispick-0.1.0/pyproject.toml +81 -0
  26. mispick-0.1.0/scripts/check_action_pins.py +95 -0
  27. mispick-0.1.0/scripts/make_demo.py +68 -0
  28. mispick-0.1.0/scripts/measure-base.sh +40 -0
  29. mispick-0.1.0/scripts/measure.sh +106 -0
  30. mispick-0.1.0/src/mispick/__init__.py +11 -0
  31. mispick-0.1.0/src/mispick/cli.py +564 -0
  32. mispick-0.1.0/src/mispick/crossserver.py +168 -0
  33. mispick-0.1.0/src/mispick/fix.py +431 -0
  34. mispick-0.1.0/src/mispick/generate.py +475 -0
  35. mispick-0.1.0/src/mispick/loader.py +102 -0
  36. mispick-0.1.0/src/mispick/metrics.py +306 -0
  37. mispick-0.1.0/src/mispick/models/__init__.py +13 -0
  38. mispick-0.1.0/src/mispick/models/anthropic.py +127 -0
  39. mispick-0.1.0/src/mispick/models/base.py +132 -0
  40. mispick-0.1.0/src/mispick/models/mock.py +234 -0
  41. mispick-0.1.0/src/mispick/models/ollama.py +228 -0
  42. mispick-0.1.0/src/mispick/models/openai.py +21 -0
  43. mispick-0.1.0/src/mispick/models/registry.py +61 -0
  44. mispick-0.1.0/src/mispick/report/__init__.py +5 -0
  45. mispick-0.1.0/src/mispick/report/badge.py +83 -0
  46. mispick-0.1.0/src/mispick/report/fixes.py +132 -0
  47. mispick-0.1.0/src/mispick/report/html.py +294 -0
  48. mispick-0.1.0/src/mispick/report/json.py +140 -0
  49. mispick-0.1.0/src/mispick/report/load.py +90 -0
  50. mispick-0.1.0/src/mispick/report/markdown.py +173 -0
  51. mispick-0.1.0/src/mispick/report/provenance.py +78 -0
  52. mispick-0.1.0/src/mispick/report/terminal.py +244 -0
  53. mispick-0.1.0/src/mispick/select.py +161 -0
  54. mispick-0.1.0/src/mispick/sources/__init__.py +7 -0
  55. mispick-0.1.0/src/mispick/sources/config.py +171 -0
  56. mispick-0.1.0/src/mispick/sources/server.py +174 -0
  57. mispick-0.1.0/src/mispick/sources/snapshot.py +81 -0
  58. mispick-0.1.0/src/mispick/types.py +127 -0
  59. mispick-0.1.0/tests/conftest.py +31 -0
  60. mispick-0.1.0/tests/fixtures/__init__.py +0 -0
  61. mispick-0.1.0/tests/fixtures/confusing_tools.json +77 -0
  62. mispick-0.1.0/tests/fixtures/crashing_server.py +6 -0
  63. mispick-0.1.0/tests/fixtures/fixture_server.py +56 -0
  64. mispick-0.1.0/tests/fixtures/second_server.py +24 -0
  65. mispick-0.1.0/tests/fixtures/two_servers.json +12 -0
  66. mispick-0.1.0/tests/test_action.py +122 -0
  67. mispick-0.1.0/tests/test_backends.py +145 -0
  68. mispick-0.1.0/tests/test_benchmark.py +233 -0
  69. mispick-0.1.0/tests/test_cli.py +202 -0
  70. mispick-0.1.0/tests/test_crossserver.py +187 -0
  71. mispick-0.1.0/tests/test_fix.py +368 -0
  72. mispick-0.1.0/tests/test_generate.py +304 -0
  73. mispick-0.1.0/tests/test_metrics.py +282 -0
  74. mispick-0.1.0/tests/test_never_calls_tools.py +87 -0
  75. mispick-0.1.0/tests/test_reports.py +315 -0
  76. mispick-0.1.0/tests/test_roundtrip.py +161 -0
  77. mispick-0.1.0/tests/test_select.py +137 -0
  78. mispick-0.1.0/tests/test_sources.py +256 -0
  79. mispick-0.1.0/uv.lock +1432 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: Request a benchmark re-run
3
+ about: Ask for a server to be re-measured, or point out a result that looks wrong
4
+ title: 'Re-run: <server name>'
5
+ labels: benchmark
6
+ ---
7
+
8
+ **Server**
9
+ <!-- The registry name as it appears on the leaderboard, e.g. io.github.owner/repo -->
10
+
11
+ **Why**
12
+ <!-- Pick whichever applies:
13
+ - the descriptions changed since the snapshot was captured
14
+ - the snapshot is missing tools, or has tools that no longer exist
15
+ - the result looks wrong for a reason you can name
16
+ - the server was skipped but does start without credentials
17
+ -->
18
+
19
+ **Anything we should know about running it**
20
+ <!-- e.g. it needs a flag, or it only exposes its full tool list after some setup -->
21
+
22
+ ---
23
+
24
+ A reminder on what the benchmark is: one model, one seed, one day, reading only
25
+ `initialize` and `tools/list`. A low score is a finding about how that model reads a tool
26
+ surface, not a judgement of the server. If you think the framing on your server's page is
27
+ unfair, say so here and we will fix the wording.
@@ -0,0 +1,76 @@
1
+ name: benchmark
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ limit:
7
+ description: 'How many servers to pick from the registry'
8
+ default: '30'
9
+ refresh-snapshots:
10
+ description: 'Re-capture tool lists from the registry (otherwise use the committed ones)'
11
+ type: boolean
12
+ default: false
13
+ schedule:
14
+ # Weekly, not nightly: tool surfaces do not change that fast, and every run starts other
15
+ # people's servers.
16
+ - cron: '17 4 * * 1'
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ concurrency:
22
+ group: benchmark
23
+ cancel-in-progress: false
24
+
25
+ jobs:
26
+ benchmark:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - uses: astral-sh/setup-uv@v10.2.0
31
+ with:
32
+ python-version: '3.12'
33
+ enable-cache: true
34
+ - run: uv sync
35
+
36
+ - name: Pick servers
37
+ if: ${{ inputs.refresh-snapshots }}
38
+ run: uv run python benchmark/pick.py --limit "${{ inputs.limit || '30' }}"
39
+
40
+ - name: Capture tool lists
41
+ if: ${{ inputs.refresh-snapshots }}
42
+ run: uv run python benchmark/capture.py
43
+
44
+ - name: Install Ollama
45
+ run: |
46
+ curl -fsSL https://ollama.com/install.sh | sh
47
+ ollama serve &
48
+ for _ in $(seq 1 30); do
49
+ curl -sf http://localhost:11434/api/tags >/dev/null && break
50
+ sleep 1
51
+ done
52
+ ollama pull qwen3.5:4b
53
+
54
+ - name: Measure
55
+ run: |
56
+ uv run python benchmark/measure.py \
57
+ --snapshots benchmark/snapshots \
58
+ --out benchmark/results \
59
+ --model ollama/qwen3.5:4b \
60
+ --queries 8 --runs 3 --temperature 0 --seed 1 --concurrency 4
61
+
62
+ - name: Build the site
63
+ run: uv run python benchmark/build_site.py
64
+
65
+ - uses: actions/upload-artifact@v7
66
+ with:
67
+ name: benchmark
68
+ path: |
69
+ benchmark/results
70
+ benchmark/site
71
+
72
+ # Publishing is a separate, manually reviewed step: the site names other people's
73
+ # projects, so a bad run must not go live on a schedule.
74
+ - uses: actions/upload-pages-artifact@v5
75
+ with:
76
+ path: benchmark/site
@@ -0,0 +1,72 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, macos-latest]
18
+ python: ['3.11', '3.12', '3.13']
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: astral-sh/setup-uv@v10.2.0
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ enable-cache: true
25
+ - run: uv sync --all-extras
26
+ - name: Lint
27
+ run: uv run ruff check src tests
28
+ - name: Types
29
+ run: uv run mypy src
30
+ - name: Tests
31
+ run: uv run pytest -q
32
+ # The suite must pass with no network and no model backend. This is the check that
33
+ # keeps that true.
34
+ - name: Tests are offline
35
+ run: uv run pytest -q
36
+ env:
37
+ OPENAI_API_KEY: ''
38
+ ANTHROPIC_API_KEY: ''
39
+ # Point outbound HTTP at a dead port, so a test that reaches the network fails
40
+ # here instead of passing quietly because the runner happens to be online.
41
+ # NO_PROXY='*' would do the opposite: it permits direct connections.
42
+ HTTP_PROXY: 'http://127.0.0.1:9'
43
+ HTTPS_PROXY: 'http://127.0.0.1:9'
44
+
45
+ workflows:
46
+ # Two failure modes that arrive with no job and no log, so they are checked here:
47
+ # a workflow file GitHub rejects outright (e.g. two env keys differing only in case),
48
+ # and a `uses:` pin whose tag does not exist, which fails job setup.
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v7
52
+ - uses: docker://rhysd/actionlint:latest
53
+ with:
54
+ args: -color
55
+ - name: Every action pin resolves
56
+ run: python3 scripts/check_action_pins.py
57
+ env:
58
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59
+
60
+ action:
61
+ # Proves action.yml at least parses and skips cleanly with no model key.
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+ - uses: ./
66
+ with:
67
+ snapshot: tests/fixtures/confusing_tools.json
68
+ comment: 'false'
69
+ - name: The run should have skipped, not failed
70
+ run: |
71
+ test -f mispick-comment.md
72
+ grep -q "skipped" mispick-comment.md
@@ -0,0 +1,41 @@
1
+ name: pages
2
+
3
+ # Deliberately manual. The leaderboard names other people's projects, so a run is reviewed
4
+ # before it is published - a scheduled deploy could put a bad measurement in front of them.
5
+ on:
6
+ workflow_dispatch:
7
+ inputs:
8
+ run-id:
9
+ description: 'The benchmark workflow run whose site artifact should be published'
10
+ required: true
11
+
12
+ permissions:
13
+ contents: read
14
+ pages: write
15
+ id-token: write
16
+
17
+ concurrency:
18
+ group: pages
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ deploy:
23
+ environment:
24
+ name: github-pages
25
+ url: ${{ steps.deploy.outputs.page_url }}
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - name: Download the reviewed site
30
+ uses: actions/download-artifact@v8
31
+ with:
32
+ name: benchmark
33
+ run-id: ${{ inputs.run-id }}
34
+ github-token: ${{ secrets.GITHUB_TOKEN }}
35
+ path: downloaded
36
+ - uses: actions/configure-pages@v6
37
+ - uses: actions/upload-pages-artifact@v5
38
+ with:
39
+ path: downloaded/site
40
+ - id: deploy
41
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,89 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI via Trusted Publishing - no API token is stored in this repo.
4
+ # Set up once at https://pypi.org/manage/account/publishing/ with:
5
+ # owner: msc35 repository: mispick workflow: release.yml environment: pypi
6
+ on:
7
+ push:
8
+ tags: ['v*']
9
+ workflow_dispatch:
10
+ inputs:
11
+ target:
12
+ description: 'Where to publish'
13
+ type: choice
14
+ options: [testpypi, pypi]
15
+ default: testpypi
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ - uses: astral-sh/setup-uv@v10.2.0
26
+ with:
27
+ python-version: '3.12'
28
+
29
+ # Never ship a release that does not pass its own tests.
30
+ - run: uv sync --all-extras
31
+ - run: uv run ruff check .
32
+ - run: uv run mypy src
33
+ - run: uv run pytest -q
34
+
35
+ - name: Check the tag matches the version
36
+ if: startsWith(github.ref, 'refs/tags/v')
37
+ run: |
38
+ version=$(uv run python -c 'import mispick; print(mispick.__version__)')
39
+ tag="${GITHUB_REF_NAME#v}"
40
+ if [ "$version" != "$tag" ]; then
41
+ echo "::error::tag $tag does not match mispick.__version__ $version"
42
+ exit 1
43
+ fi
44
+ echo "tag $tag matches version $version"
45
+
46
+ - run: uv build
47
+
48
+ # Prove the built wheel actually runs before it is published, rather than after.
49
+ - name: Smoke-test the wheel
50
+ run: |
51
+ uvx --from ./dist/*.whl mispick version
52
+ uvx --from ./dist/*.whl mispick run \
53
+ --snapshot tests/fixtures/confusing_tools.json --model mock -n 2 -k 1 \
54
+ --cache-dir "$(mktemp -d)"
55
+
56
+ - uses: actions/upload-artifact@v7
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ testpypi:
62
+ needs: build
63
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
64
+ runs-on: ubuntu-latest
65
+ environment: testpypi
66
+ permissions:
67
+ id-token: write
68
+ steps:
69
+ - uses: actions/download-artifact@v8
70
+ with:
71
+ name: dist
72
+ path: dist/
73
+ - uses: pypa/gh-action-pypi-publish@release/v1
74
+ with:
75
+ repository-url: https://test.pypi.org/legacy/
76
+
77
+ pypi:
78
+ needs: build
79
+ if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
80
+ runs-on: ubuntu-latest
81
+ environment: pypi
82
+ permissions:
83
+ id-token: write
84
+ steps:
85
+ - uses: actions/download-artifact@v8
86
+ with:
87
+ name: dist
88
+ path: dist/
89
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .toolconf/
12
+ *-badge.svg
13
+ .DS_Store
14
+ benchmark/site/
15
+ benchmark/results/
16
+ mispick-report.*
17
+ mispick-comment.md
18
+ mispick-badge.svg
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — unreleased
4
+
5
+ First release. Not yet published to PyPI.
6
+
7
+ - Load tools from a stdio server, a Streamable HTTP server, a captured snapshot, or every server
8
+ in a `claude_desktop_config.json` / `.mcp.json` at once. Only `initialize` and `tools/list` are
9
+ ever sent; `tools/call` is unreachable by construction and by test.
10
+ - Generate a test set per tool (straightforward, paraphrased, hard negatives against the nearest
11
+ neighbour, plus "no tool fits" cases), cached to an editable `.mispick/queries.yaml` keyed on a
12
+ per-tool fingerprint.
13
+ - Measure selection accuracy, per-tool precision and recall, a confusion matrix with `(none)` and
14
+ `(phantom)` rows, argument validity against `inputSchema`, stability across K runs, over-
15
+ triggering, and a tool-list token estimate. Every rate carries a Wilson 95% interval.
16
+ - `mispick fix`: rewrite the worst confused pairs, re-run the same queries, and accept a rewrite
17
+ only when an exact McNemar test agrees. Rejected rewrites are shown.
18
+ - Cross-server analysis: name collisions plus measured leakage between servers.
19
+ - Reports: terminal, JSON (versioned schema), Markdown for PR comments, a self-contained HTML page,
20
+ and an SVG badge. `mispick report` re-renders a saved run; `mispick compare` diffs two runs.
21
+ - Backends: Ollama (default, local, free), OpenAI, Anthropic, and a deterministic mock.
22
+ - GitHub Action with a sticky PR comment, base-branch comparison and `--fail-under`.
23
+ - Benchmark scripts and a static leaderboard generator.
@@ -0,0 +1,29 @@
1
+ # CLAUDE.md
2
+
3
+ Read `SPEC.md` fully before doing anything. It is the source of truth.
4
+
5
+ ## How to work on this repo
6
+ - Work milestone by milestone (SPEC section 10). Start with **M0 research**. Do not write product code until `docs/research.md` exists and the user has reviewed it.
7
+ - After each milestone: run tests, update README, commit, and stop to summarize what changed and what's next.
8
+ - If research shows a competitor already does the four differentiators in SPEC section 2, stop and report. Don't build a duplicate.
9
+ - Ask before adding any dependency not listed in SPEC section 5.
10
+
11
+ ## Non-negotiables
12
+ - **Never call `tools/call` on any MCP server.** Only `initialize` and `tools/list`. There's a test for this. Keep it passing.
13
+ - Default model backend is local Ollama. Tests use a mocked backend and must pass offline.
14
+ - Never modify the user's MCP server source. Fix mode only suggests.
15
+ - Reports always state model, N, K, and date.
16
+
17
+ ## Commands
18
+ - `uv sync --all-extras` — install. **Use `--all-extras`**: plain `uv sync` leaves the
19
+ `anthropic` extra out, and `[[tool.mypy.overrides]]` ignores its imports when missing, so
20
+ `mypy` silently skips `models/anthropic.py`. That hid a backend that raised `TypeError` on
21
+ every call.
22
+ - `uv run pytest` — tests
23
+ - `uv run ruff check . && uv run mypy src` — lint and types
24
+ - `uv run mispick run --snapshot tests/fixtures/confusing_tools.json` — quick manual check
25
+
26
+ ## Style
27
+ - Python 3.11+, type hints everywhere, pydantic models for data.
28
+ - Small modules, small functions. No frameworks beyond SPEC section 5.
29
+ - Clear error messages that tell the user what to do next.
@@ -0,0 +1,60 @@
1
+ # Contributing
2
+
3
+ ```bash
4
+ uv sync --all-extras # --all-extras matters, see below
5
+ uv run pytest
6
+ uv run ruff check . && uv run mypy src
7
+ ```
8
+
9
+ **Always `--all-extras`.** Plain `uv sync` leaves the optional `anthropic` dependency out, and
10
+ mypy is configured to ignore its imports when absent — so `models/anthropic.py` goes unchecked
11
+ and nothing tells you. That is exactly how a backend that passed `temperature` to an API which
12
+ accepts no `temperature`, and would have raised `TypeError` on every call, survived a green
13
+ local run. CI uses `--all-extras` and caught it on the first push.
14
+
15
+ Workflows are linted too; `ruff` does not read them:
16
+
17
+ ```bash
18
+ actionlint # brew install actionlint
19
+ python3 scripts/check_action_pins.py # every `uses:` tag actually exists
20
+ ```
21
+
22
+ Both guard against failures that arrive with **no job and no log**, which are miserable to debug:
23
+
24
+ - A workflow with two `env` keys differing only in case is rejected by GitHub wholesale, giving a
25
+ zero-second run and the message "likely a workflow file issue".
26
+ - A `uses:` pin whose tag does not exist fails *job setup*, before any step runs. Note that not
27
+ every repository publishes moving major tags — `actions/*` do, but `astral-sh/setup-uv` and
28
+ `marocchino/sticky-pull-request-comment` publish only exact versions, so reading the latest
29
+ release name is not enough. `release.yml` and `pages.yml` only run at release time, so a stale
30
+ pin there would surface at the worst possible moment.
31
+
32
+ ## Things the tests enforce, so a reviewer does not have to
33
+
34
+ **mispick never calls a tool.** `tests/test_never_calls_tools.py` fails the build if any module
35
+ under `src/mispick/` so much as references `call_tool` or the string `tools/call`, if anything
36
+ returned by `sources/` exposes a client, or if a tool load succeeds with `Client.call_tool`
37
+ monkeypatched to raise. This is the promise that makes mispick safe to point at a destructive
38
+ server. If you need to change it, you need a different tool.
39
+
40
+ **The suite runs offline.** No network, no Ollama, no API keys. Use `--model mock`. CI re-runs the
41
+ suite with the key environment variables blanked to keep this true.
42
+
43
+ **Every report states the model, N, K and the date**, and carries the caveat that results depend on
44
+ the model. There are tests for this in every format, including one that renders the terminal report
45
+ at 52 columns to make sure the date is not truncated.
46
+
47
+ **Unmeasured rates are `null`, not `0`.** A K=1 run has no stability; reporting 0% would be a lie.
48
+
49
+ **Benchmark wording stays neutral.** "Most confused pair", never "worst server". Tested.
50
+
51
+ ## Style
52
+
53
+ Python 3.11+, type hints everywhere, pydantic for data, small modules. Error messages say what to
54
+ do next, not just what went wrong. `ruff` and `mypy --strict` clean.
55
+
56
+ ## Adding a model backend
57
+
58
+ Implement `mispick.models.base.Backend` (`choose` and `generate`), register it in
59
+ `models/registry.py`, and set `supports_seed` honestly — reports use it to decide whether to claim
60
+ a run is reproducible.
mispick-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mispick contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mispick-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.5
2
+ Name: mispick
3
+ Version: 0.1.0
4
+ Summary: Find out which of your MCP tools the model mixes up - measured, not guessed.
5
+ Project-URL: Homepage, https://github.com/msc35/mispick
6
+ Project-URL: Repository, https://github.com/msc35/mispick
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: confusion-matrix,evaluation,llm,mcp,model-context-protocol,tools
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: jsonschema>=4.20.0
18
+ Requires-Dist: mcp>=2.2.0
19
+ Requires-Dist: openai>=1.60.0
20
+ Requires-Dist: pydantic>=2.12.0
21
+ Requires-Dist: pyyaml>=6.0
22
+ Requires-Dist: rich>=13.9.4
23
+ Requires-Dist: typer>=0.16.0
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # mispick
29
+
30
+ **Find out which of your MCP tools the model mixes up — measured, not guessed.**
31
+
32
+ <p align="center">
33
+ <img src="docs/media/demo.svg" alt="mispick's terminal report: a confusion matrix showing search_docs and search_issues being mistaken for each other, per-tool precision and recall with confidence intervals, and a 0-100 score." width="900">
34
+ </p>
35
+
36
+ ```bash
37
+ uvx mispick run --cmd "python -m my_server"
38
+ ```
39
+
40
+ Point it at any MCP server. It generates realistic user requests, asks a model to pick a tool for
41
+ each one, and shows you a **confusion matrix**: which tools get chosen for which intent. Then it
42
+ proposes description rewrites for the confused pairs and **re-runs to prove the fix helped**.
43
+
44
+ It never calls a tool. Only `initialize` and `tools/list`, so it is safe to point at a server that
45
+ deletes things.
46
+
47
+ ---
48
+
49
+ ## Why this rather than a static linter
50
+
51
+ `mcp-lint`, `mcpolish`, `oh-my-mcp`, `mcp-conform` and `mcp-cve-lint` check tool definitions
52
+ against rules, and they are good at it — run one. But a description can pass every rule and still
53
+ be indistinguishable from the tool next to it. Rule-based linters predict problems; this measures
54
+ them, by putting your tool list in front of a real model and counting what it picks. Two other
55
+ projects do measure selection — [`whichtool`](https://github.com/mattagame/whichtool) (local
56
+ models, but no rewrites, one server per run) and [`toolfit`](https://pypi.org/project/toolfit/)
57
+ (rewrites, but cloud-only, one server per run). Neither looks across servers. The full comparison,
58
+ with what each one actually does, is in [docs/research.md](docs/research.md).
59
+
60
+ ## How it works
61
+
62
+ 1. Connect to your server and read `tools/list` (never `tools/call`).
63
+ 2. For each tool, generate N realistic requests — plus **hard negatives** written to sit as close
64
+ as possible to its nearest neighbour, and some requests no tool should answer.
65
+ 3. Ask the model to choose a tool for each request, K times, showing it the real tool definitions
66
+ a client would send.
67
+ 4. Score it: accuracy, per-tool precision and recall, a confusion matrix, argument validity,
68
+ stability across the K runs — every rate with a Wilson 95% interval.
69
+ 5. Propose rewrites for the worst pairs, re-run the same requests against them, and keep only the
70
+ ones that measurably won.
71
+
72
+ ## Install
73
+
74
+ > **Not on PyPI yet.** `mispick` has not been released, so the commands below do not work
75
+ > today. Until then, run it from a checkout:
76
+ >
77
+ > ```bash
78
+ > git clone https://github.com/msc35/mispick && cd mispick
79
+ > uv sync --all-extras
80
+ > uv run mispick --help
81
+ > ```
82
+
83
+ ```bash
84
+ uvx mispick --help # no install
85
+ uv tool install mispick # or keep it
86
+ pip install mispick
87
+ ```
88
+
89
+ The default backend is a local model through [Ollama](https://ollama.com), so it costs nothing and
90
+ needs no account:
91
+
92
+ ```bash
93
+ ollama pull qwen3.5:4b
94
+ ```
95
+
96
+ Cloud models are optional: `--model openai/gpt-4.1-mini` or
97
+ `--model anthropic/claude-haiku-4-5-20251001`, with the matching key in the environment.
98
+
99
+ ## Use
100
+
101
+ ```bash
102
+ # measure a local server, a remote one, or a captured snapshot
103
+ mispick run --cmd "python -m my_server"
104
+ mispick run --url https://example.com/mcp
105
+ mispick run --snapshot tools.json
106
+
107
+ # capture a snapshot so later runs are offline and reproducible
108
+ mispick snapshot --cmd "python -m my_server" -o tools.json
109
+
110
+ # every server in your config at once, and what they do to each other
111
+ mispick run --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
112
+
113
+ # rewrites for the worst pairs, each one re-tested
114
+ mispick fix --cmd "python -m my_server" --patch fix.diff
115
+
116
+ # reports and CI
117
+ mispick run --snapshot tools.json --format html --out report.html --badge badge.svg
118
+ mispick run --snapshot tools.json --fail-under 80 # exit 1 below the threshold
119
+ mispick compare base.json head.json --max-drop 5 # exit 1 on a regression
120
+ ```
121
+
122
+ Generated requests are cached in `.mispick/queries.yaml`. **Read that file and edit it** — it is
123
+ your test set, and it only regenerates for a tool whose name, description or schema changed.
124
+ Commit it.
125
+
126
+ ### Proven fixes
127
+
128
+ `mispick fix` rewrites both descriptions in a confused pair, then re-runs the *same* requests
129
+ against the rewrite and reports before/after with an exact McNemar test — the trials are paired,
130
+ so an unpaired test would be the wrong one. Rewrites that fail are shown too.
131
+
132
+ That re-test is the whole point. Hasan et al. rewrote tool descriptions with an LLM across 231
133
+ tasks and found success rose by a median 5.85 points — but **regressed in 16.67% of cases**
134
+ ([arXiv 2602.14878](https://arxiv.org/abs/2602.14878)). An unmeasured rewrite is a one-in-six
135
+ chance of making things worse. mispick also caps rewrite length, so a rewrite cannot win by
136
+ crowding out its neighbours, and it never edits your source — it prints a suggestion.
137
+
138
+ **Expect rejections, especially on a small local model.** In our own runs against
139
+ `ollama/qwen3.5:4b`, two of three proposed rewrites measurably made selection *worse* — one of them
140
+ took a tool from 78% to 58% accuracy, breaking 14 trials and fixing none, and it replicated when
141
+ re-run with three times the data. Its stated reasoning read perfectly sensibly. The third improved
142
+ by 10 points but over only two changed trials, which is not enough to call, so mispick reported it
143
+ as inconclusive rather than banking it. A 4B model is good enough to *find* confusion and often not
144
+ good enough to *write its way out of it*; fix mode is more productive with a larger model. Either
145
+ way the verdicts are the product — a run where nothing is accepted has told you something true.
146
+
147
+ ### Across servers
148
+
149
+ Point `--config` at a whole `claude_desktop_config.json` and mispick loads every server at once,
150
+ because that is how a client sees them. It reports which bare tool names collide across servers
151
+ and — the part that matters — how often a request for one server's tool is answered by another
152
+ server's. On the two-server fixture, 17% of trials cross servers and `workspace:search_docs` falls
153
+ to 0% recall: every request for it is answered by `wiki:search_docs`. The MCP spec names this
154
+ problem and warns that `serverInfo.name` is not unique across servers, so mispick keys on your
155
+ config label instead.
156
+
157
+ ### In CI
158
+
159
+ ```yaml
160
+ - uses: actions/checkout@v4
161
+ - uses: msc35/mispick@v0.1.0
162
+ with:
163
+ snapshot: tools.json
164
+ model: anthropic/claude-haiku-4-5-20251001
165
+ fail-under: '80'
166
+ compare-base: 'true'
167
+ env:
168
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
169
+ ```
170
+
171
+ It posts the report as a sticky PR comment, and with `compare-base` the comment reads
172
+ `84 → 91 (▲7)` and names the confusions that appeared or disappeared. With no model key it skips
173
+ with an explanation instead of failing — a check that goes red over a missing secret is a check
174
+ people learn to ignore. See [examples/github-action](examples/github-action/).
175
+
176
+ ## Benchmark
177
+
178
+ `benchmark/` runs the pipeline over servers from the official MCP registry that start without
179
+ credentials, and builds a static leaderboard. Snapshots are committed so results are reproducible,
180
+ only `initialize` and `tools/list` are ever sent, and the page lists the servers it could *not*
181
+ measure so the coverage is not overstated. See [benchmark/README.md](benchmark/README.md).
182
+
183
+ ## Limitations
184
+
185
+ Read these before quoting a number.
186
+
187
+ - **Results depend on the model.** A score is one model's reading of one tool surface on one day,
188
+ not a property of your server. Every report says so, and states the model, N, K and the date.
189
+ - **Small local models are noisy.** That is why every rate carries a Wilson 95% interval and why
190
+ `--runs` exists. With K=3 the intervals are wide; read them, not the point estimate.
191
+ - **The requests are generated, so they are not your traffic.** They are a stand-in for it. Edit
192
+ `.mispick/queries.yaml` to make them yours.
193
+ - **It measures one selection turn.** Not multi-step agent behaviour, not tool output quality, not
194
+ recovery after a bad call. Arguments are checked against `inputSchema` and no further.
195
+ - **A low score can be correct.** Two tools may genuinely overlap. mispick tells you the model
196
+ cannot separate them; whether they *should* be separable is your call.
197
+ - **`mispick fix` often accepts nothing**, and that is working as intended. See the note above.
198
+ - **Token counts are estimates**, and lower bounds — a real tokenizer and the provider's framing
199
+ both add to them.
200
+ - **Reasoning models** spend heavily on hidden thinking. mispick turns thinking off by default for
201
+ Ollama (measured: 89s → 2.2s per call on `qwen3.5:4b`, same answers); `--think` restores it.
202
+
203
+ ## Development
204
+
205
+ ```bash
206
+ uv sync
207
+ uv run pytest # offline; no model or network needed
208
+ uv run ruff check . && uv run mypy src
209
+ uv run mispick run --snapshot tests/fixtures/confusing_tools.json --model mock
210
+ ```
211
+
212
+ `--model mock` is a deterministic offline backend. The test suite uses it, so `pytest` passes with
213
+ no Ollama, no keys and no network.
214
+
215
+ ## License
216
+
217
+ MIT. See [LICENSE](LICENSE).