Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 25 additions & 0 deletions .github/workflows/shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ 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: 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
2 changes: 1 addition & 1 deletion docs/client/session-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Create a `ClientSessionGroup` and call **`connect_to_server`** once per server:
Put `client.py` next to the two servers and run it. The second `connect_to_server` refuses:

```text
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
mcp_client.shared.exceptions.MCPError: {'search'} already exist in group tools.
```

That is an `MCPError`, raised before anything from the second server is registered. A name must
Expand Down
2 changes: 1 addition & 1 deletion docs/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ That is the whole API. There is no per-method switch, and you don't want one: th
`Error executing tool old_log`, and the captured server log names the culprit:

```text
mcp.shared.exceptions.MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
mcp_client.shared.exceptions.MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```

One line of pytest configuration, and a deprecated call can never sneak back into your
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 docs/run/legacy-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Two things about it matter more than what it does.
The whole request fails, as a top-level protocol error:

```text
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
mcp_client.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.

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: Regenerate and commit the translated run/legacy-clients.md pages with this traceback update. All twelve locale pages still show mcp.shared.exceptions.MCPError, so those sites document a traceback that no longer matches the extracted client package.

(Based on your team's feedback about keeping translated docs aligned with the English source.) .

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/run/legacy-clients.md, line 136:

<comment>Regenerate and commit the translated `run/legacy-clients.md` pages with this traceback update. All twelve locale pages still show `mcp.shared.exceptions.MCPError`, so those sites document a traceback that no longer matches the extracted client package.

(Based on your team's feedback about keeping translated docs aligned with the English source.) .</comment>

<file context>
@@ -133,7 +133,7 @@ Two things about it matter more than what it does.
 
     ```text
-    mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
+    mcp_client.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
     ```
 
</file context>

```

`Resolve` did not save you. On a `2025-11-25` connection it *has* to send `elicitation/create`,
Expand Down
2 changes: 1 addition & 1 deletion docs/servers/handling-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Now swap `ToolError` for `MCPError`.
Same lookup, same miss, but now the call *raises* on the client side instead of returning:

```text
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
mcp_client.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
```

The first version handed the model a sentence it could react to. This one hands it nothing.
Expand Down
2 changes: 1 addition & 1 deletion docs/servers/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ That is the entire life of a prompt: listed by name, rendered on demand, dropped
request itself fails with a JSON-RPC error (code `-32603`):

```text
mcp.shared.exceptions.MCPError: Internal server error
mcp_client.shared.exceptions.MCPError: Internal server error
```

There is no tool-style error result to hand back to a model, because no model is in the loop:
Expand Down
8 changes: 4 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def main() -> None:
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| ...
| mcp.shared.exceptions.MCPError: No forecast for 'Atlantis'.
| mcp_client.shared.exceptions.MCPError: No forecast for 'Atlantis'.
+------------------------------------
```

Expand Down Expand Up @@ -167,7 +167,7 @@ async with Client("https://mcp.example.com/mcp") as client:
```

```text
mcp.shared.exceptions.MCPError: Server returned an error response
mcp_client.shared.exceptions.MCPError: Server returned an error response
```

The words the server actually sent, `421` and `Invalid Host header`, never reach you: the 421 body has no `Content-Type: application/json`, so the client cannot parse it. They are in the **server's log**, which is where to look next:
Expand Down Expand Up @@ -334,7 +334,7 @@ async def test_book_table() -> None:
```

```text
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
mcp_client.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
```

**A legacy connection on a `stateless_http=True` server.** Statelessness means every request is its own world: no session, no server-to-client stream, and so nowhere to send an `elicitation/create` (or `sampling/createMessage`, or `roots/list`) even for the era that has them:
Expand Down Expand Up @@ -375,7 +375,7 @@ async def main() -> None:
```

```text
mcp.shared.exceptions.MCPError: Invalid or expired requestState
mcp_client.shared.exceptions.MCPError: Invalid or expired requestState
```

The message is deliberately frozen: the wire never reveals which check failed. The reason goes to the **server log**, and reading it is the whole diagnosis:
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
18 changes: 13 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,15 @@ 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/docs/build_config.py",
"scripts/docs/translations.py",
]
Expand Down Expand Up @@ -198,6 +200,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 +251,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 +309,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
Loading
Loading