chore: import upstream snapshot with attribution
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) Has been cancelled
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) Has been cancelled
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# 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.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
|
||||
|
||||
- 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.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` — 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
|
||||
Reference in New Issue
Block a user