Перейти к содержанию

Что нового в v2

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

В v2 одновременно произошли две перемены. SDK перестроен: новый движок под клиентом и под сервером, полноценный Client и набор переименований, с которыми кодовая база на v1 сталкивается при первом же импорте. И протокол ушёл вперёд: v2 говорит на ревизии MCP 2026-07-28, которая убирает рукопожатие при подключении, сессию и все запросы, инициируемые сервером, — не оставляя при этом за бортом клиенты, которые у вас уже есть.

Эта страница — обзор обеих половин: по разделу на каждую главную новость, и каждый заканчивается ссылкой на страницу, которой принадлежит тема. Это не руководство по переносу. Им служит Руководство по миграции: все ломающие изменения с кодом до и после.

v2 — стабильная ветка

pip install mcp устанавливает 2.x, а на странице Установка есть готовая строка установки для копирования. Если что-то в v2 ломается, удивляет или тормозит вас, сообщите нам.

SDK: от v1 к v2

FastMCP теперь MCPServer

Высокоуровневый класс сервера переименован, а вместе с ним и его модуль. Это первое, на что натыкается каждый сервер на v1, потому что старый путь импорта удалён, а не объявлен устаревшим:

from mcp.server import MCPServer  # v1: from mcp.server.fastmcp import FastMCP

mcp = MCPServer("Demo")  # v1: FastMCP("Demo")

Для сервера, собранного на декораторах, это заодно и почти весь перенос. @mcp.tool(), @mcp.resource() и @mcp.prompt() принимают то же, что принимали в v1 (@mcp.resource() добавляет один необязательный именованный аргумент security=), а входная схема по-прежнему строится по аннотациям типов. По мелочам: всё, что лежало под mcp.server.fastmcp.*, теперь живёт под mcp.server.mcpserver.*, ctx.fastmcp стал ctx.mcp_server, get_context() удалён (вместо него объявите параметр ctx: Context), а базовый класс исключений FastMCPError теперь MCPServerError. Таблица импортов — в Руководстве по миграции.

Resolve: новый способ запросить ввод у пользователя

Не всё, что нужно инструменту, должно приходить от модели. Новое в v2: параметр инструмента с аннотацией Resolve(fn) заполняет функция, которую пишете вы, незаметно для модели, и эта функция может вернуть Elicit(...), чтобы задать вопрос пользователю. Это предпочтительный способ получить что-либо от клиента посреди вызова: SDK передаёт вопрос тем механизмом, который поддерживает подключение, — живой запрос элицитации (elicitation) для клиента старого поколения или многораундовый запрос (multi-round-trip) на 2026-07-28, — так что одно тело инструмента обслуживает оба поколения. Подробнее — на странице Зависимости.

Note

Две другие формы остаются на случай, когда они нужны: ctx.elicit() по-прежнему работает для клиентов на подключениях старого поколения (Элицитация), а обработчик может сам вернуть InputRequiredResult и вести раунды вручную — именно так на 2026-07-28 путешествуют и запросы сэмплирования (sampling) и корневых каталогов (roots) (Многораундовые запросы).

Полноценный Client

v1 выдавала три вложенных слоя: контекстный менеджер транспорта, отдающий сырые потоки, обёрнутый вокруг них ClientSession и вызываемый вручную await session.initialize(). В v2 объект один:

client.py
from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")


@mcp.tool()
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."


async def main() -> None:
    async with Client(mcp) as client:
        print(client.server_info)
        print(client.server_capabilities)
        print(client.protocol_version)
        print(client.instructions)

Client принимает объект сервера (в памяти, без транспорта — это сценарий для тестов), URL (Streamable HTTP) или любой контекстный менеджер транспорта, например stdio_client(...). Вход в async with подключается и согласует версию протокола, на каком бы поколении ни говорил сервер; после этого client.server_capabilities и client.protocol_version просто доступны, как и client.server_info, когда сервер себя идентифицирует (теперь это Implementation | None, поскольку в поколении 2026 идентификация необязательна). Колбэки сэмплирования и элицитации, зарегистрированные в v1, по-прежнему работают (их тела затрагивает то же переименование атрибутов в snake_case, что и всё остальное на этой странице), теперь они ещё и отвечают на запросы внутри результатов в стиле 2026 (см. ниже) и выполняются параллельно, а не по одному. ClientSession по-прежнему лежит в основе для тех, кому нужна низкоуровневая поверхность, и client.session её отдаёт; она тоже изменилась (работает на новом движке-диспетчере, и некоторые её собственные сигнатуры поменялись), так что прежде чем спускаться на этот уровень, прочитайте Руководство по миграции.

