diff --git a/.github/workflows/comment-on-release.yml b/.github/workflows/comment-on-release.yml index 66f1fcc32a..6959688dea 100644 --- a/.github/workflows/comment-on-release.yml +++ b/.github/workflows/comment-on-release.yml @@ -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; + + 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; + } - return previousRelease.tag_name; + 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) { diff --git a/README.v2.md b/README.v2.md index bae230c3f9..25cf5ac959 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`, 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). ## 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)_ -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)_ -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. + ```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)_ + + ### 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 diff --git a/RELEASE.md b/RELEASE.md index 6555a1c2d8..dce346b27a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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 + ``` + +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. 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/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 5a182dde17..749af47ab6 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 = [ @@ -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",