mcp-linter 1.2.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 (114) hide show
  1. mcp_linter-1.2.0/.github/workflows/ci.yml +74 -0
  2. mcp_linter-1.2.0/.github/workflows/lock-action.yml +85 -0
  3. mcp_linter-1.2.0/.github/workflows/release.yml +93 -0
  4. mcp_linter-1.2.0/.gitignore +13 -0
  5. mcp_linter-1.2.0/.pre-commit-hooks.yaml +7 -0
  6. mcp_linter-1.2.0/CLAUDE.md +58 -0
  7. mcp_linter-1.2.0/LICENSE +21 -0
  8. mcp_linter-1.2.0/PKG-INFO +447 -0
  9. mcp_linter-1.2.0/README.md +419 -0
  10. mcp_linter-1.2.0/RESULTS.md +127 -0
  11. mcp_linter-1.2.0/action.yml +88 -0
  12. mcp_linter-1.2.0/eval/REPORT.md +208 -0
  13. mcp_linter-1.2.0/eval/chart/success.html +94 -0
  14. mcp_linter-1.2.0/eval/chart/success.png +0 -0
  15. mcp_linter-1.2.0/eval/fixes/markitdown-mcp.json +6 -0
  16. mcp_linter-1.2.0/eval/fixes/mcp-server-git.json +16 -0
  17. mcp_linter-1.2.0/eval/fixes/server-filesystem.json +6 -0
  18. mcp_linter-1.2.0/eval/lint-fixtures/filesystem.json +7 -0
  19. mcp_linter-1.2.0/eval/lint-fixtures/git.json +7 -0
  20. mcp_linter-1.2.0/eval/lint-fixtures/markitdown.json +5 -0
  21. mcp_linter-1.2.0/eval/results/excluded.jsonl +24 -0
  22. mcp_linter-1.2.0/eval/results/lint/filesystem-baseline.json +756 -0
  23. mcp_linter-1.2.0/eval/results/lint/filesystem-fixed.json +688 -0
  24. mcp_linter-1.2.0/eval/results/lint/git-baseline.json +840 -0
  25. mcp_linter-1.2.0/eval/results/lint/git-fixed.json +688 -0
  26. mcp_linter-1.2.0/eval/results/lint/markitdown-baseline.json +212 -0
  27. mcp_linter-1.2.0/eval/results/lint/markitdown-fixed.json +160 -0
  28. mcp_linter-1.2.0/eval/results/pilot-roots-bug.jsonl +4 -0
  29. mcp_linter-1.2.0/eval/results/pilot.jsonl +2 -0
  30. mcp_linter-1.2.0/eval/results/results.jsonl +105 -0
  31. mcp_linter-1.2.0/eval/results/run-ablation.err +0 -0
  32. mcp_linter-1.2.0/eval/results/run-ablation.log +16 -0
  33. mcp_linter-1.2.0/eval/results/run-main.err +0 -0
  34. mcp_linter-1.2.0/eval/results/run-main.log +52 -0
  35. mcp_linter-1.2.0/eval/results/tables-main.md +37 -0
  36. mcp_linter-1.2.0/eval/results/tables.md +45 -0
  37. mcp_linter-1.2.0/eval/tasks/filesystem.json +140 -0
  38. mcp_linter-1.2.0/eval/tasks/git.json +140 -0
  39. mcp_linter-1.2.0/eval/tasks/markitdown.json +140 -0
  40. mcp_linter-1.2.0/pyproject.toml +40 -0
  41. mcp_linter-1.2.0/scripts/action_lock_check.py +159 -0
  42. mcp_linter-1.2.0/scripts/check_release_version.py +98 -0
  43. mcp_linter-1.2.0/src/mcp_lint/__init__.py +3 -0
  44. mcp_linter-1.2.0/src/mcp_lint/__main__.py +5 -0
  45. mcp_linter-1.2.0/src/mcp_lint/cli.py +677 -0
  46. mcp_linter-1.2.0/src/mcp_lint/client.py +343 -0
  47. mcp_linter-1.2.0/src/mcp_lint/eval.py +596 -0
  48. mcp_linter-1.2.0/src/mcp_lint/evaltasks.py +358 -0
  49. mcp_linter-1.2.0/src/mcp_lint/fixtures.py +262 -0
  50. mcp_linter-1.2.0/src/mcp_lint/install.py +1341 -0
  51. mcp_linter-1.2.0/src/mcp_lint/lock.py +844 -0
  52. mcp_linter-1.2.0/src/mcp_lint/models.py +103 -0
  53. mcp_linter-1.2.0/src/mcp_lint/probe.py +111 -0
  54. mcp_linter-1.2.0/src/mcp_lint/proxy.py +258 -0
  55. mcp_linter-1.2.0/src/mcp_lint/report.py +128 -0
  56. mcp_linter-1.2.0/src/mcp_lint/rules/__init__.py +19 -0
  57. mcp_linter-1.2.0/src/mcp_lint/rules/probe.py +385 -0
  58. mcp_linter-1.2.0/src/mcp_lint/rules/security.py +254 -0
  59. mcp_linter-1.2.0/src/mcp_lint/rules/static.py +304 -0
  60. mcp_linter-1.2.0/src/mcp_lint/safe_exec.py +36 -0
  61. mcp_linter-1.2.0/src/mcp_lint/score.py +209 -0
  62. mcp_linter-1.2.0/src/mcp_lint/text.py +85 -0
  63. mcp_linter-1.2.0/survey/README.md +37 -0
  64. mcp_linter-1.2.0/survey/fixtures/aws-docs.json +7 -0
  65. mcp_linter-1.2.0/survey/fixtures/chrome-devtools.json +1 -0
  66. mcp_linter-1.2.0/survey/fixtures/context7.json +5 -0
  67. mcp_linter-1.2.0/survey/fixtures/desktop-commander.json +7 -0
  68. mcp_linter-1.2.0/survey/fixtures/duckduckgo.json +7 -0
  69. mcp_linter-1.2.0/survey/fixtures/filesystem.json +15 -0
  70. mcp_linter-1.2.0/survey/fixtures/markitdown.json +6 -0
  71. mcp_linter-1.2.0/survey/make_sandbox.py +27 -0
  72. mcp_linter-1.2.0/survey/tools/aws-docs.json +202 -0
  73. mcp_linter-1.2.0/survey/tools/chrome-devtools.json +1232 -0
  74. mcp_linter-1.2.0/survey/tools/context7.json +71 -0
  75. mcp_linter-1.2.0/survey/tools/desktop-commander.json +852 -0
  76. mcp_linter-1.2.0/survey/tools/duckduckgo.json +108 -0
  77. mcp_linter-1.2.0/survey/tools/everything.json +294 -0
  78. mcp_linter-1.2.0/survey/tools/exa.json +72 -0
  79. mcp_linter-1.2.0/survey/tools/fetch.json +46 -0
  80. mcp_linter-1.2.0/survey/tools/filesystem.json +304 -0
  81. mcp_linter-1.2.0/survey/tools/firecrawl.json +2500 -0
  82. mcp_linter-1.2.0/survey/tools/git.json +335 -0
  83. mcp_linter-1.2.0/survey/tools/github.json +1049 -0
  84. mcp_linter-1.2.0/survey/tools/markitdown.json +28 -0
  85. mcp_linter-1.2.0/survey/tools/memory.json +262 -0
  86. mcp_linter-1.2.0/survey/tools/notion.json +6421 -0
  87. mcp_linter-1.2.0/survey/tools/playwright.json +781 -0
  88. mcp_linter-1.2.0/survey/tools/tavily.json +341 -0
  89. mcp_linter-1.2.0/survey/tools/time.json +53 -0
  90. mcp_linter-1.2.0/tests/fixtures/demo.lock +96 -0
  91. mcp_linter-1.2.0/tests/fixtures/demo_fixtures.json +12 -0
  92. mcp_linter-1.2.0/tests/fixtures/demo_server.py +72 -0
  93. mcp_linter-1.2.0/tests/fixtures/demo_tampered.lock +96 -0
  94. mcp_linter-1.2.0/tests/fixtures/demo_tampered_tools.json +83 -0
  95. mcp_linter-1.2.0/tests/fixtures/env_server.py +18 -0
  96. mcp_linter-1.2.0/tests/fixtures/poisoned_tools.json +69 -0
  97. mcp_linter-1.2.0/tests/fixtures/sample_tools.json +31 -0
  98. mcp_linter-1.2.0/tests/test_action.py +220 -0
  99. mcp_linter-1.2.0/tests/test_cli.py +177 -0
  100. mcp_linter-1.2.0/tests/test_error_quality.py +135 -0
  101. mcp_linter-1.2.0/tests/test_eval.py +333 -0
  102. mcp_linter-1.2.0/tests/test_fixtures.py +148 -0
  103. mcp_linter-1.2.0/tests/test_http.py +728 -0
  104. mcp_linter-1.2.0/tests/test_install.py +734 -0
  105. mcp_linter-1.2.0/tests/test_lock.py +1688 -0
  106. mcp_linter-1.2.0/tests/test_p2_p3.py +115 -0
  107. mcp_linter-1.2.0/tests/test_probe_rules.py +94 -0
  108. mcp_linter-1.2.0/tests/test_proxy.py +179 -0
  109. mcp_linter-1.2.0/tests/test_release_version.py +125 -0
  110. mcp_linter-1.2.0/tests/test_score.py +137 -0
  111. mcp_linter-1.2.0/tests/test_security_rules.py +344 -0
  112. mcp_linter-1.2.0/tests/test_static_rules.py +75 -0
  113. mcp_linter-1.2.0/tests/test_survey_tools.py +44 -0
  114. mcp_linter-1.2.0/tests/test_toolset_rules.py +119 -0
