From 3bfe5065e984f78111032b45797ddde40f2d80ad Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:23:45 +0000 Subject: [PATCH 1/4] Prepare release machinery for v2 pre-releases - Point the package readme at README.v2.md so PyPI renders the v2 docs on pre-release pages; refresh its stale banner, replace GitHub-only alert syntax that PyPI's renderer shows as literal text, and convert relative links to absolute URLs (PyPI has no base URL to resolve them) - Mark the distribution Development Status :: 3 - Alpha - comment-on-release: select the previous release by ancestry on the same major line instead of list order, so a release on one line never compares against a release on another; paginate the commit range instead of truncating at one response; tolerate releases whose tag no longer resolves; label comments on pre-releases as such - Document the v2 pre-release flow in RELEASE.md (pre-release flag, --target, curated notes, yank-don't-delete) - State the pre-release security-fix policy in SECURITY.md --- .github/workflows/comment-on-release.yml | 79 ++++++++++++++++++------ README.v2.md | 23 +++---- RELEASE.md | 24 +++++++ SECURITY.md | 9 +++ pyproject.toml | 4 +- 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/.github/workflows/comment-on-release.yml b/.github/workflows/comment-on-release.yml index 66f1fcc32a..b35b90a34d 100644 --- a/.github/workflows/comment-on-release.yml +++ b/.github/workflows/comment-on-release.yml @@ -42,17 +42,50 @@ jobs: return null; } - // Get previous release (next in the list since they're sorted by date desc) - const previousRelease = releases[currentIndex + 1]; + const major = tag => (tag.match(/^v?(\d+)/) || [])[1]; + + // Releases are sorted by date, but with multiple release lines + // (v1.x and the v2 pre-release series) the most recent release may + // be on a different branch. Walk older releases until we find one + // whose tag is an ancestor of the current tag AND on the same + // major line, so the compare never spans branches. For the first + // release of a new major line no such release exists, and we skip + // commenting rather than compare across the entire new line's + // history. per_page=1 because only comparison.status is needed + // here; the commits are fetched in the next step. + for (const candidate of releases.slice(currentIndex + 1)) { + let comparison; + try { + ({ data: comparison } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: candidate.tag_name, + head: currentTag, + per_page: 1 + })); + } catch (error) { + console.log(`Skipping ${candidate.tag_name}: compare failed (${error.message})`); + continue; + } - if (!previousRelease) { - console.log('No previous release found, this might be the first release'); - return null; - } + // 'identical' covers a release re-cut on the same commit; it + // yields an empty commit range downstream, hence no comments. + if (comparison.status !== 'ahead' && comparison.status !== 'identical') { + console.log(`Skipping ${candidate.tag_name}: not an ancestor of ${currentTag} (status: ${comparison.status})`); + continue; + } - console.log(`Found previous release: ${previousRelease.tag_name}`); + if (major(candidate.tag_name) === undefined || major(candidate.tag_name) !== major(currentTag)) { + console.log(`Skipping ${candidate.tag_name}: different release line (${major(candidate.tag_name)} vs ${major(currentTag)})`); + continue; + } + + console.log(`Found previous release: ${candidate.tag_name}`); + return candidate.tag_name; + } - return previousRelease.tag_name; + console.log(`No previous release found for ${currentTag} on its major line (it may be the first); skipping comments`); + return null; - name: Get merged PRs between releases id: get_prs @@ -72,15 +105,21 @@ jobs: console.log(`Finding PRs between ${previousTag} and ${currentTag}`); - // Get commits between previous and current release - const comparison = await github.rest.repos.compareCommits({ - owner: context.repo.owner, - repo: context.repo.repo, - base: previousTag, - head: currentTag - }); - - const commits = comparison.data.commits; + // Get commits between previous and current release. A single + // compare response caps the commit list, so paginate. + const commits = []; + for (let page = 1; ; page++) { + const { data: comparison } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: previousTag, + head: currentTag, + per_page: 100, + page + }); + commits.push(...comparison.commits); + if (comparison.commits.length < 100) break; + } console.log(`Found ${commits.length} commits`); // Get PRs associated with each commit using GitHub API @@ -114,13 +153,15 @@ jobs: PR_NUMBERS_JSON: ${{ steps.get_prs.outputs.result }} RELEASE_TAG: ${{ github.event.release.tag_name }} RELEASE_URL: ${{ github.event.release.html_url }} + RELEASE_IS_PRERELEASE: ${{ github.event.release.prerelease }} with: script: | const prNumbers = JSON.parse(process.env.PR_NUMBERS_JSON); const releaseTag = process.env.RELEASE_TAG; const releaseUrl = process.env.RELEASE_URL; + const releaseKind = process.env.RELEASE_IS_PRERELEASE === 'true' ? 'pre-release' : 'release'; - const comment = `This pull request is included in [${releaseTag}](${releaseUrl})`; + const comment = `This pull request is included in ${releaseKind} [${releaseTag}](${releaseUrl})`; let commentedCount = 0; @@ -135,7 +176,7 @@ jobs: }); const alreadyCommented = comments.some(c => - c.user.type === 'Bot' && c.body.includes(releaseTag) + c.user.type === 'Bot' && c.body.includes(`[${releaseTag}]`) ); if (alreadyCommented) { diff --git a/README.v2.md b/README.v2.md index bae230c3f9..e9e803877a 100644 --- a/README.v2.md +++ b/README.v2.md @@ -15,12 +15,13 @@ -> [!IMPORTANT] -> **This documents v2 of the SDK (currently in development, pre-alpha on `main`).** +> **⚠️ Important: this documents v2 of the SDK, which is in alpha.** Pre-releases are published to PyPI as `2.0.0aN`. > -> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade. +> v2 is a major rework with breaking changes — see the [migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md). Each pre-release may itself contain breaking changes until the API stabilizes. We expect to enter beta once the SDK targets the 2026-07-28 revision of the MCP specification; there is no committed date for the stable v2 release. > -> For v1 documentation (the current stable release), see [`README.md`](README.md). +> **v1.x remains the recommended version for production use.** Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0aN` or `pip install --pre mcp`), so existing installs are unaffected. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade. +> +> For v1 documentation (the current stable release), see [the v1.x README](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/README.md). ## Table of Contents @@ -1052,7 +1053,7 @@ if __name__ == "__main__": _Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_ -For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/). +For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth/). **Architecture:** @@ -1060,7 +1061,7 @@ For a complete example with separate Authorization Server and Resource Server im - **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources - **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server -See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation. +See [TokenVerifier](https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/server/auth/provider.py) for more details on implementing token validation. ### MCPServer Properties @@ -1339,8 +1340,8 @@ _Full example: [examples/snippets/servers/streamable_starlette_mount.py](https:/ For low level server with Streamable HTTP implementations, see: -- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/) -- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/) +- Stateful server: [`examples/servers/simple-streamablehttp/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-streamablehttp/) +- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-streamablehttp-stateless/) The streamable HTTP transport supports: @@ -2088,7 +2089,7 @@ _Full example: [examples/snippets/clients/pagination_client.py](https://github.c - **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page) - **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics -See the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation. +See the [simple-pagination example](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-pagination) for a complete implementation. ### Writing MCP Clients @@ -2397,7 +2398,7 @@ if __name__ == "__main__": _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_ -For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). +For a complete working example, see [`examples/clients/simple-auth-client/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/clients/simple-auth-client/). ### Parsing Tool Results @@ -2499,7 +2500,7 @@ MCP servers declare capabilities during initialization: ## Contributing -We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started. +We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/CONTRIBUTING.md) to get started. ## License diff --git a/RELEASE.md b/RELEASE.md index 6555a1c2d8..a4e7d4a46c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,3 +11,27 @@ Create a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the and the release title being the same. Then ask someone to review the release. The package version will be set automatically from the tag. + +## v2 Pre-releases + +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. + +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. +2. Create the release as a pre-release, passing the exact commit verified in + step 1 as `--target` (otherwise the tag is created from whatever `main`'s + HEAD is by then). The pre-release flag keeps GitHub's "Latest" badge and + `/releases/latest` pointing at the stable v1.x line: + + ```shell + gh release create v2.0.0aN --prerelease --title v2.0.0aN --target + ``` + +3. Curate the release notes instead of relying on auto-generated ones: what + changed since the previous pre-release, what is known-incomplete, the + install line (`pip install mcp==2.0.0aN`), and a link to the + [migration guide](docs/migration.md). +4. If a pre-release turns out to be broken, yank it on PyPI and cut the next + one. Never delete a release from PyPI — version numbers cannot be reused. diff --git a/SECURITY.md b/SECURITY.md index 5029242009..e8b51cc08d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,6 +2,15 @@ Thank you for helping keep the Model Context Protocol and its ecosystem secure. +## Supported Versions + +Security fixes are released for the most recent stable (v1.x) release line. + +v2 pre-releases (`2.0.0aN`, …) are development snapshots: fixes land only in +the newest pre-release, and already-published pre-releases are not patched. If +you are testing the v2 line, track the latest pre-release; for production use, +stay on the latest stable release. + ## Reporting Security Issues If you discover a security vulnerability in this repository, please report it through diff --git a/pyproject.toml b/pyproject.toml index 5a182dde17..043e6bf2f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "mcp" dynamic = ["version"] description = "Model Context Protocol SDK" -readme = "README.md" +readme = "README.v2.md" requires-python = ">=3.10" authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }] maintainers = [ @@ -14,7 +14,7 @@ maintainers = [ keywords = ["git", "mcp", "llm", "automation"] license = { text = "MIT" } classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", From 14c691d92d5d49a8625c0d2dad1b661feff9f144 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:56:23 +0000 Subject: [PATCH 2/4] Reword the v2 README banner State the alpha status and per-alpha breakage expectation, the beta and stable target dates alongside the 2026-07-28 spec release, the planned backwards-compatibility shims, v1.x's maintenance-mode status, the mcp<2 upper-bound ask for dependents, and the Discord feedback channel. --- README.v2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.v2.md b/README.v2.md index e9e803877a..986a57678b 100644 --- a/README.v2.md +++ b/README.v2.md @@ -15,13 +15,13 @@ -> **⚠️ Important: this documents v2 of the SDK, which is in alpha.** Pre-releases are published to PyPI as `2.0.0aN`. +> **Important: this documents v2 of the SDK, which is in alpha.** Pre-releases are published to PyPI as `2.0.0aN`, and each alpha may contain breaking changes from the previous one. > -> v2 is a major rework with breaking changes — see the [migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md). Each pre-release may itself contain breaking changes until the API stabilizes. We expect to enter beta once the SDK targets the 2026-07-28 revision of the MCP specification; there is no committed date for the stable v2 release. +> v2 is a major rework of the SDK, both to support the [2026-07-28 MCP specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) and to fix long-standing architectural issues. See the [migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md) for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27, alongside the spec release. Before stable, we plan to add a significant set of backwards compatibility shims so the final upgrade is much smaller than today's diff. > -> **v1.x remains the recommended version for production use.** Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0aN` or `pip install --pre mcp`), so existing installs are unaffected. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade. +> **v1.x is the only stable release line and remains recommended for production.** It is in maintenance mode and continues to receive critical bug fixes and security patches. Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0aN`), so existing installs are unaffected. **If your package depends on `mcp`, add an upper bound (`mcp>=1.x,<2`) before the stable release lands.** > -> For v1 documentation (the current stable release), see [the v1.x README](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/README.md). +> Try the alpha and tell us what breaks: [#python-sdk-dev on the MCP Contributors Discord](https://discord.gg/6CSzBmMkjX). For v1 documentation, see [the v1.x README](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/README.md). ## Table of Contents From 29944e1067c0e1cea5f6ecf170da070006912dd4 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:58:53 +0000 Subject: [PATCH 3/4] Use a concrete specifier example in the upper-bound ask --- README.v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.v2.md b/README.v2.md index 986a57678b..20ac37d7c2 100644 --- a/README.v2.md +++ b/README.v2.md @@ -19,7 +19,7 @@ > > v2 is a major rework of the SDK, both to support the [2026-07-28 MCP specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) and to fix long-standing architectural issues. See the [migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md) for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27, alongside the spec release. Before stable, we plan to add a significant set of backwards compatibility shims so the final upgrade is much smaller than today's diff. > -> **v1.x is the only stable release line and remains recommended for production.** It is in maintenance mode and continues to receive critical bug fixes and security patches. Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0aN`), so existing installs are unaffected. **If your package depends on `mcp`, add an upper bound (`mcp>=1.x,<2`) before the stable release lands.** +> **v1.x is the only stable release line and remains recommended for production.** It is in maintenance mode and continues to receive critical bug fixes and security patches. Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0aN`), so existing installs are unaffected. **If your package depends on `mcp`, add a `<2` upper bound to your version constraint (for example `mcp>=1.27,<2`) before the stable release lands.** > > Try the alpha and tell us what breaks: [#python-sdk-dev on the MCP Contributors Discord](https://discord.gg/6CSzBmMkjX). For v1 documentation, see [the v1.x README](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/README.md). From e6b1f64a4f938cbb767bb14ff9390b38c1a0ff82 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:46:33 +0000 Subject: [PATCH 4/4] Address review findings on the pre-release machinery - comment-on-release: pick the previous release by smallest ahead_by across all same-major releases instead of trusting list order (releases published from drafts keep their draft creation date, so list order can hide the true predecessor); check the major line before comparing so cross-line candidates cost no API calls; skip only candidates whose tag no longer resolves and fail loudly on other compare errors; skip drafts; paginate release and comment listings; cap the commit range at 250 and skip commenting beyond it; derive pre-release wording from the tag as well as the release flag - RELEASE.md: stable releases must target the v1.x branch (the UI defaults to main, which is the v2 rework); the --target commit must contain the release tooling and is ignored if the tag already exists; release notes need absolute links; yanked versions should point at their replacement; bump the Development Status classifier when the line changes stage - README.v2.md: pin install and quickstart commands while v2 is in pre-release; point the docs badge and API reference at the v2 docs site; fix examples against the v2 API (Icon mime_type/sizes, snake_case CallToolResult fields via a re-synced snippet, keyword-only max_tokens, elicitation result shape, request context fields) - snippets: fix the resource-read isinstance in the stdio client example (TextResourceContents, not TextContent) - pyproject: drop the leftover "git" keyword --- .github/workflows/comment-on-release.yml | 87 +++++++++++++++-------- README.v2.md | 45 ++++++------ RELEASE.md | 29 ++++++-- examples/snippets/clients/stdio_client.py | 2 +- pyproject.toml | 2 +- 5 files changed, 108 insertions(+), 57 deletions(-) diff --git a/.github/workflows/comment-on-release.yml b/.github/workflows/comment-on-release.yml index b35b90a34d..6959688dea 100644 --- a/.github/workflows/comment-on-release.yml +++ b/.github/workflows/comment-on-release.yml @@ -27,33 +27,42 @@ jobs: script: | const currentTag = process.env.CURRENT_TAG; - // Get all releases - const { data: releases } = await github.rest.repos.listReleases({ + // Paginate: with two release lines publishing interleaved, the + // previous release on this line can sit far down the list. + const releases = await github.paginate(github.rest.repos.listReleases, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); - // Find current release index - const currentIndex = releases.findIndex(r => r.tag_name === currentTag); - - if (currentIndex === -1) { + if (!releases.some(r => r.tag_name === currentTag)) { console.log('Current release not found in list'); return null; } const major = tag => (tag.match(/^v?(\d+)/) || [])[1]; - // Releases are sorted by date, but with multiple release lines - // (v1.x and the v2 pre-release series) the most recent release may - // be on a different branch. Walk older releases until we find one - // whose tag is an ancestor of the current tag AND on the same - // major line, so the compare never spans branches. For the first - // release of a new major line no such release exists, and we skip - // commenting rather than compare across the entire new line's - // history. per_page=1 because only comparison.status is needed - // here; the commits are fetched in the next step. - for (const candidate of releases.slice(currentIndex + 1)) { + if (major(currentTag) === undefined) { + console.log(`Cannot parse a major version from ${currentTag}; skipping comments`); + return null; + } + + // The list is ordered by release creation date, which does not + // reliably reflect tag topology (for example, a release published + // from a long-lived draft keeps its draft creation date). Instead + // of trusting list order, compare every same-major release and + // pick the nearest ancestor of the current tag: the one the + // smallest number of commits behind it. The major check runs + // first so cross-line candidates cost no API calls; per_page=1 + // because only status/ahead_by are needed here (the commits are + // fetched in the next step). For the first release of a new major + // line there is no same-line predecessor, and we skip commenting + // rather than compare across the entire new line's history. + let best = null; + for (const candidate of releases) { + if (candidate.tag_name === currentTag || candidate.draft) continue; + if (major(candidate.tag_name) !== major(currentTag)) continue; + let comparison; try { ({ data: comparison } = await github.rest.repos.compareCommits({ @@ -64,8 +73,14 @@ jobs: per_page: 1 })); } catch (error) { - console.log(`Skipping ${candidate.tag_name}: compare failed (${error.message})`); - continue; + // Tolerate only candidates whose tag no longer resolves; + // anything else (rate limits, server errors) must fail the + // job rather than silently produce a wrong comparison base. + if (error.status === 404) { + console.log(`Skipping ${candidate.tag_name}: tag does not resolve`); + continue; + } + throw error; } // 'identical' covers a release re-cut on the same commit; it @@ -75,17 +90,18 @@ jobs: continue; } - if (major(candidate.tag_name) === undefined || major(candidate.tag_name) !== major(currentTag)) { - console.log(`Skipping ${candidate.tag_name}: different release line (${major(candidate.tag_name)} vs ${major(currentTag)})`); - continue; + if (best === null || comparison.ahead_by < best.aheadBy) { + best = { tagName: candidate.tag_name, aheadBy: comparison.ahead_by }; } + } - console.log(`Found previous release: ${candidate.tag_name}`); - return candidate.tag_name; + if (best === null) { + console.log(`No previous release found for ${currentTag} on its major line (it may be the first); skipping comments`); + return null; } - console.log(`No previous release found for ${currentTag} on its major line (it may be the first); skipping comments`); - return null; + console.log(`Found previous release: ${best.tagName} (${best.aheadBy} commits behind ${currentTag})`); + return best.tagName; - name: Get merged PRs between releases id: get_prs @@ -106,7 +122,10 @@ jobs: console.log(`Finding PRs between ${previousTag} and ${currentTag}`); // Get commits between previous and current release. A single - // compare response caps the commit list, so paginate. + // compare response caps the commit list, so paginate — but bound + // the total: a range this large means a mis-selected base, and + // commenting on hundreds of PRs is worse than commenting on none. + const MAX_COMMITS = 250; const commits = []; for (let page = 1; ; page++) { const { data: comparison } = await github.rest.repos.compareCommits({ @@ -118,6 +137,10 @@ jobs: page }); commits.push(...comparison.commits); + if (commits.length > MAX_COMMITS) { + console.log(`Range ${previousTag}...${currentTag} exceeds ${MAX_COMMITS} commits; skipping comments`); + return []; + } if (comparison.commits.length < 100) break; } console.log(`Found ${commits.length} commits`); @@ -159,7 +182,10 @@ jobs: const prNumbers = JSON.parse(process.env.PR_NUMBERS_JSON); const releaseTag = process.env.RELEASE_TAG; const releaseUrl = process.env.RELEASE_URL; - const releaseKind = process.env.RELEASE_IS_PRERELEASE === 'true' ? 'pre-release' : 'release'; + // Trust the tag as well as the flag, in case the release manager + // forgets to tick the pre-release checkbox. + const isPrerelease = process.env.RELEASE_IS_PRERELEASE === 'true' || /\d(a|b|rc)\d/.test(releaseTag); + const releaseKind = isPrerelease ? 'pre-release' : 'release'; const comment = `This pull request is included in ${releaseKind} [${releaseTag}](${releaseUrl})`; @@ -167,8 +193,11 @@ jobs: for (const prNumber of prNumbers) { try { - // Check if we've already commented on this PR for this release - const { data: comments } = await github.rest.issues.listComments({ + // Check if we've already commented on this PR for this + // release. Paginate: comments are returned oldest-first, so + // on a busy PR an earlier bot comment is exactly what would + // fall off a single page. + const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, diff --git a/README.v2.md b/README.v2.md index 20ac37d7c2..25cf5ac959 100644 --- a/README.v2.md +++ b/README.v2.md @@ -85,7 +85,7 @@ [python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg [python-url]: https://www.python.org/downloads/ [docs-badge]: https://img.shields.io/badge/docs-python--sdk-blue.svg -[docs-url]: https://modelcontextprotocol.github.io/python-sdk/ +[docs-url]: https://py.sdk.modelcontextprotocol.io/v2/ [protocol-badge]: https://img.shields.io/badge/protocol-modelcontextprotocol.io-blue.svg [protocol-url]: https://modelcontextprotocol.io [spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg @@ -116,15 +116,17 @@ If you haven't created a uv-managed project yet, create one: Then add MCP to your project dependencies: ```bash - uv add "mcp[cli]" + uv add "mcp[cli]==2.0.0a1" ``` Alternatively, for projects using pip for dependencies: ```bash -pip install "mcp[cli]" +pip install "mcp[cli]==2.0.0a1" ``` +> While v2 is in pre-release, you must pin the version explicitly: unpinned installs resolve to the latest stable v1.x release, which these docs do not describe. Check the [release history](https://pypi.org/project/mcp/#history) for the newest pre-release. The same applies to ad-hoc commands: use `uv run --with "mcp==2.0.0a1"` rather than `uv run --with mcp`. + ### Running the standalone MCP development tools To run the mcp command with uv: @@ -189,7 +191,7 @@ _Full example: [examples/snippets/servers/mcpserver_quickstart.py](https://githu You can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server: ```bash -uv run --with mcp examples/snippets/servers/mcpserver_quickstart.py +uv run --with "mcp==2.0.0a1" examples/snippets/servers/mcpserver_quickstart.py ``` Then add it to Claude Code: @@ -605,8 +607,8 @@ from mcp.server.mcpserver import MCPServer, Icon # Create an icon from a file path or URL icon = Icon( src="icon.png", - mimeType="image/png", - sizes="64x64" + mime_type="image/png", + sizes=["64x64"] ) # Add icons to server @@ -926,7 +928,8 @@ The `elicit()` method returns an `ElicitationResult` with: - `action`: "accept", "decline", or "cancel" - `data`: The validated response (only when accepted) -- `validation_error`: Any validation error message + +If the client returns data that doesn't match the schema, `elicit()` raises a `pydantic.ValidationError`. ### Sampling @@ -1099,7 +1102,7 @@ The session object accessible via `ctx.session` provides advanced control over c - `ctx.session.client_params` - Client initialization parameters and declared capabilities - `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control -- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion +- `await ctx.session.create_message(messages, max_tokens=...)` - Request LLM sampling/completion (`max_tokens` is keyword-only) - `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates - `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed - `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed @@ -1129,9 +1132,9 @@ The request context accessible via `ctx.request_context` contains request-specif - Database connections, configuration objects, shared services - Type-safe access to resources defined in your server's lifespan function - `ctx.request_context.meta` - Request metadata from the client including: - - `progressToken` - Token for progress notifications + - `progress_token` - Token for progress notifications - Other client-provided metadata -- `ctx.request_context.request` - The original MCP request object for advanced processing +- `ctx.request_context.request` - Data the transport attached to this message (for example the HTTP request object on HTTP transports; `None` on stdio) - `ctx.request_context.request_id` - Unique identifier for this request ```python @@ -2158,7 +2161,7 @@ async def run(): # Read a resource (greeting resource from mcpserver_quickstart) resource_content = await session.read_resource("greeting://World") content_block = resource_content.contents[0] - if isinstance(content_block, types.TextContent): + if isinstance(content_block, types.TextResourceContents): print(f"Resource content: {content_block.text}") # Call a tool (add tool from mcpserver_quickstart) @@ -2404,6 +2407,7 @@ For a complete working example, see [`examples/clients/simple-auth-client/`](htt When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs. + ```python """examples/snippets/clients/parsing_tool_results.py""" @@ -2415,9 +2419,7 @@ from mcp.client.stdio import stdio_client async def parse_tool_results(): """Demonstrates how to parse different types of content in CallToolResult.""" - server_params = StdioServerParameters( - command="python", args=["path/to/mcp_server.py"] - ) + server_params = StdioServerParameters(command="python", args=["path/to/mcp_server.py"]) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: @@ -2431,9 +2433,9 @@ async def parse_tool_results(): # Example 2: Parsing structured content from JSON tools result = await session.call_tool("get_user", {"id": "123"}) - if hasattr(result, "structuredContent") and result.structuredContent: + if hasattr(result, "structured_content") and result.structured_content: # Access structured data directly - user_data = result.structuredContent + user_data = result.structured_content print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}") # Example 3: Parsing embedded resources @@ -2443,18 +2445,18 @@ async def parse_tool_results(): resource = content.resource if isinstance(resource, types.TextResourceContents): print(f"Config from {resource.uri}: {resource.text}") - elif isinstance(resource, types.BlobResourceContents): + else: print(f"Binary data from {resource.uri}") # Example 4: Parsing image content result = await session.call_tool("generate_chart", {"data": [1, 2, 3]}) for content in result.content: if isinstance(content, types.ImageContent): - print(f"Image ({content.mimeType}): {len(content.data)} bytes") + print(f"Image ({content.mime_type}): {len(content.data)} bytes") # Example 5: Handling errors result = await session.call_tool("failing_tool", {}) - if result.isError: + if result.is_error: print("Tool execution failed!") for content in result.content: if isinstance(content, types.TextContent): @@ -2469,6 +2471,9 @@ if __name__ == "__main__": asyncio.run(main()) ``` +_Full example: [examples/snippets/clients/parsing_tool_results.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/parsing_tool_results.py)_ + + ### MCP Primitives The MCP protocol defines three core primitives that servers can implement: @@ -2493,7 +2498,7 @@ MCP servers declare capabilities during initialization: ## Documentation -- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/) +- [API Reference](https://py.sdk.modelcontextprotocol.io/v2/api/mcp/) - [Model Context Protocol documentation](https://modelcontextprotocol.io) - [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest) - [Officially supported servers](https://github.com/modelcontextprotocol/servers) diff --git a/RELEASE.md b/RELEASE.md index a4e7d4a46c..dce346b27a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,8 +7,12 @@ ## Major or Minor Release -Create a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the version, -and the release title being the same. Then ask someone to review the release. +Stable releases are cut from the `v1.x` branch. Create a GitHub release via UI +with the tag being `vX.Y.Z` where `X.Y.Z` is the version and the release title +being the same, and **set the tag's target to the `v1.x` branch** — the UI +defaults to `main`, which is the v2 rework, and a v1 tag created there would +publish the v2 codebase as a stable release. Then ask someone to review the +release. The package version will be set automatically from the tag. @@ -22,8 +26,13 @@ for alphas, later `bN`/`rcN` for betas and release candidates. passed — check the individual jobs. 2. Create the release as a pre-release, passing the exact commit verified in step 1 as `--target` (otherwise the tag is created from whatever `main`'s - HEAD is by then). The pre-release flag keeps GitHub's "Latest" badge and - `/releases/latest` pointing at the stable v1.x line: + HEAD is by then). The tagged commit determines everything about the + release — the workflows that run and the package metadata (readme, + classifiers) that gets published — so it must contain the current release + tooling, not just pass tests. `--target` is ignored if the tag already + exists: when re-creating a release, delete the old tag first and + double-check where the new tag points. The pre-release flag keeps GitHub's + "Latest" badge and `/releases/latest` pointing at the stable v1.x line: ```shell gh release create v2.0.0aN --prerelease --title v2.0.0aN --target @@ -31,7 +40,15 @@ for alphas, later `bN`/`rcN` for betas and release candidates. 3. Curate the release notes instead of relying on auto-generated ones: what changed since the previous pre-release, what is known-incomplete, the - install line (`pip install mcp==2.0.0aN`), and a link to the - [migration guide](docs/migration.md). + install line (`pip install mcp==2.0.0aN`), and a link to the migration + guide. Use the absolute URL + (`https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md`) + because relative links don't resolve in GitHub release bodies. 4. If a pre-release turns out to be broken, yank it on PyPI and cut the next one. Never delete a release from PyPI — version numbers cannot be reused. + Yanking doesn't stop `==` pins from installing the broken version, so set + the yank reason (and edit the GitHub release notes) to point at the + replacement version. +5. When the line moves to a new stage (first beta, first release candidate, + stable), update the `Development Status` classifier in `pyproject.toml` + before tagging — PyPI uploads are immutable. diff --git a/examples/snippets/clients/stdio_client.py b/examples/snippets/clients/stdio_client.py index c1f85f42a3..3f7c4b981b 100644 --- a/examples/snippets/clients/stdio_client.py +++ b/examples/snippets/clients/stdio_client.py @@ -59,7 +59,7 @@ async def run(): # Read a resource (greeting resource from mcpserver_quickstart) resource_content = await session.read_resource("greeting://World") content_block = resource_content.contents[0] - if isinstance(content_block, types.TextContent): + if isinstance(content_block, types.TextResourceContents): print(f"Resource content: {content_block.text}") # Call a tool (add tool from mcpserver_quickstart) diff --git a/pyproject.toml b/pyproject.toml index 043e6bf2f2..749af47ab6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ maintainers = [ { name = "Max Isbey", email = "maxisbey@anthropic.com" }, { name = "Felix Weinberger", email = "fweinberger@anthropic.com" }, ] -keywords = ["git", "mcp", "llm", "automation"] +keywords = ["mcp", "llm", "automation"] license = { text = "MIT" } classifiers = [ "Development Status :: 3 - Alpha",