Към съдържанието
27/30Глава 27 от 30

Пускане на MCP сървър: TypeScript и Python, измерени

Един и същ сървър, написан два пъти: 3 инструмента, ресурс и prompt. 94 пакета срещу 28; cold start 145 ms срещу 709.

На тази страница

Ето целия спор за езика, измерен, преди да е изречена и дума от него.

spawn → tools/list answered, median of 25 launchesTEXT
node ./incidents.js         144.5 ms
python incidents.py         709.4 ms
npx incidents-mcp           712.6 ms

Първите два реда са сравнението, което всички искат. Третият ред е същият TypeScript сървър от първия ред, стартиран така, както реално би бил разпространяван — и се приземява на три милисекунди от Python.

Глава 26 прочете Model Context Protocol спрямо собствената му спецификация с raw JSON-RPC, защото raw JSON-RPC няма език. Тази глава има два, и тежестта на аргумента пада тук: същият сървър, написан два пъти. Три инструмента, един ресурс, един prompt, двата SDK, без преки пътища от нито една страна. После транспортите, инспекторът, 401, и числата, които никой не е публикувал.

Журнал на инциденти. Три инструмента, защото разделението в Глава 18 между четене и запис трябва да се вижда: search_incidents чете, open_incident пише и връща handle, resolve_incident взема този handle и затваря. Един ресурс, incidents://open, защото четенето на текущия списък е нещо, което приложението прикачва. Един prompt, postmortem, защото „напиши това“ е slash command на човек. Това е йерархията на контрол от Глава 26 — модел, приложение, човек — превърната в пет регистрации.

Handle-ът е по-важен, отколкото изглежда. Глава 26 счупи играчен календар, като държеше състоянието му в масив на ниво модул: протоколът няма сесия, затова инструмент за създаване връща непрозрачен идентификатор и всяко следващо извикване го приема като обикновен аргумент. Нищо в нито един от двата файла не приема, че извикващият е процесът, който го е отворил.

Ето същия инструмент и на двата езика, регистриран един до друг:

incidents.tsTS
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 }) }] };
  },
);
incidents.pyPYTHON
@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.")

Първо прочетете какво е еднакво, защото това е изводът. И двата декларират име, описание, два описани string аргумента и три annotations; и двата са една функция; нито един не споменава JSON-RPC, framing, stdout или версия на протокола. Двата SDK са се събрали около една и съща форма, което е каквото „Tier 1“ трябва да означава.1

Две разлики са реални и и двете се връщат по-късно. TypeScript описва аргументите със schema библиотека — тук Zod — и schema е стойност, която пишете. Python ги описва със собствените type hints на функцията и ги чете при import time, затова знае неща за функцията, които 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. Приемете това с нужната доза сол — броенето на редове измерва formatter толкова, колкото и език, затова нито едно от двете числа не е в заглавната таблица по-долу.

Доказателството, че езикът е невидим, е един клиент, пуснат два пъти, в единадесет реда:

client.tsTS
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));

Насочете го последователно към всеки сървър. Реален output, съкратен:

TEXT
$ 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 клиент не може да разбере на какво е написан сървърът и никога не пита. Това е цялото обещание на протокола, спазено.

Сега погледнете whitespace във втория резултат, защото не е козметика: Python SDK serialises payloads с pydantic_core.to_json(result, fallback=str, indent=2). При четене на ресурса с два инцидента в списъка TypeScript тялото е 136 знака и 37 o200k_base token-а; Python тялото е 185 и 62. Шестдесет и осем процента повече token-и за идентични редове, платени от този, който чете ресурса в prompt, всеки път.

Каталогът има същата история с по-голяма причина. И двата сървъра, същите три инструмента, tools/list претеглено ключ по ключ:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
общо342480