Страница Объект Client знакомит с ним, Транспорты клиента описывает три формы подключения, Колбэки клиента — сами колбэки, а Тестирование показывает шаблон работы в памяти, который заменяет вспомогательную функцию create_connected_server_and_client_session() из v1.

Низкоуровневый Server перестроен, а не переименован

Если вы работаете на уровне JSON-RPC, это та часть v2, где «всё по-другому». Вот один и тот же сервер с одним инструментом в обоих вариантах; нажмите на маркеры, чтобы увидеть, что изменилось.

v1
from typing import Any

import mcp.types as types
from mcp.server.lowlevel import Server

server = Server("Bookshop")


@server.list_tools()  # (1)!
async def list_tools() -> list[types.Tool]:
    return [  # (2)!
        types.Tool(
            name="search_books",
            description="Search the catalog by title or author.",
            inputSchema={  # (3)!
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        )
    ]


@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:  # (4)!
    if name != "search_books":
        raise ValueError(f"Unknown tool: {name}")  # (5)!
    ctx = server.request_context  # (6)!
    return [types.TextContent(type="text", text=f"Found 3 books matching {arguments['query']!r}.")]  # (7)!
  1. Обработчики регистрируются декораторами (вызываемыми, со скобками) в любой момент после создания сервера.
  2. Возвращается голый list[Tool], а SDK оборачивает его в ListToolsResult.
  3. Поля в Python — в camelCase, а схема применяется принудительно: SDK проверяет по ней аргументы call_tool через jsonschema до запуска вашей функции, поэтому обращение arguments["query"] ниже безопасно.
  4. Один обработчик call_tool обслуживает все инструменты и получает имя инструмента и уже проверенные аргументы — распакованные и никогда не None.
  5. Исключение — так инструмент в v1 сообщает о неудаче: любое исключение перехватывается и возвращается как CallToolResult(isError=True) с текстом str(e), так что вызывающая модель читает это сообщение и может повторить попытку.
  6. Контекст берётся из фоновой ContextVar, к которой посреди запроса обращаются через объект сервера.
  7. Голые блоки содержимого оборачиваются в CallToolResult за вас.
v2
from mcp import MCPError
from mcp.server import Server, ServerRequestContext
from mcp.types import (
    INVALID_PARAMS,
    CallToolRequestParams,
    CallToolResult,
    ListToolsResult,
    PaginatedRequestParams,
    TextContent,
    Tool,
)

SEARCH_BOOKS = Tool(
    name="search_books",
    description="Search the catalog by title or author.",
    input_schema={  # (1)!
        "type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"],
    },
)


async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:  # (2)!
    return ListToolsResult(tools=[SEARCH_BOOKS])  # (3)!


async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:  # (4)!
    if params.name != "search_books":
        raise MCPError(INVALID_PARAMS, f"Unknown tool: {params.name}")  # (5)!
    args = params.arguments or {}  # (6)!
    text = f"Found 3 books matching {args['query']!r}."
    return CallToolResult(content=[TextContent(type="text", text=text)])  # (7)!


server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)  # (8)!
  1. Поля теперь в snake_case, а схема объявляется, но никогда не применяется: до запуска обработчика аргументы ничто не проверяет.
  2. У всех обработчиков одна форма: async (ctx, params) -> result. Контекст — первый аргумент (на нём живут ctx.session, ctx.request_id, ctx.protocol_version); сюда и переехал server.request_context.
  3. Полный ListToolsResult вы собираете сами. Возврат голого списка теперь даёт TypeError на стороне сервера, а не оборачивается SDK.
  4. На входе — типизированные параметры (params.name, params.arguments), на выходе — полный результат. Ничего не распаковывается, не оборачивается и не преобразуется за вас.
  5. Та же проверка, другой глагол. ValueError здесь дошёл бы до модели как непрозрачный -32603 (см. ниже), поэтому намеренная ошибка уровня протокола выбрасывается как MCPError: она проходит насквозь с нетронутыми кодом и сообщением, а -32602 с этим текстом — ответ на неизвестный инструмент, прописанный в самой спецификации.
  6. params.arguments может быть None; v1 подставляла {} ещё до того, как ваш код его видел. Раз перед обработчиком нет проверки, без этой строки не обойтись.
  7. Неожиданное исключение, выброшенное здесь, становится очищенной ошибкой протокола, -32603 "Internal server error": модель никогда не увидит сообщения. Для неудачи, которую модель должна прочитать и на которую должна отреагировать, возвращайте CallToolResult(is_error=True, ...).
  8. Обработчики — аргументы конструктора, так что поверхность сервера полна в момент его создания; add_request_handler() — запасной выход после создания и дверь к пользовательским методам.

