-
Notifications
You must be signed in to change notification settings - Fork 3.3k
docs: modernize development guidelines and rename to AGENTS.md #2413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maxisbey
wants to merge
6
commits into
main
Choose a base branch
from
docs/modernize-agents-md
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+139
−175
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
33745c8
docs: modernize CLAUDE.md development guidelines
maxisbey b8895ae
docs: rename CLAUDE.md to AGENTS.md with import stub
maxisbey 740b8f8
docs: address review feedback on AGENTS.md
maxisbey bf7fcfb
docs: clarify CI matrix dimensions; drop stale filterwarnings entry
maxisbey fd36a52
docs: drop lowest-direct repro hint
maxisbey b1af585
docs: explain UV_FROZEN=1 in targeted coverage block
maxisbey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # Development Guidelines | ||
|
|
||
| ## Branching Model | ||
|
|
||
| <!-- TODO: drop this section once v2 ships and main becomes the stable line --> | ||
|
|
||
| - `main` is currently the V2 rework. Breaking changes are expected here — when removing or | ||
| replacing an API, delete it outright and document the change in | ||
| `docs/migration.md`. Do not add `@deprecated` shims or backward-compat layers | ||
| on `main`. | ||
| - `v1.x` is the release branch for the current stable line. Backport PRs target | ||
| this branch and use a `[v1.x]` title prefix. | ||
| - `README.md` is frozen at v1 (a pre-commit hook rejects edits). Edit | ||
| `README.v2.md` instead. | ||
|
|
||
| ## Package Management | ||
|
|
||
| - ONLY use uv, NEVER pip | ||
| - Installation: `uv add <package>` | ||
| - Running tools: `uv run --frozen <tool>`. Always pass `--frozen` so uv doesn't | ||
| rewrite `uv.lock` as a side effect. | ||
| - Cross-version testing: `uv run --frozen --python 3.10 pytest ...` to run | ||
| against a specific interpreter (CI covers 3.10–3.14). | ||
| - Upgrading: `uv lock --upgrade-package <package>` | ||
| - FORBIDDEN: `uv pip install`, `@latest` syntax | ||
| - Don't raise dependency floors for CVEs alone. The `>=` constraint already | ||
| lets users upgrade. Only raise a floor when the SDK needs functionality from | ||
| the newer version, and don't add SDK code to work around a dependency's | ||
| vulnerability. See Kludex/uvicorn#2643 and python-sdk #1552 for reasoning. | ||
|
|
||
| ## Code Quality | ||
|
|
||
| - Type hints required for all code | ||
| - Public APIs must have docstrings. When a public API raises exceptions a | ||
| caller would reasonably catch, document them in a `Raises:` section. Don't | ||
| list exceptions from argument validation or programmer error. | ||
| - `src/mcp/__init__.py` defines the public API surface via `__all__`. Adding a | ||
| symbol there is a deliberate API decision, not a convenience re-export. | ||
| - IMPORTANT: All imports go at the top of the file — inline imports hide | ||
| dependencies and obscure circular-import bugs. Only exception: when a | ||
| top-level import genuinely can't work (lazy-loading optional deps, or | ||
| tests that re-import a module). | ||
|
|
||
| ## Testing | ||
|
|
||
| - Framework: `uv run --frozen pytest` | ||
| - Async testing: use anyio, not asyncio | ||
| - Do not use `Test` prefixed classes, use functions | ||
| - IMPORTANT: Tests should be fast and deterministic. Prefer in-memory async execution; | ||
| reach for threads only when necessary, and subprocesses only as a last resort. | ||
| - For end-to-end behavior, an in-memory `Client(server)` is usually the | ||
| cleanest approach (see `tests/client/test_client.py` for the canonical | ||
| pattern). For narrower changes, testing the function directly is fine. Use | ||
| judgment. | ||
| - Test files mirror the source tree: `src/mcp/client/stdio.py` → | ||
| `tests/client/test_stdio.py`. Add tests to the existing file for that module. | ||
| - Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead: | ||
| - Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test | ||
| - For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()` | ||
| - Exception: `sleep()` is appropriate when testing time-based features (e.g., timeouts) | ||
| - Wrap indefinite waits (`event.wait()`, `stream.receive()`) in `anyio.fail_after(5)` to prevent hangs | ||
| - Pytest is configured with `filterwarnings = ["error"]`, so warnings fail | ||
| tests. Don't silence warnings from your own code; fix the underlying cause. | ||
| Scoped `ignore::` entries for upstream libraries are acceptable in | ||
| `pyproject.toml` with a comment explaining why. | ||
|
|
||
| ### Coverage | ||
|
|
||
| CI requires 100% (`fail_under = 100`, `branch = true`). | ||
|
|
||
| - Full check: `./scripts/test` (~23s). Runs coverage + `strict-no-cover` on the | ||
| default Python. Not identical to CI: CI runs 3.10–3.14 × {ubuntu, windows} | ||
| × {locked, lowest-direct}, and some branch-coverage quirks only surface on | ||
| specific matrix entries. | ||
| - Targeted check while iterating (~4s, deterministic): | ||
|
|
||
| ```bash | ||
| uv run --frozen coverage erase | ||
| uv run --frozen coverage run -m pytest tests/path/test_foo.py | ||
| uv run --frozen coverage combine | ||
| uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0 | ||
| # UV_FROZEN=1 propagates --frozen to the uv subprocess strict-no-cover spawns | ||
| UV_FROZEN=1 uv run --frozen strict-no-cover | ||
claude[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
claude[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0` | ||
| and `--include` scope the report. `strict-no-cover` has no false positives on | ||
| partial runs — if your new test executes a line marked `# pragma: no cover`, | ||
| even a single-file run catches it. | ||
|
|
||
| Avoid adding new `# pragma: no cover`, `# type: ignore`, or `# noqa` comments. | ||
| In tests, use `assert isinstance(x, T)` to narrow types instead of | ||
| `# type: ignore`. In library code (`src/`), a `# pragma: no cover` needs very | ||
| good reasoning — it usually means a test is missing. Audit before pushing: | ||
|
|
||
| ```bash | ||
| git diff origin/main... | grep -E '^\+.*(pragma|type: ignore|noqa)' | ||
| ``` | ||
|
|
||
| What the existing pragmas mean: | ||
|
|
||
| - `# pragma: no cover` — line is never executed. CI's `strict-no-cover` (skipped | ||
| on Windows runners) fails if it IS executed. When your test starts covering | ||
| such a line, remove the pragma. | ||
| - `# pragma: lax no cover` — excluded from coverage but not checked by | ||
| `strict-no-cover`. Use for lines covered on some platforms/versions but not | ||
| others. | ||
| - `# pragma: no branch` — excludes branch arcs only. coverage.py misreports the | ||
| `->exit` arc for nested `async with` on Python 3.11+ (worse on 3.14/Windows). | ||
|
|
||
| ## Breaking Changes | ||
|
|
||
| When making breaking changes, document them in `docs/migration.md`. Include: | ||
|
|
||
| - What changed | ||
| - Why it changed | ||
| - How to migrate existing code | ||
|
|
||
| Search for related sections in the migration guide and group related changes together | ||
| rather than adding new standalone sections. | ||
|
|
||
| ## Formatting & Type Checking | ||
|
|
||
| - Format: `uv run --frozen ruff format .` | ||
| - Lint: `uv run --frozen ruff check . --fix` | ||
| - Type check: `uv run --frozen pyright` | ||
| - Pre-commit runs all of the above plus markdownlint, a `uv.lock` consistency | ||
| check, and README checks — see `.pre-commit-config.yaml` | ||
|
|
||
| ## Exception Handling | ||
|
|
||
| - **Always use `logger.exception()` instead of `logger.error()` when catching exceptions** | ||
| - Don't include the exception in the message: `logger.exception("Failed")` not `logger.exception(f"Failed: {e}")` | ||
| - **Catch specific exceptions** where possible: | ||
| - File ops: `except (OSError, PermissionError):` | ||
| - JSON: `except json.JSONDecodeError:` | ||
| - Network: `except (ConnectionError, TimeoutError):` | ||
| - **FORBIDDEN** `except Exception:` - unless in top-level handlers | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,174 +1 @@ | ||
| # Development Guidelines | ||
|
|
||
| This document contains critical information about working with this codebase. Follow these guidelines precisely. | ||
|
|
||
| ## Core Development Rules | ||
|
|
||
| 1. Package Management | ||
| - ONLY use uv, NEVER pip | ||
| - Installation: `uv add <package>` | ||
| - Running tools: `uv run <tool>` | ||
| - Upgrading: `uv lock --upgrade-package <package>` | ||
| - FORBIDDEN: `uv pip install`, `@latest` syntax | ||
|
|
||
| 2. Code Quality | ||
| - Type hints required for all code | ||
| - Public APIs must have docstrings | ||
| - Functions must be focused and small | ||
| - Follow existing patterns exactly | ||
| - Line length: 120 chars maximum | ||
| - FORBIDDEN: imports inside functions. THEY SHOULD BE AT THE TOP OF THE FILE. | ||
|
|
||
| 3. Testing Requirements | ||
| - Framework: `uv run --frozen pytest` | ||
| - Async testing: use anyio, not asyncio | ||
| - Do not use `Test` prefixed classes, use functions | ||
| - Coverage: test edge cases and errors | ||
| - New features require tests | ||
| - Bug fixes require regression tests | ||
| - IMPORTANT: The `tests/client/test_client.py` is the most well designed test file. Follow its patterns. | ||
| - IMPORTANT: Be minimal, and focus on E2E tests: Use the `mcp.client.Client` whenever possible. | ||
| - Coverage: CI requires 100% (`fail_under = 100`, `branch = true`). | ||
| - Full check: `./scripts/test` (~23s). Runs coverage + `strict-no-cover` on the | ||
| default Python. Not identical to CI: CI also runs 3.10–3.14 × {ubuntu, windows}, | ||
| and some branch-coverage quirks only surface on specific matrix entries. | ||
| - Targeted check while iterating (~4s, deterministic): | ||
|
|
||
| ```bash | ||
| uv run --frozen coverage erase | ||
| uv run --frozen coverage run -m pytest tests/path/test_foo.py | ||
| uv run --frozen coverage combine | ||
| uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0 | ||
| UV_FROZEN=1 uv run --frozen strict-no-cover | ||
| ``` | ||
|
|
||
| Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0` | ||
| and `--include` scope the report. `strict-no-cover` has no false positives on | ||
| partial runs — if your new test executes a line marked `# pragma: no cover`, | ||
| even a single-file run catches it. | ||
| - Coverage pragmas: | ||
| - `# pragma: no cover` — line is never executed. CI's `strict-no-cover` fails if | ||
| it IS executed. When your test starts covering such a line, remove the pragma. | ||
| - `# pragma: lax no cover` — excluded from coverage but not checked by | ||
| `strict-no-cover`. Use for lines covered on some platforms/versions but not | ||
| others. | ||
| - `# pragma: no branch` — excludes branch arcs only. coverage.py misreports the | ||
| `->exit` arc for nested `async with` on Python 3.11+ (worse on 3.14/Windows). | ||
| - Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead: | ||
| - Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test | ||
| - For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()` | ||
| - Exception: `sleep()` is appropriate when testing time-based features (e.g., timeouts) | ||
| - Wrap indefinite waits (`event.wait()`, `stream.receive()`) in `anyio.fail_after(5)` to prevent hangs | ||
|
|
||
| Test files mirror the source tree: `src/mcp/client/streamable_http.py` → `tests/client/test_streamable_http.py` | ||
| Add tests to the existing file for that module. | ||
|
|
||
| - For commits fixing bugs or adding features based on user reports add: | ||
|
|
||
| ```bash | ||
| git commit --trailer "Reported-by:<name>" | ||
| ``` | ||
|
|
||
| Where `<name>` is the name of the user. | ||
|
|
||
| - For commits related to a Github issue, add | ||
|
|
||
| ```bash | ||
| git commit --trailer "Github-Issue:#<number>" | ||
| ``` | ||
|
|
||
| - NEVER ever mention a `co-authored-by` or similar aspects. In particular, never | ||
| mention the tool used to create the commit message or PR. | ||
|
|
||
| ## Pull Requests | ||
|
|
||
| - Create a detailed message of what changed. Focus on the high level description of | ||
| the problem it tries to solve, and how it is solved. Don't go into the specifics of the | ||
| code unless it adds clarity. | ||
|
|
||
| - NEVER ever mention a `co-authored-by` or similar aspects. In particular, never | ||
| mention the tool used to create the commit message or PR. | ||
|
|
||
| ## Breaking Changes | ||
|
|
||
| When making breaking changes, document them in `docs/migration.md`. Include: | ||
|
|
||
| - What changed | ||
| - Why it changed | ||
| - How to migrate existing code | ||
|
|
||
| Search for related sections in the migration guide and group related changes together | ||
| rather than adding new standalone sections. | ||
|
|
||
| ## Python Tools | ||
|
|
||
| ## Code Formatting | ||
|
|
||
| 1. Ruff | ||
| - Format: `uv run --frozen ruff format .` | ||
| - Check: `uv run --frozen ruff check .` | ||
| - Fix: `uv run --frozen ruff check . --fix` | ||
| - Critical issues: | ||
| - Line length (88 chars) | ||
| - Import sorting (I001) | ||
| - Unused imports | ||
| - Line wrapping: | ||
| - Strings: use parentheses | ||
| - Function calls: multi-line with proper indent | ||
| - Imports: try to use a single line | ||
|
|
||
| 2. Type Checking | ||
| - Tool: `uv run --frozen pyright` | ||
| - Requirements: | ||
| - Type narrowing for strings | ||
| - Version warnings can be ignored if checks pass | ||
|
|
||
| 3. Pre-commit | ||
| - Config: `.pre-commit-config.yaml` | ||
| - Runs: on git commit | ||
| - Tools: Prettier (YAML/JSON), Ruff (Python) | ||
| - Ruff updates: | ||
| - Check PyPI versions | ||
| - Update config rev | ||
| - Commit config first | ||
|
|
||
| ## Error Resolution | ||
|
|
||
| 1. CI Failures | ||
| - Fix order: | ||
| 1. Formatting | ||
| 2. Type errors | ||
| 3. Linting | ||
| - Type errors: | ||
| - Get full line context | ||
| - Check Optional types | ||
| - Add type narrowing | ||
| - Verify function signatures | ||
|
|
||
| 2. Common Issues | ||
| - Line length: | ||
| - Break strings with parentheses | ||
| - Multi-line function calls | ||
| - Split imports | ||
| - Types: | ||
| - Add None checks | ||
| - Narrow string types | ||
| - Match existing patterns | ||
|
|
||
| 3. Best Practices | ||
| - Check git status before commits | ||
| - Run formatters before type checks | ||
| - Keep changes minimal | ||
| - Follow existing patterns | ||
| - Document public APIs | ||
| - Test thoroughly | ||
|
|
||
| ## Exception Handling | ||
|
|
||
| - **Always use `logger.exception()` instead of `logger.error()` when catching exceptions** | ||
| - Don't include the exception in the message: `logger.exception("Failed")` not `logger.exception(f"Failed: {e}")` | ||
| - **Catch specific exceptions** where possible: | ||
| - File ops: `except (OSError, PermissionError):` | ||
| - JSON: `except json.JSONDecodeError:` | ||
| - Network: `except (ConnectionError, TimeoutError):` | ||
| - **FORBIDDEN** `except Exception:` - unless in top-level handlers | ||
| @AGENTS.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.