@@ -0,0 +1,74 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ # A newer push to the same branch cancels the run still in progress.
11
+ concurrency:
12
+ group: ci-${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ test:
20
+ name: Tests (py${{ matrix.python-version }}, ${{ matrix.os }})
21
+ runs-on: ${{ matrix.os }}
22
+ timeout-minutes: 15
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ os: [ubuntu-latest]
27
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
28
+ include:
29
+ - os: windows-latest
30
+ python-version: "3.13"
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: ${{ matrix.python-version }}
37
+ cache: pip
38
+ cache-dependency-path: pyproject.toml
39
+
40
+ - name: Install package
41
+ run: |
42
+ python -m pip install --upgrade pip
43
+ python -m pip install -e .
44
+
45
+ - name: Run tests
46
+ run: python -m unittest discover -s tests -v
47
+
48
+ build:
49
+ name: Build package
50
+ runs-on: ubuntu-latest
51
+ timeout-minutes: 10
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.13"
58
+
59
+ - name: Build sdist and wheel
60
+ run: |
61
+ python -m pip install --upgrade pip build
62
+ python -m build
63
+
64
+ - name: Install the built wheel and smoke-test the CLI
65
+ run: |
66
+ python -m venv /tmp/wheel-env
67
+ /tmp/wheel-env/bin/pip install dist/*.whl
68
+ /tmp/wheel-env/bin/mcp-lint --help
69
+
70
+ - uses: actions/upload-artifact@v4
71
+ with:
72
+ name: dist
73
+ path: dist/
74
+ retention-days: 7
@@ -0,0 +1,85 @@
1
+ name: Lock action
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lock-check:
15
+ name: Lock action (${{ matrix.os }})
16
+ runs-on: ${{ matrix.os }}
17
+ timeout-minutes: 15
18
+ strategy:
19
+ fail-fast: false
20
+ matrix:
21
+ os: [ubuntu-latest, windows-latest]
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.13"
28
+
29
+ - name: Install package (the demo server needs the MCP SDK)
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install -e .
33
+
34
+ - name: Clean lock passes
35
+ id: clean
36
+ uses: ./
37
+ with:
38
+ command: python tests/fixtures/demo_server.py
39
+ lock: tests/fixtures/demo.lock
40
+ setup-python: "false"
41
+
42
+ - name: Tampered lock fails
43
+ id: tampered
44
+ continue-on-error: true
45
+ uses: ./
46
+ with:
47
+ command: python tests/fixtures/demo_server.py
48
+ lock: tests/fixtures/demo_tampered.lock
49
+ setup-python: "false"
50
+
51
+ - name: Require that the tampered check failed
52
+ env:
53
+ TAMPERED_OUTCOME: ${{ steps.tampered.outcome }}
54
+ shell: bash
55
+ run: |
56
+ if [ "$TAMPERED_OUTCOME" != "failure" ]; then
57
+ echo "::error::expected the tampered lock check to fail, got outcome: $TAMPERED_OUTCOME"
58
+ exit 1
59
+ fi
60
+ echo "Tampered lock check failed as expected."
61
+
62
+ - name: Start HTTP demo server
63
+ if: runner.os == 'Linux'
64
+ shell: bash
65
+ run: |
66
+ # Detach the server from this step's stdout, or the step can wait on it.
67
+ nohup python tests/fixtures/demo_server.py --http 8765 > "$RUNNER_TEMP/demo-http.log" 2>&1 &
68
+ for i in $(seq 1 30); do
69
+ if timeout 1 bash -c 'exec 3<>/dev/tcp/127.0.0.1/8765' 2>/dev/null; then
70
+ echo "HTTP demo server is up."
71
+ exit 0
72
+ fi
73
+ sleep 1
74
+ done
75
+ echo "::error::HTTP demo server did not open port 8765 within 30 seconds"
76
+ exit 1
77
+
78
+ - name: HTTP lock check
79
+ if: runner.os == 'Linux'
80
+ uses: ./
81
+ with:
82
+ url: http://127.0.0.1:8765/mcp
83
+ lock: tests/fixtures/demo.lock
84
+ fail-on: error
85
+ setup-python: "false"
@@ -0,0 +1,93 @@
1
+ name: Release
2
+
3
+ # Builds the package on every run. A published GitHub release publishes to PyPI.
4
+ # A manual run (workflow_dispatch) publishes to TestPyPI instead.
5
+ # Publishing uses PyPI Trusted Publishing (OIDC): no API token is stored.
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ name: Build and check distributions
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.13"
25
+
26
+ - name: Install build tools
27
+ run: python -m pip install --upgrade pip build twine
28
+
29
+ - name: Check that the release tag matches the package version
30
+ if: github.event_name == 'release'
31
+ env:
32
+ TAG: ${{ github.event.release.tag_name }}
33
+ run: python scripts/check_release_version.py "$TAG"
34
+
35
+ - name: Build sdist and wheel
36
+ run: python -m build
37
+
38
+ - name: Check package metadata
39
+ run: python -m twine check --strict dist/*
40
+
41
+ - name: Install the built wheel and smoke-test the CLI
42
+ run: |
43
+ python -m venv /tmp/wheel-env
44
+ /tmp/wheel-env/bin/pip install dist/*.whl
45
+ /tmp/wheel-env/bin/mcp-lint --help
46
+
47
+ - uses: actions/upload-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+ retention-days: 7
52
+
53
+ publish-pypi:
54
+ name: Publish to PyPI
55
+ needs: build
56
+ if: github.event_name == 'release'
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 10
59
+ environment:
60
+ name: pypi
61
+ url: https://pypi.org/p/mcp-linter
62
+ permissions:
63
+ id-token: write
64
+ steps:
65
+ - uses: actions/download-artifact@v4
66
+ with:
67
+ name: dist
68
+ path: dist/
69
+
70
+ - name: Publish distributions to PyPI (Trusted Publishing)
71
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
72
+
73
+ publish-testpypi:
74
+ name: Publish to TestPyPI
75
+ needs: build
76
+ if: github.event_name == 'workflow_dispatch'
77
+ runs-on: ubuntu-latest
78
+ timeout-minutes: 10
79
+ environment:
80
+ name: testpypi
81
+ url: https://test.pypi.org/p/mcp-linter
82
+ permissions:
83
+ id-token: write
84
+ steps:
85
+ - uses: actions/download-artifact@v4
86
+ with:
87
+ name: dist
88
+ path: dist/
89
+
90
+ - name: Publish distributions to TestPyPI (Trusted Publishing)
91
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
92
+ with:
93
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ report.json
10
+ .env
11
+ survey/sandbox/
12
+ .claude/settings.local.json
13
+ eval/results/*.lock
@@ -0,0 +1,7 @@
1
+ - id: mcp-lint-lock
2
+ name: mcp-lint lock --check
3
+ description: Fail if an MCP server's tools changed since mcp-lint.lock was written.
4
+ entry: mcp-lint lock --check
5
+ language: python
6
+ pass_filenames: false
7
+ always_run: true
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ mcp-lint lints MCP servers against Anthropic's "Writing effective tools for agents" guidance. It reads a server's tool definitions (and optionally calls the tools) and reports findings, a token-weighted score, and can serve the server back through a fixing proxy. Python 3.10+, one runtime dependency (`mcp>=2.1`), built with hatchling.
6
+
7
+ ## Commands
8
+
9
+ ```bash
10
+ python -m venv .venv
11
+ .venv/Scripts/pip install -e . # Linux/macOS: .venv/bin/pip
12
+
13
+ python -m unittest discover -s tests # full suite (what CI runs)
14
+ python -m unittest discover -s tests -p test_cli.py # one file
15
+ python -m unittest tests.test_cli.CheckCommandTests # one class
16
+ python -m unittest tests.test_cli -k fails_on_error # by name substring
17
+
18
+ python -m build # sdist + wheel into dist/
19
+ mcp-lint check tests/fixtures/sample_tools.json # lint a saved tools/list offline
20
+ mcp-lint run -- python tests/fixtures/demo_server.py # lint a live stdio server
21
+ python tests/fixtures/demo_server.py --http 8000 # serve the demo over Streamable HTTP at 127.0.0.1:8000/mcp (run in another terminal)
22
+ mcp-lint run --url http://127.0.0.1:8000/mcp # lint that HTTP server
23
+ ```
24
+
25
+ - Tests use stdlib `unittest`, not pytest. `tests/` has no `__init__.py`, so don't pass `-t .` to discover.
26
+ - The package must be installed (`pip install -e .`) for tests to import `mcp_lint`.
27
+ - `mcp-lint check`/`run` exit 1 whenever a finding is at or above `--fail-on` (default `error`), so exit 1 on a sample file is normal. Exit 2 is a usage or connection error.
28
+ - Some tests start `tests/fixtures/demo_server.py` as a real MCP server through `sys.executable`. The server's tracebacks in test output (e.g. `UnexpectedToolError: Error executing tool search_contacts`) are expected, not failures.
29
+ - No linter or formatter is configured.
30
+ - CI (`.github/workflows/ci.yml`) runs the suite on Ubuntu with Python 3.10–3.13 and Windows with 3.13, and builds and installs the wheel. `lock-action.yml` tests the Action on Ubuntu and Windows, and `release.yml` builds on every run and publishes on a GitHub release.
31
+
32
+ ## Architecture
33
+
34
+ Pipeline for `run`/`check` (`cli.py:main`): get `ToolDef`s → `rules.run_static` → (`run` with `--probe`/`--error-probes`) `client.fetch_and_probe` + `rules.probe.run_probe_rules` → `report.render_*` / `to_json` → `score.score_server` → exit code.
35
+
36
+ - **`models.py`**: `ToolDef`, `Finding`, `Severity`. Rules work on these, never on MCP SDK objects, so rules are testable without a server. `ToolDef.read_only` is true only for an explicit `readOnlyHint: true`. That is the probe safety gate; `destructiveHint: false` does not count.
37
+ - **Rules** are plain functions returning `list[Finding]`, registered in lists that the runners iterate:
38
+ - `rules/static.py`: `STATIC_RULES` (per tool: S1–S3) and `TOOLSET_RULES` (whole tool list: S4–S7).
39
+ - `rules/security.py`: `SECURITY_RULES` (S8 hidden text). S8 is not scored.
40
+ - `rules/probe.py`: `PROBE_RULES` (P1–P5), each taking `(tools_by_name, probe_results)`.
41
+ - A new rule needs adding to its list, and to `score.FIXED_COSTS` (or `score.MEASURED`) if it should affect the score. The README's rule tables document every rule ID and threshold; keep them in sync.
42
+ - **`client.py`**: stdio and Streamable HTTP (`--url`) connections via the MCP SDK, paginated `tools/list`, probe calls on one session. HTTP uses `HttpTarget` (URL plus headers), `parse_headers` (name and value checks, no duplicates, and `--header-env` refused over plain `http://` unless the host is loopback) and `redact`. The URL and header values never reach output, reports or the lock; only `command_sha256` of `["--url", scheme://host:port/path]` is stored (`cli._http_identity`). `--header-env` values and URL user info/query are secret: `run` and `lock --check` scrub them from server text at the source and in the rendered output, and `lock`/`init-fixtures` refuse to write them. Plain `http://` clients ignore proxy env vars (`build_http_client`). Stdio servers get only the SDK's minimal default environment plus names passed with `--pass-env` (`--pass-env` doesn't apply to `--url`). Never forward the full environment: keeping API keys away from linted servers is a stated guarantee. `safe_exec.py` applies the same minimal environment when the eval launches servers directly.
43
+ - **`fixtures.py` / `probe.py`**: the fixtures file format, argument and safety checks before any call (non-read-only tools need `"allow_destructive": true`), generated bad-argument probes for P4 (read-only tools only), and `ProbeResult` capture.
44
+ - **`score.py`**: turns findings into estimated wasted tokens per session; scores halve every `HALF_LIFE` tokens. All cost constants live here.
45
+ - **`text.py`**: token estimation (4 chars per token of compact JSON; always labelled as an estimate), name splitting and naming-style detection.
46
+ - **`install.py`** (`mcp-lint check-config`, and the pre-launch refusal in `run`/`proxy`/`lock`/`init-fixtures`): install lint, rule I1. A backstop scans every argv piece for `KNOWN_BAD` regardless of the parser. `lint_command(argv)` flags unpinned packages and sources, images without a digest, pip installs without hashes, and `KNOWN_BAD` releases; `run` shows the other findings under the server section, while a `known_bad` launch is refused on stderr before any connection. `load_client_config()` reads the server maps of client config files and never keeps `env` values. I1 is not in `score.py`. New kinds need a row in the README's Install lint table.
47
+ - **`proxy.py`** (`mcp-lint proxy`): an MCP server that wraps an upstream one and applies the fixes (paging with `offset`/`limit`, description replacement from `eval/fixes/*.json`, `isError` on error-as-success). Its pure functions (`rewrite_tool`, `page_text`, `fix_result`) are unit-tested without a server.
48
+ - **`lock.py`** (`mcp-lint lock`, `lock --check`): the rug-pull lockfile. `canonical()` normalises a `ToolDef` (every MCP `Tool` field) so equal schemas hash equal; `diff_lock()` classifies changes as `Finding(rule="LOCK", kind=...)`. The rule it enforces: any change to text the model or user reads, or that makes a schema accept or ask for more, is an error; anything else that changes the hash is at least a warning (fail closed). New change classes need a row in the README's Lockfile table. The lock never stores the raw launch command, only its hash.
49
+ - **`eval.py` / `evaltasks.py`** (`mcp-lint eval`): before/after experiment that runs headless `claude -p` against each server directly (`baseline`) and behind the proxy (`fixed`, plus single-fix ablations). Results append to `eval/results/results.jsonl` and the runner is resumable. `--run` makes real Claude Code calls; don't start it unless asked.
50
+ - **`action.yml` + `scripts/action_lock_check.py`**: composite GitHub Action for `lock --check`. Inputs reach the script only through environment variables, and the script runs mcp-lint from an argv list without a shell. Never put `${{ inputs.* }}` into a `run:` script.
51
+ - **`.pre-commit-hooks.yaml`**: pre-commit hook `mcp-lint-lock` (`lock --check`, always runs).
52
+ - **`.github/workflows/release.yml` + `scripts/check_release_version.py`**: a published GitHub release checks that the tag matches the version in both `pyproject.toml` and `__init__.py`, builds, and publishes to PyPI with Trusted Publishing (no token). A manual run publishes to TestPyPI. The PyPI name is `mcp-linter`.
53
+
54
+ ## Data directories
55
+
56
+ - `tests/fixtures/`: demo MCP servers and tool lists used by tests (`poisoned_tools.json` holds S8 attack strings on purpose).
57
+ - `survey/tools/`: saved `tools/list` results from 18 real servers. `tests/test_survey_tools.py` fails if S8 raises a warning or error on any of them, so S8 changes must stay quiet on this benign corpus. `survey/fixtures/` and `make_sandbox.py` reproduce the probe numbers in `RESULTS.md`.
58
+ - `eval/`: tasks, hand-written description fixes, recorded results, and `REPORT.md`. Results files are experiment records, not generated scratch; don't rewrite them.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex
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.