Перейти до змісту

Що нового у 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 її віддає; вона теж змінилася (працює на новому рушії диспетчера, і деякі її власні сигнатури змінилися), тож прочитайте Посібник з міграції, перш ніж спускатися нижче.

Клієнт знайомить із ним, Транспорти клієнта описує чотири форми під'єднання, Колбеки клієнта — самі колбеки, а Тестування показує шаблон роботи в пам'яті, що замінює допоміжну функцію 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 за сховищем довіри операційної системи, а не за вбудованим списком CA від certifi. Більшість середовищ цього не помітять; мінімальний контейнер без системного сховища CA або приватний CA, про який знав лише набір 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, і це головна новина для експлуатації: ніщо не прив'язує сучасний запит до робочого процесу, тож відповісти на нього може будь-яка репліка за звичайним балансувальником із циклічним розподілом. Два чесні застереження. Ваші клієнти покоління 2025 (а сьогодні це більшість клієнтів) і далі відкривають сесії й потребують тієї самої прив'язки, що й на v1; для них нічого не змінюється. А єдине, що багатораундова повторна спроба мусить перенести між робочими процесами, — це її запечатаний request_state, типовий ключ якого карбується окремо в кожному процесі, тож горизонтально масштабоване розгортання передає RequestStateSecurity(keys=[...]). (stateless_http=True тут ні до чого: він впливає лише на обслуговування клієнтів покоління 2025, а трафік 2026 його ніколи не читає; якщо ви вже задали його у v1, нічого не змінюється.)

Версії протоколу — клієнтський бік цього, Розгортання й масштабування — контрольний список оператора (список дозволених Host, ключ request_state, сповіщення між репліками), а Обслуговування клієнтів старого покоління — розповідь про обидва покоління водночас.

Сервер не може викликати клієнта: багатораундові запити

На 2026-07-28 зникли всі запити, ініційовані сервером: push-еліцитація, семплювання, roots/list. На з'єднанні 2026 для них немає каналу, тож 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) за замовчуванням узгоджує з вашим сервером v2 версію 2026-07-28, тож інструмент, що викликає ctx.elicit(), падає в тесті, який на v1 проходив. Перенесіть запитання в параметр Resolve(...) (працює в обох поколіннях) або зафіксуйте тестовий клієнт на mode="legacy", якщо вам справді потрібна push-поведінка.

Кореневі каталоги, семплювання та протокольне логування застарілі; ping вилучено

SEP-2577 оголошує застарілими три цілі можливості на всіх версіях протоколу: кореневі каталоги, семплювання та логування рівня MCP (ctx.info() і подібні). Це окрема вісь щодо відсутнього зворотного каналу (back-channel) вище; статус застарілого — рекомендаційний, усе й далі працює із сесіями покоління 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 — підмножина, яку сервер погодився доставляти.

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

Решта, коротко

  • Ідентичність — необов'язкові метадані кожного повідомлення. Ключ _meta clientInfo на боці запиту необов'язковий (обов'язкова пара — 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; ми читаємо все.