Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- mkdocs.yml
- src/mcp/**
- src/mcp-types/**
- src/mcp-client/**
- scripts/build-docs.sh
- scripts/docs/**
- pyproject.toml
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
- name: Build
run: |
uv build --package mcp
uv build --package mcp-client
uv build --package mcp-types
- name: Upload artifacts
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@ jobs:
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"

packages:

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.

Why do we have this job?

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: 0.9.5
- name: Build all distributions
run: |
uv build --package mcp-types
uv build --package mcp-client
uv build --package mcp
- name: Exercise the client wheel and sdist without the server SDK
run: |
for package in dist/mcp_client-*.whl dist/mcp_client-*.tar.gz; do
uv run --isolated --no-project --find-links dist --with "$package" \

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: The package checks run under only the runner's implicit Python version, so they do not validate the built wheel and sdist at the supported 3.10 and 3.14 endpoints. Add a Python matrix or explicit --python runs; the later source-tree matrix does not cover artifact installation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/shared.yml, line 68:

<comment>The package checks run under only the runner's implicit Python version, so they do not validate the built wheel and sdist at the supported 3.10 and 3.14 endpoints. Add a Python matrix or explicit `--python` runs; the later source-tree matrix does not cover artifact installation.</comment>

<file context>
@@ -48,6 +48,31 @@ jobs:
+      - name: Exercise the client wheel and sdist without the server SDK
+        run: |
+          for package in dist/mcp_client-*.whl dist/mcp_client-*.tar.gz; do
+            uv run --isolated --no-project --find-links dist --with "$package" \
+              python scripts/check_client_package.py
+          done
</file context>

python scripts/check_client_package.py
done
- name: Type-check the standalone client package
run: |
uv export --frozen --package mcp-client --no-default-groups --no-dev --no-emit-workspace \
--output-file "$RUNNER_TEMP/client-requirements.txt"
uv run --isolated --no-project --find-links dist --with dist/mcp_client-*.whl \
--with-requirements "$RUNNER_TEMP/client-requirements.txt" --with pyright==1.1.405 \
python scripts/check_client_types.py
- name: Import the full SDK with the client package first
run: |
uv run --isolated --no-project --find-links dist --with dist/mcp-*.whl python -c \
'import mcp_client, mcp; from typing import get_type_hints; assert mcp.Client is mcp_client.Client; get_type_hints(mcp.Client)'

test:
name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion DEPENDENCY_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## How requirements are declared

Every runtime dependency is a `>=` floor set to the oldest version that provides what the SDK uses, with no upper bound unless a dependency's next major is known to break the SDK. The one exception is `mcp-types`, the wire-types package released in lockstep with `mcp`: each `mcp` release requires exactly its own version of it, so it is the other half of the SDK rather than an independent constraint.
Every runtime dependency is a `>=` floor set to the oldest version that provides what the SDK uses, with no upper bound unless a dependency's next major is known to break the SDK. The exceptions are `mcp-client` and `mcp-types`, which release in lockstep with `mcp`: each `mcp` release requires exactly its own version of both, and `mcp-client` requires the matching `mcp-types`. They are parts of the SDK rather than independent constraints.

## When a floor moves

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ uv add "mcp[cli]" # or: pip install "mcp[cli]"

The `cli` extra adds the `mcp` command-line tool (`mcp dev`, `mcp run`, `mcp install`) on top of the SDK; install plain `mcp` if you don't need it. For one-off commands, `uv run --with "mcp[cli]" mcp ...` works without a project.

For a client-only project, use `uv add mcp-client` and `from mcp_client import Client`.
It includes the client transports and OAuth support without the HTTP server dependencies.
See [client-only installation](https://py.sdk.modelcontextprotocol.io/get-started/installation/#client-only-installation).

## A server in 15 lines

Create a `server.py`:
Expand Down
19 changes: 11 additions & 8 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ move; this is the mechanics.

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`.
`[tool.hatch.metadata.hooks.uv-dynamic-versioning].dependencies`, as do
`mcp-client`'s dependencies in `src/mcp-client/pyproject.toml`.
2. Regenerate the lock with `uv lock` (or `uv lock --upgrade-package <package>`
to move just that package's locked version). The committed `uv.lock` is a
normal (default-strategy) resolution; the `lowest-direct` resolution that
Expand All @@ -19,8 +20,8 @@ move; this is the mechanics.
Two branches ship, and the package version comes from the git tag
(`uv-dynamic-versioning`). Publishing a GitHub release runs `publish-pypi.yml`
**from the tagged commit**, so the workflow that fires is the tagged branch's
own: a `main` tag builds and publishes two distributions (`mcp` and
`mcp-types`, lock-stepped via `Requires-Dist: mcp-types=={{ version }}`), and a
own: a `main` tag builds and publishes three distributions (`mcp`,
`mcp-client`, and `mcp-types`, with exact matching-version dependencies), and a
`v1.x` tag builds and publishes `mcp` only.

| Line | Branch | Tag | GitHub release flags |
Expand All @@ -29,11 +30,13 @@ own: a `main` tag builds and publishes two distributions (`mcp` and
| Maintenance (previous major) | `v1.x` | `v1.X.Y` | not a pre-release; **not** Latest |
| Pre-releases | `main` | `v2.X.YaN` / `bN` / `rcN` | **Pre-release** ticked, never Latest |

The `Development Status` classifier in both `pyproject.toml` files is
The `Development Status` classifier in all three `pyproject.toml` files is
permanently `5 - Production/Stable`; it is not bumped as part of any release.
The `mcp-types` PyPI project carries the same trusted publisher as `mcp` (this
repository, workflow `publish-pypi.yml`, environment `release`). For a release
cut from `main`, if only some of the four files upload, fix the cause and
repository, workflow `publish-pypi.yml`, environment `release`). Before the
first `mcp-client` release, verify ownership of the existing PyPI project and
configure that same trusted publisher for it too. For a release cut from `main`, if only some of the six files upload,
correct the cause and
re-run the publish job — its `skip-existing` setting makes it skip whatever
already landed (the `v1.x` workflow publishes a single distribution and has no
such setting).
Expand Down Expand Up @@ -77,7 +80,7 @@ before the tag.
URLs (relative links don't resolve in GitHub release bodies).
5. If a stable release turns out to be broken, yank it on PyPI and release the
fix as the next patch version. Never delete a release from PyPI — version
numbers cannot be reused. Yank `mcp` and `mcp-types` together (they are one
numbers cannot be reused. Yank `mcp`, `mcp-client`, and `mcp-types` together (they are one
release), and set the yank reason and the GitHub release notes to point at
the replacement version, since yanking doesn't stop `==` pins from installing
the broken version.
Expand Down Expand Up @@ -134,6 +137,6 @@ specifier that names a pre-release version, or `--pre`.
4. Curate the release notes: what changed since the previous pre-release, what
is known-incomplete, the install line (`pip install mcp==2.X.YbN`), and a
link to the migration guide, with absolute URLs.
5. If a pre-release turns out to be broken, yank both `mcp` and `mcp-types` on PyPI
5. If a pre-release turns out to be broken, yank `mcp`, `mcp-client`, and `mcp-types` on PyPI
and cut the next one, pointing the yank reason and the GitHub release notes
at the replacement version.
4 changes: 2 additions & 2 deletions VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ What a version number of `mcp` promises: which changes can arrive in a minor rel

## The version number

[Semantic Versioning](https://semver.org/) semantics in [PEP 440](https://peps.python.org/pep-0440/) syntax, taken from the git tag: in `2.X.Y`, **X** (minor) carries new functionality and every non-breaking change, **Y** (patch) carries bug fixes only, and a breaking change to the public API lands only in a new **major**. Pre-releases are cut from `main` as `aN`/`bN`/`rcN`; installers prefer final releases by default, so an unpinned `pip install mcp` stays on a stable release whenever one satisfies your requirement. `mcp` and its wire-types package `mcp-types` release in lockstep, each `mcp` requiring exactly the matching `mcp-types`.
[Semantic Versioning](https://semver.org/) semantics in [PEP 440](https://peps.python.org/pep-0440/) syntax, taken from the git tag: in `2.X.Y`, **X** (minor) carries new functionality and every non-breaking change, **Y** (patch) carries bug fixes only, and a breaking change to the public API lands only in a new **major**. Pre-releases are cut from `main` as `aN`/`bN`/`rcN`; installers prefer final releases by default, so an unpinned `pip install mcp` stays on a stable release whenever one satisfies your requirement. `mcp`, `mcp-client`, and `mcp-types` release in lockstep. Each `mcp` requires exactly the matching `mcp-client` and `mcp-types`; `mcp-client` also requires exactly the matching `mcp-types`.

## The public API

The promise covers every name exported by `mcp` and `mcp_types` (their `__all__`), the import paths, signatures, and behavior documented on the [documentation site](https://py.sdk.modelcontextprotocol.io/) and in its [API Reference](https://py.sdk.modelcontextprotocol.io/api/mcp/). It does not cover underscore-prefixed names, undocumented modules, or the wording of log lines, warnings, and exception messages (their types and documented raise conditions are covered). APIs labelled **provisional** (for example the middleware chain) may still change in a minor release; **experimental** APIs are opt-in previews.
The promise covers every name exported by `mcp`, `mcp_client`, and `mcp_types` (their `__all__`), the import paths, signatures, and behavior documented on the [documentation site](https://py.sdk.modelcontextprotocol.io/) and in its [API Reference](https://py.sdk.modelcontextprotocol.io/api/mcp/). It does not cover underscore-prefixed names, undocumented modules, or the wording of log lines, warnings, and exception messages (their types and documented raise conditions are covered). APIs labelled **provisional** (for example the middleware chain) may still change in a minor release; **experimental** APIs are opt-in previews.

## Breaking and non-breaking changes

Expand Down
35 changes: 35 additions & 0 deletions docs/get-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,45 @@ These docs describe **v2**, the current stable release line:
covers every one. If your *package* depends on `mcp` and isn't ready to migrate, keep a
`<2` upper bound (for example `mcp>=1.28,<2`) so an unpinned resolve stays on the 1.x line.

## Client-only installation

```bash
uv add mcp-client
```

```python
import anyio

from mcp_client import Client


async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
tools = await client.list_tools()
for tool in tools.tools:
print(tool.name)


anyio.run(main)
```

Run this example against an MCP server listening at `http://localhost:8000/mcp`.

Use `mcp-client` when you only connect to servers. It includes the client transports,
OAuth support, and shared protocol machinery without installing Starlette, Uvicorn,
`sse-starlette`, or `python-multipart`. Import client APIs from `mcp_client`, OAuth
support from `mcp_client.client.auth`, and protocol types from `mcp_types`.

Install `mcp` if you also build servers, use the CLI, or pass a server instance to
`Client(server)` for in-process testing. Existing `mcp` imports keep working and
refer to the same client implementation. All three distributions release together;
`mcp` requires its exact `mcp-client` version, which requires its exact `mcp-types` version.

## What gets installed

You don't need to know any of this to use the SDK, but if you're wondering what each dependency is for:

* `mcp-client`: the client API, transports, OAuth support, and shared protocol machinery, versioned in lockstep with the SDK.
* `mcp-types`: every protocol type (requests, results, content blocks) as its own package, versioned in lockstep with the SDK. Code that depends on `mcp` imports it through the `mcp.types` alias (every `from mcp.types import ...` in these docs); import `mcp_types` directly only in a project that installs `mcp-types` without the SDK.
* [`anyio`](https://anyio.readthedocs.io/): the async runtime. The whole SDK is written against anyio, so it runs on either `asyncio` or `trio`.
* [`pydantic`](https://docs.pydantic.dev/): what every `mcp.types` model is built on, plus all schema generation and validation.
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ plugins:
- mkdocstrings:
handlers:
python:
paths: [src, src/mcp-types]
paths: [src, src/mcp-client, src/mcp-types]
# Zensical renders pages in undefined (filesystem-dependent) order
# against one shared griffe collection, so a cross-package re-export
# (`mcp` -> `mcp_types`) resolves only if its target package happens
Expand Down
19 changes: 14 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ dependencies = [
"anyio>=4.10; python_version >= '3.14'",
"anyio>=4.9; python_version < '3.14'",
"httpx2>=2.5.0",
"mcp-client=={{ version }}",
"mcp-types=={{ version }}",
"pydantic>=2.12.0",
"starlette>=0.48.0; python_version >= '3.14'",
Expand All @@ -140,7 +141,6 @@ dependencies = [
"sse-starlette>=3.0.0",
"uvicorn>=0.31.1; sys_platform != 'emscripten'",
"jsonschema>=4.20.0",
"pywin32>=311; sys_platform == 'win32'",
"pyjwt[crypto]>=2.10.1",
"typing-extensions>=4.13.0",
"typing-inspection>=0.4.1",
Expand All @@ -160,13 +160,16 @@ packages = ["src/mcp"]
typeCheckingMode = "strict"
include = [
"src/mcp",
"src/mcp-client/mcp_client",
"src/mcp-types/mcp_types",
"tests",
"docs_src",
"examples/stories",
"examples/servers",
"examples/snippets",
"examples/clients",
"scripts/check_client_package.py",
"scripts/check_client_types.py",
"scripts/docs/build_config.py",
"scripts/docs/translations.py",
]
Expand Down Expand Up @@ -198,6 +201,10 @@ executionEnvironments = [
# docs_src/ holds the complete, runnable code examples included into docs/*.md.
# Decorated (@mcp.tool/...) module-level functions are never called by name.
{ root = "docs_src", reportUnusedFunction = false },
# Compatibility modules re-export the original module namespaces, including private names.
{ root = "src/mcp/client", reportPrivateUsage = false },
{ root = "src/mcp/shared", reportPrivateUsage = false },
{ root = "src/mcp/os", reportPrivateUsage = false },
]

[tool.ruff]
Expand Down Expand Up @@ -245,10 +252,11 @@ max-returns = 13 # Default is 6
max-statements = 102 # Default is 50

[tool.uv.workspace]
members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"]
members = ["src/mcp-client", "src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"]

[tool.uv.sources]
mcp = { workspace = true }
mcp-client = { workspace = true }
mcp-example-stories = { workspace = true }
mcp-types = { workspace = true }
strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
Expand Down Expand Up @@ -302,12 +310,13 @@ MD059 = false # descriptive-link-text
branch = true
patch = ["subprocess"]
concurrency = ["multiprocessing", "thread"]
source = ["src", "src/mcp-types/mcp_types", "tests"]
source = ["src", "src/mcp-client/mcp_client", "src/mcp-types/mcp_types", "tests"]
omit = [
"src/mcp/client/__main__.py",
"src/mcp-client/mcp_client/client/__main__.py",
"src/mcp/server/__main__.py",
"src/mcp/os/posix/utilities.py",
"src/mcp/os/win32/utilities.py",
"src/mcp-client/mcp_client/os/posix/utilities.py",
"src/mcp-client/mcp_client/os/win32/utilities.py",
]

# https://coverage.readthedocs.io/en/latest/config.html#report
Expand Down
71 changes: 71 additions & 0 deletions scripts/check_client_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Exercise an installed client distribution without the SDK's server dependencies.

The peer uses raw messages because the server package must be absent from this environment.
"""

import importlib
import importlib.util
import pkgutil
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import get_type_hints

import anyio
import mcp_client
from mcp_client.shared import exceptions
from mcp_client.shared.memory import MessageStream, create_client_server_memory_streams
from mcp_client.shared.message import SessionMessage
from mcp_types import JSONRPCRequest, JSONRPCResponse, ListToolsResult, Tool

get_type_hints(mcp_client.Client.__init__)

for error_type in (
exceptions.MCPError,
exceptions.MCPDeprecationWarning,
exceptions.NoBackChannelError,
exceptions.UrlElicitationRequiredError,
):
assert error_type.__module__ == "mcp_client.shared.exceptions"

for name in ("mcp", "starlette", "uvicorn", "sse_starlette", "multipart"):
assert importlib.util.find_spec(name) is None, name

for info in pkgutil.walk_packages(mcp_client.__path__, prefix="mcp_client."):
if not any(part.startswith("_") for part in info.name.split(".")):
importlib.import_module(info.name)

RESULT = ListToolsResult(tools=[Tool(name="example", input_schema={"type": "object"})])


@asynccontextmanager
async def transport() -> AsyncIterator[MessageStream]:
async with create_client_server_memory_streams() as (client_streams, server_streams):
read, write = server_streams

async def respond() -> None:
received = await read.receive()
assert isinstance(received, SessionMessage)
message = received.message
assert isinstance(message, JSONRPCRequest)
assert message.method == "tools/list"
await write.send(
SessionMessage(
JSONRPCResponse(
jsonrpc="2.0", id=message.id, result=RESULT.model_dump(by_alias=True, exclude_none=True)
)
)
)

async with anyio.create_task_group() as group:
group.start_soon(respond)
yield client_streams


async def main() -> None:
"""Verify a client request and response through the installed public API."""
with anyio.fail_after(5):
async with mcp_client.Client(transport(), mode="2026-07-28") as client:
assert await client.list_tools() == RESULT


anyio.run(main)
48 changes: 48 additions & 0 deletions scripts/check_client_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Type-check the installed client package and reject an invalid constructor argument."""

import importlib.util
import json
import subprocess
import sys
import tempfile
from pathlib import Path

import mcp_client

assert importlib.util.find_spec("mcp") is None

with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
consumer = root / "consumer.py"
consumer.write_text(
"from mcp_client import Client, StdioServerParameters\n"
'Client("https://example.com/mcp")\n'
'Client(StdioServerParameters(command="python"))\n'
"Client(42)\n",
encoding="utf-8",
)
config = root / "pyrightconfig.json"
config.write_text(
json.dumps(
{
"pythonPath": sys.executable,
"typeCheckingMode": "strict",
}
),
encoding="utf-8",
)
result = subprocess.run(
["pyright", "--project", str(config), "--outputjson", *mcp_client.__path__, str(consumer)],
cwd=root,
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
diagnostics = json.loads(result.stdout)["generalDiagnostics"]
errors = [diagnostic for diagnostic in diagnostics if diagnostic["severity"] == "error"]
assert result.returncode == 1, result.stdout + result.stderr
assert len(errors) == 1, result.stdout
assert errors[0]["file"] == str(consumer), result.stdout
assert errors[0]["range"]["start"]["line"] == 3, result.stdout
assert errors[0]["rule"] == "reportArgumentType", result.stdout
Loading
Loading