Python input schemas са по-евтини — TypeScript Zod bridge отпечатва $schema и additionalProperties върху всяка. Цялата разлика от 138 token-а е output schema, която никой не е писал. resolve_incident е annotated -> Incident, затова SDK изведе JSON Schema за return type и я изпрати. Това е наистина полезно — то позволява на клиента да валидира structuredContent — и е 187 token-а от вашия context window, пристигащи заради type hint. Правилото от Глава 24 за definitions, които изместват важния материал, важи и за schemas, за които не сте знаели, че имате.

Счупете го нарочно: съобщението за грешка, което изтече

Връзка към раздела: Счупете го нарочно: съобщението за грешка, което изтече

Двата error paths по-горе не са въпрос на стил. Дайте на всеки сървър инструмент, който се проваля така, както се проваля реална интеграция, и прочетете какво достига до модела.

tools/call on a tool that raisesTEXT
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 постави вътрешен адрес, порт, име на база данни и service account в context на модела. Python SDK не постави нищо от това там; traceback отиде в stderr и остана на сървъра.

Нито едното не е bug. И двете са решения, а решението на Python е записано в собствения му docstring: ToolError е „провал, който сте очаквали“ и съобщението му се връща „в content, за да го прочете моделът“; всичко друго „се третира като crash: моделът вижда само Error executing tool <name>, а сървърът записва traceback в ERROR“. Класът за crash случая казва останалото на глас — „нищо от оригинала не достига до клиента“.

И двете поведения са грешни в половината случаи. Глава 18 твърдеше, че validation error трябва да се върне като резултат от инструмент, който моделът може да прочете и коригира, защото това е редът с най-голям leverage в повечето интеграции; от страната на Python това изисква изрично да се хвърли ToolError, а голо ValueError изхвърля полезното изречение. Аргументът на Глава 30 върви в другата посока: всичко, което инструмент връща, попада в context, от който по-късен prompt injection може да се опита да го прочете обратно, а непроверен exception string е най-малко одитираният текст във вашата система.

Правилото, което оцелява и след двете: решете, за всеки инструмент, какво има право да каже един провал, и напишете този string сами. Никога не оставяйте default текста на exception да решава, на който и да е език.

Официалният 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 цитира нормативната версия — сървър „MUST NOT write anything to its stdout that is not a valid MCP message“.2

Добавете един ред към всеки сървър и прочетете raw stream:

raw stdout, first two linesTEXT
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 вместо терминал, получава block-buffered stream, така че stray редът се flush-ва когато buffer-ът реши — тук, при exit, след отговор, преди който е бил написан. Повредата не се появява там, където е bug-ът. Добавете flush=True, или библиотека, която flush-ва, и тя се мести.

После идва частта, която обяснява защо това стига до production. Подайте счупения сървър на три клиента:

TEXT
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 умира веднага. Официалният клиент и Inspector и двата свиват рамене — пропускат реда и продължават. Правило, което чупи само клиентите, които никой не използва, е правило, което стига до production непокътнато, затова си струва да го счупите нарочно тук, вместо в клиентски log.

CLI режимът на Inspector е половината, която се забравя: npx @modelcontextprotocol/inspector --cli <command> --method tools/list отпечатва каталог и излиза, което го прави scriptable по начин, по който browser UI не е.3

И двата SDK се инсталираха чисто, в собствени директории, без нищо споделено:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
latest protocol revision implemented2025-11-252026-07-28
transitive packages installed9428
installed size13.9 MiB44.3 MiB
files on disk3 3862 018
third-party packages loaded to serve stdio8 от 9418 от 28
bare interpreter start, median19.4 ms11.1 ms
spawn → tools/list answered, median of 25144.5 ms709.4 ms
tools/list catalogue, o200k_base tokens342480

Всеки ред изненадва в различна посока, затова сравнението си струва да се направи, вместо да се предполага.

TypeScript инсталира повече от три пъти повече пакети и по-малко от една трета от байтовете. 94 dependencies е npm екосистемата такава, каквато е — fast-deep-equal, es-errors, dunder-proto. Python има 28, по-малко и огромни: cryptography, pydantic-core и uvicorn са compiled artefacts. Ако инстинктът ви е, че dependency count е това, за което да се тревожите, този ред е контрапримерът.

