From ca16a6273ebbdfb24a991faf75822e1b98273ff2 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Sun, 28 Jun 2026 11:13:33 +0000 Subject: [PATCH] Promote the v2 README to README.md ahead of the first v2 beta The repo front page still showed the v1 README while every release cut from main is a v2 pre-release. Make the v2 README (README.v2.md) the root README, with a banner that makes the status unmissable: alpha/beta pre-release line, breaking changes between pre-releases, not for production, v1.x is the stable line. Retire the freeze machinery that kept README.md pinned at v1: - the readme-v1-frozen pre-commit hook and the CI freeze check with its override-readme-freeze label gate - the --readme indirection in scripts/update_readme_snippets.py (the script now reads README.md, its only remaining purpose) - dead config that existed only for the split: the ruff hook exclude, the pyproject ruff extend-exclude entries, the SKIP env in shared.yml, and the examples/ trigger on the readme-snippets hook Point pyproject's readme at README.md so PyPI keeps publishing the v2 long description, and add a release-checklist step to bump the version examples in README.md and the docs when cutting a pre-release - the tagged commit's README is what PyPI shows. --- .github/workflows/shared.yml | 17 +- .pre-commit-config.yaml | 9 +- AGENTS.md | 3 +- CONTRIBUTING.md | 2 +- README.md | 2556 +---------------------------- README.v2.md | 132 -- RELEASE.md | 16 +- examples/README.md | 10 +- pyproject.toml | 3 +- scripts/update_readme_snippets.py | 8 +- tests/docs_src/test_shape.py | 8 +- tests/test_examples.py | 3 +- 12 files changed, 84 insertions(+), 2683 deletions(-) delete mode 100644 README.v2.md diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 3ab4753568..1dc2692eb1 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -27,8 +27,6 @@ jobs: - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 with: extra_args: --all-files --verbose - env: - SKIP: no-commit-to-branch,readme-v1-frozen - name: Surface types match vendored schema run: | @@ -42,19 +40,6 @@ 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" - # TODO(Max): Drop this in v2. Deliberate updates (e.g. the v2 status - # banner) go through the 'override-readme-freeze' label. - - name: Check README.md is not modified - if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'override-readme-freeze') - run: | - git fetch --no-tags --depth=1 origin "$BASE_SHA" - if git diff --name-only "$BASE_SHA" -- README.md | grep -q .; then - echo "::error::README.md is frozen at v1. Edit README.v2.md instead." - exit 1 - fi - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - test: name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} @@ -116,7 +101,7 @@ jobs: run: uv sync --frozen --all-extras --python 3.10 - name: Check README snippets are up to date - run: uv run --frozen scripts/update_readme_snippets.py --check --readme README.v2.md + run: uv run --frozen scripts/update_readme_snippets.py --check # `mkdocs.yml` sets `strict: true` and `pymdownx.snippets: check_paths: true`, # but until this job existed the docs were only ever built post-merge by diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f88f229ed5..321b60bc52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,6 @@ repos: types: [python] language: system pass_filenames: false - exclude: ^README(\.v2)?\.md$ - id: pyright name: pyright entry: uv run --frozen pyright @@ -55,15 +54,9 @@ repos: language: system files: ^(pyproject\.toml|uv\.lock)$ pass_filenames: false - # TODO(Max): Drop this in v2. - - id: readme-v1-frozen - name: README.md is frozen (v1 docs) - entry: README.md is frozen at v1. Edit README.v2.md instead. - language: fail - files: ^README\.md$ - id: readme-snippets name: Check README snippets are up to date entry: uv run --frozen python scripts/update_readme_snippets.py --check language: system - files: ^(README\.v2\.md|docs_src/.*\.py|examples/.*\.py|scripts/update_readme_snippets\.py)$ + files: ^(README\.md|docs_src/.*\.py|scripts/update_readme_snippets\.py)$ pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index efe321db00..6c51e89819 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,7 @@ shim) must be documented in `docs/migration.md`. - `v1.x` is the release branch for the current stable line. Backport PRs target this branch and use a `[v1.x]` title prefix. -- `README.md` is frozen at v1 (a pre-commit hook rejects edits). Edit - `README.v2.md` instead. +- `README.md` documents v2. The v1 README lives on the `v1.x` branch. ## Package Management diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ff66e6c41..a36dedd8da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,7 +105,7 @@ uv run ruff check . uv run ruff format . ``` -7. Update README snippets if you modified example code: +7. Update README snippets if you modified `docs_src/` code embedded in the README: ```bash uv run scripts/update_readme_snippets.py diff --git a/README.md b/README.md index 319ad6a115..88b74ad97c 100644 --- a/README.md +++ b/README.md @@ -13,2559 +13,117 @@ - - -> [!NOTE] -> **This README documents v1.x of the MCP Python SDK (the current stable release).** +> [!CAUTION] +> **This README documents v2 of the MCP Python SDK — a pre-release (alpha/beta) line under active development. Do not use v2 in production.** Pre-releases are published to PyPI as `2.0.0aN` / `2.0.0bN`, and **each pre-release may contain breaking changes from the previous one**. Pin an exact version and expect to update your code when you bump the pin. > -> **v2 is in alpha.** Pre-releases are published to PyPI as `2.0.0aN` and can be installed with an explicit pin, for example `pip install mcp==2.0.0a1`. See [`README.v2.md`](README.v2.md) for the v2 documentation and the [migration guide](docs/migration.md) for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27. 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. +> **v1.x is the only stable release line and remains recommended for production.** It lives on the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x) and continues to receive critical bug fixes and security patches; see [the v1.x README](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/README.md) for its documentation. `pip` and `uv` don't select a pre-release unless you explicitly request one, 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.** > -> For v1.x code and documentation, see the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x). v1.x is in maintenance mode and continues to receive critical bug fixes and security patches. - - -## Table of Contents - -- [MCP Python SDK](#mcp-python-sdk) - - [Overview](#overview) - - [Installation](#installation) - - [Adding MCP to your python project](#adding-mcp-to-your-python-project) - - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools) - - [Quickstart](#quickstart) - - [What is MCP?](#what-is-mcp) - - [Core Concepts](#core-concepts) - - [Server](#server) - - [Resources](#resources) - - [Tools](#tools) - - [Structured Output](#structured-output) - - [Prompts](#prompts) - - [Images](#images) - - [Context](#context) - - [Getting Context in Functions](#getting-context-in-functions) - - [Context Properties and Methods](#context-properties-and-methods) - - [Completions](#completions) - - [Elicitation](#elicitation) - - [Sampling](#sampling) - - [Logging and Notifications](#logging-and-notifications) - - [Authentication](#authentication) - - [FastMCP Properties](#fastmcp-properties) - - [Session Properties and Methods](#session-properties-and-methods) - - [Request Context Properties](#request-context-properties) - - [Running Your Server](#running-your-server) - - [Development Mode](#development-mode) - - [Claude Desktop Integration](#claude-desktop-integration) - - [Direct Execution](#direct-execution) - - [Streamable HTTP Transport](#streamable-http-transport) - - [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients) - - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server) - - [StreamableHTTP servers](#streamablehttp-servers) - - [Basic mounting](#basic-mounting) - - [Host-based routing](#host-based-routing) - - [Multiple servers with path configuration](#multiple-servers-with-path-configuration) - - [Path configuration at initialization](#path-configuration-at-initialization) - - [SSE servers](#sse-servers) - - [Advanced Usage](#advanced-usage) - - [Low-Level Server](#low-level-server) - - [Structured Output Support](#structured-output-support) - - [Pagination (Advanced)](#pagination-advanced) - - [Writing MCP Clients](#writing-mcp-clients) - - [Client Display Utilities](#client-display-utilities) - - [OAuth Authentication for Clients](#oauth-authentication-for-clients) - - [Parsing Tool Results](#parsing-tool-results) - - [MCP Primitives](#mcp-primitives) - - [Server Capabilities](#server-capabilities) - - [Documentation](#documentation) - - [Contributing](#contributing) - - [License](#license) - -[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg -[pypi-url]: https://pypi.org/project/mcp/ -[mit-badge]: https://img.shields.io/pypi/l/mcp.svg -[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE -[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/ -[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 -[spec-url]: https://modelcontextprotocol.io/specification/latest - -## Overview +> 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://py.sdk.modelcontextprotocol.io/v2/migration/) for what's changed. Stable v2 is targeted for 2026-07-27, alongside the spec release. Try the pre-releases and tell us what breaks: [#python-sdk-dev on the MCP Contributors Discord](https://discord.gg/6CSzBmMkjX). -The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to: - -- Build MCP clients that can connect to any MCP server -- Create MCP servers that expose resources, prompts and tools -- Use standard transports like stdio, SSE, and Streamable HTTP -- Handle all MCP protocol messages and lifecycle events +## Documentation -## Installation +**The documentation lives at .** -### Adding MCP to your python project +It has the full [tutorial](https://py.sdk.modelcontextprotocol.io/v2/tutorial/), the [API reference](https://py.sdk.modelcontextprotocol.io/v2/api/mcp/), and the [migration guide](https://py.sdk.modelcontextprotocol.io/v2/migration/). -We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects. +## What is MCP? -If you haven't created a uv-managed project yet, create one: +The [Model Context Protocol](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but designed for LLM interactions. With this SDK you can: - ```bash - uv init mcp-server-demo - cd mcp-server-demo - ``` +- **Build MCP servers** that expose tools, resources, and prompts to any MCP host +- **Build MCP clients** that connect to any MCP server +- Speak every standard transport: stdio, Streamable HTTP, and SSE - Then add MCP to your project dependencies: +## Requirements - ```bash - uv add "mcp[cli]" - ``` +Python 3.10+. -Alternatively, for projects using pip for dependencies: +## Installation ```bash -pip install "mcp[cli]" +uv add "mcp[cli]==2.0.0a3" # or: pip install "mcp[cli]==2.0.0a3" ``` -### Running the standalone MCP development tools - -To run the mcp command with uv: - -```bash -uv run mcp -``` +The pin matters while v2 is in pre-release: an unpinned install resolves to the latest stable v1.x, which this README does not describe. Check [PyPI](https://pypi.org/project/mcp/#history) for the newest pre-release, and use `uv run --with "mcp==2.0.0a3"` for one-off commands. -## Quickstart +## A server in 15 lines -Let's create a simple MCP server that exposes a calculator tool and some data: +Create a `server.py`: - + ```python -""" -FastMCP quickstart example. - -Run from the repository root: - uv run examples/snippets/servers/fastmcp_quickstart.py -""" +from mcp.server import MCPServer -from mcp.server.fastmcp import FastMCP +mcp = MCPServer("Demo") -# Create an MCP server -mcp = FastMCP("Demo", json_response=True) - -# Add an addition tool @mcp.tool() def add(a: int, b: int) -> int: - """Add two numbers""" + """Add two numbers.""" return a + b -# Add a dynamic greeting resource @mcp.resource("greeting://{name}") -def get_greeting(name: str) -> str: - """Get a personalized greeting""" +def greeting(name: str) -> str: + """Greet someone by name.""" return f"Hello, {name}!" - - -# Add a prompt -@mcp.prompt() -def greet_user(name: str, style: str = "friendly") -> str: - """Generate a greeting prompt""" - styles = { - "friendly": "Please write a warm, friendly greeting", - "formal": "Please write a formal, professional greeting", - "casual": "Please write a casual, relaxed greeting", - } - - return f"{styles.get(style, styles['friendly'])} for someone named {name}." - - -# Run with streamable HTTP transport -if __name__ == "__main__": - mcp.run(transport="streamable-http") ``` -_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/fastmcp_quickstart.py)_ +_Full example: [docs_src/index/tutorial001.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs_src/index/tutorial001.py)_ -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/fastmcp_quickstart.py -``` - -Then add it to Claude Code: - -```bash -claude mcp add --transport http my-server http://localhost:8000/mcp -``` - -Alternatively, you can test it with the MCP Inspector. Start the server as above, then in a separate terminal: +That's a complete MCP server: one tool, one templated resource. Open it in the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): ```bash -npx -y @modelcontextprotocol/inspector -``` - -In the inspector UI, connect to `http://localhost:8000/mcp`. - -## What is MCP? - -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: - -- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) -- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) -- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) -- And more! - -## Core Concepts - -### Server - -The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: - - -```python -"""Example showing lifespan support for startup/shutdown with strong typing.""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from dataclasses import dataclass - -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession - - -# Mock database class for example -class Database: - """Mock database class for example.""" - - @classmethod - async def connect(cls) -> "Database": - """Connect to database.""" - return cls() - - async def disconnect(self) -> None: - """Disconnect from database.""" - pass - - def query(self) -> str: - """Execute a query.""" - return "Query result" - - -@dataclass -class AppContext: - """Application context with typed dependencies.""" - - db: Database - - -@asynccontextmanager -async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: - """Manage application lifecycle with type-safe context.""" - # Initialize on startup - db = await Database.connect() - try: - yield AppContext(db=db) - finally: - # Cleanup on shutdown - await db.disconnect() - - -# Pass lifespan to server -mcp = FastMCP("My App", lifespan=app_lifespan) - - -# Access type-safe lifespan context in tools -@mcp.tool() -def query_db(ctx: Context[ServerSession, AppContext]) -> str: - """Tool that uses initialized resources.""" - db = ctx.request_context.lifespan_context.db - return db.query() -``` - -_Full example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lifespan_example.py)_ - - -### Resources - -Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: - - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP(name="Resource Example") - - -@mcp.resource("file://documents/{name}") -def read_document(name: str) -> str: - """Read a document by name.""" - # This would normally read from disk - return f"Content of {name}" - - -@mcp.resource("config://settings") -def get_settings() -> str: - """Get application settings.""" - return """{ - "theme": "dark", - "language": "en", - "debug": false -}""" -``` - -_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/basic_resource.py)_ - - -### Tools - -Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: - - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP(name="Tool Example") - - -@mcp.tool() -def sum(a: int, b: int) -> int: - """Add two numbers together.""" - return a + b - - -@mcp.tool() -def get_weather(city: str, unit: str = "celsius") -> str: - """Get weather for a city.""" - # This would normally call a weather API - return f"Weather in {city}: 22degrees{unit[0].upper()}" -``` - -_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/basic_tool.py)_ - - -Tools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities: - - -```python -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession - -mcp = FastMCP(name="Progress Example") - - -@mcp.tool() -async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str: - """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") - - for i in range(steps): - progress = (i + 1) / steps - await ctx.report_progress( - progress=progress, - total=1.0, - message=f"Step {i + 1}/{steps}", - ) - await ctx.debug(f"Completed step {i + 1}") - - return f"Task '{task_name}' completed" -``` - -_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/tool_progress.py)_ - - -#### Structured Output - -Tools will return structured results by default, if their return type -annotation is compatible. Otherwise, they will return unstructured results. - -Structured output supports these return types: - -- Pydantic models (BaseModel subclasses) -- TypedDicts -- Dataclasses and other classes with type hints -- `dict[str, T]` (where T is any JSON-serializable type) -- Primitive types (str, int, float, bool, bytes, None) - wrapped in `{"result": value}` -- Generic types (list, tuple, Union, Optional, etc.) - wrapped in `{"result": value}` - -Classes without type hints cannot be serialized for structured output. Only -classes with properly annotated attributes will be converted to Pydantic models -for schema generation and validation. - -Structured results are automatically validated against the output schema -generated from the annotation. This ensures the tool returns well-typed, -validated data that clients can easily process. - -**Note:** For backward compatibility, unstructured results are also -returned. Unstructured results are provided for backward compatibility -with previous versions of the MCP specification, and are quirks-compatible -with previous versions of FastMCP in the current version of the SDK. - -**Note:** In cases where a tool function's return type annotation -causes the tool to be classified as structured _and this is undesirable_, -the classification can be suppressed by passing `structured_output=False` -to the `@tool` decorator. - -##### Advanced: Direct CallToolResult - -For full control over tool responses including the `_meta` field (for passing data to client applications without exposing it to the model), you can return `CallToolResult` directly: - - -```python -"""Example showing direct CallToolResult return for advanced control.""" - -from typing import Annotated - -from pydantic import BaseModel - -from mcp.server.fastmcp import FastMCP -from mcp.types import CallToolResult, TextContent - -mcp = FastMCP("CallToolResult Example") - - -class ValidationModel(BaseModel): - """Model for validating structured output.""" - - status: str - data: dict[str, int] - - -@mcp.tool() -def advanced_tool() -> CallToolResult: - """Return CallToolResult directly for full control including _meta field.""" - return CallToolResult( - content=[TextContent(type="text", text="Response visible to the model")], - _meta={"hidden": "data for client applications only"}, - ) - - -@mcp.tool() -def validated_tool() -> Annotated[CallToolResult, ValidationModel]: - """Return CallToolResult with structured output validation.""" - return CallToolResult( - content=[TextContent(type="text", text="Validated response")], - structuredContent={"status": "success", "data": {"result": 42}}, - _meta={"internal": "metadata"}, - ) - - -@mcp.tool() -def empty_result_tool() -> CallToolResult: - """For empty results, return CallToolResult with empty content.""" - return CallToolResult(content=[]) -``` - -_Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/direct_call_tool_result.py)_ - - -**Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`. - - -```python -"""Example showing structured output with tools.""" - -from typing import TypedDict - -from pydantic import BaseModel, Field - -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("Structured Output Example") - - -# Using Pydantic models for rich structured data -class WeatherData(BaseModel): - """Weather information structure.""" - - temperature: float = Field(description="Temperature in Celsius") - humidity: float = Field(description="Humidity percentage") - condition: str - wind_speed: float - - -@mcp.tool() -def get_weather(city: str) -> WeatherData: - """Get weather for a city - returns structured data.""" - # Simulated weather data - return WeatherData( - temperature=22.5, - humidity=45.0, - condition="sunny", - wind_speed=5.2, - ) - - -# Using TypedDict for simpler structures -class LocationInfo(TypedDict): - latitude: float - longitude: float - name: str - - -@mcp.tool() -def get_location(address: str) -> LocationInfo: - """Get location coordinates""" - return LocationInfo(latitude=51.5074, longitude=-0.1278, name="London, UK") - - -# Using dict[str, Any] for flexible schemas -@mcp.tool() -def get_statistics(data_type: str) -> dict[str, float]: - """Get various statistics""" - return {"mean": 42.5, "median": 40.0, "std_dev": 5.2} - - -# Ordinary classes with type hints work for structured output -class UserProfile: - name: str - age: int - email: str | None = None - - def __init__(self, name: str, age: int, email: str | None = None): - self.name = name - self.age = age - self.email = email - - -@mcp.tool() -def get_user(user_id: str) -> UserProfile: - """Get user profile - returns structured data""" - return UserProfile(name="Alice", age=30, email="alice@example.com") - - -# Classes WITHOUT type hints cannot be used for structured output -class UntypedConfig: - def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType] - self.setting1 = setting1 - self.setting2 = setting2 - - -@mcp.tool() -def get_config() -> UntypedConfig: - """This returns unstructured output - no schema generated""" - return UntypedConfig("value1", "value2") - - -# Lists and other types are wrapped automatically -@mcp.tool() -def list_cities() -> list[str]: - """Get a list of cities""" - return ["London", "Paris", "Tokyo"] - # Returns: {"result": ["London", "Paris", "Tokyo"]} - - -@mcp.tool() -def get_temperature(city: str) -> float: - """Get temperature as a simple float""" - return 22.5 - # Returns: {"result": 22.5} -``` - -_Full example: [examples/snippets/servers/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/structured_output.py)_ - - -### Prompts - -Prompts are reusable templates that help LLMs interact with your server effectively: - - -```python -from mcp.server.fastmcp import FastMCP -from mcp.server.fastmcp.prompts import base - -mcp = FastMCP(name="Prompt Example") - - -@mcp.prompt(title="Code Review") -def review_code(code: str) -> str: - return f"Please review this code:\n\n{code}" - - -@mcp.prompt(title="Debug Assistant") -def debug_error(error: str) -> list[base.Message]: - return [ - base.UserMessage("I'm seeing this error:"), - base.UserMessage(error), - base.AssistantMessage("I'll help debug that. What have you tried so far?"), - ] -``` - -_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/basic_prompt.py)_ - - -### Icons - -MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts: - -```python -from mcp.server.fastmcp import FastMCP, Icon - -# Create an icon from a file path or URL -icon = Icon( - src="icon.png", - mimeType="image/png", - sizes="64x64" -) - -# Add icons to server -mcp = FastMCP( - "My Server", - website_url="https://example.com", - icons=[icon] -) - -# Add icons to tools, resources, and prompts -@mcp.tool(icons=[icon]) -def my_tool(): - """Tool with an icon.""" - return "result" - -@mcp.resource("demo://resource", icons=[icon]) -def my_resource(): - """Resource with an icon.""" - return "content" -``` - -_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/fastmcp/icons_demo.py)_ - -### Images - -FastMCP provides an `Image` class that automatically handles image data: - - -```python -"""Example showing image handling with FastMCP.""" - -from PIL import Image as PILImage - -from mcp.server.fastmcp import FastMCP, Image - -mcp = FastMCP("Image Example") - - -@mcp.tool() -def create_thumbnail(image_path: str) -> Image: - """Create a thumbnail from an image""" - img = PILImage.open(image_path) - img.thumbnail((100, 100)) - return Image(data=img.tobytes(), format="png") -``` - -_Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/images.py)_ - - -### Context - -The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata. - -#### Getting Context in Functions - -To use context in a tool or resource function, add a parameter with the `Context` type annotation: - -```python -from mcp.server.fastmcp import Context, FastMCP - -mcp = FastMCP(name="Context Example") - - -@mcp.tool() -async def my_tool(x: int, ctx: Context) -> str: - """Tool that uses context capabilities.""" - # The context parameter can have any name as long as it's type-annotated - return await process_with_context(x, ctx) +uv run mcp dev server.py ``` -#### Context Properties and Methods - -The Context object provides the following capabilities: - -- `ctx.request_id` - Unique ID for the current request -- `ctx.client_id` - Client ID if available -- `ctx.fastmcp` - Access to the FastMCP server instance (see [FastMCP Properties](#fastmcp-properties)) -- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods)) -- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties)) -- `await ctx.debug(message)` - Send debug log message -- `await ctx.info(message)` - Send info log message -- `await ctx.warning(message)` - Send warning log message -- `await ctx.error(message)` - Send error log message -- `await ctx.log(level, message, logger_name=None)` - Send log with custom level -- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress -- `await ctx.read_resource(uri)` - Read a resource by URI -- `await ctx.elicit(message, schema)` - Request additional information from user with validation - - -```python -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession - -mcp = FastMCP(name="Progress Example") - - -@mcp.tool() -async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str: - """Execute a task with progress updates.""" - await ctx.info(f"Starting: {task_name}") - - for i in range(steps): - progress = (i + 1) / steps - await ctx.report_progress( - progress=progress, - total=1.0, - message=f"Step {i + 1}/{steps}", - ) - await ctx.debug(f"Completed step {i + 1}") - - return f"Task '{task_name}' completed" -``` +Call `add` with `a=1`, `b=2` and you get `3` back. -_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/tool_progress.py)_ - +Notice what you did **not** write: no JSON Schema (`a: int, b: int` _is_ the schema), no request parsing, no validation code, no protocol handling. Two type-hinted Python functions and a docstring. -### Completions +[The tutorial](https://py.sdk.modelcontextprotocol.io/v2/tutorial/) takes it from here. -MCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values: +## A client in 10 lines -Client usage: +The same package is a full MCP **client**. `Client` connects to a URL, a stdio subprocess, a custom transport, or (for tests) straight to a server object in memory with no transport at all: - ```python -""" -cd to the `examples/snippets` directory and run: - uv run completion-client -""" - import asyncio -import os - -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client -from mcp.types import PromptReference, ResourceTemplateReference - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="uv", # Using uv to run the server - args=["run", "server", "completion", "stdio"], # Server with completion support - env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, -) - - -async def run(): - """Run the completion client example.""" - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write) as session: - # Initialize the connection - await session.initialize() - - # List available resource templates - templates = await session.list_resource_templates() - print("Available resource templates:") - for template in templates.resourceTemplates: - print(f" - {template.uriTemplate}") - - # List available prompts - prompts = await session.list_prompts() - print("\nAvailable prompts:") - for prompt in prompts.prompts: - print(f" - {prompt.name}") - # Complete resource template arguments - if templates.resourceTemplates: - template = templates.resourceTemplates[0] - print(f"\nCompleting arguments for resource template: {template.uriTemplate}") +from mcp import Client - # Complete without context - result = await session.complete( - ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate), - argument={"name": "owner", "value": "model"}, - ) - print(f"Completions for 'owner' starting with 'model': {result.completion.values}") +from server import mcp - # Complete with context - repo suggestions based on owner - result = await session.complete( - ref=ResourceTemplateReference(type="ref/resource", uri=template.uriTemplate), - argument={"name": "repo", "value": ""}, - context_arguments={"owner": "modelcontextprotocol"}, - ) - print(f"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}") - # Complete prompt arguments - if prompts.prompts: - prompt_name = prompts.prompts[0].name - print(f"\nCompleting arguments for prompt: {prompt_name}") +async def main() -> None: + async with Client(mcp) as client: + result = await client.call_tool("add", {"a": 1, "b": 2}) + print(result.structured_content) # {'result': 3} - result = await session.complete( - ref=PromptReference(type="ref/prompt", name=prompt_name), - argument={"name": "style", "value": ""}, - ) - print(f"Completions for 'style' argument: {result.completion.values}") - -def main(): - """Entry point for the completion client.""" - asyncio.run(run()) - - -if __name__ == "__main__": - main() +asyncio.run(main()) ``` -_Full example: [examples/snippets/clients/completion_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/completion_client.py)_ - -### Elicitation - -Request additional information from users. This example shows an Elicitation during a Tool Call: +Swap `mcp` for `"http://localhost:8000/mcp"` and the exact same code talks to a remote server. - -```python -"""Elicitation examples demonstrating form and URL mode elicitation. +## Contributing -Form mode elicitation collects structured, non-sensitive data through a schema. -URL mode elicitation directs users to external URLs for sensitive operations -like OAuth flows, credential collection, or payment processing. -""" +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. -import uuid +## License -from pydantic import BaseModel, Field +This project is licensed under the MIT License. See the [LICENSE](https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE) file for details. -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession -from mcp.shared.exceptions import UrlElicitationRequiredError -from mcp.types import ElicitRequestURLParams - -mcp = FastMCP(name="Elicitation Example") - - -class BookingPreferences(BaseModel): - """Schema for collecting user preferences.""" - - checkAlternative: bool = Field(description="Would you like to check another date?") - alternativeDate: str = Field( - default="2024-12-26", - description="Alternative date (YYYY-MM-DD)", - ) - - -@mcp.tool() -async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str: - """Book a table with date availability check. - - This demonstrates form mode elicitation for collecting non-sensitive user input. - """ - # Check if date is available - if date == "2024-12-25": - # Date unavailable - ask user for alternative - result = await ctx.elicit( - message=(f"No tables available for {party_size} on {date}. Would you like to try another date?"), - schema=BookingPreferences, - ) - - if result.action == "accept" and result.data: - if result.data.checkAlternative: - return f"[SUCCESS] Booked for {result.data.alternativeDate}" - return "[CANCELLED] No booking made" - return "[CANCELLED] Booking cancelled" - - # Date available - return f"[SUCCESS] Booked for {date} at {time}" - - -@mcp.tool() -async def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str: - """Process a secure payment requiring URL confirmation. - - This demonstrates URL mode elicitation using ctx.elicit_url() for - operations that require out-of-band user interaction. - """ - elicitation_id = str(uuid.uuid4()) - - result = await ctx.elicit_url( - message=f"Please confirm payment of ${amount:.2f}", - url=f"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}", - elicitation_id=elicitation_id, - ) - - if result.action == "accept": - # In a real app, the payment confirmation would happen out-of-band - # and you'd verify the payment status from your backend - return f"Payment of ${amount:.2f} initiated - check your browser to complete" - elif result.action == "decline": - return "Payment declined by user" - return "Payment cancelled" - - -@mcp.tool() -async def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str: - """Connect to a third-party service requiring OAuth authorization. - - This demonstrates the "throw error" pattern using UrlElicitationRequiredError. - Use this pattern when the tool cannot proceed without user authorization. - """ - elicitation_id = str(uuid.uuid4()) - - # Raise UrlElicitationRequiredError to signal that the client must complete - # a URL elicitation before this request can be processed. - # The MCP framework will convert this to a -32042 error response. - raise UrlElicitationRequiredError( - [ - ElicitRequestURLParams( - mode="url", - message=f"Authorization required to connect to {service_name}", - url=f"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}", - elicitationId=elicitation_id, - ) - ] - ) -``` - -_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/elicitation.py)_ - - -Elicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms. - -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 - -### Sampling - -Tools can interact with LLMs through sampling (generating text): - - -```python -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession -from mcp.types import SamplingMessage, TextContent - -mcp = FastMCP(name="Sampling Example") - - -@mcp.tool() -async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str: - """Generate a poem using LLM sampling.""" - prompt = f"Write a short poem about {topic}" - - result = await ctx.session.create_message( - messages=[ - SamplingMessage( - role="user", - content=TextContent(type="text", text=prompt), - ) - ], - max_tokens=100, - ) - - # Since we're not passing tools param, result.content is single content - if result.content.type == "text": - return result.content.text - return str(result.content) -``` - -_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/sampling.py)_ - - -### Logging and Notifications - -Tools can send logs and notifications through the context: - - -```python -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession - -mcp = FastMCP(name="Notifications Example") - - -@mcp.tool() -async def process_data(data: str, ctx: Context[ServerSession, None]) -> str: - """Process data with logging.""" - # Different log levels - await ctx.debug(f"Debug: Processing '{data}'") - await ctx.info("Info: Starting processing") - await ctx.warning("Warning: This is experimental") - await ctx.error("Error: (This is just a demo)") - - # Notify about resource changes - await ctx.session.send_resource_list_changed() - - return f"Processed: {data}" -``` - -_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/notifications.py)_ - - -### Authentication - -Authentication can be used by servers that want to expose tools accessing protected resources. - -`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery. - -MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol: - - -```python -""" -Run from the repository root: - uv run examples/snippets/servers/oauth_server.py -""" - -from pydantic import AnyHttpUrl - -from mcp.server.auth.provider import AccessToken, TokenVerifier -from mcp.server.auth.settings import AuthSettings -from mcp.server.fastmcp import FastMCP - - -class SimpleTokenVerifier(TokenVerifier): - """Simple token verifier for demonstration.""" - - async def verify_token(self, token: str) -> AccessToken | None: - pass # This is where you would implement actual token validation - - -# Create FastMCP instance as a Resource Server -mcp = FastMCP( - "Weather Service", - json_response=True, - # Token verifier for authentication - token_verifier=SimpleTokenVerifier(), - # Auth settings for RFC 9728 Protected Resource Metadata - auth=AuthSettings( - issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL - resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL - required_scopes=["user"], - ), -) - - -@mcp.tool() -async def get_weather(city: str = "London") -> dict[str, str]: - """Get weather data for a city""" - return { - "city": city, - "temperature": "22", - "condition": "Partly cloudy", - "humidity": "65%", - } - - -if __name__ == "__main__": - mcp.run(transport="streamable-http") -``` - -_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/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/). - -**Architecture:** - -- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance -- **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. - -### FastMCP Properties - -The FastMCP server instance accessible via `ctx.fastmcp` provides access to server configuration and metadata: - -- `ctx.fastmcp.name` - The server's name as defined during initialization -- `ctx.fastmcp.instructions` - Server instructions/description provided to clients -- `ctx.fastmcp.website_url` - Optional website URL for the server -- `ctx.fastmcp.icons` - Optional list of icons for UI display -- `ctx.fastmcp.settings` - Complete server configuration object containing: - - `debug` - Debug mode flag - - `log_level` - Current logging level - - `host` and `port` - Server network configuration - - `mount_path`, `sse_path`, `streamable_http_path` - Transport paths - - `stateless_http` - Whether the server operates in stateless mode - - And other configuration options - -```python -@mcp.tool() -def server_info(ctx: Context) -> dict: - """Get information about the current server.""" - return { - "name": ctx.fastmcp.name, - "instructions": ctx.fastmcp.instructions, - "debug_mode": ctx.fastmcp.settings.debug, - "log_level": ctx.fastmcp.settings.log_level, - "host": ctx.fastmcp.settings.host, - "port": ctx.fastmcp.settings.port, - } -``` - -### Session Properties and Methods - -The session object accessible via `ctx.session` provides advanced control over client communication: - -- `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.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 -- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed -- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed - -```python -@mcp.tool() -async def notify_data_update(resource_uri: str, ctx: Context) -> str: - """Update data and notify clients of the change.""" - # Perform data update logic here - - # Notify clients that this specific resource changed - await ctx.session.send_resource_updated(AnyUrl(resource_uri)) - - # If this affects the overall resource list, notify about that too - await ctx.session.send_resource_list_changed() - - return f"Updated {resource_uri} and notified clients" -``` - -### Request Context Properties - -The request context accessible via `ctx.request_context` contains request-specific information and resources: - -- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup - - 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 - - Other client-provided metadata -- `ctx.request_context.request` - The original MCP request object for advanced processing -- `ctx.request_context.request_id` - Unique identifier for this request - -```python -# Example with typed lifespan context -@dataclass -class AppContext: - db: Database - config: AppConfig - -@mcp.tool() -def query_with_config(query: str, ctx: Context) -> str: - """Execute a query using shared database and configuration.""" - # Access typed lifespan context - app_ctx: AppContext = ctx.request_context.lifespan_context - - # Use shared resources - connection = app_ctx.db - settings = app_ctx.config - - # Execute query with configuration - result = connection.execute(query, timeout=settings.query_timeout) - return str(result) -``` - -_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lifespan_example.py)_ - -## Running Your Server - -### Development Mode - -The fastest way to test and debug your server is with the MCP Inspector: - -```bash -uv run mcp dev server.py - -# Add dependencies -uv run mcp dev server.py --with pandas --with numpy - -# Mount local code -uv run mcp dev server.py --with-editable . -``` - -### Claude Desktop Integration - -Once your server is ready, install it in Claude Desktop: - -```bash -uv run mcp install server.py - -# Custom name -uv run mcp install server.py --name "My Analytics Server" - -# Environment variables -uv run mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://... -uv run mcp install server.py -f .env -``` - -### Direct Execution - -For advanced scenarios like custom deployments: - - -```python -"""Example showing direct execution of an MCP server. - -This is the simplest way to run an MCP server directly. -cd to the `examples/snippets` directory and run: - uv run direct-execution-server - or - python servers/direct_execution.py -""" - -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - - -@mcp.tool() -def hello(name: str = "World") -> str: - """Say hello to someone.""" - return f"Hello, {name}!" - - -def main(): - """Entry point for the direct execution server.""" - mcp.run() - - -if __name__ == "__main__": - main() -``` - -_Full example: [examples/snippets/servers/direct_execution.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/direct_execution.py)_ - - -Run it with: - -```bash -python servers/direct_execution.py -# or -uv run mcp run servers/direct_execution.py -``` - -Note that `uv run mcp run` or `uv run mcp dev` only supports server using FastMCP and not the low-level server variant. - -### Streamable HTTP Transport - -> **Note**: Streamable HTTP transport is the recommended transport for production deployments. Use `stateless_http=True` and `json_response=True` for optimal scalability. - - -```python -""" -Run from the repository root: - uv run examples/snippets/servers/streamable_config.py -""" - -from mcp.server.fastmcp import FastMCP - -# Stateless server with JSON responses (recommended) -mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True) - -# Other configuration options: -# Stateless server with SSE streaming responses -# mcp = FastMCP("StatelessServer", stateless_http=True) - -# Stateful server with session persistence -# mcp = FastMCP("StatefulServer") - - -# Add a simple tool to demonstrate the server -@mcp.tool() -def greet(name: str = "World") -> str: - """Greet someone by name.""" - return f"Hello, {name}!" - - -# Run server with streamable_http transport -if __name__ == "__main__": - mcp.run(transport="streamable-http") -``` - -_Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_config.py)_ - - -You can mount multiple FastMCP servers in a Starlette application: - - -```python -""" -Run from the repository root: - uvicorn examples.snippets.servers.streamable_starlette_mount:app --reload -""" - -import contextlib - -from starlette.applications import Starlette -from starlette.routing import Mount - -from mcp.server.fastmcp import FastMCP - -# Create the Echo server -echo_mcp = FastMCP(name="EchoServer", stateless_http=True, json_response=True) - - -@echo_mcp.tool() -def echo(message: str) -> str: - """A simple echo tool""" - return f"Echo: {message}" - - -# Create the Math server -math_mcp = FastMCP(name="MathServer", stateless_http=True, json_response=True) - - -@math_mcp.tool() -def add_two(n: int) -> int: - """Tool to add two to the input""" - return n + 2 - - -# Create a combined lifespan to manage both session managers -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - async with contextlib.AsyncExitStack() as stack: - await stack.enter_async_context(echo_mcp.session_manager.run()) - await stack.enter_async_context(math_mcp.session_manager.run()) - yield - - -# Create the Starlette app and mount the MCP servers -app = Starlette( - routes=[ - Mount("/echo", echo_mcp.streamable_http_app()), - Mount("/math", math_mcp.streamable_http_app()), - ], - lifespan=lifespan, -) - -# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp -# To mount at the root of each path (e.g., /echo instead of /echo/mcp): -# echo_mcp.settings.streamable_http_path = "/" -# math_mcp.settings.streamable_http_path = "/" -``` - -_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_starlette_mount.py)_ - - -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/) - -The streamable HTTP transport supports: - -- Stateful and stateless operation modes -- Resumability with event stores -- JSON or SSE response formats -- Better scalability for multi-node deployments - -#### CORS Configuration for Browser-Based Clients - -If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it: - -```python -from starlette.applications import Starlette -from starlette.middleware.cors import CORSMiddleware - -# Create your Starlette app first -starlette_app = Starlette(routes=[...]) - -# Then wrap it with CORS middleware -starlette_app = CORSMiddleware( - starlette_app, - allow_origins=["*"], # Configure appropriately for production - allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods - expose_headers=["Mcp-Session-Id"], -) -``` - -This configuration is necessary because: - -- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management -- Browsers restrict access to response headers unless explicitly exposed via CORS -- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses - -### Mounting to an Existing ASGI Server - -By default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below. - -For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). - -#### StreamableHTTP servers - -You can mount the StreamableHTTP server to an existing ASGI server using the `streamable_http_app` method. This allows you to integrate the StreamableHTTP server with other ASGI applications. - -##### Basic mounting - - -```python -""" -Basic example showing how to mount StreamableHTTP server in Starlette. - -Run from the repository root: - uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload -""" - -import contextlib - -from starlette.applications import Starlette -from starlette.routing import Mount - -from mcp.server.fastmcp import FastMCP - -# Create MCP server -mcp = FastMCP("My App", json_response=True) - - -@mcp.tool() -def hello() -> str: - """A simple hello tool""" - return "Hello from MCP!" - - -# Create a lifespan context manager to run the session manager -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - async with mcp.session_manager.run(): - yield - - -# Mount the StreamableHTTP server to the existing ASGI server -app = Starlette( - routes=[ - Mount("/", app=mcp.streamable_http_app()), - ], - lifespan=lifespan, -) -``` - -_Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_http_basic_mounting.py)_ - - -##### Host-based routing - - -```python -""" -Example showing how to mount StreamableHTTP server using Host-based routing. - -Run from the repository root: - uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload -""" - -import contextlib - -from starlette.applications import Starlette -from starlette.routing import Host - -from mcp.server.fastmcp import FastMCP - -# Create MCP server -mcp = FastMCP("MCP Host App", json_response=True) - - -@mcp.tool() -def domain_info() -> str: - """Get domain-specific information""" - return "This is served from mcp.acme.corp" - - -# Create a lifespan context manager to run the session manager -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - async with mcp.session_manager.run(): - yield - - -# Mount using Host-based routing -app = Starlette( - routes=[ - Host("mcp.acme.corp", app=mcp.streamable_http_app()), - ], - lifespan=lifespan, -) -``` - -_Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_http_host_mounting.py)_ - - -##### Multiple servers with path configuration - - -```python -""" -Example showing how to mount multiple StreamableHTTP servers with path configuration. - -Run from the repository root: - uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload -""" - -import contextlib - -from starlette.applications import Starlette -from starlette.routing import Mount - -from mcp.server.fastmcp import FastMCP - -# Create multiple MCP servers -api_mcp = FastMCP("API Server", json_response=True) -chat_mcp = FastMCP("Chat Server", json_response=True) - - -@api_mcp.tool() -def api_status() -> str: - """Get API status""" - return "API is running" - - -@chat_mcp.tool() -def send_message(message: str) -> str: - """Send a chat message""" - return f"Message sent: {message}" - - -# Configure servers to mount at the root of each path -# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp -api_mcp.settings.streamable_http_path = "/" -chat_mcp.settings.streamable_http_path = "/" - - -# Create a combined lifespan to manage both session managers -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - async with contextlib.AsyncExitStack() as stack: - await stack.enter_async_context(api_mcp.session_manager.run()) - await stack.enter_async_context(chat_mcp.session_manager.run()) - yield - - -# Mount the servers -app = Starlette( - routes=[ - Mount("/api", app=api_mcp.streamable_http_app()), - Mount("/chat", app=chat_mcp.streamable_http_app()), - ], - lifespan=lifespan, -) -``` - -_Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_http_multiple_servers.py)_ - - -##### Path configuration at initialization - - -```python -""" -Example showing path configuration during FastMCP initialization. - -Run from the repository root: - uvicorn examples.snippets.servers.streamable_http_path_config:app --reload -""" - -from starlette.applications import Starlette -from starlette.routing import Mount - -from mcp.server.fastmcp import FastMCP - -# Configure streamable_http_path during initialization -# This server will mount at the root of wherever it's mounted -mcp_at_root = FastMCP( - "My Server", - json_response=True, - streamable_http_path="/", -) - - -@mcp_at_root.tool() -def process_data(data: str) -> str: - """Process some data""" - return f"Processed: {data}" - - -# Mount at /process - endpoints will be at /process instead of /process/mcp -app = Starlette( - routes=[ - Mount("/process", app=mcp_at_root.streamable_http_app()), - ] -) -``` - -_Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/streamable_http_path_config.py)_ - - -#### SSE servers - -> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http). - -You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications. - -```python -from starlette.applications import Starlette -from starlette.routing import Mount, Host -from mcp.server.fastmcp import FastMCP - - -mcp = FastMCP("My App") - -# Mount the SSE server to the existing ASGI server -app = Starlette( - routes=[ - Mount('/', app=mcp.sse_app()), - ] -) - -# or dynamically mount as host -app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) -``` - -When mounting multiple MCP servers under different paths, you can configure the mount path in several ways: - -```python -from starlette.applications import Starlette -from starlette.routing import Mount -from mcp.server.fastmcp import FastMCP - -# Create multiple MCP servers -github_mcp = FastMCP("GitHub API") -browser_mcp = FastMCP("Browser") -curl_mcp = FastMCP("Curl") -search_mcp = FastMCP("Search") - -# Method 1: Configure mount paths via settings (recommended for persistent configuration) -github_mcp.settings.mount_path = "/github" -browser_mcp.settings.mount_path = "/browser" - -# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting) -# This approach doesn't modify the server's settings permanently - -# Create Starlette app with multiple mounted servers -app = Starlette( - routes=[ - # Using settings-based configuration - Mount("/github", app=github_mcp.sse_app()), - Mount("/browser", app=browser_mcp.sse_app()), - # Using direct mount path parameter - Mount("/curl", app=curl_mcp.sse_app("/curl")), - Mount("/search", app=search_mcp.sse_app("/search")), - ] -) - -# Method 3: For direct execution, you can also pass the mount path to run() -if __name__ == "__main__": - search_mcp.run(transport="sse", mount_path="/search") -``` - -For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). - -## Advanced Usage - -### Low-Level Server - -For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API: - - -```python -""" -Run from the repository root: - uv run examples/snippets/servers/lowlevel/lifespan.py -""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager -from typing import Any - -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - - -# Mock database class for example -class Database: - """Mock database class for example.""" - - @classmethod - async def connect(cls) -> "Database": - """Connect to database.""" - print("Database connected") - return cls() - - async def disconnect(self) -> None: - """Disconnect from database.""" - print("Database disconnected") - - async def query(self, query_str: str) -> list[dict[str, str]]: - """Execute a query.""" - # Simulate database query - return [{"id": "1", "name": "Example", "query": query_str}] - - -@asynccontextmanager -async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]: - """Manage server startup and shutdown lifecycle.""" - # Initialize resources on startup - db = await Database.connect() - try: - yield {"db": db} - finally: - # Clean up on shutdown - await db.disconnect() - - -# Pass lifespan to server -server = Server("example-server", lifespan=server_lifespan) - - -@server.list_tools() -async def handle_list_tools() -> list[types.Tool]: - """List available tools.""" - return [ - types.Tool( - name="query_db", - description="Query the database", - inputSchema={ - "type": "object", - "properties": {"query": {"type": "string", "description": "SQL query to execute"}}, - "required": ["query"], - }, - ) - ] - - -@server.call_tool() -async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]: - """Handle database query tool call.""" - if name != "query_db": - raise ValueError(f"Unknown tool: {name}") - - # Access lifespan context - ctx = server.request_context - db = ctx.lifespan_context["db"] - - # Execute query - results = await db.query(arguments["query"]) - - return [types.TextContent(type="text", text=f"Query results: {results}")] - - -async def run(): - """Run the server with lifespan management.""" - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="example-server", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) -``` - -_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lowlevel/lifespan.py)_ - - -The lifespan API provides: - -- A way to initialize resources when the server starts and clean them up when it stops -- Access to initialized resources through the request context in handlers -- Type-safe context passing between lifespan and request handlers - - -```python -""" -Run from the repository root: -uv run examples/snippets/servers/lowlevel/basic.py -""" - -import asyncio - -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -# Create a server instance -server = Server("example-server") - - -@server.list_prompts() -async def handle_list_prompts() -> list[types.Prompt]: - """List available prompts.""" - return [ - types.Prompt( - name="example-prompt", - description="An example prompt template", - arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)], - ) - ] - - -@server.get_prompt() -async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: - """Get a specific prompt by name.""" - if name != "example-prompt": - raise ValueError(f"Unknown prompt: {name}") - - arg1_value = (arguments or {}).get("arg1", "default") - - return types.GetPromptResult( - description="Example prompt", - messages=[ - types.PromptMessage( - role="user", - content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"), - ) - ], - ) - - -async def run(): - """Run the basic low-level server.""" - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - asyncio.run(run()) -``` - -_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lowlevel/basic.py)_ - - -Caution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server. - -#### Structured Output Support - -The low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output: - - -```python -""" -Run from the repository root: - uv run examples/snippets/servers/lowlevel/structured_output.py -""" - -import asyncio -from typing import Any - -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -server = Server("example-server") - - -@server.list_tools() -async def list_tools() -> list[types.Tool]: - """List available tools with structured output schemas.""" - return [ - types.Tool( - name="get_weather", - description="Get current weather for a city", - inputSchema={ - "type": "object", - "properties": {"city": {"type": "string", "description": "City name"}}, - "required": ["city"], - }, - outputSchema={ - "type": "object", - "properties": { - "temperature": {"type": "number", "description": "Temperature in Celsius"}, - "condition": {"type": "string", "description": "Weather condition"}, - "humidity": {"type": "number", "description": "Humidity percentage"}, - "city": {"type": "string", "description": "City name"}, - }, - "required": ["temperature", "condition", "humidity", "city"], - }, - ) - ] - - -@server.call_tool() -async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: - """Handle tool calls with structured output.""" - if name == "get_weather": - city = arguments["city"] - - # Simulated weather data - in production, call a weather API - weather_data = { - "temperature": 22.5, - "condition": "partly cloudy", - "humidity": 65, - "city": city, # Include the requested city - } - - # low-level server will validate structured output against the tool's - # output schema, and additionally serialize it into a TextContent block - # for backwards compatibility with pre-2025-06-18 clients. - return weather_data - else: - raise ValueError(f"Unknown tool: {name}") - - -async def run(): - """Run the structured output server.""" - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="structured-output-example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - asyncio.run(run()) -``` - -_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lowlevel/structured_output.py)_ - - -Tools can return data in four ways: - -1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18) -2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18) -3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility -4. **Direct CallToolResult**: Return `CallToolResult` directly for full control (including `_meta` field) - -When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early. - -##### Returning CallToolResult Directly - -For full control over the response including the `_meta` field (for passing data to client applications without exposing it to the model), return `CallToolResult` directly: - - -```python -""" -Run from the repository root: - uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py -""" - -import asyncio -from typing import Any - -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -server = Server("example-server") - - -@server.list_tools() -async def list_tools() -> list[types.Tool]: - """List available tools.""" - return [ - types.Tool( - name="advanced_tool", - description="Tool with full control including _meta field", - inputSchema={ - "type": "object", - "properties": {"message": {"type": "string"}}, - "required": ["message"], - }, - ) - ] - - -@server.call_tool() -async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult: - """Handle tool calls by returning CallToolResult directly.""" - if name == "advanced_tool": - message = str(arguments.get("message", "")) - return types.CallToolResult( - content=[types.TextContent(type="text", text=f"Processed: {message}")], - structuredContent={"result": "success", "message": message}, - _meta={"hidden": "data for client applications only"}, - ) - - raise ValueError(f"Unknown tool: {name}") - - -async def run(): - """Run the server.""" - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - asyncio.run(run()) -``` - -_Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_ - - -**Note:** When returning `CallToolResult`, you bypass the automatic content/structured conversion. You must construct the complete response yourself. - -### Pagination (Advanced) - -For servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items. - -#### Server-side Implementation - - -```python -""" -Example of implementing pagination with MCP server decorators. -""" - -from pydantic import AnyUrl - -import mcp.types as types -from mcp.server.lowlevel import Server - -# Initialize the server -server = Server("paginated-server") - -# Sample data to paginate -ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items - - -@server.list_resources() -async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult: - """List resources with pagination support.""" - page_size = 10 - - # Extract cursor from request params - cursor = request.params.cursor if request.params is not None else None - - # Parse cursor to get offset - start = 0 if cursor is None else int(cursor) - end = start + page_size - - # Get page of resources - page_items = [ - types.Resource(uri=AnyUrl(f"resource://items/{item}"), name=item, description=f"Description for {item}") - for item in ITEMS[start:end] - ] - - # Determine next cursor - next_cursor = str(end) if end < len(ITEMS) else None - - return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor) -``` - -_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/pagination_example.py)_ - - -#### Client-side Consumption - - -```python -""" -Example of consuming paginated MCP endpoints from a client. -""" - -import asyncio - -from mcp.client.session import ClientSession -from mcp.client.stdio import StdioServerParameters, stdio_client -from mcp.types import PaginatedRequestParams, Resource - - -async def list_all_resources() -> None: - """Fetch all resources using pagination.""" - async with stdio_client(StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])) as ( - read, - write, - ): - async with ClientSession(read, write) as session: - await session.initialize() - - all_resources: list[Resource] = [] - cursor = None - - while True: - # Fetch a page of resources - result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor)) - all_resources.extend(result.resources) - - print(f"Fetched {len(result.resources)} resources") - - # Check if there are more pages - if result.nextCursor: - cursor = result.nextCursor - else: - break - - print(f"Total resources: {len(all_resources)}") - - -if __name__ == "__main__": - asyncio.run(list_all_resources()) -``` - -_Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/pagination_client.py)_ - - -#### Key Points - -- **Cursors are opaque strings** - the server defines the format (numeric offsets, timestamps, etc.) -- **Return `nextCursor=None`** when there are no more pages -- **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. - -### Writing MCP Clients - -The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports): - - -```python -""" -cd to the `examples/snippets/clients` directory and run: - uv run client -""" - -import asyncio -import os - -from pydantic import AnyUrl - -from mcp import ClientSession, StdioServerParameters, types -from mcp.client.stdio import stdio_client -from mcp.shared.context import RequestContext - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="uv", # Using uv to run the server - args=["run", "server", "fastmcp_quickstart", "stdio"], # We're already in snippets dir - env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, -) - - -# Optional: create a sampling callback -async def handle_sampling_message( - context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams -) -> types.CreateMessageResult: - print(f"Sampling request: {params.messages}") - return types.CreateMessageResult( - role="assistant", - content=types.TextContent( - type="text", - text="Hello, world! from model", - ), - model="gpt-3.5-turbo", - stopReason="endTurn", - ) - - -async def run(): - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session: - # Initialize the connection - await session.initialize() - - # List available prompts - prompts = await session.list_prompts() - print(f"Available prompts: {[p.name for p in prompts.prompts]}") - - # Get a prompt (greet_user prompt from fastmcp_quickstart) - if prompts.prompts: - prompt = await session.get_prompt("greet_user", arguments={"name": "Alice", "style": "friendly"}) - print(f"Prompt result: {prompt.messages[0].content}") - - # List available resources - resources = await session.list_resources() - print(f"Available resources: {[r.uri for r in resources.resources]}") - - # List available tools - tools = await session.list_tools() - print(f"Available tools: {[t.name for t in tools.tools]}") - - # Read a resource (greeting resource from fastmcp_quickstart) - resource_content = await session.read_resource(AnyUrl("greeting://World")) - content_block = resource_content.contents[0] - if isinstance(content_block, types.TextContent): - print(f"Resource content: {content_block.text}") - - # Call a tool (add tool from fastmcp_quickstart) - result = await session.call_tool("add", arguments={"a": 5, "b": 3}) - result_unstructured = result.content[0] - if isinstance(result_unstructured, types.TextContent): - print(f"Tool result: {result_unstructured.text}") - result_structured = result.structuredContent - print(f"Structured tool result: {result_structured}") - - -def main(): - """Entry point for the client script.""" - asyncio.run(run()) - - -if __name__ == "__main__": - main() -``` - -_Full example: [examples/snippets/clients/stdio_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/stdio_client.py)_ - - -Clients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http): - - -```python -""" -Run from the repository root: - uv run examples/snippets/clients/streamable_basic.py -""" - -import asyncio - -from mcp import ClientSession -from mcp.client.streamable_http import streamable_http_client - - -async def main(): - # Connect to a streamable HTTP server - async with streamable_http_client("http://localhost:8000/mcp") as ( - read_stream, - write_stream, - _, - ): - # Create a session using the client streams - async with ClientSession(read_stream, write_stream) as session: - # Initialize the connection - await session.initialize() - # List available tools - tools = await session.list_tools() - print(f"Available tools: {[tool.name for tool in tools.tools]}") - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -_Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/streamable_basic.py)_ - - -### Client Display Utilities - -When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts: - - -```python -""" -cd to the `examples/snippets` directory and run: - uv run display-utilities-client -""" - -import asyncio -import os - -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client -from mcp.shared.metadata_utils import get_display_name - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="uv", # Using uv to run the server - args=["run", "server", "fastmcp_quickstart", "stdio"], - env={"UV_INDEX": os.environ.get("UV_INDEX", "")}, -) - - -async def display_tools(session: ClientSession): - """Display available tools with human-readable names""" - tools_response = await session.list_tools() - - for tool in tools_response.tools: - # get_display_name() returns the title if available, otherwise the name - display_name = get_display_name(tool) - print(f"Tool: {display_name}") - if tool.description: - print(f" {tool.description}") - - -async def display_resources(session: ClientSession): - """Display available resources with human-readable names""" - resources_response = await session.list_resources() - - for resource in resources_response.resources: - display_name = get_display_name(resource) - print(f"Resource: {display_name} ({resource.uri})") - - templates_response = await session.list_resource_templates() - for template in templates_response.resourceTemplates: - display_name = get_display_name(template) - print(f"Resource Template: {display_name}") - - -async def run(): - """Run the display utilities example.""" - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write) as session: - # Initialize the connection - await session.initialize() - - print("=== Available Tools ===") - await display_tools(session) - - print("\n=== Available Resources ===") - await display_resources(session) - - -def main(): - """Entry point for the display utilities client.""" - asyncio.run(run()) - - -if __name__ == "__main__": - main() -``` - -_Full example: [examples/snippets/clients/display_utilities.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/display_utilities.py)_ - - -The `get_display_name()` function implements the proper precedence rules for displaying names: - -- For tools: `title` > `annotations.title` > `name` -- For other objects: `title` > `name` - -This ensures your client UI shows the most user-friendly names that servers provide. - -### OAuth Authentication for Clients - -The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers: - - -```python -""" -Before running, specify running MCP RS server URL. -To spin up RS server locally, see - examples/servers/simple-auth/README.md - -cd to the `examples/snippets` directory and run: - uv run oauth-client -""" - -import asyncio -from urllib.parse import parse_qs, urlparse - -import httpx -from pydantic import AnyUrl - -from mcp import ClientSession -from mcp.client.auth import OAuthClientProvider, TokenStorage -from mcp.client.streamable_http import streamable_http_client -from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken - - -class InMemoryTokenStorage(TokenStorage): - """Demo In-memory token storage implementation.""" - - def __init__(self): - self.tokens: OAuthToken | None = None - self.client_info: OAuthClientInformationFull | None = None - - async def get_tokens(self) -> OAuthToken | None: - """Get stored tokens.""" - return self.tokens - - async def set_tokens(self, tokens: OAuthToken) -> None: - """Store tokens.""" - self.tokens = tokens - - async def get_client_info(self) -> OAuthClientInformationFull | None: - """Get stored client information.""" - return self.client_info - - async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: - """Store client information.""" - self.client_info = client_info - - -async def handle_redirect(auth_url: str) -> None: - print(f"Visit: {auth_url}") - - -async def handle_callback() -> tuple[str, str | None]: - callback_url = input("Paste callback URL: ") - params = parse_qs(urlparse(callback_url).query) - return params["code"][0], params.get("state", [None])[0] - - -async def main(): - """Run the OAuth client example.""" - oauth_auth = OAuthClientProvider( - server_url="http://localhost:8001", - client_metadata=OAuthClientMetadata( - client_name="Example MCP Client", - redirect_uris=[AnyUrl("http://localhost:3000/callback")], - grant_types=["authorization_code", "refresh_token"], - response_types=["code"], - scope="user", - ), - storage=InMemoryTokenStorage(), - redirect_handler=handle_redirect, - callback_handler=handle_callback, - ) - - async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client: - async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write, _): - async with ClientSession(read, write) as session: - await session.initialize() - - tools = await session.list_tools() - print(f"Available tools: {[tool.name for tool in tools.tools]}") - - resources = await session.list_resources() - print(f"Available resources: {[r.uri for r in resources.resources]}") - - -def run(): - asyncio.run(main()) - - -if __name__ == "__main__": - run() -``` - -_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/oauth_client.py)_ - - -For a complete working example, see [`examples/clients/simple-auth-client/`](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""" - -import asyncio - -from mcp import ClientSession, StdioServerParameters, types -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"] - ) - - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - - # Example 1: Parsing text content - result = await session.call_tool("get_data", {"format": "text"}) - for content in result.content: - if isinstance(content, types.TextContent): - print(f"Text: {content.text}") - - # Example 2: Parsing structured content from JSON tools - result = await session.call_tool("get_user", {"id": "123"}) - if hasattr(result, "structuredContent") and result.structuredContent: - # Access structured data directly - user_data = result.structuredContent - print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}") - - # Example 3: Parsing embedded resources - result = await session.call_tool("read_config", {}) - for content in result.content: - if isinstance(content, types.EmbeddedResource): - resource = content.resource - if isinstance(resource, types.TextResourceContents): - print(f"Config from {resource.uri}: {resource.text}") - elif isinstance(resource, types.BlobResourceContents): - 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") - - # Example 5: Handling errors - result = await session.call_tool("failing_tool", {}) - if result.isError: - print("Tool execution failed!") - for content in result.content: - if isinstance(content, types.TextContent): - print(f"Error: {content.text}") - - -async def main(): - await parse_tool_results() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -### MCP Primitives - -The MCP protocol defines three core primitives that servers can implement: - -| Primitive | Control | Description | Example Use | -|-----------|-----------------------|-----------------------------------------------------|------------------------------| -| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options | -| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses | -| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates | - -### Server Capabilities - -MCP servers declare capabilities during initialization: - -| Capability | Feature Flag | Description | -|--------------|------------------------------|------------------------------------| -| `prompts` | `listChanged` | Prompt template management | -| `resources` | `subscribe`
`listChanged`| Resource exposure and updates | -| `tools` | `listChanged` | Tool discovery and execution | -| `logging` | - | Server logging configuration | -| `completions`| - | Argument completion suggestions | - -## Documentation - -- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/) -- [Experimental Features (Tasks)](https://modelcontextprotocol.github.io/python-sdk/experimental/tasks/) -- [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. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. +[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg +[pypi-url]: https://pypi.org/project/mcp/ +[mit-badge]: https://img.shields.io/pypi/l/mcp.svg +[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE +[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://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 +[spec-url]: https://modelcontextprotocol.io/specification/latest diff --git a/README.v2.md b/README.v2.md deleted file mode 100644 index 9b9971ec32..0000000000 --- a/README.v2.md +++ /dev/null @@ -1,132 +0,0 @@ -# MCP Python SDK - -
- -Python implementation of the Model Context Protocol (MCP) - -[![PyPI][pypi-badge]][pypi-url] -[![MIT licensed][mit-badge]][mit-url] -[![Python Version][python-badge]][python-url] -[![Documentation][docs-badge]][docs-url] -[![Protocol][protocol-badge]][protocol-url] -[![Specification][spec-badge]][spec-url] - -
- - - -> **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 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://py.sdk.modelcontextprotocol.io/v2/migration/) 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.0a3`), 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). - -## Documentation - -**The documentation lives at .** - -It has the full [tutorial](https://py.sdk.modelcontextprotocol.io/v2/tutorial/), the [API reference](https://py.sdk.modelcontextprotocol.io/v2/api/mcp/), and the [migration guide](https://py.sdk.modelcontextprotocol.io/v2/migration/). - -## What is MCP? - -The [Model Context Protocol](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but designed for LLM interactions. With this SDK you can: - -- **Build MCP servers** that expose tools, resources, and prompts to any MCP host -- **Build MCP clients** that connect to any MCP server -- Speak every standard transport: stdio, Streamable HTTP, and SSE - -## Requirements - -Python 3.10+. - -## Installation - -```bash -uv add "mcp[cli]==2.0.0a3" # or: pip install "mcp[cli]==2.0.0a3" -``` - -The pin matters while v2 is in pre-release: an unpinned install resolves to the latest stable v1.x, which this README does not describe. Check [PyPI](https://pypi.org/project/mcp/#history) for the newest pre-release, and use `uv run --with "mcp==2.0.0a3"` for one-off commands. - -## A server in 15 lines - -Create a `server.py`: - - -```python -from mcp.server import MCPServer - -mcp = MCPServer("Demo") - - -@mcp.tool() -def add(a: int, b: int) -> int: - """Add two numbers.""" - return a + b - - -@mcp.resource("greeting://{name}") -def greeting(name: str) -> str: - """Greet someone by name.""" - return f"Hello, {name}!" -``` - -_Full example: [docs_src/index/tutorial001.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs_src/index/tutorial001.py)_ - - -That's a complete MCP server: one tool, one templated resource. Open it in the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): - -```bash -uv run mcp dev server.py -``` - -Call `add` with `a=1`, `b=2` and you get `3` back. - -Notice what you did **not** write: no JSON Schema (`a: int, b: int` _is_ the schema), no request parsing, no validation code, no protocol handling. Two type-hinted Python functions and a docstring. - -[The tutorial](https://py.sdk.modelcontextprotocol.io/v2/tutorial/) takes it from here. - -## A client in 10 lines - -The same package is a full MCP **client**. `Client` connects to a URL, a stdio subprocess, a custom transport, or (for tests) straight to a server object in memory with no transport at all: - -```python -import asyncio - -from mcp import Client - -from server import mcp - - -async def main() -> None: - async with Client(mcp) as client: - result = await client.call_tool("add", {"a": 1, "b": 2}) - print(result.structured_content) # {'result': 3} - - -asyncio.run(main()) -``` - -Swap `mcp` for `"http://localhost:8000/mcp"` and the exact same code talks to a remote server. - -## 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](https://github.com/modelcontextprotocol/python-sdk/blob/main/CONTRIBUTING.md) to get started. - -## License - -This project is licensed under the MIT License. See the [LICENSE](https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE) file for details. - -[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg -[pypi-url]: https://pypi.org/project/mcp/ -[mit-badge]: https://img.shields.io/pypi/l/mcp.svg -[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE -[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://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 -[spec-url]: https://modelcontextprotocol.io/specification/latest diff --git a/RELEASE.md b/RELEASE.md index fba7115bbc..cfd4d927cb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -33,11 +33,17 @@ the publish job — `skip-existing` makes it skip whatever already landed. The `Development Status` classifier in both `pyproject.toml` files is permanently `5 - Production/Stable`; it is not bumped as part of any release. -1. Check the full test matrix is green on the release commit. The matrix runs +1. Update the pre-release version examples in `README.md` and the docs + (grep the outgoing version — the pins live in the README Installation + section, `docs/index.md`, and `docs/installation.md`) so the tagged + commit — and therefore the README PyPI publishes — names the version + being released. When entering a new phase (alpha → beta → rc), update + the banner wording too. +2. 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 +3. Create the release as a pre-release, passing the exact commit verified in + step 2 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 @@ -50,13 +56,13 @@ the publish job — `skip-existing` makes it skip whatever already landed. The 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 +4. 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 +5. 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 diff --git a/examples/README.md b/examples/README.md index 0a283e1356..4bfa140bdf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,9 +4,9 @@ example per protocol feature, each with its own README. Start with [`stories/tools/`](stories/tools/); the [stories README](stories/README.md) has the full table and how to run them. -- [`snippets/`](snippets/) — short extracts embedded into `README.v2.md`. Kept - minimal and in sync with the top-level README; not intended to be run - standalone. +- [`snippets/`](snippets/) — short extracts that were embedded into the v1 + README (now on the `v1.x` branch); superseded by `docs_src/`, which the docs + and README embed today. Retained pending consolidation into `stories/`. - [`servers/everything-server/`](servers/everything-server/) — the conformance target for the cross-SDK [conformance suite](https://github.com/modelcontextprotocol/conformance). @@ -15,8 +15,8 @@ migration guide; superseded by `stories/` and slated for removal. - [`clients/`](clients/) and the remaining [`servers/`](servers/) directories (`simple-*`, `sse-polling-demo`, `structured-output-lowlevel`) — standalone - v1-era projects still linked from `README.v2.md`; retained pending - consolidation into `stories/`. + v1-era projects retained pending consolidation into `stories/` (the + `simple-auth` pair is still linked from `docs/advanced/`). For real-world servers see the [servers repository](https://github.com/modelcontextprotocol/servers). diff --git a/pyproject.toml b/pyproject.toml index 22ba4d4f4c..7b947588fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "mcp" dynamic = ["version", "dependencies"] description = "Model Context Protocol SDK" -readme = "README.v2.md" +readme = "README.md" requires-python = ">=3.10" authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }] maintainers = [ @@ -175,7 +175,6 @@ executionEnvironments = [ [tool.ruff] line-length = 120 target-version = "py310" -extend-exclude = ["README.md", "README.v2.md"] [tool.ruff.lint] select = [ diff --git a/scripts/update_readme_snippets.py b/scripts/update_readme_snippets.py index 413c980175..99e9237a4c 100755 --- a/scripts/update_readme_snippets.py +++ b/scripts/update_readme_snippets.py @@ -93,16 +93,16 @@ def process_snippet_block(match: re.Match[str], check_mode: bool = False) -> str return full_match -def update_readme_snippets(readme_path: Path = Path("README.md"), check_mode: bool = False) -> bool: +def update_readme_snippets(check_mode: bool = False) -> bool: """Update code snippets in README.md with live code from source files. Args: - readme_path: Path to the README file check_mode: If True, only check if updates are needed without modifying Returns: True if file is up to date or was updated, False if check failed """ + readme_path = Path("README.md") if not readme_path.exists(): print(f"Error: README file not found: {readme_path}") return False @@ -146,12 +146,10 @@ def main(): parser.add_argument( "--check", action="store_true", help="Check mode - verify snippets are up to date without modifying" ) - # TODO(v2): Drop the `--readme` argument when v2 is released, and set to `README.md`. - parser.add_argument("--readme", default="README.v2.md", help="Path to README file (default: README.v2.md)") args = parser.parse_args() - success = update_readme_snippets(Path(args.readme), check_mode=args.check) + success = update_readme_snippets(check_mode=args.check) if not success: sys.exit(1) diff --git a/tests/docs_src/test_shape.py b/tests/docs_src/test_shape.py index 98fb2503de..1636bd825e 100644 --- a/tests/docs_src/test_shape.py +++ b/tests/docs_src/test_shape.py @@ -67,12 +67,8 @@ def _retired_names_used(source: str) -> list[str]: def _referenced_examples() -> set[str]: - """Every `docs_src/...` path that some docs page or the README actually includes. - - The README is globbed rather than named so this survives the planned - `README.v2.md` -> `README.md` rename instead of crashing on a missing file. - """ - pages = [*sorted((REPO_ROOT / "docs").rglob("*.md")), *sorted(REPO_ROOT.glob("README*.md"))] + """Every `docs_src/...` path that some docs page or the README actually includes.""" + pages = [*sorted((REPO_ROOT / "docs").rglob("*.md")), REPO_ROOT / "README.md"] return {ref for page in pages for ref in _INCLUDE_DIRECTIVE.findall(page.read_text(encoding="utf-8"))} diff --git a/tests/test_examples.py b/tests/test_examples.py index f24e932bed..f139f418a1 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -93,7 +93,6 @@ async def test_desktop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): assert "file2.txt" in content.text -# TODO(v2): Change back to README.md when v2 is released. # `--8<--` include directives lint clean as Python, so pages built from # `docs_src/` includes cost nothing here; the real validation of those files is # pyright + ruff + tests/docs_src/. @@ -101,7 +100,7 @@ async def test_desktop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): "example", list( find_examples( - "README.v2.md", + "README.md", "docs/index.md", "docs/installation.md", "docs/tutorial",