Skip to content

Split protocol types into a standalone mcp-types package#2973

Merged
Kludex merged 8 commits into
mainfrom
worktree-eager-growing-unicorn
Jun 25, 2026
Merged

Split protocol types into a standalone mcp-types package#2973
Kludex merged 8 commits into
mainfrom
worktree-eager-growing-unicorn

Conversation

@Kludex

@Kludex Kludex commented Jun 25, 2026

Copy link
Copy Markdown
Member

What

Extract the protocol wire types from src/mcp/types/ into a separate distribution, mcp-types (imported as mcp_types), wired into the existing uv workspace as a member under src/mcp-types/ (same nesting as httpx2/httpcore2).

mcp-types depends only on pydantic + typing-extensions, so tooling and lightweight clients can (de)serialize MCP traffic without pulling in httpx, starlette, uvicorn, and the rest of the server/transport stack.

Key changes

  • src/mcp/types/src/mcp-types/mcp_types/ (_types, jsonrpc, _wire_base, methods, v2025_11_25/, v2026_07_28/), moved with git mv so history is preserved.
  • src/mcp/shared/version.pysrc/mcp-types/mcp_types/version.py. It is a pure leaf with no mcp dependencies, which is what lets mcp_types depend on nothing in mcp (methods.py needs KNOWN_PROTOCOL_VERSIONS).
  • mcp.types and mcp.shared.version are removed (no shim). All importers are rewritten to mcp_types / mcp_types.version.
  • mcp.__init__ still re-exports the type names from mcp_types, so from mcp import Tool is unchanged.
  • Workspace/build wiring: [tool.uv.workspace] member, [tool.uv.sources], mcp dependency, pyright include, ruff per-file-ignore, coverage source + [tool.coverage.paths] mapping, and scripts/gen_surface_types.py output path + --base-class.

Breaking change

from mcp.types import X and from mcp.shared.version import X no longer work; use from mcp_types import X / from mcp_types.version import X. The top-level from mcp import X re-exports are unchanged. Documented in docs/migration.md.

Verification

  • 2714 tests pass; pyright reports 0 errors; ruff/format clean.
  • 100.00% coverage, strict-no-cover clean.
  • mcp-types builds standalone (wheel includes py.typed and all modules).
  • scripts/gen_surface_types.py --check regenerates the v* validators identically from the new path.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

Review in cubic

Kludex added 2 commits June 25, 2026 17:21
Extract the wire types from `src/mcp/types/` into a separate distribution,
`mcp-types` (imported as `mcp_types`), wired into the uv workspace. The new
package depends only on pydantic, so tooling and lightweight clients can
(de)serialize MCP traffic without the full server/transport stack.

`mcp.shared.version` moves to `mcp_types.version` (a pure leaf with no `mcp`
deps), which lets `mcp_types` depend on nothing in `mcp`. The `mcp.types`
submodule and `mcp.shared.version` are removed; `mcp` re-exports the type
names at the top level, so `from mcp import Tool` is unchanged. All importers
are rewritten to `mcp_types`. Documented in docs/migration.md.
The client-conformance CI launches .github/actions/conformance/client.py,
which still did `from mcp import types`; that name is gone after the split,
so every scenario failed with ImportError. Use `import mcp_types as types`.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcp-types/README.md">

<violation number="1" location="mcp-types/README.md:6">
P3: README dependency claim is incorrect; package also depends on `typing-extensions`. This can mislead lightweight consumers about install footprint.</violation>
</file>

<file name="examples/servers/simple-prompt/mcp_simple_prompt/server.py">

<violation number="1" location="examples/servers/simple-prompt/mcp_simple_prompt/server.py:3">
P2: Directly importing `mcp_types` without declaring `mcp-types` as a direct dependency creates undeclared runtime coupling. Add `mcp-types` to this example package dependencies.</violation>
</file>

Partial review: This PR has more than 50 files, so cubic reviewed the highest-priority files first. During the trial, paid plans get a higher file limit.
You can try an ultrareview to bypass the file limit, comment @cubic-dev-ai ultrareview. Learn more.