Python interpreter стартира по-бързо от Node, и разликата не е малка — 11.1 ms срещу 19.4 ms при празна програма. Значи 565 ms в реда за cold-start не са езикът. Това е SDK, а редът с loaded-packages казва защо:

third-party modules loaded to answer one tools/list over stdioTEXT
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, …

Сървър, чийто единствен I/O е pipe, import-ва ASGI web server, HTTP client и TLS библиотека, преди да прочете първия си ред. TypeScript SDK също носи Express, Hono, jose и eventsource — те седят на диска непрочетени, защото границата на package-а ги държи извън server/stdio.js import. Python package е един import graph, така че import mcp е всичко: python -X importtime приписва 727 ms на import mcp.server.mcpserver — число, измерено под import profiler, затова излиза над 709 ms, които непрофилираното пускане взема от spawn до отговор — и 269 от тях на mcp.types subtree само — wire types са Pydantic модели, по един клас за всяко protocol message за всяка revision, а изграждането им е работа, свършена при import. Това е design trade, не немарливост — eager imports са причината Python SDK да може да ви даде run(transport="streamable-http") на следващия ред без втора инсталация.

И после последният ред от началния блок разваля аргумента. Package-нете TypeScript сървъра правилно — bin entry, shebang, npm link, нищо за изтегляне — и го стартирайте чрез npx с --no-install, както всъщност се стартира публикуван stdio сървър:

median of 25, spawn → tools/list answeredTEXT
node ./incidents.js       144.5 ms
npx incidents-mcp         712.6 ms      (+568.1 ms of launcher)
python incidents.py       709.4 ms

Launcher-ът струва 568 ms на старт — четири и половина пъти целия TypeScript SDK import — и се плаща при всяко стартиране, защото MCP host стартира stdio сървър, като изпълнява тази команда. Така че честната форма на „TypeScript стартира пет пъти по-бързо“ е: така е, докато не го разпространите по нормалния начин. Същата уговорка вероятно важи за uvx; на тази машина нямаше инсталиран uv, затова такъв ред не съществува. Нищо неизмерено не влиза в таблицата.

Глава 26 покри framing-а на stdio. Две неща останаха за тук.

Първото: стартирането на сървър с npx или uvx е stdio транспортът. Няма отделен „package mode“. Конфигурацията на host посочва команда и аргументи; host-ът я spawn-ва и говори през pipes. Затова „как да разпространя това“ и „кой транспорт говори“ са един въпрос локално, и затова цената на launcher-а принадлежи в глава за shipping.

Второто: stdio изобщо няма секция за authorization, и спецификацията го казва в един ред — implementations using stdio „SHOULD NOT follow this specification, and instead retrieve credentials from the environment“.4 Неговият security model е този на операционната система, и такъв е и лимитът му: локален subprocess обслужва точно една машина и един потребител.

Другият жив транспорт е Streamable HTTP: един endpoint, който приема POST, една HTTP заявка за всяко JSON-RPC message, и Accept header, който трябва да изброява и application/json, и text/event-stream, защото сървърът избира за всяка заявка с кое от двете да отговори.5 Глава 14 разбори този event stream на ръка, така че нищо в wire format не е ново — само това, което го обвива. Три задължения в текущата revision лесно се пропускат и и трите могат да се тестват:

Всеки POST носи MCP-Protocol-Version, и стойността му трябва да съвпада с protocolVersion вътре в собственото _meta на заявката. Несъответствие е 400 с header-mismatch error, не свиване на рамене.5

Mcp-Method отразява method при всяка заявка; Mcp-Name отразява params.name или params.uri при tools/call, resources/read и prompts/get. Те съществуват, за да може proxy да routing-ва без parsing на bodies.5

Старите форми ги няма, и отговарят с отказ

Връзка към раздела: Старите форми ги няма, и отговарят с отказ