Пример и есть шаблон. В общем виде: у всех обработчиков одна форма — типизированные параметры на входе и полный тип результата на выходе; прежней проверки аргументов инструмента через jsonschema больше нет; исключение — это ошибка протокола и никогда не результат инструмента с is_error=True; фоновой ContextVar server.request_context больше нет. Пользовательские методы в пространстве имён поставщика полноценно поддерживаются через add_request_handler(method, params_type, handler), который проверяет входящие параметры по вашей модели до запуска обработчика. А список middleware (намеренно помеченный как предварительный) оборачивает каждое входящее сообщение, заменяя приватные методы _handle_*, которые раньше переопределяли.

Внутри приёмный цикл BaseSession из v1 заменён движком-диспетчером, который теперь общий для клиента и сервера, и именно он делает верными сразу несколько утверждений этой страницы: один объект Server обслуживает оба поколения протокола, Client(server) диспетчеризует внутри процесса без JSON-RPC-обрамления, а запрос клиента, у которого истёк таймаут, теперь действительно отменяет обработчик на стороне сервера.

Подробнее — на странице Низкоуровневый Server; Руководство по миграции проходит по каждому удалённому хуку. Если вы никогда не спускались ниже MCPServer, ничто из этого вас не касается.

Типы протокола переехали в mcp-types, и все поля теперь в snake_case

Типы протокола теперь живут в собственном дистрибутиве mcp-types. Он не зависит ни от чего, кроме pydantic и typing-extensions, так что шлюз, прокси или генератор кода может использовать формы передаваемых MCP данных, не устанавливая HTTP-стек: такой проект устанавливает mcp-types и импортирует mcp_types. Сам mcp зависит от этого пакета с точной версией и реэкспортирует его, так что код, зависящий от SDK, по-прежнему пишет import mcp.types as types и from mcp.types import Tool (постоянный псевдоним, каждое имя — тот же объект) и объявляет только одну свою настоящую зависимость, mcp. Правило простое: импортируйте через тот пакет, от которого действительно зависите.

У этих типов каждый атрибут в Python теперь в snake_case: result.is_error, tool.input_schema, listing.next_cursor. JSON в передаваемых данных — в camelCase, ровно как раньше; изменилось только написание атрибутов. Заодно появились два более строгих значения по умолчанию: неизвестные поля игнорируются, а не возвращаются обратно (дополнительное кладите в _meta), и обе стороны проверяют трафик по согласованной версии протокола. Таблица переименований — в Руководстве по миграции.

Настройка транспорта переехала в run()

MCPServer(...) описывает, чем ваш сервер является: имя, инструкции, жизненный цикл (lifespan), авторизацию. То, как он обслуживается, теперь относится к run() и сборщикам приложений — туда ушли host, port, stateless_http, json_response, пути эндпоинтов и transport_security (MCPServer("x", port=9000) — это TypeError). Перегрузки типизированы по транспортам, так что редактор подскажет, какие параметры принимает stdio, а какие — streamable-http. Одно удаление стоит знать: mount_path больше нет; поддерживаемый способ обслуживать под префиксом — монтировать ASGI-приложение.

Параметры описаны на странице Запуск сервера; монтирование — на странице Добавление в существующее приложение.

Поведение, которое меняется без ошибки импорта

