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.
- mispick-0.1.0/.github/ISSUE_TEMPLATE/benchmark-rerun.md +27 -0
- mispick-0.1.0/.github/workflows/benchmark.yml +76 -0
- mispick-0.1.0/.github/workflows/ci.yml +72 -0
- mispick-0.1.0/.github/workflows/pages.yml +41 -0
- mispick-0.1.0/.github/workflows/release.yml +89 -0
- mispick-0.1.0/.gitignore +18 -0
- mispick-0.1.0/CHANGELOG.md +23 -0
- mispick-0.1.0/CLAUDE.md +29 -0
- mispick-0.1.0/CONTRIBUTING.md +60 -0
- mispick-0.1.0/LICENSE +21 -0
- mispick-0.1.0/PKG-INFO +217 -0
- mispick-0.1.0/README.md +190 -0
- mispick-0.1.0/SPEC.md +210 -0
- mispick-0.1.0/action.yml +181 -0
- mispick-0.1.0/benchmark/README.md +42 -0
- mispick-0.1.0/benchmark/build_site.py +185 -0
- mispick-0.1.0/benchmark/capture.py +116 -0
- mispick-0.1.0/benchmark/measure.py +175 -0
- mispick-0.1.0/benchmark/pick.py +142 -0
- mispick-0.1.0/benchmark/snapshots/README.md +19 -0
- mispick-0.1.0/docs/launch-checklist.md +103 -0
- mispick-0.1.0/docs/media/demo.svg +229 -0
- mispick-0.1.0/docs/research.md +549 -0
- mispick-0.1.0/examples/github-action/README.md +86 -0
- mispick-0.1.0/pyproject.toml +81 -0
- mispick-0.1.0/scripts/check_action_pins.py +95 -0
- mispick-0.1.0/scripts/make_demo.py +68 -0
- mispick-0.1.0/scripts/measure-base.sh +40 -0
- mispick-0.1.0/scripts/measure.sh +106 -0
- mispick-0.1.0/src/mispick/__init__.py +11 -0
- mispick-0.1.0/src/mispick/cli.py +564 -0
- mispick-0.1.0/src/mispick/crossserver.py +168 -0
- mispick-0.1.0/src/mispick/fix.py +431 -0
- mispick-0.1.0/src/mispick/generate.py +475 -0
- mispick-0.1.0/src/mispick/loader.py +102 -0
- mispick-0.1.0/src/mispick/metrics.py +306 -0
- mispick-0.1.0/src/mispick/models/__init__.py +13 -0
- mispick-0.1.0/src/mispick/models/anthropic.py +127 -0
- mispick-0.1.0/src/mispick/models/base.py +132 -0
- mispick-0.1.0/src/mispick/models/mock.py +234 -0
- mispick-0.1.0/src/mispick/models/ollama.py +228 -0
- mispick-0.1.0/src/mispick/models/openai.py +21 -0
- mispick-0.1.0/src/mispick/models/registry.py +61 -0
- mispick-0.1.0/src/mispick/report/__init__.py +5 -0
- mispick-0.1.0/src/mispick/report/badge.py +83 -0
- mispick-0.1.0/src/mispick/report/fixes.py +132 -0
- mispick-0.1.0/src/mispick/report/html.py +294 -0
- mispick-0.1.0/src/mispick/report/json.py +140 -0
- mispick-0.1.0/src/mispick/report/load.py +90 -0
- mispick-0.1.0/src/mispick/report/markdown.py +173 -0
- mispick-0.1.0/src/mispick/report/provenance.py +78 -0
- mispick-0.1.0/src/mispick/report/terminal.py +244 -0
- mispick-0.1.0/src/mispick/select.py +161 -0
- mispick-0.1.0/src/mispick/sources/__init__.py +7 -0
- mispick-0.1.0/src/mispick/sources/config.py +171 -0
- mispick-0.1.0/src/mispick/sources/server.py +174 -0
- mispick-0.1.0/src/mispick/sources/snapshot.py +81 -0
- mispick-0.1.0/src/mispick/types.py +127 -0
- mispick-0.1.0/tests/conftest.py +31 -0
- mispick-0.1.0/tests/fixtures/__init__.py +0 -0
- mispick-0.1.0/tests/fixtures/confusing_tools.json +77 -0
- mispick-0.1.0/tests/fixtures/crashing_server.py +6 -0
- mispick-0.1.0/tests/fixtures/fixture_server.py +56 -0
- mispick-0.1.0/tests/fixtures/second_server.py +24 -0
- mispick-0.1.0/tests/fixtures/two_servers.json +12 -0
- mispick-0.1.0/tests/test_action.py +122 -0
- mispick-0.1.0/tests/test_backends.py +145 -0
- mispick-0.1.0/tests/test_benchmark.py +233 -0
- mispick-0.1.0/tests/test_cli.py +202 -0
- mispick-0.1.0/tests/test_crossserver.py +187 -0
- mispick-0.1.0/tests/test_fix.py +368 -0
- mispick-0.1.0/tests/test_generate.py +304 -0
- mispick-0.1.0/tests/test_metrics.py +282 -0
- mispick-0.1.0/tests/test_never_calls_tools.py +87 -0
- mispick-0.1.0/tests/test_reports.py +315 -0
- mispick-0.1.0/tests/test_roundtrip.py +161 -0
- mispick-0.1.0/tests/test_select.py +137 -0
- mispick-0.1.0/tests/test_sources.py +256 -0
- 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
|
mispick-0.1.0/.gitignore
ADDED
|
@@ -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.
|
mispick-0.1.0/CLAUDE.md
ADDED
|
@@ -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).
|