GET stream, Mcp-Session-Id и Last-Event-ID resumption бяха премахнати. Сървър, който говори само тази revision, трябва да отговори 405 Method Not Allowed на GET или DELETE, да игнорира session header без да издава такъв, и да игнорира Last-Event-ID.5

Сега измерването, което преобръща цялата глава. Изпратете current-revision заявка до всеки сървър през HTTP.

POST /mcp, MCP-Protocol-Version: 2026-07-28TEXT
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 заявката от стъпката по-горе и Python сървърът отговаря 400 с error -32020 и съобщението „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, който се каните да инсталирате; това е един ред, и единственото твърдение в тази глава, което още ще има значение след година.

Преместете сървър извън лаптопа си и клиент на непознат се появява с token. Това е половината, която Глава 26 остави настрана, и половината, която multi-user продукт не може да пропусне.

Спецификацията поставя MCP сървъра в OAuth 2.1 роля и я назовава: защитен MCP сървър е resource server, клиентът е 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“ е anti-passthrough правилото, и затова съществува целият audience апарат. Сървър, който replay-ва bearer token, който му е подаден, към third-party API, е confused deputy: той заема собственото си доверие на този, който го е извикал. Правилото забранява повторната употреба, не само съхранението.

За да стане това enforceable, трябват четири RFC, по една задача за всяко.6 RFC 9728 е как клиентът изобщо намира authorization server: MCP сървърът сервира protected-resource-metadata документ и 401 сочи към него. RFC 8707 е параметърът resource — клиентът трябва да изпрати canonical URI на сървъра и в authorization request, и в token request, „regardless of whether authorization servers support it“, така че издаденият token да назове audience. RFC 9207 затваря цикъла от другата страна: клиентът записва issuer преди redirect и сравнява върнатия iss като точен string, без normalisation — без case folding, без default-port elision, без trailing slash. А RFC 7591, Dynamic Client Registration, вече е deprecated в полза на Client ID Metadata Documents, „retained for backwards compatibility with authorization servers that do not support“ them.4

Свържете това и на двата сървъра с token verifier, който не прави нищо освен да проверява audience. TypeScript стълбицата:

POST /mcp — TypeScript, with requireBearerAuthTEXT
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":[…]}}
GET /.well-known/oauth-protected-resource/mcpTEXT
{"resource":"http://127.0.0.1:8931/mcp",
 "authorization_servers":["https://auth.example.com/"],
 "scopes_supported":["incidents:read","incidents:write"],
 "resource_name":"Incidents"}

И двата SDK сервират този документ и и двата насочват 401 към него, което е цялата история на discovery: клиент, който никога не е виждал вашия сървър, научава къде да се authenticate от отказ. 403 е друго животно — token-ът е наред, scope не е — и challenge назовава какво липсва, за да може клиентът да step up, вместо да започва отначало.

Две стъпала се различават, и нито една разлика не е в спецификацията. TypeScript SDK отказва token без expiry claim; Python връща 200, защото expires_at е optional в неговия AccessToken и None означава „нямам мнение“. А Python 403 носи error_description="Required scope: incidents:read" без параметъра scope, който спецификацията казва, че сървърите трябва да включват. Verifier не е място да приемате library default: audience check е ваш да напишете на който и да е език, и expiry също.

Една честна дреболия от същото пускане. GET към endpoint отговори 404 при Express wiring и 400 Bad Request: Missing session ID при Python варианта, където спецификацията иска 405 Method Not Allowed и където „session ID“ е vocabulary, който тази revision е премахнала. Нито едното не е опасно; и двете са форма на екосистема по средата на migration.

Последното парче от shipping е къде публикувате, и то има отговор с число. Crawled днес, всеки сървър в официалния registry на последната си версия:7

servers
total (latest version, not deleted)28 170
active / deprecated27 853 / 317
ship at least one installable package13 065
remote only — URL, нищо за инсталиране14 696
npm8 275
PyPI3 603
OCI images867
mcpb bundles706
NuGet / Cargo107 / 43