Переименования заявляют о себе сами. А вот эти изменения — нет:

  • Синхронные функции выполняются в рабочем потоке. Инструмент (а также ресурс, промпт или резолвер), объявленный через def, больше не блокирует цикл событий; плата за это — его тело больше не выполняется в потоке цикла событий, что важно для кода, привязанного к потоку. Обработчики async def не затронуты. Руководство по миграции.
  • MCPError (McpError в v1), выброшенный внутри инструмента, теперь ошибка протокола. Модель его никогда не видит. Любое другое исключение по-прежнему становится результатом с is_error=True, который модель может прочитать и на который может отреагировать. Разграничение — на странице Обработка ошибок.
  • Результаты проверяются перед отправкой. Собранный вручную Tool, у которого input_schema равна {}, теперь проваливает tools/list (спецификация требует "type": "object"). Серверы, построенные на @mcp.tool(), с этим не сталкиваются: их схемы пишет SDK.
  • Ваш клиент проверяет то, что получает. list_tools() и call_tool() сверяют ответ сервера с согласованной версией протокола, так что не совсем валидный сервер, который терпел снисходительный разбор v1, теперь вызывает pydantic.ValidationError. Если подключаетесь к серверам, которые не контролируете, будьте готовы оказаться тем, кто их обнаружит; подробности — в Руководстве по миграции.
  • Шаблоны URI теперь настоящий RFC 6570. {+path}, {?query} и им подобные работают, сопоставление точное, а не приблизительное через регулярные выражения, и обход путей в извлечённых значениях по умолчанию отклоняется. Более строгие шаблоны падают в момент декорирования, а не на первом запросе. Шаблоны URI.
  • Жизненный цикл streamable HTTP выполняется один раз, при запуске, и его состояние общее для всех сессий и запросов. В v1 он выполнялся один раз на сессию, а при stateless_http=True — один раз на запрос. Пулы и кэши, созданные в жизненном цикле, становятся радикально дешевле; всё, что захватывало там ресурс на одно подключение, теперь относится к телу обработчика. Жизненный цикл.
  • mcp dev и mcp install закрепляют порождаемое окружение за установленной у вас версией SDK. Обе команды запускают сервер в свежем окружении uv run --with ..., которое раньше разрешало mcp в новейший стабильный выпуск, а не в версию, против которой вы разрабатываете. Руководство по миграции.
  • HTTP-клиент теперь httpx2, а не httpx. Смена зависимости меняет то, что ваш код перехватывает и передаёт (httpx2.AsyncClient, httpx2.ConnectError), и меняет способ проверки TLS-сертификатов: httpx2 проверяет через truststore по хранилищу доверия операционной системы, а не по встроенному списку УЦ из certifi. Большинство окружений ничего не заметят; минимальный контейнер без системного хранилища УЦ или частный УЦ, о котором знал только набор certifi, начинает проваливать TLS-рукопожатие. Задайте SSL_CERT_FILE/SSL_CERT_DIR или передайте клиенту verify=ssl_context. Руководство по миграции.

Удалено полностью

Каждому пункту посвящён раздел в Руководстве по миграции:

  • Транспорт WebSocket с обеих сторон и дополнение mcp[ws]. Он никогда не входил в спецификацию MCP.
  • API экспериментальных Tasks (mcp.*.experimental). 2026-07-28 выносит задачи из ядра протокола в официальное расширение (SEP-2663), которое этот SDK пока не реализует.
  • mcp.shared.version, mcp.shared.progress и mcp.shared.session (с заглушкой RequestResponder, которую импортировали аннотации message_handler в v1) как пути импорта. (mcp.types не удалён: он остаётся постоянным псевдонимом отдельного пакета mcp_types.)
  • Устаревшее написание streamablehttp_client и колбэк get_session_id у streamable_http_client (который теперь отдаёт ровно два потока).
  • McpError, переименованный в MCPError с прямым конструктором (code, message, data).
  • MCPServer.get_context(), mount_path=, а также методы-декораторы, ContextVar и словари обработчиков низкоуровневого Server.

Протокол: от 2025-11-25 к 2026-07-28

v2 реализует ревизию 2026-07-28 и обслуживает обе ревизии одновременно: одно и то же streamable_http_app() (и один и тот же stdio-сервер) отвечает на initialize клиента поколения 2025 и на запросы клиента поколения 2026 — ничего не нужно настраивать, переключать флаг или разворачивать отдельно. Обслуживание новой ревизии не оставляет за бортом клиент на старой. Дальше — о том, что меняет сама новая ревизия.

Ни рукопожатия, ни сессии

Клиент 2026-07-28 не открывает подключение, не договаривается и лишь потом говорит. Каждый запрос несёт версию протокола, сведения о клиенте и возможности клиента в _meta, а единственный вызов обнаружения, server/discover, — обычный запрос, как любой другой. Client по умолчанию поступает правильно: один раз пробует server/discover и откатывается к рукопожатию initialize, если сервер старше.

По Streamable HTTP на пути 2026 нет Mcp-Session-Id, и это главная эксплуатационная новость: ничто не привязывает современный запрос к воркеру, так что ответить на него может любая реплика за обычным балансировщиком с round-robin. Две честные оговорки. Ваши клиенты поколения 2025 (сегодня это большинство клиентов) по-прежнему открывают сессии и по-прежнему требуют той же привязки, что требовали на v1; для них ничего не меняется. А единственное, что повтор многораундового запроса должен перенести между воркерами, — это его запечатанный request_state, ключ для которого по умолчанию создаётся на каждый процесс, поэтому масштабированное развёртывание передаёт RequestStateSecurity(keys=[...]). (stateless_http=True тут ни при чём: он влияет только на обслуживание клиентов поколения 2025, и трафик 2026 его никогда не читает; если вы уже задали его в v1, ничего не меняется.)

