Skip to content

Commit 941089e

Browse files
authored
docs: modernize development guidelines and rename to AGENTS.md (#2413)
1 parent 8f806da commit 941089e

File tree

3 files changed

+139
-175
lines changed

3 files changed

+139
-175
lines changed

AGENTS.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Development Guidelines
2+
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. When a public API raises exceptions a
35+
caller would reasonably catch, document them in a `Raises:` section. Don't
36+
list exceptions from argument validation or programmer error.
37+
- `src/mcp/__init__.py` defines the public API surface via `__all__`. Adding a
38+
symbol there is a deliberate API decision, not a convenience re-export.
39+
- IMPORTANT: All imports go at the top of the file — inline imports hide
40+
dependencies and obscure circular-import bugs. Only exception: when a
41+
top-level import genuinely can't work (lazy-loading optional deps, or
42+
tests that re-import a module).
43+
44+
## Testing
45+
46+
- Framework: `uv run --frozen pytest`
47+
- Async testing: use anyio, not asyncio
48+
- Do not use `Test` prefixed classes, use functions
49+
- IMPORTANT: Tests should be fast and deterministic. Prefer in-memory async execution;
50+
reach for threads only when necessary, and subprocesses only as a last resort.
51+
- For end-to-end behavior, an in-memory `Client(server)` is usually the
52+
cleanest approach (see `tests/client/test_client.py` for the canonical
53+
pattern). For narrower changes, testing the function directly is fine. Use
54+
judgment.
55+
- Test files mirror the source tree: `src/mcp/client/stdio.py`
56+
`tests/client/test_stdio.py`. Add tests to the existing file for that module.
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+
- Pytest is configured with `filterwarnings = ["error"]`, so warnings fail
63+
tests. Don't silence warnings from your own code; fix the underlying cause.
64+
Scoped `ignore::` entries for upstream libraries are acceptable in
65+
`pyproject.toml` with a comment explaining why.
66+
67+
### Coverage
68+
69+
CI requires 100% (`fail_under = 100`, `branch = true`).
70+
71+
- Full check: `./scripts/test` (~23s). Runs coverage + `strict-no-cover` on the
72+
default Python. Not identical to CI: CI runs 3.10–3.14 × {ubuntu, windows}
73+
× {locked, lowest-direct}, and some branch-coverage quirks only surface on
74+
specific matrix entries.
75+
- Targeted check while iterating (~4s, deterministic):
76+
77+
```bash
78+
uv run --frozen coverage erase
79+
uv run --frozen coverage run -m pytest tests/path/test_foo.py
80+
uv run --frozen coverage combine
81+
uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0
82+
# UV_FROZEN=1 propagates --frozen to the uv subprocess strict-no-cover spawns
83+
UV_FROZEN=1 uv run --frozen strict-no-cover
84+
```
85+
86+
Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0`
87+
and `--include` scope the report. `strict-no-cover` has no false positives on
88+
partial runs — if your new test executes a line marked `# pragma: no cover`,
89+
even a single-file run catches it.
90+
91+
Avoid adding new `# pragma: no cover`, `# type: ignore`, or `# noqa` comments.
92+
In tests, use `assert isinstance(x, T)` to narrow types instead of
93+
`# type: ignore`. In library code (`src/`), a `# pragma: no cover` needs very
94+
good reasoning — it usually means a test is missing. Audit before pushing:
95+
96+
```bash
97+
git diff origin/main... | grep -E '^\+.*(pragma|type: ignore|noqa)'
98+
```
99+
100+
What the existing pragmas mean:
101+
102+
- `# pragma: no cover` — line is never executed. CI's `strict-no-cover` (skipped
103+
on Windows runners) fails if it IS executed. When your test starts covering
104+
such a line, remove the pragma.
105+
- `# pragma: lax no cover` — excluded from coverage but not checked by
106+
`strict-no-cover`. Use for lines covered on some platforms/versions but not
107+
others.
108+
- `# pragma: no branch` — excludes branch arcs only. coverage.py misreports the
109+
`->exit` arc for nested `async with` on Python 3.11+ (worse on 3.14/Windows).
110+
111+
## Breaking Changes
112+
113+
When making breaking changes, document them in `docs/migration.md`. Include:
114+
115+
- What changed
116+
- Why it changed
117+
- How to migrate existing code
118+
119+
Search for related sections in the migration guide and group related changes together
120+
rather than adding new standalone sections.
121+
122+
## Formatting & Type Checking
123+
124+
- Format: `uv run --frozen ruff format .`
125+
- Lint: `uv run --frozen ruff check . --fix`
126+
- Type check: `uv run --frozen pyright`
127+
- Pre-commit runs all of the above plus markdownlint, a `uv.lock` consistency
128+
check, and README checks — see `.pre-commit-config.yaml`
129+
130+
## Exception Handling
131+
132+
- **Always use `logger.exception()` instead of `logger.error()` when catching exceptions**
133+
- Don't include the exception in the message: `logger.exception("Failed")` not `logger.exception(f"Failed: {e}")`
134+
- **Catch specific exceptions** where possible:
135+
- File ops: `except (OSError, PermissionError):`
136+
- JSON: `except json.JSONDecodeError:`
137+
- Network: `except (ConnectionError, TimeoutError):`
138+
- **FORBIDDEN** `except Exception:` - unless in top-level handlers

CLAUDE.md

Lines changed: 1 addition & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1 @@
1-
# Development Guidelines
2-
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:
67-
68-
```bash
69-
git commit --trailer "Reported-by:<name>"
70-
```
71-
72-
Where `<name>` is the name of the user.
73-
74-
- For commits related to a Github issue, add
75-
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.
82-
83-
## Pull Requests
84-
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.
91-
92-
## Breaking Changes
93-
94-
When making breaking changes, document them in `docs/migration.md`. Include:
95-
96-
- What changed
97-
- Why it changed
98-
- How to migrate existing code
99-
100-
Search for related sections in the migration guide and group related changes together
101-
rather than adding new standalone sections.
102-
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
165-
166-
## Exception Handling
167-
168-
- **Always use `logger.exception()` instead of `logger.error()` when catching exceptions**
169-
- Don't include the exception in the message: `logger.exception("Failed")` not `logger.exception(f"Failed: {e}")`
170-
- **Catch specific exceptions** where possible:
171-
- File ops: `except (OSError, PermissionError):`
172-
- JSON: `except json.JSONDecodeError:`
173-
- Network: `except (ConnectionError, TimeoutError):`
174-
- **FORBIDDEN** `except Exception:` - unless in top-level handlers
1+
@AGENTS.md

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ filterwarnings = [
185185
# This should be fixed on Uvicorn's side.
186186
"ignore::DeprecationWarning:websockets",
187187
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
188-
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel",
189188
# pywin32 internal deprecation warning
190189
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
191190
]

0 commit comments

Comments
 (0)