11# Development Guidelines
22
3- This document contains critical information about working with this codebase. Follow these guidelines precisely.
4-
5- ## Core Development Rules
6-
7- 1 . Package Management
8- - ONLY use uv, NEVER pip
9- - Installation: ` uv add <package> `
10- - Running tools: ` uv run <tool> `
11- - Upgrading: ` uv lock --upgrade-package <package> `
12- - FORBIDDEN: ` uv pip install ` , ` @latest ` syntax
13-
14- 2 . Code Quality
15- - Type hints required for all code
16- - Public APIs must have docstrings
17- - Functions must be focused and small
18- - Follow existing patterns exactly
19- - Line length: 120 chars maximum
20- - FORBIDDEN: imports inside functions. THEY SHOULD BE AT THE TOP OF THE FILE.
21-
22- 3 . Testing Requirements
23- - Framework: ` uv run --frozen pytest `
24- - Async testing: use anyio, not asyncio
25- - Do not use ` Test ` prefixed classes, use functions
26- - Coverage: test edge cases and errors
27- - New features require tests
28- - Bug fixes require regression tests
29- - IMPORTANT: The ` tests/client/test_client.py ` is the most well designed test file. Follow its patterns.
30- - IMPORTANT: Be minimal, and focus on E2E tests: Use the ` mcp.client.Client ` whenever possible.
31- - Coverage: CI requires 100% (` fail_under = 100 ` , ` branch = true ` ).
32- - Full check: ` ./scripts/test ` (~ 23s). Runs coverage + ` strict-no-cover ` on the
33- default Python. Not identical to CI: CI also runs 3.10–3.14 × {ubuntu, windows},
34- and some branch-coverage quirks only surface on specific matrix entries.
35- - Targeted check while iterating (~ 4s, deterministic):
36-
37- ``` bash
38- uv run --frozen coverage erase
39- uv run --frozen coverage run -m pytest tests/path/test_foo.py
40- uv run --frozen coverage combine
41- uv run --frozen coverage report --include=' src/mcp/path/foo.py' --fail-under=0
42- UV_FROZEN=1 uv run --frozen strict-no-cover
43- ```
44-
45- Partial runs can' t hit 100% (coverage tracks `tests/` too), so `--fail-under=0`
46- and `--include` scope the report. `strict-no-cover` has no false positives on
47- partial runs — if your new test executes a line marked `# pragma: no cover`,
48- even a single-file run catches it.
49- - Coverage pragmas:
50- - `# pragma: no cover` — line is never executed. CI' s ` strict-no-cover` fails if
51- it IS executed. When your test starts covering such a line, remove the pragma.
52- - ` # pragma: lax no cover` — excluded from coverage but not checked by
53- ` strict-no-cover` . Use for lines covered on some platforms/versions but not
54- others.
55- - ` # pragma: no branch` — excludes branch arcs only. coverage.py misreports the
56- ` -> exit` arc for nested ` async with` on Python 3.11+ (worse on 3.14/Windows).
57- - Avoid `anyio.sleep ()` with a fixed duration to wait for async operations. Instead:
58- - Use ` anyio.Event` — set it in the callback/handler, ` await event.wait ()` in the test
59- - For stream messages, use ` await stream.receive ()` instead of ` sleep()` + ` receive_nowait()`
60- - Exception: ` sleep()` is appropriate when testing time-based features (e.g., timeouts)
61- - Wrap indefinite waits (` event.wait()` , `stream.receive ()` ) in ` anyio.fail_after(5)` to prevent hangs
62-
63- Test files mirror the source tree: ` src/mcp/client/streamable_http.py` → ` tests/client/test_streamable_http.py`
64- Add tests to the existing file for that module.
65-
66- - For commits fixing bugs or adding features based on user reports add:
3+ ## Branching Model
4+
5+ <!-- TODO: drop this section once v2 ships and main becomes the stable line -->
6+
7+ - ` main ` is currently the V2 rework. Breaking changes are expected here — when removing or
8+ replacing an API, delete it outright and document the change in
9+ ` docs/migration.md ` . Do not add ` @deprecated ` shims or backward-compat layers
10+ on ` main ` .
11+ - ` v1.x ` is the release branch for the current stable line. Backport PRs target
12+ this branch and use a ` [v1.x] ` title prefix.
13+ - ` README.md ` is frozen at v1 (a pre-commit hook rejects edits). Edit
14+ ` README.v2.md ` instead.
15+
16+ ## Package Management
17+
18+ - ONLY use uv, NEVER pip
19+ - Installation: ` uv add <package> `
20+ - Running tools: ` uv run --frozen <tool> ` . Always pass ` --frozen ` so uv doesn't
21+ rewrite ` uv.lock ` as a side effect.
22+ - Cross-version testing: ` uv run --frozen --python 3.10 pytest ... ` to run
23+ against a specific interpreter (CI covers 3.10–3.14).
24+ - Upgrading: ` uv lock --upgrade-package <package> `
25+ - FORBIDDEN: ` uv pip install ` , ` @latest ` syntax
26+ - Don't raise dependency floors for CVEs alone. The ` >= ` constraint already
27+ lets users upgrade. Only raise a floor when the SDK needs functionality from
28+ the newer version, and don't add SDK code to work around a dependency's
29+ vulnerability. See Kludex/uvicorn #2643 and python-sdk #1552 for reasoning.
30+
31+ ## Code Quality
32+
33+ - Type hints required for all code
34+ - Public APIs must have docstrings
35+ - ` src/mcp/__init__.py ` defines the public API surface via ` __all__ ` . Adding a
36+ symbol there is a deliberate API decision, not a convenience re-export.
37+ - IMPORTANT: All imports go at the top of the file — inline imports hide
38+ dependencies and obscure circular-import bugs. Only exception: when a
39+ top-level import genuinely can't work (lazy-loading optional deps, or
40+ tests that re-import a module).
41+
42+ ## Testing
43+
44+ - Framework: ` uv run --frozen pytest `
45+ - Async testing: use anyio, not asyncio
46+ - Do not use ` Test ` prefixed classes, use functions
47+ - IMPORTANT: Tests should be fast and deterministic. Prefer in-memory async execution;
48+ reach for threads only when necessary, and subprocesses only as a last resort.
49+ - For end-to-end behavior, an in-memory ` Client(server) ` is usually the
50+ cleanest approach (see ` tests/client/test_client.py ` for the canonical
51+ pattern). For narrower changes, testing the function directly is fine. Use
52+ judgment.
53+ - Test files mirror the source tree: ` src/mcp/client/streamable_http.py ` →
54+ ` tests/client/test_streamable_http.py ` . Add tests to the existing file for that module.
55+ - Avoid ` anyio.sleep() ` with a fixed duration to wait for async operations. Instead:
56+ - Use ` anyio.Event ` — set it in the callback/handler, ` await event.wait() ` in the test
57+ - For stream messages, use ` await stream.receive() ` instead of ` sleep() ` + ` receive_nowait() `
58+ - Exception: ` sleep() ` is appropriate when testing time-based features (e.g., timeouts)
59+ - Wrap indefinite waits (` event.wait() ` , ` stream.receive() ` ) in ` anyio.fail_after(5) ` to prevent hangs
60+ - Pytest is configured with ` filterwarnings = ["error"] ` , so warnings fail
61+ tests. Don't silence them with ` filterwarnings ` or ` warnings.catch_warnings() ` ;
62+ fix the underlying cause instead.
63+
64+ ### Coverage
65+
66+ CI requires 100% (` fail_under = 100 ` , ` branch = true ` ).
67+
68+ - Full check: ` ./scripts/test ` (~ 23s). Runs coverage + ` strict-no-cover ` on the
69+ default Python. Not identical to CI: CI also runs 3.10–3.14 × {ubuntu, windows},
70+ and some branch-coverage quirks only surface on specific matrix entries.
71+ - Targeted check while iterating (~ 4s, deterministic):
6772
6873 ``` bash
69- git commit --trailer " Reported-by:<name>"
74+ uv run --frozen coverage erase
75+ uv run --frozen coverage run -m pytest tests/path/test_foo.py
76+ uv run --frozen coverage combine
77+ uv run --frozen coverage report --include=' src/mcp/path/foo.py' --fail-under=0
78+ UV_FROZEN=1 uv run --frozen strict-no-cover
7079 ```
7180
72- Where ` < name> ` is the name of the user.
81+ Partial runs can't hit 100% (coverage tracks ` tests/ ` too), so ` --fail-under=0 `
82+ and ` --include ` scope the report. ` strict-no-cover ` has no false positives on
83+ partial runs — if your new test executes a line marked ` # pragma: no cover ` ,
84+ even a single-file run catches it.
7385
74- - For commits related to a Github issue, add
86+ Avoid adding new ` # pragma: no cover ` , ` # type: ignore ` , or ` # noqa ` comments.
87+ In tests, use ` assert isinstance(x, T) ` to narrow types instead of
88+ ` # type: ignore ` . In library code (` src/ ` ), a ` # pragma: no cover ` needs very
89+ good reasoning — it usually means a test is missing. Audit before pushing:
7590
76- ` ` ` bash
77- git commit --trailer " Github-Issue:#<number>"
78- ` ` `
79-
80- - NEVER ever mention a ` co-authored-by` or similar aspects. In particular, never
81- mention the tool used to create the commit message or PR.
91+ ``` bash
92+ git diff origin/main... | grep -E ' ^\+.*(pragma|type: ignore|noqa)'
93+ ```
8294
83- # # Pull Requests
95+ What the existing pragmas mean:
8496
85- - Create a detailed message of what changed. Focus on the high level description of
86- the problem it tries to solve, and how it is solved. Don' t go into the specifics of the
87- code unless it adds clarity.
88-
89- - NEVER ever mention a `co-authored-by` or similar aspects. In particular, never
90- mention the tool used to create the commit message or PR.
97+ - ` # pragma: no cover ` — line is never executed. CI's ` strict-no-cover ` fails if
98+ it IS executed. When your test starts covering such a line, remove the pragma.
99+ - ` # pragma: lax no cover ` — excluded from coverage but not checked by
100+ ` strict-no-cover ` . Use for lines covered on some platforms/versions but not
101+ others.
102+ - ` # pragma: no branch ` — excludes branch arcs only. coverage.py misreports the
103+ ` ->exit ` arc for nested ` async with ` on Python 3.11+ (worse on 3.14/Windows).
91104
92105## Breaking Changes
93106
@@ -100,68 +113,13 @@ When making breaking changes, document them in `docs/migration.md`. Include:
100113Search for related sections in the migration guide and group related changes together
101114rather than adding new standalone sections.
102115
103- ## Python Tools
104-
105- ## Code Formatting
106-
107- 1. Ruff
108- - Format: `uv run --frozen ruff format .`
109- - Check: `uv run --frozen ruff check .`
110- - Fix: `uv run --frozen ruff check . --fix`
111- - Critical issues:
112- - Line length (88 chars)
113- - Import sorting (I001)
114- - Unused imports
115- - Line wrapping:
116- - Strings: use parentheses
117- - Function calls: multi-line with proper indent
118- - Imports: try to use a single line
119-
120- 2. Type Checking
121- - Tool: `uv run --frozen pyright`
122- - Requirements:
123- - Type narrowing for strings
124- - Version warnings can be ignored if checks pass
125-
126- 3. Pre-commit
127- - Config: `.pre-commit-config.yaml`
128- - Runs: on git commit
129- - Tools: Prettier (YAML/JSON), Ruff (Python)
130- - Ruff updates:
131- - Check PyPI versions
132- - Update config rev
133- - Commit config first
134-
135- ## Error Resolution
136-
137- 1. CI Failures
138- - Fix order:
139- 1. Formatting
140- 2. Type errors
141- 3. Linting
142- - Type errors:
143- - Get full line context
144- - Check Optional types
145- - Add type narrowing
146- - Verify function signatures
147-
148- 2. Common Issues
149- - Line length:
150- - Break strings with parentheses
151- - Multi-line function calls
152- - Split imports
153- - Types:
154- - Add None checks
155- - Narrow string types
156- - Match existing patterns
157-
158- 3. Best Practices
159- - Check git status before commits
160- - Run formatters before type checks
161- - Keep changes minimal
162- - Follow existing patterns
163- - Document public APIs
164- - Test thoroughly
116+ ## Formatting & Type Checking
117+
118+ - Format: ` uv run --frozen ruff format . `
119+ - Lint: ` uv run --frozen ruff check . --fix `
120+ - Type check: ` uv run --frozen pyright `
121+ - Pre-commit runs all of the above plus markdownlint, a ` uv.lock ` consistency
122+ check, and README checks — see ` .pre-commit-config.yaml `
165123
166124## Exception Handling
167125
0 commit comments