Клиентская сторона этого — на странице Версии протокола, чек-лист оператора (список разрешённых Host, ключ request_state, уведомления между репликами) — Развёртывание и масштабирование, а история об обоих поколениях сразу — Обслуживание клиентов старого поколения.

Сервер не может вызывать клиент: многораундовые запросы

На 2026-07-28 исчезли все запросы, инициируемые сервером: push-элицитация, сэмплирование, roots/list. На подключении 2026 для них нет обратного канала (back-channel), поэтому ctx.elicit() и ctx.session.create_message() там падают с NoBackChannelError (для клиентов старого поколения они по-прежнему работают).

Замена разворачивает вызов. Инструмент, которому что-то нужно от пользователя, возвращает вопрос (InputRequiredResult), клиент отвечает на него теми же колбэками, что были всегда, и вызов повторяется с приложенными ответами. Client ведёт этот цикл за вас. На сервере вы редко собираете результат сами, потому что это делает зависимость: аннотируйте параметр Resolve(ask_quantity), где ask_quantity — обычная функция, которую вы пишете, и SDK спросит тем механизмом, который поддерживает подключение: живым запросом элицитации на сессии старого поколения или многораундовым запросом на 2026. Одно тело инструмента, оба поколения:

dual_era.py
from typing import Annotated

from pydantic import BaseModel

from mcp import Client
from mcp.client import ClientRequestContext
from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
from mcp.types import ElicitRequestParams, ElicitResult

mcp = MCPServer("Bookshop")


class Quantity(BaseModel):
    copies: int


async def ask_quantity() -> Elicit[Quantity]:
    """Resolver: ask the user how many copies to put aside."""
    return Elicit("How many copies?", Quantity)


@mcp.tool()
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
    """Reserve copies of a book, asking the user how many."""
    if isinstance(quantity, AcceptedElicitation):
        return f"Reserved {quantity.data.copies} of {title!r}."
    return "Nothing reserved."


