python-sdk Agent Skills & AI Rules

GitHub

The official Python SDK for Model Context Protocol servers and clients

23,997 stars Python Python 1 file Β· ~20,685 tokens #mcp#mcp-client#mcp-server#python
RAW Rules
6 Discovered Rules β€’ ~6,426 Estimated Tokens
Quick:
Loading skill content...

AI Agent Protocol & Instructions

AGENTS.md root-instruction ~2,210 tokens
Raw
# Development Guidelines

## Note for AI Agents

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.

## Branching Model

- `main` is the current stable line (v2); releases are cut from it (see
  `RELEASE.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 `@deprecated` shim) is a
  design decision a maintainer makes explicitly, and should generally be
  avoided.
- `docs/migration.md` is the v1 β†’ v2 record and is closed to new entries.
  Correcting errors or improving clarity in what's there is fine.
- `v1.x` is 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.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.11 pytest ...` to run
  against a specific interpreter (CI covers 3.11–3.15).
- 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

- 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__.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).
- Always pass `encoding=` to text-mode `open()`, `Path.read_text()`/`write_text()`,
  `tempfile` and `subprocess` text pipes β€” normally `"utf-8"`, or `"locale"` when the
  platform encoding is genuinely intended; the default is the process locale, not UTF-8.
  CI and `scripts/test` run pytest with `PYTHONWARNDEFAULTENCODING=1` (PEP 597), which
  makes any omission an error under the `error` filter.

## 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.11–3.15 Γ— {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).

## 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