Два прочита, сочещи в противоположни посоки. По публикувани сървъри npm води с 2.3 към 1 — числото, което хората цитират, когато казват, че екосистемата е TypeScript. По downloads води Python: през последните тридесет дни mcp взе 286.7 милиона срещу @modelcontextprotocol/sdk със 194.7 милиона, преди да добавим fastmcp със 72.1 милиона.7 И двата са Tier 1, нормативната schema е schema.ts, а официалният tutorial „Build an MCP server“ започва с Python tab.1 Която и половина да сте имали в главата си, другата половина също е вярна.

И редът, който има по-голямо значение от който и да е от двата: повече от половината registry — 14 696 от 28 170 — няма нищо за инсталиране. Това са web services. Transport tallies съвпадат от другата страна: от 14 290 package entries, 13 787 декларират stdio; от 16 640 remote entries, 15 570 декларират Streamable HTTP и 1 070 все още декларират deprecated HTTP+SSE. Така че „MCP сървър е 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/sdk1.30.0, публикуван 27 юли 2026 г.; 4 322 438 bytes unpacked, 693 files, 17 direct dependencies
latest revision it implements2025-11-25
mcp (PyPI)2.1.1, публикуван 25 август 2026 г.; 357 912-byte wheel, плюс mcp-types 2.1.1 при 69 656 bytes
latest revision it implements2026-07-28
SDK tiersTypeScript, Python, C#, Go, Rust at Tier 1; Java, Ruby at Tier 2; Swift, PHP, Kotlin at Tier 3
registry servers28 170
downloads, last 30 daysmcp 286 653 871 · fastmcp 72 097 269 · @modelcontextprotocol/sdk 194 679 333

Една migration бележка, която не е число. В mcp 2.x, FastMCP беше преименуван на MCPServer, а почти всеки tutorial онлайн все още започва със стария import. SDK доставя module, чиято единствена цел е да обясни това, което е най-внимателната deprecation в тази глава:

from mcp.server.fastmcp import FastMCPTEXT
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.

С таблицата пред вас препоръката е скучна, което е добър знак.

Ако сървърът живее вътре в web application, което вече управлявате, напишете го на TypeScript. Същият process, същият deploy, същият request handler; Streamable HTTP е endpoint, който добавяте до останалите; а 13.9 MiB и 145 ms са безплатни, защото runtime вече е бил вдигнат. Това са повечето от 14 696 remote сървъра.

Ако сървърът обвива data tooling, напишете го на Python. Това, което exposing-вате, е pandas, warehouse client, notebook с transforms, а сървър на друг език би бил subprocess call, носещ schema. Седемстотин милисекунди import в service, който стартира веднъж, не са cost; в subprocess, който host relaunch-ва цял ден, са.

И засега revision редът надделява над двете. Ако ви трябва 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — един от двата SDK го има днес, а другият не.

Вече можете да ship-нете същия сървър на който и да е от двата езика, да защитите избора с таблица вместо с предпочитание, да го пуснете през двата живи транспорта, и да му подадете token, който той ще откаже.

Това, което построихте, все още е function: schema, endpoint, детерминистично нещо, което моделът извиква. Цял клас знание не се побира в тази форма — как ние пишем postmortem, какви полета трябват на нашите incident reports, редът, в който правим нещата, и защо. Това е procedure, това е prose, и насилването му в описание на инструмент е начинът system prompts да пораснат до две хиляди token-а, платени на всеки single turn, независимо дали conversation е за инциденти.

Глава 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, всеки инсталиран в собствена throwaway директория. Timings са medians от 25 launches, wall clock от spawn до реда, носещ tools/list response; token counts са o200k_base чрез tiktoken върху JSON на всяка definition. Не беше извикан paid API: нищо тук не се нуждае от модел.