async def answer(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
    return ElicitResult(action="accept", content={"copies": 2})


async def main() -> None:
    async with (
        Client(mcp, mode="legacy", elicitation_callback=answer) as legacy,
        Client(mcp, elicitation_callback=answer) as modern,
    ):
        for client in (legacy, modern):
            result = await client.call_tool("reserve", {"title": "Dune"})
            print(client.protocol_version, result.structured_content)

В этом файле вся идея собрана в одном месте: один сервер, один инструмент на Resolve, а также клиент старого поколения и современный клиент, оба получающие свой ответ, — всё в памяти. Многораундовые запросы объясняет механизм (включая request_state, который SDK запечатывает и проверяет за вас); Элицитация описывает, как спрашивать.

Это единственное место, где перенесённый сервер v1 меняет поведение

Первыми на это натыкаются ваши собственные тесты: Client(mcp) по умолчанию согласует 2026-07-28 с вашим сервером v2, так что инструмент, вызывающий ctx.elicit(), падает в тесте, который проходил на v1. Перенесите вопрос в параметр Resolve(...) (переносимо между поколениями) или закрепите тестовый клиент на mode="legacy", если push-поведение вам действительно нужно.

Корневые каталоги, сэмплирование и протокольное логирование объявлены устаревшими; ping удалён

SEP-2577 объявляет устаревшими три возможности целиком, на всех версиях протокола: корневые каталоги, сэмплирование и логирование на уровне MCP (ctx.info() и ему подобные). Это отдельная ось, не связанная с отсутствующим обратным каналом выше; статус устаревшего — рекомендательный, всё продолжает работать с сессиями поколения 2025, и в передаваемых данных ничего не меняется. Заметите вы MCPDeprecationWarning — это UserWarning, поэтому он выводится по умолчанию; ожидайте, что первый же ctx.info(...) после обновления об этом сообщит.

С ping строже: он удалён из протокола, а не объявлен устаревшим. Так же на 2026-07-28 удалены два отдельных метода устаревших возможностей — logging/setLevel и клиентское notifications/roots/list_changed, — а уведомления о ходе выполнения теперь идут только от сервера к клиенту.

Полная таблица, замена для каждого пункта и однострочный фильтр на случай, если нужен тихий лог, пока вы обслуживаете клиенты старого поколения, — на странице Устаревшие возможности.

Уведомления об изменениях становятся одним потоком

На 2026-07-28 отдельный поток HTTP GET и resources/subscribe заменены на subscriptions/listen: клиент открывает один долгоживущий поток и называет виды уведомлений, которые хочет получать. MCPServer обслуживает его по умолчанию; публикуете вы через await ctx.notify_resource_updated(uri) (а также notify_tools_changed() и так далее), middleware может отклонить запрос на прослушивание для конкретного вызывающего, а развёртывания с несколькими репликами подключают общую SubscriptionBus. На клиенте поток открывает async with client.listen(...): фильтр передаётся именованными аргументами, обратно приходят типизированные события изменений, а sub.honored — подмножество, которое сервер согласился доставлять.

Публикация и обслуживание — на странице Подписки, наблюдающая сторона — на её клиентской паре, а шина — на странице Развёртывание и масштабирование.

Остальное, вкратце

  • Идентификация — необязательные метаданные каждого сообщения. Ключ clientInfo в _meta на стороне запроса необязателен (обязательная пара — protocolVersion + clientCapabilities), а serverInfo ушёл из тела результата server/discover: вместо этого серверы проставляют его в _meta каждого результата поколения 2026 (spec #3002). SDK проставляет всегда; client.server_info равен None, когда сервер себя не идентифицирует (например, middleware убрал ключ). Низкоуровневый Server показывает эту отметку в передаваемых данных.
  • Запросы маршрутизируются без разбора тел. Современные HTTP-запросы несут Mcp-Method (а для трёх инструментоподобных вызовов — ещё и Mcp-Name); свойство входной схемы инструмента с аннотацией x-mcp-header дублируется в заголовок Mcp-Param-* и перекрёстно проверяется сервером (SEP-2243). Шлюзы и ограничители частоты могут маршрутизировать по одним заголовкам; правила — в Руководстве по миграции.
  • Результаты несут подсказки кэширования. Результаты списков и чтения объявляют ttlMs и cacheScope (SEP-2549); вы задаёте их по методам через cache_hints=, а Client учитывает их встроенным кэшем ответов. Сервер, не отправляющий подсказок (любой сервер до 2026), видит идентичный, некэшированный трафик. Подсказки кэширования.
  • Расширения поддерживаются полноценно. Серверы и клиенты объявляют необязательные наборы возможностей под идентификаторами в обратной DNS-нотации (SEP-2133); встроенное расширение Apps (MCP Apps) служит эталоном. Расширения и MCP Apps.
  • Коды ошибок стандартизированы. Отсутствующий ресурс — это -32602 с URI в error.data, а новые коды, зарезервированные спецификацией, появляются как -32020 (несовпадение заголовка), -32021 (отсутствует обязательная возможность) и -32022 (неподдерживаемая версия протокола). Устранение неполадок построено по точным сообщениям.
  • Авторизацию стало сложнее использовать неправильно. Клиент проверяет iss, возвращаемый вместе с кодом авторизации (RFC 9207; ваш callback_handler теперь возвращает AuthorizationCodeResult), отправляет application_type при регистрации и никогда не воспроизводит учётные данные на другом сервере авторизации. Новое в корпоративном углу: поток подтверждения идентичности из SEP-990. Все изменения OAuth перечислены в Руководстве по миграции; страницы — OAuth для клиентов и Подтверждение идентичности.
  • Каждый сервер трассируется. OpenTelemetry включён по умолчанию как middleware: каждый запрос получает серверный спан, и это ничего не стоит, пока процесс не настроит экспортёр. Когда на SDK работают обе стороны, клиент также распространяет контекст трассировки W3C в _meta, так что трассы соединяются. OpenTelemetry.

Переходите с v1?

  • Руководство по миграции — полный и точный список того, что менять; эта страница объясняла зачем.
  • v1.x никуда не денется. Она переходит на поддержку, продолжает получать критические исправления и патчи безопасности, и ничто в выпуске спецификации 2026-07-28 её не ломает; её документация живёт по адресу /v1/. Если вы публикуете библиотеку, зависящую от mcp, и не готовы мигрировать, оставьте верхнюю границу (например, mcp>=1.28,<2), чтобы незакреплённое разрешение зависимостей оставалось на 1.x.
  • Что-то сырое, непонятное или сломанное? Оставьте отзыв о v2 — читают всё.