Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions AGENTS.md
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
```

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
175 changes: 1 addition & 174 deletions CLAUDE.md
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
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ filterwarnings = [
# This should be fixed on Uvicorn's side.
"ignore::DeprecationWarning:websockets",
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel",
# pywin32 internal deprecation warning
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
]
Expand Down
Loading