Prepare release machinery for v2 pre-releases (#2831)

This commit is contained in:
Max
2026-06-11 09:03:27 +01:00
committed by GitHub
parent 6d0c160113
commit e196857bc9
6 changed files with 188 additions and 62 deletions
+95 -25
View File
@@ -27,32 +27,81 @@ 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;
}
// 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];
if (!previousRelease) {
console.log('No previous release found, this might be the first release');
if (major(currentTag) === undefined) {
console.log(`Cannot parse a major version from ${currentTag}; skipping comments`);
return null;
}
console.log(`Found previous release: ${previousRelease.tag_name}`);
// 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;
return previousRelease.tag_name;
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) {
// 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
// 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;
}
if (best === null || comparison.ahead_by < best.aheadBy) {
best = { tagName: candidate.tag_name, aheadBy: comparison.ahead_by };
}
}
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(`Found previous release: ${best.tagName} (${best.aheadBy} commits behind ${currentTag})`);
return best.tagName;
- name: Get merged PRs between releases
id: get_prs
@@ -72,15 +121,28 @@ 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 — 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({
owner: context.repo.owner,
repo: context.repo.repo,
base: previousTag,
head: currentTag,
per_page: 100,
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`);
// Get PRs associated with each commit using GitHub API
@@ -114,20 +176,28 @@ 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;
// 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 [${releaseTag}](${releaseUrl})`;
const comment = `This pull request is included in ${releaseKind} [${releaseTag}](${releaseUrl})`;
let commentedCount = 0;
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,
@@ -135,7 +205,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) {
+37 -31
View File
@@ -15,12 +15,13 @@
<!-- TODO(v2): Move this content back to README.md when v2 is released -->
> [!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`, and each alpha may contain breaking changes from the previous one.
>
> 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 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.
>
> For v1 documentation (the current stable release), see [`README.md`](README.md).
> **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).
<!-- omit in toc -->
## Table of Contents
@@ -84,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
@@ -115,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:
@@ -188,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:
@@ -604,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
@@ -925,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
@@ -1052,7 +1056,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)_
<!-- /snippet-source -->
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 +1064,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
@@ -1098,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
@@ -1128,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
@@ -1339,8 +1343,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 +2092,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
@@ -2157,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)
@@ -2397,12 +2401,13 @@ 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)_
<!-- /snippet-source -->
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
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.
<!-- snippet-source examples/snippets/clients/parsing_tool_results.py -->
```python
"""examples/snippets/clients/parsing_tool_results.py"""
@@ -2414,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:
@@ -2430,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
@@ -2442,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):
@@ -2468,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)_
<!-- /snippet-source -->
### MCP Primitives
The MCP protocol defines three core primitives that servers can implement:
@@ -2492,14 +2498,14 @@ 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)
## 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
+43 -2
View File
@@ -7,7 +7,48 @@
## 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.
## 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 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 <commit-sha>
```
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. 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.
+9
View File
@@ -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
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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 = [
@@ -11,10 +11,10 @@ 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 :: 4 - Beta",
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",