Fix all with cubic | Re-trigger cubic

Comment thread pyproject.toml Outdated
import anyio
import click
from mcp import types
import mcp_types as types

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Directly importing mcp_types without declaring mcp-types as a direct dependency creates undeclared runtime coupling. Add mcp-types to this example package dependencies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/servers/simple-prompt/mcp_simple_prompt/server.py, line 3:

<comment>Directly importing `mcp_types` without declaring `mcp-types` as a direct dependency creates undeclared runtime coupling. Add `mcp-types` to this example package dependencies.</comment>

<file context>
@@ -1,6 +1,6 @@
 import anyio
 import click
-from mcp import types
+import mcp_types as types
 from mcp.server import Server, ServerRequestContext
 
</file context>
Fix with cubic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying. The examples use import mcp_types as types purely as the types namespace alias that replaced the old from mcp import types; they depend on the mcp SDK conceptually, and mcp pins mcp-types=={{ version }}, so it is always present. Declaring it across 13 unpublished demo packages is churn for no resolution benefit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent comment was wrong here: this alias is already covered by the pinned mcp -> mcp-types relationship, so adding direct dependencies to all 13 demo packages would be churn without benefit. I’ll drop this one.

Comment thread src/mcp-types/README.md Outdated
The wire types for the [Model Context Protocol](https://modelcontextprotocol.io).

This package holds the protocol message models, JSON-RPC envelope types, per-version
surface validators, and the protocol-version registry. It depends only on `pydantic`,

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: README dependency claim is incorrect; package also depends on typing-extensions. This can mislead lightweight consumers about install footprint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcp-types/README.md, line 6:

<comment>README dependency claim is incorrect; package also depends on `typing-extensions`. This can mislead lightweight consumers about install footprint.</comment>

<file context>
@@ -0,0 +1,16 @@
+The wire types for the [Model Context Protocol](https://modelcontextprotocol.io).
+
+This package holds the protocol message models, JSON-RPC envelope types, per-version
+surface validators, and the protocol-version registry. It depends only on `pydantic`,
+so it can be installed on its own when you need to (de)serialize MCP traffic without
+pulling in the full `mcp` SDK.
</file context>
Fix with cubic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a3b86 - now states the runtime deps are pydantic and typing-extensions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a6a3b86 — the README now lists both pydantic and typing-extensions.

Comment thread .github/actions/conformance/client.py Outdated
Comment thread pyproject.toml Outdated
Kludex added 4 commits June 25, 2026 17:39
Relocate the package from a repo-root `mcp-types/` to `src/mcp-types/`,
mirroring how httpx2/httpcore2 nest each workspace member under src/.
Update the workspace member path, pyright include, ruff per-file-ignore,
coverage source + path mapping, and the codegen TYPES_DIR accordingly.
After the split, `uv build` only produced the root `mcp` distribution, so
`mcp-types` would never reach PyPI. Build both packages explicitly (not
`--all-packages`, which would also bundle the unpublishable examples/* members).
Both share one git-tag-driven version via uv-dynamic-versioning, like httpx2.
Mirror the httpx2/httpcore2 setup: make mcp's dependencies dynamic and move
them into the uv-dynamic-versioning metadata hook so mcp-types can be pinned
with `mcp-types=={{ version }}`. Each mcp release now requires the exact
mcp-types built from the same commit. The workspace source keeps local/editable
installs resolving to the in-tree package regardless of the pin.
…g-unicorn

# Conflicts:
#	tests/client/test_session.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pyproject.toml">

<violation number="1" location="pyproject.toml:3">
P3: Making `project.dependencies` dynamic breaks normal `uv add` workflows for runtime deps, creating a maintenance trap for contributors.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread pyproject.toml
[project]
name = "mcp"
dynamic = ["version"]
dynamic = ["version", "dependencies"]

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Making project.dependencies dynamic breaks normal uv add workflows for runtime deps, creating a maintenance trap for contributors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 3:

<comment>Making `project.dependencies` dynamic breaks normal `uv add` workflows for runtime deps, creating a maintenance trap for contributors.</comment>

<file context>
@@ -1,6 +1,6 @@
 [project]
 name = "mcp"
-dynamic = ["version"]
+dynamic = ["version", "dependencies"]
 description = "Model Context Protocol SDK"
 readme = "README.v2.md"
</file context>
Fix with cubic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying: intentional. Making dependencies dynamic is what lets the uv-dynamic-versioning hook expand mcp-types=={{ version }} to the exact sibling version, exactly as httpx2 pins httpcore2. uv add still works - it edits the hook table and uv reconciles. The lockstep pin is worth the tradeoff.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent comment was wrong here: dependencies is intentionally dynamic so uv-dynamic-versioning can expand mcp-types=={{ version }} to the exact sibling version, and uv add still reconciles correctly.

Force-include the repo-root LICENSE into the mcp-types sdist (it builds from
src/mcp-types/, so it wasn't picked up), mirroring how httpx2 pulls the root
LICENSE into each member.

Remove the [tool.coverage.paths] mcp_types block: src/mcp-types/mcp_types lives
under src/, so the existing 'source' alias already canonicalizes its local/CI/
Windows paths. Coverage still hits 100%.
@maxisbey

Copy link
Copy Markdown
Contributor

From Claude:

I went through this in depth — the diff, both built artifacts, the install paths CI doesn't exercise, and the docs/release plumbing that lives outside the PR's file set. The short version: the split itself is done right. A few things below need fixing, and the two most important ones are invisible to every green check on this PR because neither the publish path nor the docs build runs in PR CI.

What I verified is solid, so nobody has to re-derive it:

  • The mcp-types=={{ version }} mechanism works on every path. uv build --package mcp --wheel renders Requires-Dist: mcp-types==2.0.0a3.dev39+2981ca7, byte-identical to the version mcp-types builds with from the same commit. The same holds when building the mcp wheel from its sdist in a directory with no .git (the pip install --no-binary path), so the uv-dynamic-versioning no-git fallback feeds the metadata hook correctly. The mcp-types wheel and sdist both carry py.typed and LICENSE.
  • The import surface is name-for-name intact. Every moved module's __all__ is identical to main's (mcp_types 208, methods 22, jsonrpc/version byte-identical); mcp.__all__ is unchanged at 67 names and every one of them is the same object as its mcp_types counterpart, so isinstance can never split across the two import paths. The only import paths that ceased to exist are exactly the two documented ones.
  • The ~200-file rewrite is purely mechanical — I diffed every changed module's import bindings against main and the only residual anywhere is from mcp import typesimport mcp_types as types, which binds the same object.
  • The exact == pin is the right design and I'd keep it over a range. The coupling is total (~60 re-exports plus internal imports of methods/version/the v* packages), both packages are versioned from the same tag, and an alpha series regenerates the types per protocol revision — a >=/< range would let the resolver pair an mcp with a mcp-types it was never tested against.

Now the findings, in priority order.

1. The publish path can ship a globally broken mcp — please harden before the next release tag

mcp-types already exists on PyPI (your 0.0.1/0.2.0 experiment, owned by Kludex alone), while mcp is owned by dsp/jspahrsummers/maxisbey. The publish job (publish-pypi.yml#L29-L32) builds both into one dist/ and uploads everything in a single pypa/gh-action-pypi-publish step via OIDC trusted publishing — which is configured per PyPI project.

If the mcp-types trusted publisher isn't set up before the next tag, the chain is: the minted token isn't scoped to mcp-typesmcp-2.0.0aN-*.whl sorts first and uploads successfully → the mcp_types-* upload 403s → the job goes red, but mcp==2.0.0aN is already live on PyPI with an unsatisfiable Requires-Dist: mcp-types==2.0.0aN, and pip install mcp is broken for everyone until it's yanked. The earlier review thread that named this got resolved when the workflow build step was added, but the PyPI-side half can't be fixed by a commit, so it fell off the list.

Two halves:

Out of repo (only you can do this one): on pypi.org/manage/project/mcp-types/settings/publishing/, add a Trusted Publisher matching mcp's exactly (repo modelcontextprotocol/python-sdk, workflow publish-pypi.yml, environment release), and add the same owners as mcp. Also worth deciding on 0.0.1/0.2.0 — a bare pip install mcp-types will keep resolving to the unrelated 0.2.0 until the first stable 2.x, since pip excludes pre-releases by default. Yanking them is the clean answer.

In repo — make the order of operations not matter. Publish the leaf dependency first, so any failure stops before anything mcp-named uploads and the worst case is a harmless orphan mcp-types release:

      - name: Build
        run: |
          uv build --package mcp-types -o dist/types
          uv build --package mcp        -o dist/mcp
  # ...
      - name: Publish mcp-types
        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
        with: { packages-dir: dist/types, skip-existing: true }
      - name: Publish mcp
        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
        with: { packages-dir: dist/mcp, skip-existing: true }

skip-existing: true makes a partial failure recoverable by re-running. I'd also add a short note to RELEASE.md: a release now publishes two projects at the same version, the trusted-publisher precondition, and the fact that step 5's Development Status classifier bump now applies to both pyprojects.

2. The published API reference silently goes wrong (PR-introduced, no CI sees it)

docs/hooks/gen_ref_pages.py#L10-L13 globs src/**/*.py and derives the mkdocstrings identifier from the path relative to src/. For the new package that yields ::: mcp-types.mcp_types, ::: mcp-types.mcp_types.methods, … — six identifiers whose top component contains a hyphen.

The trap is that this does not fail the strict build, which is what I expected. Griffe resolves modules by walking the filesystem, not importlib, so it treats src/mcp-types/ as a namespace package and happily collects it:

mcp-types.mcp_types          -> COLLECTED  kind=module  members=216
mcp-types.mcp_types.methods  -> COLLECTED  kind=module  members=44
mcp.types                    -> FAILED: KeyError: 'types'

So mkdocs build is green, the deploy succeeds, and the published v2 API reference documents the entire type system under the heading mcp-types.mcp_types — a dotted path that's a syntax error to import — inside a mcp-types ▸ mcp_types ▸ … nav, while every existing link to /api/mcp/types/* 404s. No PR job builds the docs (deploy-docs.yml is on: push: branches: [main]), and this PR's pyproject.toml/uv.lock changes match its path filter, so it fires on the first post-merge push.

The fix is small and I validated it end to end (all 97 emitted identifiers, zero hyphens, every one resolves):

src = root / "src"
package_roots = {src / "mcp": src, src / "mcp-types" / "mcp_types": src / "mcp-types"}

for pkg, base in package_roots.items():
    for path in sorted(pkg.rglob("*.py")):
        module_path = path.relative_to(base).with_suffix("")
        doc_path = path.relative_to(base).with_suffix(".md")
        # ... rest of the loop unchanged

Plus three one-liners that belong in the same commit: paths: [src, src/mcp-types] at mkdocs.yml#L120, src/mcp-types added to watch: at mkdocs.yml#L104-L105, and src/mcp-types/** in the trigger filter at deploy-docs.yml#L8-L15. Separately and pre-existing: there's no mkdocs build --strict PR check at all, which is why this class of thing can hide — worth its own follow-up.

3. docs/migration.md has two real defects

(a) The headline "After (v2)" example raises ImportError. docs/migration.md#L238:

# Top-level re-exports are unchanged:
from mcp import Tool, CallToolResult
ImportError: cannot import name 'CallToolResult' from 'mcp'. Did you mean: 'CallToolRequest'?

mcp.__all__ was untouched by this PR and CallToolResult was never in it; the doc picked, as its proof that re-exports are unchanged, a name that was never re-exported. It's the only false one across every from mcp import X line the docs claim. Swapping CallToolResult for a name that's actually exported fixes the doc. There's a broader pre-existing observation worth a separate decision: mcp.__all__ lacks CallToolResult, TextContent, ContentBlock, EmptyResult, CallToolRequestParams, and removing mcp.types is what makes that gap user-facing for the first time — but widening __all__ is a deliberate API call, not a doc fix.

(b) Four **Before (v1):** code blocks were caught by the mechanical sed and now show from mcp_types import … inside blocks documenting v1, where mcp_types doesn't exist: L438, L682, L904, L989. Two are now wrong in both worlds, not just v1: L682 calls ClientRequest.model_validate(data) / .root against mcp_types, where ClientRequest is a plain union, not a RootModel; L904 claims mcp_types.Resource(uri=AnyUrl(...)) "Would fail validation" — false, uri: str accepts it. And the file contradicts itself, since the new section's own Before block at L227 correctly uses from mcp.types import …. Reverting exactly those four lines fixes it.

Two small siblings while in the file: the version-constants prose at L816 tells migrators to switch to HANDSHAKE_PROTOCOL_VERSIONS but never says where it now lives (mcp_types.version, in a different distribution, and not in mcp_types.__all__); and "It depends only on pydantic" at migration.md#L215 and src/mcp-types/README.md#L6 should also name typing-extensions (cubic flagged the README one; it's right).

4. Nothing tests the headline contract

No test would fail if someone later added import httpx inside mcp_types. A plain import mcp_types test can't prove it, because tests/conftest.py#L18 imports mcp.shared._otel before any test runs, so the SDK and its whole transport stack are always already in sys.modules. "Depends only on pydantic" is the point of the PR and is currently enforced by nothing.

Here's a test that does pin it — a sys.meta_path finder that evicts the SDK and rejects any forbidden import while re-importing every mcp_types module from scratch. In-process, sync, deterministic, restores sys.modules exactly. I verified it passes alone, passes after the real conftest preload, and fails precisely when an import anyio is injected into a copy of _types.py.

tests/types/test_standalone.py
"""mcp_types is installable without the mcp SDK: it must import nothing outside
pydantic and typing-extensions (and the standard library)."""

import importlib
import sys
from importlib.machinery import ModuleSpec
from types import ModuleType

import pytest

# Top-level packages mcp_types must never import. `mcp` is the headline contract;
# the rest are the SDK's own runtime dependencies that mcp_types does not declare.
FORBIDDEN_ROOTS = frozenset(
    {
        "mcp",
        "anyio",
        "httpx",
        "httpx_sse",
        "starlette",
        "sse_starlette",
        "uvicorn",
        "jsonschema",
        "pydantic_settings",
        "opentelemetry",
    }
)
MCP_TYPES_MODULES = ("mcp_types", "mcp_types.methods", "mcp_types.v2025_11_25", "mcp_types.v2026_07_28")


class _ForbiddenImportFinder:
    """meta_path hook that fails the test the moment a forbidden package is imported."""

    def find_spec(self, fullname: str, path: object = None, target: object = None) -> ModuleSpec | None:
        if fullname.partition(".")[0] in FORBIDDEN_ROOTS:
            raise AssertionError(f"mcp_types transitively imported {fullname!r}; it must stay SDK-free")
        return None


def test_mcp_types_imports_nothing_from_the_sdk_or_its_dependencies() -> None:
    """Re-import every mcp_types module from scratch with the SDK evicted from
    sys.modules and a finder that rejects any SDK import. A cached `import mcp`
    would otherwise satisfy a stray `from mcp... import` silently."""
    evicted: dict[str, ModuleType] = {}
    finder = _ForbiddenImportFinder()
    sys.meta_path.insert(0, finder)
    try:
        for name in list(sys.modules):
            if name.partition(".")[0] in FORBIDDEN_ROOTS or name.partition(".")[0] == "mcp_types":
                evicted[name] = sys.modules.pop(name)
        for name in MCP_TYPES_MODULES:
            importlib.import_module(name)
    finally:
        sys.meta_path.remove(finder)
        for name in list(sys.modules):
            if name.partition(".")[0] == "mcp_types":
                del sys.modules[name]
        sys.modules.update(evicted)


def test_forbidden_import_finder_rejects_an_evicted_sdk_module() -> None:
    """Prove the guard has teeth: importing `mcp` itself under the finder raises."""
    evicted: dict[str, ModuleType] = {}
    finder = _ForbiddenImportFinder()
    sys.meta_path.insert(0, finder)
    try:
        for name in list(sys.modules):
            if name.partition(".")[0] in FORBIDDEN_ROOTS:
                evicted[name] = sys.modules.pop(name)
        with pytest.raises(AssertionError, match="SDK-free"):
            importlib.import_module("mcp")
    finally:
        sys.meta_path.remove(finder)
        sys.modules.update(evicted)

While in that directory: tests/shared/test_version.py should git mv to tests/types/test_version.py — its source module moved, and the test-tree mirror is the convention this PR follows everywhere else.

5. AGENTS.md is now wrong about uv add

cubic flagged the dynamic = ["dependencies"] ergonomics and it's right, though the conclusion isn't "revert the design." Measured in a throwaway clone: uv add <runtime-pkg> on the root project now fails — and not cleanly. uv writes a static [project].dependencies key while leaving dependencies in dynamic, then dies inside the build backend with ValueError: 'dependencies' is dynamic, a traceback that never points at the real edit site. (Plain uv add reverts the file on failure, so nothing is corrupted; the misleading error is the cost.) uv add --group/--optional, uv lock, uv lock --check, uv lock --upgrade-package all still work — I checked each.

AGENTS.md#L21 still says verbatim "Installation: uv add <package>", which is now wrong for the one case it most obviously describes. For calibration, across the last ~10 months only about three commits added a runtime dependency, so the cost is roughly one confusing failure per quarter — easily worth the exact pin. It just needs the two-line AGENTS.md amendment pointing at [tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies for runtime deps.

Smaller

  • schema/README.md#L6 still says the generator writes src/mcp/types/v<version>/__init__.py — the one straggler in the whole repo (the generator itself was updated).
  • src/mcp-types/pyproject.toml#L50-L51 force-includes ../../LICENSE into the sdist target only, so the wheel gets License-File/dist-info/licenses/LICENSE only when built via the sdist round-trip; uv build --wheel directly gets neither. (The publish path happens to be the working one, which is why it looks right.) Sturdier and simpler: a real LICENSE file at src/mcp-types/ and delete the force-include block — hatchling's default discovery then does the right thing in every mode.

One naming question, while it's still free

mcp_types.version is now load-bearing public API: KNOWN_PROTOCOL_VERSIONS, HANDSHAKE_PROTOCOL_VERSIONS, is_version_at_least and the rest are reachable from nowhere else (only LATEST_PROTOCOL_VERSION/DEFAULT_NEGOTIATED_VERSION made it into mcp_types.__all__). It holds the protocol-revision registry, but in a standalone distribution <pkg>.version conventionally means the distribution's own version (pydantic.version.VERSION). mcp.shared.version was a 1:1 move so this is faithful — but the import is already breaking, so this is the one free moment to either re-export the registry from mcp_types top level (and make version private) or name it for what it is, e.g. mcp_types.protocol_versions. After the first mcp-types release it stops being free. Genuinely fine either way; just deciding it deliberately is the win.


For completeness on cubic's third point — the examples importing mcp_types without declaring mcp-types directly — I'd let that one go: the example packages are never published, [tool.uv.sources] flows to every workspace member, and the repo already treats transitive deps that way in the examples.

Net: I'd merge once §2 and §3 are in (about 30 lines total); §1 is really a precondition on the next release tag rather than on the merge, but since this PR is what creates the hazard I'd put the workflow hardening here too.

AI Disclaimer

Comment thread docs/migration.md
- README: mcp-types also depends on typing-extensions, not just pydantic.
- migration.md: revert four 'Before (v1)' code blocks the bulk mcp.types ->
  mcp_types rewrite wrongly touched; v1 code imported from mcp.types.
@Kludex Kludex merged commit 0ee7f1b into main Jun 25, 2026
55 checks passed
@Kludex Kludex deleted the worktree-eager-growing-unicorn branch June 25, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants