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

Что нового в 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
import anyio

from mcp import Client


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


if __name__ == "__main__":
    anyio.run(main)

Client принимает URL (Streamable HTTP), StdioServerParameters (подпроцесс stdio), любой другой контекстный менеджер транспорта, например sse_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, но до модели доходит только сообщение ToolError: любое другое исключение теперь читается как Error executing tool <name>, а трассировка попадает в лог сервера. Разграничение — на странице Обработка ошибок.
  • Результаты проверяются перед отправкой. Собранный вручную 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. Одно тело инструмента, оба поколения:

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve

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."
client.py
import anyio

from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitResult


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


async def main() -> None:
    async with (
        Client("http://localhost:8000/mcp", mode="legacy", elicitation_callback=answer) as legacy,
        Client("http://localhost:8000/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)


if __name__ == "__main__":
    anyio.run(main)

В этих двух файлах вся идея целиком: один сервер, один инструмент на 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 — читают всё.