Запускаем MCP server: TypeScript и Python в цифрах
Один server написан дважды — три инструмента, ресурс и prompt. 94 пакета против 28, cold start 145 мс против 709.
На этой странице
Вот весь спор о языках — измеренный еще до того, как он начался.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msПервые две строки — сравнение, которого хотят все. Третья строка — тот же TypeScript server из первой строки, запущенный так, как его действительно будут распространять, — и он оказывается в трех миллисекундах от Python.
Глава 26 читала Model Context Protocol по его собственной спецификации через сырой JSON-RPC, потому что у сырого JSON-RPC нет языка. В этой главе их два, и главный вес аргумента здесь: один и тот же server, написанный дважды. Три инструмента, один ресурс, один prompt, оба SDK, без коротких путей с любой стороны. Затем транспорты, Inspector, 401 и числа, которые никто не публиковал.
Server и почему в нем именно эти пять вещей
Ссылка на раздел: Server и почему в нем именно эти пять вещейЖурнал инцидентов. Три инструмента, потому что разделение чтения и записи из главы 18 должно быть видно: search_incidents читает, open_incident пишет и возвращает handle, resolve_incident принимает этот handle и закрывает. Один ресурс, incidents://open, потому что чтение текущего списка — это то, что подключает приложение. Один prompt, postmortem, потому что «напиши это» — slash-команда человека. Это и есть иерархия управления из главы 26 — модель, приложение, человек — превращенная в пять регистраций.
Handle важнее, чем кажется. В главе 26 мы сломали игрушечный календарь, храня его состояние в массиве на уровне модуля: у протокола нет сессии, поэтому инструмент создания возвращает непрозрачный идентификатор, а каждый последующий вызов принимает его как обычный аргумент. Ни один из файлов не предполагает, что вызывающий — это процесс, который его открыл.
Вот один и тот же инструмент на обоих языках, зарегистрированный рядом:
server.registerTool(
"resolve_incident",
{
description:
"Close an incident by handle and record its cause.",
inputSchema: {
id: z.string().describe(
"The handle returned by open_incident, e.g. INC-3."),
cause: z.string().describe(
"One sentence. What actually broke."),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
},
},
async ({ id, cause }) => {
const at = OPEN.findIndex((i) => i.id === id);
if (at < 0) {
return { isError: true, content: [{ type: "text",
text: `No open incident ${id}. ` +
`Call search_incidents first.` }] };
}
const [done] = OPEN.splice(at, 1);
return { content: [{ type: "text",
text: JSON.stringify({ ...done, cause }) }] };
},
);@server.tool(
description=
"Close an incident by handle and record its cause.",
annotations=ToolAnnotations(
readOnlyHint=False,
destructiveHint=True,
idempotentHint=True,
),
)
def resolve_incident(
id: Annotated[str, Field(description=
"The handle returned by open_incident, e.g. INC-3.")],
cause: Annotated[str, Field(description=
"One sentence. What actually broke.")],
) -> Incident:
for at, i in enumerate(OPEN):
if i["id"] == id:
done = OPEN.pop(at)
return {**done, "cause": cause}
raise ValueError(
f"No open incident {id}. Call search_incidents first.")Сначала посмотрите, что одинаково, потому что это и есть результат. Оба объявляют имя, описание, два описанных строковых аргумента и три аннотации; оба — одна функция; ни один не упоминает JSON-RPC, framing, stdout или версию протокола. Два SDK сошлись к одной форме — именно это и должно означать «Tier 1».1
Два различия реальны, и оба вернутся позже. TypeScript описывает аргументы через библиотеку схем — здесь Zod, — и схема является значением, которое вы пишете. Python описывает их собственными type hints функции и читает их во время import, поэтому знает о функции вещи, которые TypeScript-файл ему никогда не сообщал. И путь ошибки: TypeScript возвращает результат инструмента с isError, Python бросает исключение. Запомните это.
Остальные четыре регистрации структурно ничем не отличаются. Ресурс — это server.registerResource("open-incidents", "incidents://open", …) против @server.resource("incidents://open", …); prompt — registerPrompt против @server.prompt. Последняя строка каждого файла — транспорт: await server.connect(new StdioServerTransport()) против server.run().
Полные файлы: 81 непустая строка и 3 060 байт TypeScript против 63 и 2 555. Отнеситесь к этому с должной долей скепсиса: число строк измеряет форматтер не меньше, чем язык, поэтому ни одно из этих чисел не попало в таблицу ниже.
Один client, оба server
Ссылка на раздел: Один client, оба serverДоказательство того, что язык невидим, — один client, запущенный дважды, в одиннадцать строк:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const client = new Client({ name: "incident-cli", version: "1.0.0" });
await client.connect(new StdioClientTransport({
command: process.argv[2], args: process.argv.slice(3) }));
const { tools } = await client.listTools();
console.log("tools:", tools.map((t) => t.name).join(", "));
const opened = await client.callTool({ name: "open_incident",
arguments: { title: "Queue backed up", severity: "sev2" } });
console.log("open_incident ->", JSON.stringify(opened.content));Укажите его на каждый server по очереди. Реальный вывод, сокращенно:
$ node client.ts node incidents.ts
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\"id\":\"INC-3\"}"}]
$ node client.ts ./py/.venv/bin/python ./py/incidents.py
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\n \"id\": \"INC-3\"\n}"}]Те же инструменты, тот же порядок, тот же handle. TypeScript client не может понять, на чем написан server, и никогда об этом не спрашивает. Это обещание протокола — и оно выполняется.
Теперь посмотрите на пробелы во втором результате, потому что это не косметика: Python SDK сериализует payloads с pydantic_core.to_json(result, fallback=str, indent=2). При чтении ресурса с двумя инцидентами в списке тело TypeScript — 136 символов и 37 o200k_base token; тело Python — 185 и 62. На шестьдесят восемь процентов больше token для идентичных строк — платит тот, кто читает ресурс в prompt, каждый раз.
С каталогом та же история, но причина крупнее. Оба server, те же три инструмента, tools/list взвешен по ключам:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Input-схемы Python дешевле — мост Zod в TypeScript ставит $schema и additionalProperties на каждую. Весь разрыв в 138 token — это output-схема, которую никто не писал. resolve_incident аннотирован как -> Incident, поэтому SDK вывел JSON Schema для return type и отправил ее. Это действительно полезно — именно это позволяет client валидировать structuredContent — и это 187 token вашего context window, пришедшие из-за type hint. Правило главы 24 о том, что определения вытесняют важный материал, относится и к схемам, о наличии которых вы не знали.
Сломать намеренно: сообщение об ошибке, которое протекло
Ссылка на раздел: Сломать намеренно: сообщение об ошибке, которое протеклоДва пути ошибки выше — не вопрос стиля. Дайте каждому server инструмент, который падает так, как падает реальная интеграция, и прочитайте, что доходит до модели.
TypeScript {"content":[{"type":"text","text":
"connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
"isError":true}
Python {"content":[{"text":"Error executing tool boom","type":"text"}],
"isError":true}TypeScript SDK положил во context модели внутренний адрес, порт, имя базы данных и service account. Python SDK не положил туда ничего из этого; traceback ушел в stderr и остался на server.
Ни то ни другое не bug. Это решения, и решение Python записано в его собственной docstring: ToolError — это «failure you anticipated», и его сообщение возвращается «in content for the model to read»; все остальное «is treated as a crash: the model sees only Error executing tool <name>, and the server logs the traceback at ERROR». Класс для crash-сценария договаривает вслух: «nothing from the original reaches the client».
Оба поведения неверны в половине случаев. В главе 18 утверждалось, что ошибка валидации должна возвращаться как результат инструмента, который модель может прочитать и исправить, потому что это самая эффективная строка в большинстве интеграций; на стороне Python для этого нужно явно бросить ToolError, а голый ValueError выбрасывает полезное предложение. Аргумент главы 30 идет в другую сторону: все, что возвращает инструмент, попадает в context, откуда последующая prompt injection может попытаться это вычитать, а непроверенная строка исключения — самый слабо аудитируемый текст в вашей системе.
Правило, которое выдерживает оба довода: решайте для каждого инструмента, что именно разрешено говорить при сбое, и пишите эту строку сами. Никогда не позволяйте стандартному тексту исключения решать за вас — ни на одном языке.
Сломать намеренно: одна строка в standard output
Ссылка на раздел: Сломать намеренно: одна строка в standard outputОфициальный tutorial формулирует правило без оговорок: «For STDIO-based servers: Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server. The print() function writes to stdout by default, so keep it out of a STDIO server entirely.»1 Глава 26 цитировала нормативную версию — server «MUST NOT write anything to its stdout that is not a valid MCP message».2
Добавьте одну строку в каждый server и прочитайте raw stream:
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingВариант Python хуже, и причина не в MCP. Процесс, у которого stdout — pipe, а не terminal, получает block-buffered stream, поэтому лишняя строка flush-ится тогда, когда решит buffer, — здесь при выходе, после ответа, до которого она была записана. Повреждение появляется не там, где bug. Добавьте flush=True или библиотеку, которая делает flush, — и оно переместится.
А теперь часть, объясняющая, почему это доезжает до production. Подайте сломанный server трем clients:
naive parser, dirty server SyntaxError: Unexpected token 'i',
"incidents "... is not valid JSON
SDK client, dirty server tools: search_incidents, open_incident, resolve_incident
MCP Inspector, dirty server full catalogue, no warningСемистрочный parser умирает сразу. Официальный client и Inspector оба пожимают плечами — пропускают строку и продолжают. Правило, которое ломает только clients, которыми никто не пользуется, доходит до production нетронутым; поэтому здесь стоит сломать все намеренно, а не в customer log.
CLI-режим Inspector — та половина, о которой забывают: npx @modelcontextprotocol/inspector --cli <command> --method tools/list печатает каталог и выходит, поэтому его можно скриптовать так, как нельзя browser UI.3
Таблица
Ссылка на раздел: ТаблицаОба SDK установились чисто, каждый в свою директорию, без общих частей:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| latest protocol revision implemented | 2025-11-25 | 2026-07-28 |
| transitive packages installed | 94 | 28 |
| installed size | 13.9 MiB | 44.3 MiB |
| files on disk | 3,386 | 2,018 |
| third-party packages loaded to serve stdio | 8 of 94 | 18 of 28 |
| bare interpreter start, median | 19.4 ms | 11.1 ms |
spawn → tools/list answered, median of 25 | 144.5 ms | 709.4 ms |
tools/list catalogue, o200k_base tokens | 342 | 480 |
Каждая строка удивляет в свою сторону, поэтому сравнение стоит провести, а не предполагать.
TypeScript устанавливает больше чем втрое больше пакетов и меньше трети байтов. 94 dependencies — это npm-экосистема в ее обычном виде: fast-deep-equal, es-errors, dunder-proto. У Python их 28 — меньше, но они огромные: cryptography, pydantic-core и uvicorn — compiled artefacts. Если ваш инстинкт подсказывает, что беспокоиться нужно о количестве dependencies, эта строка — контрпример.
Интерпретатор Python стартует быстрее Node, причем заметно — 11.1 ms против 19.4 ms на пустой программе. Значит, 565 ms в строке cold start — это не язык. Это SDK, и строка loaded-packages объясняет почему:
TypeScript 8 of 94 sdk, zod, zod-to-json-schema, ajv, ajv-formats,
fast-deep-equal, fast-uri, json-schema-traverse
Python 18 of 28 mcp, mcp_types, pydantic, pydantic_core, anyio,
starlette, uvicorn, sse_starlette, httpx2,
cryptography, _cffi_backend, opentelemetry, click, …Server, у которого единственный I/O — pipe, импортирует ASGI web server, HTTP client и TLS library до того, как прочитает первую строку. TypeScript SDK тоже поставляет Express, Hono, jose и eventsource — они лежат на диске непрочитанными, потому что граница package не пускает их в import server/stdio.js. Package Python — один import graph, поэтому import mcp означает все сразу: python -X importtime приписывает 727 ms import mcp.server.mcpserver — число измерено под import profiler, поэтому выходит выше 709 ms, которые запуск без profiler занимает от spawn до ответа, — и 269 из них одному только поддереву mcp.types. Wire types — это Pydantic models, по одному классу на protocol message на revision, и их построение — работа, выполняемая при import. Это design trade, не небрежность: eager imports — причина, по которой Python SDK может выдать вам run(transport="streamable-http") на следующей строке без второй установки.
А затем последняя строка вступительного блока обнуляет аргумент. Упакуйте TypeScript server правильно — entry bin, shebang, npm link, ничего скачивать не нужно — и запустите его через npx с --no-install, то есть так, как опубликованный stdio server действительно стартует:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLauncher стоит 568 ms на каждый старт — в четыре с половиной раза больше всего import TypeScript SDK — и эта цена платится при каждом запуске, потому что MCP host запускает stdio server выполнением этой команды. Поэтому честная форма фразы «TypeScript стартует в пять раз быстрее» такая: да, пока вы не распространяете его обычным способом. Та же оговорка, вероятно, относится к uvx; на этой машине не был установлен uv, поэтому такой строки нет. Ничего неизмеренного в таблицу не попадает.
Два транспорта, и только два
Ссылка на раздел: Два транспорта, и только дваГлава 26 разобрала framing stdio. Две вещи она оставила сюда.
Первая: запуск server через npx или uvx и есть stdio transport. Нет отдельного «package mode». Конфигурация host называет command и arguments; host запускает его и разговаривает через pipes. Поэтому «как это распространять» и «на каком transport это говорит» локально являются одним вопросом, и поэтому стоимость launcher относится к главе о shipping.
Вторая: у stdio вообще нет раздела authorization, и спецификация говорит об этом одной строкой — реализации, использующие stdio, «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 Его security model — это security model операционной системы, и его предел тоже: локальный subprocess обслуживает ровно одну машину и одного пользователя.
Другой живой transport — Streamable HTTP: один endpoint, принимающий POST, один HTTP request на каждое JSON-RPC message и header Accept, который должен перечислять и application/json, и text/event-stream, потому что server для каждого request выбирает, чем из двух ответить.5 Глава 14 разбирала этот event stream вручную, так что в wire format нет ничего нового — только обертка. Три обязанности текущей revision легко пропустить, и все три проверяемы:
Version header должен совпадать с body
Ссылка на раздел: Version header должен совпадать с bodyКаждый POST несет MCP-Protocol-Version, и его значение должно совпадать с protocolVersion внутри собственного _meta request. Несовпадение — это 400 с header-mismatch error, а не пожимание плечами.5
Для compliance нужны еще два headers
Ссылка на раздел: Для compliance нужны еще два headersMcp-Method зеркалит method в каждом request; Mcp-Name зеркалит params.name или params.uri на tools/call, resources/read и prompts/get. Они существуют, чтобы proxy мог маршрутизировать, не разбирая bodies.5
Старые формы исчезли и отвечают отказом
Ссылка на раздел: Старые формы исчезли и отвечают отказомGET stream, Mcp-Session-Id и resumption Last-Event-ID были удалены. Server, который говорит только на этой revision, должен отвечать 405 Method Not Allowed на GET или DELETE, игнорировать session header без выпуска нового и игнорировать Last-Event-ID.5
Теперь измерение, которое меняет рамку всей главы. Отправьте request текущей revision каждому server по HTTP.
Python 200 {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
"tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}
TypeScript {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
2025-03-26, 2024-11-05, 2024-10-07)"}}Константы совпадают с поведением: LATEST_PROTOCOL_VERSION Python SDK читает 2026-07-28, TypeScript SDK — 2025-11-25. Отправьте header-mismatch request из шага выше, и Python server ответит 400 с error -32020 и message «mcp-protocol-version header does not match the request envelope's protocol version»; у TypeScript SDK такого кода нет, потому что он не реализует revision, которая его определяет.
Страница, где оба перечислены как Tier 1, также говорит: «Each SDK provides the same functionality».1 На дату ниже, для текущей revision, это утверждение — намерение, а не факт. Проверьте LATEST_PROTOCOL_VERSION в SDK, который собираетесь установить; это одна строка и единственное утверждение в этой главе, которое все еще будет важно через год.
401 и фраза, которую нужно цитировать
Ссылка на раздел: 401 и фраза, которую нужно цитироватьПеренесите server с ноутбука, и client незнакомца придет с token. Это половина, которую глава 26 оставила в стороне, и половина, которую multi-user продукт не может пропустить.
Спецификация помещает MCP server в роль OAuth 2.1 и называет ее: protected MCP server — это resource server, client — OAuth client, а authorization server — чужая проблема.4 Из этой роли следуют четыре обязательных пункта, процитированные полностью, потому что именно пересказ рождает ошибку:
MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. […] MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens.4
«Must not accept or transit» — это правило против passthrough, и именно поэтому существует вся конструкция audience. Server, который воспроизводит переданный ему bearer token в сторонний API, — confused deputy: он одалживает собственное trust тому, кто его вызвал. Правило запрещает повторное использование, а не только хранение.
Чтобы это стало enforceable, нужны четыре RFC, у каждого своя работа.6 RFC 9728 — это способ, которым client вообще находит authorization server: MCP server отдает protected-resource-metadata document, и 401 указывает на него. RFC 8707 — это параметр resource: client должен отправить canonical URI server и в authorization request, и в token request, «regardless of whether authorization servers support it», чтобы выпущенный token называл свою audience. RFC 9207 замыкает петлю с другой стороны: client записывает issuer до redirect и сравнивает возвращенный iss точной строкой, без normalization — без case folding, без удаления default port, без trailing slash. А RFC 7591, Dynamic Client Registration, теперь deprecated в пользу Client ID Metadata Documents, «retained for backwards compatibility with authorization servers that do not support» их.4
Подключите это на обоих servers через token verifier, который только проверяет audience. Лестница TypeScript:
no token 401 WWW-Authenticate: Bearer error="invalid_token",
error_description="Missing Authorization header",
scope="incidents:read",
resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server 401 error_description="token audience is not this server"
no exp claim 401 error_description="Token has no expiration time"
right aud, no scope 403 error="insufficient_scope", scope="incidents:read"
right aud + scope 200 {"result":{"tools":[…]}}{"resource":"http://127.0.0.1:8931/mcp",
"authorization_servers":["https://auth.example.com/"],
"scopes_supported":["incidents:read","incidents:write"],
"resource_name":"Incidents"}Оба SDK отдают этот document и оба указывают на него через 401 — это вся история discovery: client, который никогда не видел ваш server, узнает, где пройти authentication, из отказа. 403 — другой зверь: token нормальный, scope — нет; challenge называет, чего не хватает, чтобы client мог повысить права, а не начинать сначала.
Две ступени отличаются, и ни одно отличие не в спецификации. TypeScript SDK отклоняет token без expiry claim; Python возвращает 200, потому что expires_at optional в его AccessToken, а None означает «no opinion». И Python 403 несет error_description="Required scope: incidents:read" без параметра scope, который спецификация говорит servers включать. Verifier — не место, где стоит принимать library default: audience check в любом языке пишете вы, как и expiry.
Одна честная придирка из того же запуска. GET на endpoint ответил 404 в wiring Express и 400 Bad Request: Missing session ID в Python-варианте, тогда как спецификация просит 405 Method Not Allowed и где «session ID» — vocabulary, удаленная в этой revision. Ни то ни другое не опасно; оба — форма экосистемы в середине migration.
Где servers действительно живут
Ссылка на раздел: Где servers действительно живутПоследняя часть shipping — где вы публикуете, и у нее есть ответ с числом. Сегодня просканированы все servers в официальном registry в их latest version:7
| servers | |
|---|---|
| total (latest version, not deleted) | 28,170 |
| active / deprecated | 27,853 / 317 |
| ship at least one installable package | 13,065 |
| remote only — a URL, nothing to install | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
Два прочтения, указывающие в противоположные стороны. По published servers npm ведет 2.3 к 1 — это число цитируют, когда говорят, что экосистема TypeScript. По downloads ведет Python: за последние тридцать дней mcp получил 286.7 million против 194.7 million у @modelcontextprotocol/sdk, еще до добавления fastmcp с 72.1 million.7 Оба Tier 1, нормативная schema — schema.ts, и официальный tutorial «Build an MCP server» открывается на вкладке Python.1 Какая бы половина ни была у вас в голове, другая половина тоже верна.
И строка важнее обеих: больше половины registry — 14 696 из 28 170 — не требуют ничего устанавливать. Это web services. Подсчеты transport говорят то же с другой стороны: из 14 290 package entries 13 787 объявляют stdio; из 16 640 remote entries 15 570 объявляют Streamable HTTP, а 1 070 все еще объявляют deprecated HTTP+SSE. Так что «MCP server — это subprocess на вашем ноутбуке» описывает сокращающееся меньшинство, и каждому из 14 696 нужен раздел выше, а не environment variable.
Показать детали
Намеренно двуязычно — и прецедент для этого.
Это единственная двуязычная глава курса, потому что честный ответ делится: registry сегодня npm-first, а downloads — Python-first, одновременно. Написать только на одном из двух языков означало бы отдать половину вопроса и исказить экосистему в процессе. Открытый прецедент есть: Hugging Face MCP Course среди prerequisites перечисляет «Experience with at least one programming language (Python or TypeScript examples will be shown)» и учит обоим.8 Протокол, вся ценность которого в количестве реализаций, — плохое место для monolingual.
Раздел с датой: все выше, у чего есть срок годности
Ссылка на раздел: Раздел с датой: все выше, у чего есть срок годностиПрочитано и измерено 7 сентября 2026 года, против protocol revision 2026-07-28.
| value | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, published 27 July 2026; 4,322,438 bytes unpacked, 693 files, 17 direct dependencies |
| latest revision it implements | 2025-11-25 |
mcp (PyPI) | 2.1.1, published 25 August 2026; 357,912-byte wheel, plus mcp-types 2.1.1 at 69,656 bytes |
| latest revision it implements | 2026-07-28 |
| SDK tiers | TypeScript, Python, C#, Go, Rust at Tier 1; Java, Ruby at Tier 2; Swift, PHP, Kotlin at Tier 3 |
| registry servers | 28,170 |
| downloads, last 30 days | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
Одна migration note, которая не является числом. В mcp 2.x FastMCP был переименован в MCPServer, а почти каждый tutorial онлайн все еще начинается со старого import. SDK поставляет модуль, единственная цель которого — объяснить это; это самая деликатная deprecation в этой главе:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x,
where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import
MCPServer) and other APIs changed; see the migration guide … or pin 'mcp<2'
to keep running v1 code.Так какой выбрать
Ссылка на раздел: Так какой выбратьС таблицей перед глазами рекомендация скучная, а это хороший знак.
Если server живет внутри web application, которое вы уже запускаете, пишите его на TypeScript. Тот же process, тот же deploy, тот же request handler; Streamable HTTP — это endpoint, который вы добавляете рядом с остальными; а 13.9 MiB и 145 ms достаются бесплатно, потому что runtime уже поднят. Это большинство из 14 696 remote servers.
Если server оборачивает data tooling, пишите его на Python. Вы exposing pandas, warehouse client, набор transforms размером с notebook, и server на другом языке был бы subprocess call, переодетым в schema. Семьсот миллисекунд import в service, который стартует один раз, — не cost; в subprocess, который host перезапускает весь день, — cost.
И пока что строка revision переопределяет оба правила. Если вам нужна 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover, — у одного из двух SDK это уже есть, а у другого нет.
Куда дальше
Ссылка на раздел: Куда дальшеТеперь вы можете ship один и тот же server на любом языке, защищать выбор таблицей вместо предпочтения, запускать его по обоим живым transports и дать ему token, который он отклонит.
То, что вы построили, все еще function: schema, endpoint, детерминированная вещь, которую вызывает модель. Целый класс knowledge не помещается в эту форму — как мы пишем postmortem, какие поля нужны нашим incident reports, в каком порядке мы действуем и почему. Это procedure, это prose, и попытка запихнуть это в tool description — способ вырастить system prompts до двух тысяч token, оплачиваемых на каждом turn независимо от того, идет ли разговор об инцидентах.
Глава 28 — другой ответ: папка с SKILL.md внутри, которую модель читает, а не вызывает, загруженная в три уровня так, что reference material почти ничего не стоит до turn, где он нужен. У нее нет главного языка — и это первое, чему она учит.
Источники и метод
Ссылка на раздел: Источники и методВсе здесь измерено 7 сентября 2026 года на Node 22.22.3 и Python 3.14.4, против @modelcontextprotocol/sdk 1.30.0 с zod 3.25.76 и mcp 2.1.1, каждый установлен в собственную одноразовую директорию. Timings — медианы 25 запусков, wall clock от spawn до строки с response tools/list; counts token — o200k_base через tiktoken по JSON каждого definition. Ни один платный API не вызывался: model здесь не нужна.
Два servers — 81 и 63 непустые строки; один из их трех инструментов воспроизведен выше на обоих языках, а остальные четыре регистрации отличаются только так, как описано. Error-disclosure policy Python SDK процитирована из docstrings ToolError и UnexpectedToolError в mcp/server/mcpserver/exceptions.py; pretty-printing default — pydantic_core.to_json(result, fallback=str, indent=2) в mcp/server/mcpserver/resources/types.py и utilities/func_metadata.py. Protocol-version constants — LATEST_PROTOCOL_VERSION в mcp_types/version.py и в TypeScript SDK types.js, оба прочитаны из установленных packages, а не из changelog.
Сноски
Ссылка на раздел: Сноски-
SDKs,
modelcontextprotocol.io/docs/sdk, и Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, оба прочитаны 7 сентября 2026. Источник tier table, предложения «Each SDK provides the same functionality but follows the idioms and best practices of its language», порядка language-tab в tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), а также процитированного logging rule проprint()иstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Источник newline framing и правила puritystdout. Глава 26 читает эту страницу полностью; здесь она цитируется ради строки, которую нарушает сломанный server. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, прочитано 7 сентября 2026. Один package, три clients за одним binary — web,--cliи--tui— с общим core, одним набором transports и одним OAuth state на диске. CLI произвел catalog traces здесь. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, прочитано 7 сентября 2026. Источник роли resource-server; четырех token-handling clauses, процитированных полностью; требования, чтобы servers реализовывали RFC 9728, а clients использовали его для discovery; правил параметраresourceи определения canonical-URI; таблицы issuer-validation; deprecation Dynamic Client Registration; таблицы401/403/400и challengeinsufficient_scope; а также stdio exemption: «Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.» ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, и Transports overview,.../basic/transports. Источник правила single-endpoint POST, dual requirementAccept, headerMCP-Protocol-Versionи его правила must-match-the-body, headersMcp-MethodиMcp-Name, описанных как «REQUIRED for compliance», удаления GET stream, sessions иLast-Event-ID, guidance405, обязательной validationOriginи classification transport 2024-11-05 HTTP+SSE как Deprecated under SEP-2596. ↩ ↩2 ↩3 ↩4 -
Четыре документа, на которые опирается спецификация, вместе с draft, который она profiles: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. and Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, February 2020 — параметрresourceи audience, которую он связывает. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — document, на который указывает401. Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, March 2022 — параметрissи exact-string comparison. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, July 2015, deprecated для этого use. И Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, для формы challengeWWW-Authenticateвыше. ↩ -
Official MCP registry,
registry.modelcontextprotocol.io/v0/servers, просканирован 7 сентября 2026 сversion=latest: 282 pages, 28,170 servers, подсчитаноregistryTypeпо distinct server names. Download figures:api.npmjs.org/downloads/point/last-monthдля@modelcontextprotocol/sdk(194,679,333 за 8 августа – 6 сентября 2026) иpypistats.org/api/packages/<name>/recentдляmcpиfastmcp, оба прочитаны в тот же день. Package sizes взяты из npm registry document и PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, прочитано 7 сентября 2026: среди prerequisites — «Experience with at least one programming language (Python or TypeScript examples will be shown)». ↩