Wydaj MCP server: TypeScript i Python w liczbach
Ten sam server napisany dwa razy: trzy tools, resource i prompt. 94 pakiety kontra 28, cold start 145 ms kontra 709 ms.
Na tej stronie
Oto cały spór o język, zmierzony, zanim padnie choć jedno słowo.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msPierwsze dwa wiersze to porównanie, którego wszyscy chcą. Trzeci wiersz to ten sam TypeScript server z pierwszego wiersza, uruchomiony tak, jak faktycznie byłby dystrybuowany — i ląduje trzy milisekundy od Pythona.
Chapter 26 czytał Model Context Protocol względem jego własnej specyfikacji, używając surowego JSON-RPC, bo surowy JSON-RPC nie ma języka. Ten rozdział ma dwa, a ciężar argumentu spada tutaj: ten sam server, napisany dwa razy. Trzy tools, jeden resource, jeden prompt, oba SDK, bez skrótów po żadnej stronie. Potem transports, inspector, 401 i liczby, których nikt nie opublikował.
Server i dlaczego ma w sobie te pięć rzeczy
Link do sekcji: Server i dlaczego ma w sobie te pięć rzeczyDziennik incydentów. Trzy tools, bo podział z Chapter 18 na odczyty i zapisy musi być widoczny: search_incidents czyta, open_incident zapisuje i zwraca uchwyt, resolve_incident bierze ten uchwyt i zamyka. Jeden resource, incidents://open, bo odczyt bieżącej listy jest czymś, co dołącza aplikacja. Jeden prompt, postmortem, bo „opisz to” to slash command osoby. To hierarchia sterowania z Chapter 26 — model, aplikacja, człowiek — zamieniona w pięć rejestracji.
Uchwyt ma większe znaczenie, niż się wydaje. Chapter 26 zepsuł zabawkowy kalendarz, trzymając jego stan w tablicy na poziomie modułu: protocol nie ma sesji, więc tool tworzący zwraca nieprzezroczysty identyfikator, a każde późniejsze wywołanie przyjmuje go jako zwykły argument. Nic w żadnym z plików nie zakłada, że wywołujący jest procesem, który go otworzył.
Oto ten sam tool w obu językach, zarejestrowany obok siebie:
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.")Najpierw przeczytaj to, co jest takie samo, bo to jest wniosek. Oba deklarują nazwę, opis, dwa opisane argumenty typu string i trzy adnotacje; oba są jedną funkcją; żaden nie wspomina JSON-RPC, framing, stdout ani wersji protocol. Dwa SDK zbiegły się do tego samego kształtu, czyli właśnie to, co ma znaczyć „Tier 1”.1
Dwie różnice są realne i obie wrócą później. TypeScript opisuje argumenty biblioteką schematów — tutaj Zod — a schema jest wartością, którą piszesz. Python opisuje je własnymi type hints funkcji i czyta je podczas importu, dlatego wie o funkcji rzeczy, których plik TypeScript nigdy mu nie powiedział. I ścieżka błędu: TypeScript zwraca wynik tool z isError, Python rzuca wyjątek. Zapamiętaj to.
Pozostałe cztery rejestracje nie różnią się strukturalnie niczym. Resource to server.registerResource("open-incidents", "incidents://open", …) kontra @server.resource("incidents://open", …); prompt to registerPrompt kontra @server.prompt. Ostatni wiersz każdego pliku to transport: await server.connect(new StdioServerTransport()) kontra server.run().
Całe pliki: 81 niepustych wierszy i 3060 bajtów TypeScript kontra 63 i 2555. Weź to z należną szczyptą soli — liczba wierszy mierzy formatter równie mocno jak język, dlatego żadna z tych liczb nie trafia do tabeli nagłówkowej poniżej.
Jeden client, oba servers
Link do sekcji: Jeden client, oba serversDowodem na niewidoczność języka jest jeden client uruchomiony dwa razy, w jedenastu wierszach:
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));Skieruj go kolejno na każdy server. Prawdziwy output, przycięty:
$ 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}"}]Te same tools, ta sama kolejność, ten sam uchwyt. TypeScript client nie potrafi powiedzieć, w czym napisano server, i nigdy o to nie pyta. To cała obietnica protocol — i ona działa.
Teraz spójrz na whitespace w drugim wyniku, bo to nie jest kosmetyka: Python SDK serializuje payloads z pydantic_core.to_json(result, fallback=str, indent=2). Przy odczycie resource z dwoma incydentami na liście ciało TypeScript ma 136 znaków i 37 o200k_base token; ciało Pythona ma 185 i 62. Sześćdziesiąt osiem procent więcej token dla identycznych wierszy, płacone przez tego, kto wczytuje resource do prompt, za każdym razem.
Catalogue opowiada tę samą historię z większą przyczyną. Oba servers, te same trzy tools, tools/list zważone klucz po kluczu:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Pythonowe schematy input są tańsze — mostek Zod w TypeScript stempluje $schema i additionalProperties na każdym z nich. Cała luka 138 token to schema output, której nikt nie napisał. resolve_incident ma adnotację -> Incident, więc SDK wyprowadziło JSON Schema dla typu zwrotnego i ją wysłało. To naprawdę użyteczne — dzięki temu client może walidować structuredContent — i jest to 187 token twojego context window, które przybywa z powodu type hint. Reguła z Chapter 24 o definicjach wypychających materiał, który ma znaczenie, dotyczy też schemas, o których nie wiedziałeś, że je masz.
Zepsuj to celowo: komunikat błędu, który wyciekł
Link do sekcji: Zepsuj to celowo: komunikat błędu, który wyciekłDwie powyższe ścieżki błędów nie są wyborem stylistycznym. Daj każdemu server tool, który zawodzi tak, jak zawodzi prawdziwa integracja, i przeczytaj, co dociera do model.
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 włożył do context model wewnętrzny adres, port, nazwę bazy danych i konto serwisowe. Python SDK nie włożył tam niczego; traceback trafił do stderr i został na server.
Żadne z nich nie jest bugiem. Oba są decyzjami, a decyzja Pythona jest zapisana w jego własnym docstringu: ToolError to „awaria, której się spodziewałeś”, a jej komunikat jest zwracany „w content, żeby model mógł go przeczytać”; wszystko inne „jest traktowane jak crash: model widzi tylko Error executing tool <name>, a server loguje traceback w ERROR”. Klasa dla przypadku crash mówi resztę wprost — „nic z oryginału nie dociera do client”.
Oba zachowania są błędne przez połowę czasu. Chapter 18 argumentował, że błąd walidacji powinien wracać jako wynik tool, który model może przeczytać i poprawić, bo to najcenniejsza linia w większości integracji; po stronie Pythona wymaga to jawnego rzucenia ToolError, a gołe ValueError wyrzuca użyteczne zdanie. Argument z Chapter 30 idzie w drugą stronę: wszystko, co zwraca tool, ląduje w context, z którego późniejszy prompt injection może próbować to odczytać, a nieprzejrzany string wyjątku jest najmniej audytowanym tekstem w twoim systemie.
Reguła, która przetrwa jedno i drugie: zdecyduj dla każdego tool, co wolno powiedzieć awarii, i napisz ten string samodzielnie. Nigdy nie pozwalaj, by domyślny tekst wyjątku decydował, w żadnym języku.
Zepsuj to celowo: jedna linia na standard output
Link do sekcji: Zepsuj to celowo: jedna linia na standard outputOficjalny tutorial podaje regułę bez zastrzeżeń: „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 Chapter 26 cytował wersję normatywną — server „MUST NOT write anything to its stdout that is not a valid MCP message”.2
Dodaj jedną linię do każdego server i przeczytaj 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 startingPythonowy przypadek jest gorszy, a powodem nie jest MCP. Proces, którego stdout jest pipe, a nie terminalem, dostaje stream buforowany blokowo, więc zbłąkana linia zostaje flush wtedy, kiedy bufor tak zdecyduje — tutaj przy wyjściu, po odpowiedzi, choć została zapisana wcześniej. Uszkodzenie nie pojawia się tam, gdzie jest bug. Dodaj flush=True albo bibliotekę, która flushuje, i ono się przesunie.
Potem część, która tłumaczy, dlaczego to trafia do produkcji. Podaj zepsuty server trzem 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 warningParser z siedmiu wierszy ginie natychmiast. Oficjalny client i Inspector wzruszają ramionami — pomijają linię i działają dalej. Reguła, która psuje tylko clients, których nikt nie używa, dociera na produkcję nietknięta, dlatego warto zepsuć ją celowo tutaj, a nie w logu klienta.
Tryb CLI Inspectora to połowa, o której się zapomina: npx @modelcontextprotocol/inspector --cli <command> --method tools/list drukuje catalogue i kończy pracę, co czyni go skryptowalnym w sposób, w jaki UI w przeglądarce nie jest.3
Tabela
Link do sekcji: TabelaOba SDK zainstalowały się czysto, każde do własnego katalogu, bez współdzielenia:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| najnowsza zaimplementowana rewizja protocol | 2025-11-25 | 2026-07-28 |
| zainstalowane pakiety przechodnie | 94 | 28 |
| rozmiar instalacji | 13,9 MiB | 44,3 MiB |
| pliki na dysku | 3386 | 2018 |
| pakiety third-party załadowane do obsługi stdio | 8 z 94 | 18 z 28 |
| start gołego interpretera, mediana | 19,4 ms | 11,1 ms |
spawn → tools/list answered, mediana z 25 | 144,5 ms | 709,4 ms |
tools/list catalogue, o200k_base token | 342 | 480 |
Każdy wiersz zaskakuje w inną stronę, dlatego warto uruchomić porównanie, zamiast zakładać wynik.
TypeScript instaluje ponad trzy razy więcej pakietów i mniej niż jedną trzecią bajtów. 94 dependencies to ekosystem npm będący sobą — fast-deep-equal, es-errors, dunder-proto. 28 pakietów Pythona jest mniej i są ogromne: cryptography, pydantic-core i uvicorn to skompilowane artefakty. Jeśli instynkt podpowiada ci, że to liczba dependencies jest powodem do niepokoju, ten wiersz jest kontrprzykładem.
Interpreter Pythona startuje szybciej niż Node i to nie jest blisko — 11,1 ms kontra 19,4 ms dla pustego programu. Więc 565 ms w wierszu cold start nie jest językiem. To SDK, a wiersz załadowanych pakietów mówi dlaczego:
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, którego jedynym I/O jest pipe, importuje ASGI web server, HTTP client i bibliotekę TLS, zanim przeczyta pierwszą linię. TypeScript SDK też wysyła Express, Hono, jose i eventsource — siedzą na dysku nieczytane, bo granica package trzyma je poza importem server/stdio.js. Pythonowy package jest jednym grafem importów, więc import mcp jest wszystkim: python -X importtime przypisuje 727 ms do import mcp.server.mcpserver — liczba mierzona pod profilerem importów, dlatego wychodzi wyżej niż 709 ms, które nieprofilowany run bierze od spawn do odpowiedzi — i 269 z nich do samego poddrzewa mcp.types. Typy wire są modelami Pydantic, jedna klasa na komunikat protocol na rewizję, a ich budowanie to praca wykonana podczas importu. To kompromis projektowy, nie niedbalstwo — eager imports są powodem, dla którego Python SDK może podać ci run(transport="streamable-http") w następnej linii bez drugiej instalacji.
A potem ostatni wiersz bloku otwierającego unieważnia argument. Spakuj TypeScript server poprawnie — wpis bin, shebang, npm link, nic do pobrania — i uruchom go przez npx z --no-install, czyli tak, jak opublikowany stdio server faktycznie startuje:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLauncher kosztuje 568 ms na start — cztery i pół raza więcej niż cały import TypeScript SDK — i płacisz to przy każdym uruchomieniu, bo host MCP startuje stdio server, wykonując tę komendę. Uczciwa forma zdania „TypeScript startuje pięć razy szybciej” brzmi więc: tak, dopóki nie dystrybuujesz go normalnie. Ten sam caveat przypuszczalnie dotyczy uvx; na tej maszynie nie było zainstalowanego uv, więc taki wiersz nie istnieje. Nic niezmierzonego nie trafia do tabeli.
Dwa transports i tylko dwa
Link do sekcji: Dwa transports i tylko dwaChapter 26 omówił framing stdio. Dwie rzeczy zostawił na tutaj.
Pierwsza: uruchamianie server z npx albo uvx jest transportem stdio. Nie ma osobnego „trybu package”. Konfiguracja hosta nazywa komendę i argumenty; host ją spawnuje i rozmawia przez pipes. Dlatego „jak to dystrybuować” i „którym transportem mówi” są lokalnie jednym pytaniem, a koszt launchera należy do rozdziału o shipping.
Druga: stdio nie ma w ogóle sekcji authorization, a specyfikacja mówi to w jednym wierszu — implementacje używające stdio „SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Jego model bezpieczeństwa jest modelem systemu operacyjnego, tak samo jak jego limit: lokalny subprocess obsługuje dokładnie jedną maszynę i jednego użytkownika.
Drugi żywy transport to Streamable HTTP: pojedynczy endpoint przyjmujący POST, jedno żądanie HTTP na komunikat JSON-RPC i nagłówek Accept, który musi wymieniać zarówno application/json, jak i text/event-stream, bo server wybiera per żądanie, którym z dwóch odpowie.5 Chapter 14 parsował ten event stream ręcznie, więc w wire format nie ma nic nowego — tylko to, co go opakowuje. Trzy obowiązki bieżącej rewizji łatwo przeoczyć i wszystkie trzy da się przetestować:
Nagłówek wersji musi zgadzać się z body
Link do sekcji: Nagłówek wersji musi zgadzać się z bodyKażdy POST niesie MCP-Protocol-Version, a jego wartość musi pasować do protocolVersion wewnątrz własnego _meta żądania. Niezgodność to 400 z błędem header-mismatch, nie wzruszenie ramion.5
Do zgodności wymagane są jeszcze dwa nagłówki
Link do sekcji: Do zgodności wymagane są jeszcze dwa nagłówkiMcp-Method odzwierciedla metodę przy każdym żądaniu; Mcp-Name odzwierciedla params.name albo params.uri na tools/call, resources/read i prompts/get. Istnieją po to, żeby proxy mogło routować bez parsowania bodies.5
Stare kształty zniknęły i odpowiadają odmową
Link do sekcji: Stare kształty zniknęły i odpowiadają odmowąGET stream, Mcp-Session-Id i wznawianie Last-Event-ID zostały usunięte. Server mówiący tylko tą rewizją powinien odpowiedzieć 405 Method Not Allowed na GET albo DELETE, zignorować nagłówek sesji bez wybijania nowej i zignorować Last-Event-ID.5
Teraz pomiar, który przestawia ramę całego rozdziału. Wyślij żądanie bieżącej rewizji do każdego server przez 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)"}}Stałe zgadzają się z zachowaniem: LATEST_PROTOCOL_VERSION w Python SDK czyta 2026-07-28, a w TypeScript SDK czyta 2025-11-25. Wyślij żądanie header-mismatch z kroku powyżej, a Python server odpowiada 400 z błędem -32020 i komunikatem „mcp-protocol-version header does not match the request envelope's protocol version”; TypeScript SDK nie ma takiego kodu, bo nie implementuje rewizji, która go definiuje.
Strona, która wymienia oba jako Tier 1, mówi też „Each SDK provides the same functionality”.1 W dniu podanym niżej, dla bieżącej rewizji, to zdanie jest aspiracyjne. Sprawdź LATEST_PROTOCOL_VERSION w SDK, które zamierzasz zainstalować; to jedna linia i jedyne twierdzenie z tego rozdziału, które za rok nadal będzie miało znaczenie.
401 i zdanie do zacytowania
Link do sekcji: 401 i zdanie do zacytowaniaPrzenieś server poza laptop, a client obcej osoby pojawia się z token. To połowa, którą Chapter 26 zostawił w spokoju, i połowa, której produkt multi-user nie może pominąć.
Specyfikacja wkłada MCP server w rolę OAuth 2.1 i nazywa ją: chroniony MCP server jest resource server, client jest OAuth client, a authorization server to czyjś inny problem.4 Z tej roli wynikają cztery obowiązkowe klauzule, zacytowane w całości, bo parafrazowanie ich prowadzi do błędu:
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” to reguła anty-passthrough i powód, dla którego istnieje cały mechanizm audience. Server, który odtwarza bearer token przekazany mu przy third-party API, jest confused deputy: użycza własnego zaufania temu, kto go wywołał. Reguła zabrania ponownego użycia, nie tylko przechowywania.
Żeby dało się to egzekwować, potrzeba czterech RFC, po jednej robocie dla każdego.6 RFC 9728 mówi, jak client w ogóle znajduje authorization server: MCP server serwuje dokument protected-resource-metadata, a 401 na niego wskazuje. RFC 8707 to parametr resource — client musi wysłać kanoniczny URI server w obu żądaniach, authorization request i token request, „regardless of whether authorization servers support it”, żeby wydany token nazywał swoje audience. RFC 9207 zamyka pętlę z drugiej strony: client zapisuje issuer przed redirect i porównuje zwrócony iss dokładnym stringiem, bez normalizacji — bez zmiany wielkości liter, bez usuwania portu domyślnego, bez trailing slash. A RFC 7591, Dynamic Client Registration, jest teraz deprecated na rzecz Client ID Metadata Documents, „retained for backwards compatibility with authorization servers that do not support” ich.4
Podłącz to na obu servers z verifierem token, który nie robi nic poza sprawdzeniem audience. Drabinka 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"}Oba SDK serwują ten dokument i oba kierują do niego 401, co jest całą historią discovery: client, który nigdy nie widział twojego server, dowiaduje się, gdzie się uwierzytelnić, z odmowy. 403 to inny zwierz — token jest w porządku, scope nie — a challenge nazywa, czego brakuje, żeby client mógł wejść poziom wyżej zamiast zaczynać od nowa.
Dwa szczeble się różnią i żadna różnica nie jest w specyfikacji. TypeScript SDK odrzuca token bez claim wygaśnięcia; Python zwraca 200, bo expires_at jest opcjonalne na jego AccessToken, a None znaczy „brak opinii”. A pythonowy 403 niesie error_description="Required scope: incidents:read" bez parametru scope, który według specyfikacji servers powinny zawierać. Verifier to nie miejsce na akceptowanie domyślnej wartości biblioteki: audience check należy do ciebie w obu językach, expiry też.
Jedna uczciwa drobnostka z tego samego runu. GET na endpoint odpowiedział 404 w okablowaniu Express i 400 Bad Request: Missing session ID w pythonowym, podczas gdy specyfikacja prosi o 405 Method Not Allowed, a „session ID” to słownictwo usunięte w tej rewizji. Żadne nie jest groźne; oba pokazują kształt ekosystemu w trakcie migracji.
Gdzie servers naprawdę żyją
Link do sekcji: Gdzie servers naprawdę żyjąOstatnim elementem shipping jest miejsce publikacji i ma ono odpowiedź z liczbą. Zindeksowane dziś: każdy server w oficjalnym registry w najnowszej wersji:7
| servers | |
|---|---|
| łącznie (najnowsza wersja, nieusunięte) | 28170 |
| active / deprecated | 27853 / 317 |
| wysyłają co najmniej jeden instalowalny package | 13065 |
| tylko remote — URL, nic do instalowania | 14696 |
| npm | 8275 |
| PyPI | 3603 |
| OCI images | 867 |
bundlowe mcpb | 706 |
| NuGet / Cargo | 107 / 43 |
Dwa odczyty, wskazujące przeciwne kierunki. Według opublikowanych servers npm prowadzi 2,3 do 1 — to liczba, którą cytują ludzie mówiący, że ekosystem jest TypeScriptowy. Według downloads prowadzi Python: w ostatnich trzydziestu dniach mcp wziął 286,7 miliona wobec @modelcontextprotocol/sdk na poziomie 194,7 miliona, zanim doliczyć fastmcp z 72,1 miliona.7 Oba są Tier 1, normatywna schema to schema.ts, a oficjalny tutorial „Build an MCP server” otwiera się na zakładce Python.1 Niezależnie od tego, którą połowę miałeś w głowie, druga też jest prawdziwa.
I wiersz ważniejszy niż oba: ponad połowa registry — 14696 z 28170 — nie ma nic do instalowania. To web services. Zliczenia transportów zgadzają się z drugiej strony: z 14290 entries package 13787 deklaruje stdio; z 16640 entries remote 15570 deklaruje Streamable HTTP, a 1070 nadal deklaruje deprecated HTTP+SSE. Więc „MCP server to subprocess na twoim laptopie” opisuje kurczącą się mniejszość, a każdy z 14696 potrzebuje sekcji powyżej zamiast zmiennej środowiskowej.
Pokaż szczegóły
Celowo dwujęzycznie i precedens dla tego.
To jedyny dwujęzyczny rozdział kursu, bo uczciwa odpowiedź się rozdziela: registry jest npm-first, a downloads są Python-first, jednocześnie, dzisiaj. Napisanie tylko jednego z dwóch oddałoby połowę pytania i przy okazji błędnie opisało ekosystem. Jest precedens na otwartym terenie — Hugging Face MCP Course wymienia w prerekwizytach „Experience with at least one programming language (Python or TypeScript examples will be shown)” i uczy obu.8 Protocol, którego cała wartość polega na liczbie implementacji, to złe miejsce na jednojęzyczność.
Sekcja z datą: wszystko powyżej, co ma termin przydatności
Link do sekcji: Sekcja z datą: wszystko powyżej, co ma termin przydatnościPrzeczytane i zmierzone 7 września 2026, względem rewizji protocol 2026-07-28.
| value | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, opublikowane 27 lipca 2026; 4322438 bajtów po rozpakowaniu, 693 pliki, 17 bezpośrednich dependencies |
| najnowsza rewizja, którą implementuje | 2025-11-25 |
mcp (PyPI) | 2.1.1, opublikowane 25 sierpnia 2026; wheel 357912 bajtów, plus mcp-types 2.1.1 o rozmiarze 69656 bajtów |
| najnowsza rewizja, którą implementuje | 2026-07-28 |
| poziomy SDK | TypeScript, Python, C#, Go, Rust na Tier 1; Java, Ruby na Tier 2; Swift, PHP, Kotlin na Tier 3 |
| servers w registry | 28170 |
| downloads, ostatnie 30 dni | mcp 286653871 · fastmcp 72097269 · @modelcontextprotocol/sdk 194679333 |
Jedna notatka migracyjna, która nie jest liczbą. W mcp 2.x FastMCP zmieniło nazwę na MCPServer, a niemal każdy tutorial online nadal otwiera się starym importem. SDK wysyła moduł, którego jedynym celem jest to wyjaśnić, co jest najbardziej taktownym deprecation w tym rozdziale:
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.Więc który wybrać
Link do sekcji: Więc który wybraćZ tabelą przed sobą rekomendacja jest nudna, a to dobry znak.
Jeśli server żyje wewnątrz web application, którą już uruchamiasz, napisz go w TypeScript. Ten sam proces, ten sam deploy, ten sam request handler; Streamable HTTP to endpoint, który dodajesz obok innych; a 13,9 MiB i 145 ms są darmowe, bo runtime już działał. To większość z 14696 remote servers.
Jeśli server opakowuje data tooling, napisz go w Pythonie. To, co wystawiasz, to pandas, client warehouse, zestaw transformacji wart notebooka, a server w innym języku byłby wywołaniem subprocess przebranym za schema. Siedemset milisekund importu w usłudze, która startuje raz, nie jest kosztem; w subprocess, który host restartuje cały dzień, jest.
A na razie wiersz rewizji przebija oba. Jeśli potrzebujesz 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — jedno z dwóch SDK ma to dziś, a drugie nie.
Dokąd to idzie dalej
Link do sekcji: Dokąd to idzie dalejMożesz teraz wysłać ten sam server w dowolnym z dwóch języków, bronić wyboru tabelą zamiast preferencją, uruchomić go przez oba żywe transports i wręczyć mu token, który odrzuci.
To, co zbudowałeś, nadal jest funkcją: schema, endpoint, deterministyczna rzecz, którą model wywołuje. Cała klasa wiedzy nie mieści się w tym kształcie — jak my piszemy postmortem, jakich pól potrzebują nasze raporty incydentów, w jakiej kolejności robimy rzeczy i dlaczego. To procedura, to proza, a wciskanie jej do opisu tool to sposób, w jaki system prompts rosną do dwóch tysięcy token płaconych przy każdej pojedynczej turze, niezależnie od tego, czy rozmowa dotyczy incydentów.
Chapter 28 to druga odpowiedź: folder z SKILL.md w środku, który model czyta zamiast wywoływać, ładowany w trzech poziomach tak, żeby materiał referencyjny kosztował prawie nic aż do tury, w której jest potrzebny. Nie ma głównego języka i to jest pierwsza rzecz, której uczy.
Źródła i metoda
Link do sekcji: Źródła i metodaWszystko tutaj zmierzono 7 września 2026, na Node 22.22.3 i Python 3.14.4, względem @modelcontextprotocol/sdk 1.30.0 z zod 3.25.76 oraz mcp 2.1.1, każde zainstalowane do własnego jednorazowego katalogu. Timings to mediany z 25 uruchomień, wall clock od spawn do linii niosącej odpowiedź tools/list; liczby token to o200k_base przez tiktoken na JSON każdej definicji. Nie wywołano żadnego płatnego API: nic tutaj nie potrzebuje model.
Dwa servers mają 81 i 63 niepuste wiersze; jeden z ich trzech tools odtworzono powyżej w obu językach, a pozostałe cztery rejestracje różnią się tylko tak, jak opisano. Polityka ujawniania błędów Python SDK jest cytowana z docstrings ToolError i UnexpectedToolError w mcp/server/mcpserver/exceptions.py; domyślne pretty-printing to pydantic_core.to_json(result, fallback=str, indent=2) w mcp/server/mcpserver/resources/types.py i utilities/func_metadata.py. Stałe protocol-version to LATEST_PROTOCOL_VERSION w mcp_types/version.py i w types.js TypeScript SDK, oba odczytane z zainstalowanych pakietów, a nie z changeloga.
Przypisy
Link do sekcji: Przypisy-
SDKs,
modelcontextprotocol.io/docs/sdk, oraz Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, oba przeczytane 7 września 2026. Źródło tabeli tiers, zdania „Each SDK provides the same functionality but follows the idioms and best practices of its language”, kolejności zakładek językowych tutoriala (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) oraz reguły logowania cytowanej przyprint()istdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Źródło newline framing i reguły czystościstdout. Chapter 26 czyta tę stronę w całości; tutaj jest cytowana dla linii, którą łamie zepsuty server. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, przeczytane 7 września 2026. Jeden package, trzy clients za jednym binary — web,--clii--tui— współdzielące jeden core, jeden zestaw transports i jeden stan OAuth na dysku. CLI wyprodukowało tutaj ślady catalogue. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, przeczytane 7 września 2026. Źródło roli resource-server; czterech klauzul obsługi token zacytowanych w całości; wymagania, by servers implementowały RFC 9728, a clients używały go do discovery; reguł parametruresourcei definicji canonical-URI; tabeli issuer-validation; deprecation Dynamic Client Registration; tabeli401/403/400i challengeinsufficient_scope; oraz wyjątku stdio: „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, oraz Transports overview,.../basic/transports. Źródło reguły POST na pojedynczy endpoint, podwójnego wymaganiaAccept, nagłówkaMCP-Protocol-Versioni jego reguły must-match-the-body, nagłówkówMcp-MethodiMcp-Nameopisanych jako „REQUIRED for compliance”, usunięcia GET stream, sesji iLast-Event-ID, wskazówki405, obowiązkowej walidacjiOriginoraz klasyfikacji transportu HTTP+SSE z 2024-11-05 jako Deprecated według SEP-2596. ↩ ↩2 ↩3 ↩4 -
Cztery, na których opiera się specyfikacja, wraz z draftem, który profiluje: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. i Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, luty 2020 — parametrresourcei audience, które wiąże. Jones, M.B., Hunt, P. i Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, kwiecień 2025 — dokument, na który wskazuje401. Meyer zu Selhausen, K. i Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, marzec 2022 — parametrissi porównanie exact-string. Richer, J. (red.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, lipiec 2015, deprecated dla tego użycia. Oraz Jones, M. i Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, październik 2012, sekcja 3, dla kształtu challengeWWW-Authenticatepowyżej. ↩ -
Oficjalne MCP registry,
registry.modelcontextprotocol.io/v0/servers, zindeksowane 7 września 2026 za pomocąversion=latest: 282 strony, 28170 servers, zliczone przezregistryTypepo distinct server names. Dane downloads:api.npmjs.org/downloads/point/last-monthdla@modelcontextprotocol/sdk(194679333 za 8 sierpnia – 6 września 2026) ipypistats.org/api/packages/<name>/recentdlamcporazfastmcp, przeczytane tego samego dnia. Rozmiary package pochodzą z dokumentu npm registry i PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, przeczytane 7 września 2026: wśród prerekwizytów „Experience with at least one programming language (Python or TypeScript examples will be shown)”. ↩