Двата сървъра са 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 и в types.js на TypeScript SDK, и двете прочетени от инсталираните packages, а не от changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, и Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, и двете прочетени на 7 септември 2026 г. Източник на tier таблицата, на изречението „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 правилото, цитирано за print() и stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Източник на newline framing и правилото за stdout purity. Глава 26 чете тази страница изцяло; тук е цитирана за реда, който счупеният сървър нарушава.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, прочетено на 7 септември 2026 г. Един package, три клиента зад един binary — web, --cli и --tui — sharing one core, one set of transports and one OAuth state on disk. CLI произведе catalogue traces тук.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, прочетено на 7 септември 2026 г. Източник на resource-server ролята; четирите token-handling клаузи, цитирани изцяло; изискването сървърите да имплементират RFC 9728 и клиентите да го използват за discovery; правилата за параметъра resource и дефиницията на canonical-URI; issuer-validation таблицата; deprecation на Dynamic Client Registration; таблицата 401/403/400 и insufficient_scope challenge; и stdio exemption, „Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.“ 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, и Transports overview, .../basic/transports. Източник на single-endpoint POST правилото, dual Accept requirement, MCP-Protocol-Version header и неговото правило must-match-the-body, headers Mcp-Method и Mcp-Name, описани като „REQUIRED for compliance“, премахването на GET stream, sessions и Last-Event-ID, 405 guidance, задължителната Origin validation, и класификацията на 2024-11-05 HTTP+SSE transport като Deprecated under SEP-2596. 2 3 4

  6. Четирите, на които спецификацията разчита, с draft-а, който профилира: 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, който той bind-ва. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — документът, към който сочи 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 за тази употреба. И Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, за формата на WWW-Authenticate challenge по-горе.

  7. Official MCP registry, registry.modelcontextprotocol.io/v0/servers, crawled на 7 септември 2026 г. с version=latest: 282 pages, 28 170 servers, tallied by registryType over distinct server names. Download figures: api.npmjs.org/downloads/point/last-month for @modelcontextprotocol/sdk (194 679 333 за 8 август – 6 септември 2026 г.) и pypistats.org/api/packages/<name>/recent for mcp and fastmcp, и двете прочетени същия ден. Package sizes идват от npm registry document и PyPI JSON API. 2

  8. 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)“.


Създадено от

David Vicente Campos

Основател на NeuraLIA Labs и съосновател на MyRealFood

Компютърен инженер съм, завършил Университета в Леон. Съосновах MyRealFood, където като CTO създадох приложението, което милиони хора са използвали, за да се хранят по-здравословно, и основах NeuraLIA Labs, където изграждам AI продукти. Тук пиша за това, което трябваше да разбера по пътя, така, както ми се иска някой да ми го беше обяснил.

Още за автора

Публикувано от NeuraLIA Labs.

Получавайте нови публикации във входящата си поща

Новини за AI, ръководства и продуктови обновления — кратък имейл, когато публикуваме нещо, което си заслужава.

Индекс на курса

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev12 мин четене

AI моделът Jev е създаден за решения, не за проза

Jev на TypeSafe AI привлича внимание, защото разглежда софтуерната интелигентност като проблем на вероятностите: изберете правилния клон, добавете увереност и не плащайте на LLM да пише текст, когато кодът има нужда от решение.

Abstract legal research workspace with documents, search nodes and governance controls.
openai11 мин четене

Astra for Law на OpenAI е правна AI система, не нов модел

Правният старт на OpenAI е не толкова за нов базов модел, колкото за системата около него: домейн извличане, надеждни инструменти, права, бенчмаркове и пътища за преглед.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 мин четене

Инженеринг на контекста за AI агенти с дълъг хоризонт

Дълго работещите агенти не се провалят само защото прозорецът е малък. Те се провалят, когато файлове, изходи от инструменти и остаряла история изтласкат задачата, която агентът е трябвало да завърши.

Готови ли сте LIA да избира вместо вас?

Създавайте с всички AI модели на едно място — започнете безплатно още днес.