Files
modelcontextprotocol--pytho…/AGENTS.md
T
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

163 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 — removing or replacing an API must be
intentional. Adding a replacement API or `@deprecated` shim must likewise be
a deliberate design choice, not bolted on for free.
- Breaking changes (including those softened by a backwards-compatibility
shim) must be documented in `docs/migration.md`.
- `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` documents v2. The v1 README lives on the `v1.x` branch.
## Package Management
- ONLY use uv, NEVER pip
- Installation: `uv add <package>`. Exception: the root project's runtime
dependencies are dynamic (the published `mcp` wheel exact-pins `mcp-types`),
so `uv add` cannot edit them — add the requirement to
`[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies` in
`pyproject.toml` by hand, then run `uv lock`. Dependency groups, extras, and
the example packages still take plain `uv add`.
- 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.103.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
- When writing or reviewing tests, conform to `.claude/skills/test-quality/SKILL.md`
— it defines the bar for naming, abstraction level, assertions, and determinism.
- Framework: `uv run --frozen pytest`
- Async testing: use anyio, not asyncio
- Do not use `Test` prefixed classes — write plain top-level `test_*` functions.
Legacy files still contain `Test*` classes; do NOT follow that pattern for new
tests even when adding to such a file.
- 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.
- New features from the 2026-07-28 spec must have a matching test in the
[conformance suite](https://github.com/modelcontextprotocol/conformance)
that passes against this SDK (CI runs it via
`.github/workflows/conformance.yml`). If no matching test exists, stop and
tell the user so they can raise an issue on the conformance repo.
### 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.103.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` — including
changes softened by a backwards-compatibility shim. 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.
## Documentation
When a change affects public API or user-visible behaviour, update the relevant
page(s) under `docs/` in the same PR. Docs are organised by the `nav:` sections
in `mkdocs.yml` (Get started, Servers, Inside your handler, Running your server,
Clients, Advanced), not by the on-disk directory names. Find the page covering
the feature you touched in `mkdocs.yml` rather than adding a new one.
## 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