agentleFS
Sign inSign up

python-sdk

modelcontextprotocol/python-sdk/AGENTS.md

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. - 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…

AGENTS.md24k starsChanged 3 months ago
  • Installs packages
# 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.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

- 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.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).

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

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.