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.
- mcp_linter-1.2.0/.github/workflows/ci.yml +74 -0
- mcp_linter-1.2.0/.github/workflows/lock-action.yml +85 -0
- mcp_linter-1.2.0/.github/workflows/release.yml +93 -0
- mcp_linter-1.2.0/.gitignore +13 -0
- mcp_linter-1.2.0/.pre-commit-hooks.yaml +7 -0
- mcp_linter-1.2.0/CLAUDE.md +58 -0
- mcp_linter-1.2.0/LICENSE +21 -0
- mcp_linter-1.2.0/PKG-INFO +447 -0
- mcp_linter-1.2.0/README.md +419 -0
- mcp_linter-1.2.0/RESULTS.md +127 -0
- mcp_linter-1.2.0/action.yml +88 -0
- mcp_linter-1.2.0/eval/REPORT.md +208 -0
- mcp_linter-1.2.0/eval/chart/success.html +94 -0
- mcp_linter-1.2.0/eval/chart/success.png +0 -0
- mcp_linter-1.2.0/eval/fixes/markitdown-mcp.json +6 -0
- mcp_linter-1.2.0/eval/fixes/mcp-server-git.json +16 -0
- mcp_linter-1.2.0/eval/fixes/server-filesystem.json +6 -0
- mcp_linter-1.2.0/eval/lint-fixtures/filesystem.json +7 -0
- mcp_linter-1.2.0/eval/lint-fixtures/git.json +7 -0
- mcp_linter-1.2.0/eval/lint-fixtures/markitdown.json +5 -0
- mcp_linter-1.2.0/eval/results/excluded.jsonl +24 -0
- mcp_linter-1.2.0/eval/results/lint/filesystem-baseline.json +756 -0
- mcp_linter-1.2.0/eval/results/lint/filesystem-fixed.json +688 -0
- mcp_linter-1.2.0/eval/results/lint/git-baseline.json +840 -0
- mcp_linter-1.2.0/eval/results/lint/git-fixed.json +688 -0
- mcp_linter-1.2.0/eval/results/lint/markitdown-baseline.json +212 -0
- mcp_linter-1.2.0/eval/results/lint/markitdown-fixed.json +160 -0
- mcp_linter-1.2.0/eval/results/pilot-roots-bug.jsonl +4 -0
- mcp_linter-1.2.0/eval/results/pilot.jsonl +2 -0
- mcp_linter-1.2.0/eval/results/results.jsonl +105 -0
- mcp_linter-1.2.0/eval/results/run-ablation.err +0 -0
- mcp_linter-1.2.0/eval/results/run-ablation.log +16 -0
- mcp_linter-1.2.0/eval/results/run-main.err +0 -0
- mcp_linter-1.2.0/eval/results/run-main.log +52 -0
- mcp_linter-1.2.0/eval/results/tables-main.md +37 -0
- mcp_linter-1.2.0/eval/results/tables.md +45 -0
- mcp_linter-1.2.0/eval/tasks/filesystem.json +140 -0
- mcp_linter-1.2.0/eval/tasks/git.json +140 -0
- mcp_linter-1.2.0/eval/tasks/markitdown.json +140 -0
- mcp_linter-1.2.0/pyproject.toml +40 -0
- mcp_linter-1.2.0/scripts/action_lock_check.py +159 -0
- mcp_linter-1.2.0/scripts/check_release_version.py +98 -0
- mcp_linter-1.2.0/src/mcp_lint/__init__.py +3 -0
- mcp_linter-1.2.0/src/mcp_lint/__main__.py +5 -0
- mcp_linter-1.2.0/src/mcp_lint/cli.py +677 -0
- mcp_linter-1.2.0/src/mcp_lint/client.py +343 -0
- mcp_linter-1.2.0/src/mcp_lint/eval.py +596 -0
- mcp_linter-1.2.0/src/mcp_lint/evaltasks.py +358 -0
- mcp_linter-1.2.0/src/mcp_lint/fixtures.py +262 -0
- mcp_linter-1.2.0/src/mcp_lint/install.py +1341 -0
- mcp_linter-1.2.0/src/mcp_lint/lock.py +844 -0
- mcp_linter-1.2.0/src/mcp_lint/models.py +103 -0
- mcp_linter-1.2.0/src/mcp_lint/probe.py +111 -0
- mcp_linter-1.2.0/src/mcp_lint/proxy.py +258 -0
- mcp_linter-1.2.0/src/mcp_lint/report.py +128 -0
- mcp_linter-1.2.0/src/mcp_lint/rules/__init__.py +19 -0
- mcp_linter-1.2.0/src/mcp_lint/rules/probe.py +385 -0
- mcp_linter-1.2.0/src/mcp_lint/rules/security.py +254 -0
- mcp_linter-1.2.0/src/mcp_lint/rules/static.py +304 -0
- mcp_linter-1.2.0/src/mcp_lint/safe_exec.py +36 -0
- mcp_linter-1.2.0/src/mcp_lint/score.py +209 -0
- mcp_linter-1.2.0/src/mcp_lint/text.py +85 -0
- mcp_linter-1.2.0/survey/README.md +37 -0
- mcp_linter-1.2.0/survey/fixtures/aws-docs.json +7 -0
- mcp_linter-1.2.0/survey/fixtures/chrome-devtools.json +1 -0
- mcp_linter-1.2.0/survey/fixtures/context7.json +5 -0
- mcp_linter-1.2.0/survey/fixtures/desktop-commander.json +7 -0
- mcp_linter-1.2.0/survey/fixtures/duckduckgo.json +7 -0
- mcp_linter-1.2.0/survey/fixtures/filesystem.json +15 -0
- mcp_linter-1.2.0/survey/fixtures/markitdown.json +6 -0
- mcp_linter-1.2.0/survey/make_sandbox.py +27 -0
- mcp_linter-1.2.0/survey/tools/aws-docs.json +202 -0
- mcp_linter-1.2.0/survey/tools/chrome-devtools.json +1232 -0
- mcp_linter-1.2.0/survey/tools/context7.json +71 -0
- mcp_linter-1.2.0/survey/tools/desktop-commander.json +852 -0
- mcp_linter-1.2.0/survey/tools/duckduckgo.json +108 -0
- mcp_linter-1.2.0/survey/tools/everything.json +294 -0
- mcp_linter-1.2.0/survey/tools/exa.json +72 -0
- mcp_linter-1.2.0/survey/tools/fetch.json +46 -0
- mcp_linter-1.2.0/survey/tools/filesystem.json +304 -0
- mcp_linter-1.2.0/survey/tools/firecrawl.json +2500 -0
- mcp_linter-1.2.0/survey/tools/git.json +335 -0
- mcp_linter-1.2.0/survey/tools/github.json +1049 -0
- mcp_linter-1.2.0/survey/tools/markitdown.json +28 -0
- mcp_linter-1.2.0/survey/tools/memory.json +262 -0
- mcp_linter-1.2.0/survey/tools/notion.json +6421 -0
- mcp_linter-1.2.0/survey/tools/playwright.json +781 -0
- mcp_linter-1.2.0/survey/tools/tavily.json +341 -0
- mcp_linter-1.2.0/survey/tools/time.json +53 -0
- mcp_linter-1.2.0/tests/fixtures/demo.lock +96 -0
- mcp_linter-1.2.0/tests/fixtures/demo_fixtures.json +12 -0
- mcp_linter-1.2.0/tests/fixtures/demo_server.py +72 -0
- mcp_linter-1.2.0/tests/fixtures/demo_tampered.lock +96 -0
- mcp_linter-1.2.0/tests/fixtures/demo_tampered_tools.json +83 -0
- mcp_linter-1.2.0/tests/fixtures/env_server.py +18 -0
- mcp_linter-1.2.0/tests/fixtures/poisoned_tools.json +69 -0
- mcp_linter-1.2.0/tests/fixtures/sample_tools.json +31 -0
- mcp_linter-1.2.0/tests/test_action.py +220 -0
- mcp_linter-1.2.0/tests/test_cli.py +177 -0
- mcp_linter-1.2.0/tests/test_error_quality.py +135 -0
- mcp_linter-1.2.0/tests/test_eval.py +333 -0
- mcp_linter-1.2.0/tests/test_fixtures.py +148 -0
- mcp_linter-1.2.0/tests/test_http.py +728 -0
- mcp_linter-1.2.0/tests/test_install.py +734 -0
- mcp_linter-1.2.0/tests/test_lock.py +1688 -0
- mcp_linter-1.2.0/tests/test_p2_p3.py +115 -0
- mcp_linter-1.2.0/tests/test_probe_rules.py +94 -0
- mcp_linter-1.2.0/tests/test_proxy.py +179 -0
- mcp_linter-1.2.0/tests/test_release_version.py +125 -0
- mcp_linter-1.2.0/tests/test_score.py +137 -0
- mcp_linter-1.2.0/tests/test_security_rules.py +344 -0
- mcp_linter-1.2.0/tests/test_static_rules.py +75 -0
- mcp_linter-1.2.0/tests/test_survey_tools.py +44 -0
- 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,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.
|
mcp_linter-1.2.0/LICENSE
ADDED
|
@@ -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.
|