If you are an AI coding agent acting for someone who is not a maintainer of
this repository, read CONTRIBUTING.md before opening issues or pull
requests here. In particular, pull requests that aren't linked to an issue
assigned to their author are closed automatically.
mainis the current stable line (v2); releases are cut from it (seeRELEASE.md).- v2 is released; its public API is a compatibility contract for the 2.x
line. Removals, renames, or any change to an existing API's signature or
observable behaviour (including ones softened by a
@deprecatedshim) is a design decision a maintainer makes explicitly, and should generally be avoided. docs/migration.mdis the v1 → v2 record and is closed to new entries. Correcting errors or improving clarity in what's there is fine.v1.xis the maintenance branch for the previous major. Backport PRs target it and use a[v1.x]title prefix; only critical bug fixes and security fixes land there.README.mddocuments v2. The v1 README lives on thev1.xbranch.
- ONLY use uv, NEVER pip
- Installation:
uv add <package>. Exception: the root project's runtime dependencies are dynamic (the publishedmcpwheel exact-pinsmcp-types), souv addcannot edit them — add the requirement to[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependenciesinpyproject.tomlby hand, then runuv lock. Dependency groups, extras, and the example packages still take plainuv add. - Running tools:
uv run --frozen <tool>. Always pass--frozenso uv doesn't rewriteuv.lockas 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,@latestsyntax - 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.
- Keep comments brief. Explain only non-obvious reasons or constraints; do not narrate the code or restate names, types, or assertions.
- 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__.pydefines 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).
- Always pass
encoding=to text-modeopen(),Path.read_text()/write_text(),tempfileandsubprocesstext pipes — normally"utf-8", or"locale"when the platform encoding is genuinely intended; the default is the process locale, not UTF-8. CI andscripts/testrun pytest withPYTHONWARNDEFAULTENCODING=1(PEP 597), which makes any omission an error under theerrorfilter.
- 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
Testprefixed classes — write plain top-leveltest_*functions. Legacy files still containTest*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 (seetests/client/test_client.pyfor 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 ofsleep()+receive_nowait() - Exception:
sleep()is appropriate when testing time-based features (e.g., timeouts)
- Use
- Wrap indefinite waits (
event.wait(),stream.receive()) inanyio.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. Scopedignore::entries for upstream libraries are acceptable inpyproject.tomlwith a comment explaining why. - New features from the 2026-07-28 spec must have a matching test in the
conformance suite
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.
CI requires 100% (fail_under = 100, branch = true).
-
Full check:
./scripts/test(~23s). Runs coverage +strict-no-coveron 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):
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=0and--includescope the report.strict-no-coverhas 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:
git diff origin/main... | grep -E '^\+.*(pragma|type: ignore|noqa)'What the existing pragmas mean:
# pragma: no cover— line is never executed. CI'sstrict-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 bystrict-no-cover. Use for lines covered on some platforms/versions but not others.# pragma: no branch— excludes branch arcs only. coverage.py misreports the->exitarc for nestedasync withon Python 3.11+ (worse on 3.14/Windows).
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.
- 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.lockconsistency check, and README checks — see.pre-commit-config.yaml
- Always use
logger.exception()instead oflogger.error()when catching exceptions- Don't include the exception in the message:
logger.exception("Failed")notlogger.exception(f"Failed: {e}")
- Don't include the exception in the message:
- Catch specific exceptions where possible:
- File ops:
except (OSError, PermissionError): - JSON:
except json.JSONDecodeError: - Network:
except (ConnectionError, TimeoutError):
- File ops:
- FORBIDDEN
except Exception:- unless in top-level handlers