diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d28d3721f2..b958fd23ab 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,6 +12,7 @@ on: - docs_src/** - mkdocs.yml - src/mcp/** + - src/mcp-types/** - scripts/build-docs.sh - pyproject.toml - uv.lock diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d77820b1d1..41b127f923 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -58,3 +58,7 @@ jobs: - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + with: + # Lets a re-run after a partially failed upload publish the remaining + # files instead of erroring on the ones already on PyPI. + skip-existing: true diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 8989639b51..3ab4753568 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -35,6 +35,13 @@ jobs: uv sync --group codegen --frozen uv run --frozen --group codegen python scripts/gen_surface_types.py --check + # Resolves only mcp-types' declared dependencies into an empty environment, + # so an import of the SDK or anything from its stack fails here. + - name: mcp-types installs and imports standalone + run: | + uv run --isolated --no-project --with ./src/mcp-types python -c \ + "import mcp_types, mcp_types.jsonrpc, mcp_types.methods, mcp_types.version, mcp_types.v2025_11_25, mcp_types.v2026_07_28" + # TODO(Max): Drop this in v2. Deliberate updates (e.g. the v2 status # banner) go through the 'override-readme-freeze' label. - name: Check README.md is not modified diff --git a/AGENTS.md b/AGENTS.md index 1dbac17e9b..7c8ecb9545 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,12 @@ ## Package Management - ONLY use uv, NEVER pip -- Installation: `uv add ` +- Installation: `uv add `. 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 `. 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 diff --git a/RELEASE.md b/RELEASE.md index ea46ebdb41..fba7115bbc 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,7 +2,9 @@ ## Bumping Dependencies -1. Change dependency version in `pyproject.toml` +1. Change the dependency version in `pyproject.toml`. The root `mcp` project's + runtime dependencies are dynamic and live under + `[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`. 2. Upgrade lock with `uv lock --resolution lowest-direct` ## Major or Minor Release @@ -21,6 +23,16 @@ The package version will be set automatically from the tag. v2 pre-releases are cut from `main` with a PEP 440 pre-release tag: `v2.0.0aN` for alphas, later `bN`/`rcN` for betas and release candidates. +A release publishes two distributions, `mcp` and `mcp-types`, at the same +version, and the `mcp` wheel exact-pins `mcp-types`. Before the first release +that includes both, the `mcp-types` PyPI project must be given the same +trusted publisher as `mcp` (this repository, workflow `publish-pypi.yml`, +environment `release`) and the same owners — without it the `mcp-types` +upload is rejected. If only some of the files upload, fix the cause and re-run +the publish job — `skip-existing` makes it skip whatever already landed. The +`Development Status` classifier in both `pyproject.toml` files is permanently +`5 - Production/Stable`; it is not bumped as part of any release. + 1. Check the full test matrix is green on the release commit. The matrix runs with `continue-on-error`, so a green workflow run does not mean the tests passed — check the individual jobs. diff --git a/docs/hooks/gen_ref_pages.py b/docs/hooks/gen_ref_pages.py index ad8c19b45f..8e1afeee68 100644 --- a/docs/hooks/gen_ref_pages.py +++ b/docs/hooks/gen_ref_pages.py @@ -9,27 +9,32 @@ root = Path(__file__).parent.parent.parent src = root / "src" -for path in sorted(src.rglob("*.py")): - module_path = path.relative_to(src).with_suffix("") - doc_path = path.relative_to(src).with_suffix(".md") - full_doc_path = Path("api", doc_path) - - parts = tuple(module_path.parts) - - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1].startswith("_"): - continue - - nav[parts] = doc_path.as_posix() - - with mkdocs_gen_files.open(full_doc_path, "w") as fd: - ident = ".".join(parts) - fd.write(f"::: {ident}") - - mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) +# `src/mcp-types` is a distribution directory, not an import package, so each +# package's dotted module path is taken relative to its own parent: deriving it +# from `src/` would emit the unimportable `mcp-types.mcp_types.*`. +for package in (src / "mcp", src / "mcp-types" / "mcp_types"): + base = package.parent + for path in sorted(package.rglob("*.py")): + module_path = path.relative_to(base).with_suffix("") + doc_path = path.relative_to(base).with_suffix(".md") + full_doc_path = Path("api", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1].startswith("_"): + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}") + + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/migration.md b/docs/migration.md index 0ea24991fa..e64de8128c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -213,10 +213,11 @@ The WebSocket transport has been removed: `mcp.client.websocket.websocket_client ### `mcp.types` moved to the `mcp-types` package The protocol wire types now live in a standalone distribution, `mcp-types`, imported as -`mcp_types`. It depends only on `pydantic`, so code that just needs to (de)serialize MCP -traffic can install it without the full SDK. The `mcp` package depends on `mcp-types` and +`mcp_types`. Its only runtime dependencies are `pydantic` and `typing-extensions`, so code +that just needs to (de)serialize MCP traffic can install it without the full SDK. The `mcp` package depends on `mcp-types` and continues to re-export the type names at the top level, so `from mcp import Tool` is -unchanged. Only the `mcp.types` submodule and `mcp.shared.version` were removed. +unchanged. Only the `mcp.types` submodule and `mcp.shared.version` were removed. The +package's API reference is at [`mcp_types`](api/mcp_types/index.md). **Why:** keeping the wire types in their own package lets tooling and lightweight clients depend on the protocol schema without pulling in `httpx`, `starlette`, `uvicorn`, and the @@ -225,18 +226,18 @@ rest of the server/transport stack. **Before (v1):** ```python -from mcp.types import Tool, CallToolResult +from mcp.types import Tool, Resource from mcp.shared.version import LATEST_PROTOCOL_VERSION ``` **After (v2):** ```python -from mcp_types import Tool, CallToolResult +from mcp_types import Tool, Resource from mcp_types.version import LATEST_PROTOCOL_VERSION -# Top-level re-exports are unchanged: -from mcp import Tool, CallToolResult +# Names `mcp` already re-exported at the top level are unchanged: +from mcp import Tool, Resource ``` ### Removed type aliases and classes @@ -814,7 +815,7 @@ async def my_tool(ctx: Context[MyLifespanState]) -> str: ... ### Version constants -`SUPPORTED_PROTOCOL_VERSIONS` is deprecated — it's now the union of `HANDSHAKE_PROTOCOL_VERSIONS` (initialize-handshake versions) and `MODERN_PROTOCOL_VERSIONS` (per-request-envelope versions). If you were using it to mean "versions the initialize handshake accepts", switch to `HANDSHAKE_PROTOCOL_VERSIONS`. Named scalars derived from these tuples are now exported alongside them — `LATEST_HANDSHAKE_VERSION`, `LATEST_MODERN_VERSION`, `OLDEST_SUPPORTED_VERSION` — so prefer those over indexing the tuples directly. +`SUPPORTED_PROTOCOL_VERSIONS` is deprecated — it's now the union of `HANDSHAKE_PROTOCOL_VERSIONS` (initialize-handshake versions) and `MODERN_PROTOCOL_VERSIONS` (per-request-envelope versions). If you were using it to mean "versions the initialize handshake accepts", switch to `HANDSHAKE_PROTOCOL_VERSIONS`. Named scalars derived from these tuples are now exported alongside them — `LATEST_HANDSHAKE_VERSION`, `LATEST_MODERN_VERSION`, `OLDEST_SUPPORTED_VERSION` — so prefer those over indexing the tuples directly. All of these live in `mcp_types.version` (previously `mcp.shared.version`): `from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS`. ### `ProgressContext` and `progress()` context manager removed diff --git a/mkdocs.yml b/mkdocs.yml index d3bbba2119..a703713ba8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -136,7 +136,7 @@ markdown_extensions: - sane_lists # this means you can start a list from any number watch: - - src/mcp + - src - docs_src plugins: @@ -152,7 +152,7 @@ plugins: - mkdocstrings: handlers: python: - paths: [src] + paths: [src, src/mcp-types] options: relative_crossrefs: true members_order: source diff --git a/schema/README.md b/schema/README.md index 534360c51b..7bb2145f7b 100644 --- a/schema/README.md +++ b/schema/README.md @@ -3,7 +3,8 @@ JSON Schema files for each protocol version the SDK has a wire-shape surface package for, vendored from the [spec repository] at the commit recorded in `PINNED.json`. `scripts/gen_surface_types.py` reads these to regenerate -`src/mcp/types/v/__init__.py`; CI runs the generator with `--check`. +`src/mcp-types/mcp_types/v/__init__.py`; CI runs the generator with +`--check`. To bump: drop the new `schema.json` here as `.json`, update the matching entry in `PINNED.json` (commit + sha256), and run diff --git a/src/mcp-types/pyproject.toml b/src/mcp-types/pyproject.toml index 1ae7bae809..51cabf501b 100644 --- a/src/mcp-types/pyproject.toml +++ b/src/mcp-types/pyproject.toml @@ -14,7 +14,7 @@ maintainers = [ keywords = ["mcp", "llm", "automation"] license = { text = "MIT" } classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", diff --git a/tests/shared/test_version.py b/tests/types/test_version.py similarity index 100% rename from tests/shared/test_version.py rename to tests/types/test_version.py