Sari la conținut
27/30Capitolul 27 din 30

Livrează un MCP server: TypeScript și Python, măsurate

Același server scris de două ori — trei instrumente, o resursă, un prompt — apoi cântărit. 94 pachete vs 28, cold start 145 ms vs 709.

Pe această pagină

Iată întregul argument despre limbaj, măsurat, înainte să fie rostit vreun cuvânt.

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

Primele două rânduri sunt comparația pe care o vrea toată lumea. Al treilea rând este același server TypeScript din primul rând, lansat așa cum ar fi distribuit de fapt — și ajunge la trei milisecunde de Python.

Capitolul 26 a citit Model Context Protocol în raport cu propria specificație, folosind JSON-RPC brut, pentru că JSON-RPC brut nu are limbaj. Acest capitol are două, iar greutatea argumentului cade aici: același server, scris de două ori. Trei instrumente, o resursă, un prompt, ambele SDK-uri, fără scurtături de nicio parte. Apoi transporturile, inspectorul, 401-ul și cifrele pe care nu le-a publicat nimeni.

Serverul și de ce are aceste cinci lucruri în el

Link către secțiunea: Serverul și de ce are aceste cinci lucruri în el

Un jurnal de incidente. Trei instrumente, pentru că separarea din Capitolul 18 între citiri și scrieri trebuie să fie vizibilă: search_incidents citește, open_incident scrie și returnează un handle, resolve_incident ia acel handle și închide. O resursă, incidents://open, pentru că citirea listei curente este ceva ce atașează aplicația. Un prompt, postmortem, pentru că „scrie asta” este comanda slash a unei persoane. Aceasta este ierarhia de control din Capitolul 26 — model, aplicație, persoană — transformată în cinci înregistrări.

Handle-ul contează mai mult decât pare. Capitolul 26 a stricat un calendar de jucărie ținându-i starea într-un array la nivel de modul: protocolul nu are sesiune, deci un instrument de creare returnează un identificator opac, iar fiecare apel ulterior îl primește ca argument obișnuit. Nimic din niciun fișier nu presupune că apelantul este procesul care l-a deschis.

Iată același instrument în ambele limbaje, înregistrat unul lângă altul:

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.")

Citește mai întâi ce este la fel, pentru că aceasta este constatarea. Ambele declară un nume, o descriere, două argumente string descrise și trei adnotări; ambele sunt o singură funcție; niciunul nu menționează JSON-RPC, framing, stdout sau o versiune de protocol. Cele două SDK-uri au convergent spre aceeași formă, exact ce ar trebui să însemne „Tier 1”.1

Două diferențe sunt reale și ambele revin mai târziu. TypeScript descrie argumentele cu o bibliotecă de scheme — aici Zod — iar schema este o valoare pe care o scrii. Python le descrie cu propriile type hints ale funcției și le citește la import, motiv pentru care știe lucruri despre funcție pe care fișierul TypeScript nu i le-a spus niciodată. Și traseul de eroare: TypeScript returnează un rezultat de instrument cu isError, Python ridică o excepție. Ține minte asta.

Celelalte patru înregistrări nu diferă prin nimic structural. Resursa este server.registerResource("open-incidents", "incidents://open", …) față de @server.resource("incidents://open", …); prompt-ul este registerPrompt față de @server.prompt. Ultima linie din fiecare fișier este transportul: await server.connect(new StdioServerTransport()) față de server.run().

Fișierele întregi: 81 de linii nealbe și 3.060 de bytes de TypeScript față de 63 și 2.555. Ia asta cu doza de sare pe care o merită — numărul de linii măsoară un formatter la fel de mult ca un limbaj, motiv pentru care niciuna dintre cifre nu este în tabelul principal de mai jos.

Dovada că limbajul este invizibil este un client rulat de două ori, în unsprezece linii:

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));

Îndreaptă-l pe rând către fiecare server. Output real, scurtat:

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}"}]

Aceleași instrumente, aceeași ordine, același handle. Un client TypeScript nu poate spune în ce este scris serverul și nici nu întreabă vreodată. Aceasta este întreaga promisiune a unui protocol, ținându-se.

Acum uită-te la spațiile din al doilea rezultat, pentru că nu sunt cosmetice: SDK-ul Python serializează payload-urile cu pydantic_core.to_json(result, fallback=str, indent=2). La citirea resursei cu două incidente în listă, corpul TypeScript are 136 de caractere și 37 de o200k_base token; corpul Python are 185 și 62. Cu șaizeci și opt la sută mai mulți token pentru rânduri identice, plătiți de oricine citește resursa într-un prompt, de fiecare dată.

Catalogul spune aceeași poveste cu o cauză mai mare. Ambele servere, aceleași trei instrumente, tools/list cântărit cheie cu cheie:

cheieTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Schemele de input ale Python sunt mai ieftine — puntea Zod din TypeScript ștampilează un $schema și un additionalProperties pe fiecare. Întregul decalaj de 138 de token este o schemă de output pe care nu a scris-o nimeni. resolve_incident este adnotat -> Incident, așa că SDK-ul a derivat o JSON Schema pentru tipul returnat și a livrat-o. Este cu adevărat utilă — ea îi permite unui client să valideze structuredContent — și înseamnă 187 de token din context window care sosesc din cauza unui type hint. Regula din Capitolul 24, că definițiile înghesuie materialul care contează, se aplică și schemelor despre care nu știai că există.

Strică-l intenționat: mesajul de eroare care s-a scurs

Link către secțiunea: Strică-l intenționat: mesajul de eroare care s-a scurs

Cele două trasee de eroare de mai sus nu sunt o alegere de stil. Dă fiecărui server un instrument care eșuează așa cum eșuează o integrare reală și citește ce ajunge la model.

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}

SDK-ul TypeScript a pus în contextul modelului o adresă internă, un port, un nume de bază de date și un service account. SDK-ul Python nu a pus nimic din acestea acolo; traceback-ul a mers la stderr și a rămas pe server.

Niciuna nu este un bug. Ambele sunt decizii, iar cea din Python este scrisă în propriul docstring: un ToolError este „un eșec pe care l-ai anticipat”, iar mesajul lui este returnat „în content pentru ca modelul să îl citească”; orice altceva „este tratat ca un crash: modelul vede doar Error executing tool <name>, iar serverul loghează traceback-ul la ERROR”. Clasa pentru cazul de crash spune restul cu voce tare — „nimic din original nu ajunge la client”.

Ambele comportamente sunt greșite jumătate din timp. Capitolul 18 a susținut că o eroare de validare ar trebui să revină ca rezultat de instrument pe care modelul îl poate citi și corecta, pentru că aceea este linia cu cel mai mare leverage în majoritatea integrărilor; în Python asta cere să ridici explicit ToolError, iar un ValueError simplu aruncă fraza utilă. Argumentul din Capitolul 30 merge în direcția opusă: tot ce returnează un instrument ajunge într-un context din care o prompt injection ulterioară poate încerca să citească înapoi, iar un string de excepție neverificat este cel mai puțin auditat text din sistemul tău.

Regula care supraviețuiește ambelor: decide, pentru fiecare instrument, ce are voie să spună un eșec și scrie tu acel string. Nu lăsa niciodată textul implicit al unei excepții să decidă, în niciun limbaj.

Strică-l intenționat: o linie pe standard output

Link către secțiunea: Strică-l intenționat: o linie pe standard output

Tutorialul oficial formulează regula fără ezitare: „Pentru servere bazate pe STDIO: nu scrie niciodată în stdout. Scrierea în stdout va corupe mesajele JSON-RPC și îți va strica serverul. Funcția print() scrie implicit în stdout, așa că ține-o complet departe de un server STDIO.”1 Capitolul 26 a citat versiunea normativă — un server „MUST NOT write anything to its stdout that is not a valid MCP message”.2

Adaugă o linie la fiecare server și citește stream-ul brut:

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

Cel Python este mai rău, iar motivul nu este MCP. Un proces al cărui stdout este un pipe, nu un terminal, primește un stream cu buffer pe blocuri, deci linia rătăcită este flush-uită când decide bufferul — aici, la exit, după un răspuns înaintea căruia fusese scrisă. Coruperea nu apare acolo unde este bugul. Adaugă flush=True, sau o bibliotecă ce face flush, și se mută.

Apoi partea care explică de ce ajunge asta în producție. Dă serverul stricat la trei clienți:

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

Parserul de șapte linii moare imediat. Clientul oficial și Inspectorul ridică din umeri — sar peste linie și merg mai departe. O regulă care strică doar clienții pe care nu îi folosește nimeni este o regulă care ajunge intactă în producție, motiv pentru care merită încălcată intenționat aici, nu în logul unui client.

Modul CLI al Inspectorului este jumătatea care se uită: npx @modelcontextprotocol/inspector --cli <command> --method tools/list tipărește un catalog și iese, ceea ce îl face scriptabil într-un mod în care UI-ul din browser nu este.3

Ambele SDK-uri s-au instalat curat, în propriile directoare, fără nimic partajat:

TypeScriptPython
pachet@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
cea mai recentă revizie de protocol implementată2025-11-252026-07-28
pachete tranzitive instalate9428
dimensiune instalată13.9 MiB44.3 MiB
fișiere pe disc3.3862.018
pachete terțe încărcate pentru a servi stdio8 din 9418 din 28
pornire interpret gol, mediană19.4 ms11.1 ms
spawn → tools/list răspuns, mediană din 25144.5 ms709.4 ms
catalog tools/list, token o200k_base342480

Fiecare rând surprinde în altă direcție, de aceea comparația merită rulată, nu presupusă.

TypeScript instalează de peste trei ori mai multe pachete și mai puțin de o treime din bytes. 94 de dependențe înseamnă ecosistemul npm fiind el însuși — fast-deep-equal, es-errors, dunder-proto. Cele 28 din Python sunt mai puține și enorme: cryptography, pydantic-core și uvicorn sunt artefacte compilate. Dacă instinctul tău este că numărul de dependențe este lucrul de care să îți faci griji, acest rând este contraexemplul.

Interpreterul Python pornește mai repede decât Node, și nu e aproape — 11.1 ms față de 19.4 ms pe un program gol. Deci cele 565 ms din rândul de cold start nu sunt limbajul. Este SDK-ul, iar rândul cu pachetele încărcate spune de ce:

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, …

Un server al cărui singur I/O este un pipe importă un server web ASGI, un client HTTP și o bibliotecă TLS înainte să citească prima linie. SDK-ul TypeScript livrează și Express, Hono, jose și eventsource — ele stau pe disc necitite, pentru că granița pachetului le ține în afara unui import server/stdio.js. Pachetul Python este un singur graf de import, deci import mcp înseamnă tot: python -X importtime atribuie 727 ms lui import mcp.server.mcpserver — o cifră măsurată sub profilerul de import, motiv pentru care iese peste cele 709 ms pe care rularea neprofilată le ia de la spawn până la răspuns — și 269 dintre ele doar subarborelui mcp.types — tipurile de pe wire sunt modele Pydantic, câte o clasă pentru fiecare mesaj de protocol pe fiecare revizie, iar construirea lor este muncă făcută la import. Acesta este un trade-off de design, nu neglijență — importurile eager sunt motivul pentru care SDK-ul Python îți poate da run(transport="streamable-http") pe linia următoare fără a doua instalare.

Și apoi ultimul rând din blocul de deschidere anulează argumentul. Împachetează corect serverul TypeScript — o intrare bin, un shebang, npm link, nimic de descărcat — și lansează-l prin npx cu --no-install, așa cum este pornit de fapt un server stdio publicat:

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

Launcherul costă 568 ms per pornire — de patru ori și jumătate mai mult decât întregul import al SDK-ului TypeScript — și este plătit la fiecare lansare, pentru că o gazdă MCP pornește un server stdio rulând acea comandă. Deci forma onestă a propoziției „TypeScript pornește de cinci ori mai repede” este: da, până îl distribui în modul normal. Aceeași avertizare se aplică probabil pentru uvx; pe această mașină nu era instalat uv, deci acel rând nu există. Nimic nemăsurat nu intră în tabel.

Capitolul 26 a acoperit framing-ul stdio. Două lucruri au rămas pentru aici.

Primul: rularea unui server cu npx sau uvx este transportul stdio. Nu există un „mod de pachet” separat. Configurația unei gazde numește o comandă și argumente; gazda o pornește și vorbește peste pipe-uri. De aceea „cum distribui asta” și „ce transport vorbește” sunt local aceeași întrebare, iar costul launcherului aparține unui capitol despre livrare.

Al doilea: stdio nu are deloc secțiune de autorizare, iar specificația o spune într-o singură linie — implementările care folosesc stdio „SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Modelul lui de securitate este cel al sistemului de operare, la fel și limita: un subprocess local servește exact o mașină și un utilizator.

Celălalt transport viu este Streamable HTTP: un singur endpoint care acceptă POST, câte o cerere HTTP per mesaj JSON-RPC, și un header Accept care trebuie să listeze atât application/json, cât și text/event-stream, pentru că serverul alege la fiecare cerere cu care dintre cele două răspunde.5 Capitolul 14 a parsuit manual acel event stream, deci nimic din formatul de pe wire nu este nou — doar ambalajul lui. Trei obligații ale reviziei curente sunt ușor de ratat și toate trei pot fi testate:

Headerul de versiune trebuie să fie de acord cu corpul

Link către secțiunea: Headerul de versiune trebuie să fie de acord cu corpul

Fiecare POST poartă MCP-Protocol-Version, iar valoarea lui trebuie să corespundă cu protocolVersion din propriul _meta al cererii. O nepotrivire este un 400 cu o eroare de header-mismatch, nu o ridicare din umeri.5

Încă două headere sunt necesare pentru conformitate

Link către secțiunea: Încă două headere sunt necesare pentru conformitate

Mcp-Method oglindește metoda la fiecare cerere; Mcp-Name oglindește params.name sau params.uri pe tools/call, resources/read și prompts/get. Ele există pentru ca un proxy să poată ruta fără să parseze corpuri.5

Formele vechi au dispărut și răspund cu un refuz

Link către secțiunea: Formele vechi au dispărut și răspund cu un refuz

Stream-ul GET, Mcp-Session-Id și reluarea Last-Event-ID au fost toate eliminate. Un server care vorbește doar această revizie ar trebui să răspundă 405 Method Not Allowed la un GET sau DELETE, să ignore un header de sesiune fără să emită unul și să ignore Last-Event-ID.5

Acum măsurătoarea care reîncadrează întregul capitol. Trimite fiecărui server o cerere de revizie curentă peste 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)"}}

Constantele sunt de acord cu comportamentul: LATEST_PROTOCOL_VERSION din SDK-ul Python citește 2026-07-28, cea din SDK-ul TypeScript citește 2025-11-25. Trimite cererea cu header-mismatch de la pasul de mai sus și serverul Python răspunde 400 cu eroarea -32020 și mesajul „headerul mcp-protocol-version nu corespunde cu versiunea de protocol din anvelopa cererii”; SDK-ul TypeScript nu are asemenea cod, pentru că nu implementează revizia care îl definește.

Pagina care le listează pe ambele la Tier 1 spune și „Each SDK provides the same functionality”.1 La data de mai jos, pentru revizia curentă, propoziția este aspirațională. Verifică LATEST_PROTOCOL_VERSION în SDK-ul pe care urmează să îl instalezi; este o singură linie și singura afirmație din acest capitol care va mai conta peste un an.

Mută un server de pe laptopul tău și apare clientul unui străin cu un token. Aceasta este jumătatea pe care Capitolul 26 a lăsat-o deoparte și jumătatea pe care un produs multi-user nu o poate sări.

Specificația pune serverul MCP într-un rol OAuth 2.1 și îl numește: un server MCP protejat este un resource server, clientul este un client OAuth, iar authorization server este problema altcuiva.4 Din acel rol decurg patru clauze obligatorii, citate integral pentru că parafrazarea lor este felul în care se face greșeala:

Serverele MCP, acționând în rolul lor de resource server OAuth 2.1, MUST valideze access tokens așa cum este descris în OAuth 2.1 Secțiunea 5.2. Serverele MCP MUST valida că access tokens au fost emise specific pentru ele ca audiență intenționată, conform RFC 8707 Secțiunea 2. […] Clienții MCP MUST NOT trimite tokens către serverul MCP în afara celor emise de authorization server al serverului MCP. Serverele MCP MUST accepta doar tokens care sunt valide pentru utilizare cu propriile resurse. Serverele MCP MUST NOT accepta sau tranzita alți tokens.4

„Must not accept or transit” este regula anti-passthrough și acesta este motivul pentru care există tot aparatul de audiență. Un server care reia bearer token-ul primit la un API terț este un confused deputy: își împrumută propria încredere celui care l-a apelat. Regula interzice reutilizarea, nu doar stocarea.

Ca asta să poată fi impus cere patru RFC-uri, fiecare cu treaba lui.6 RFC 9728 este felul în care clientul găsește authorization server-ul: serverul MCP servește un document protected-resource-metadata, iar un 401 indică spre el. RFC 8707 este parametrul resource — clientul trebuie să trimită URI-ul canonic al serverului atât în cererea de autorizare, cât și în cererea de token, „indiferent dacă authorization servers îl suportă”, astfel încât token-ul emis să își numească audiența. RFC 9207 închide bucla din cealaltă parte: clientul înregistrează emitentul înainte de redirect și compară iss returnat prin string exact, fără normalizare — fără case folding, fără eliminarea portului implicit, fără slash final. Iar RFC 7591, Dynamic Client Registration, este acum deprecated în favoarea Client ID Metadata Documents, „păstrat pentru compatibilitate inversă cu authorization servers care nu le suportă”.4

Leagă asta pe ambele servere cu un verifier de token care nu face nimic în afară de verificarea audienței. Scara 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"}

Ambele SDK-uri servesc acel document și ambele îndreaptă un 401 către el, ceea ce este întreaga poveste de discovery: un client care nu ți-a văzut niciodată serverul află unde să se autentifice dintr-un refuz. 403 este alt animal — token-ul este bun, scope-ul nu — iar challenge-ul numește ce lipsește, astfel încât clientul să poată urca o treaptă, nu să o ia de la capăt.

Două trepte diferă, iar nicio diferență nu este în specificație. SDK-ul TypeScript refuză un token fără claim de expirare; cel Python returnează 200, pentru că expires_at este opțional pe AccessToken, iar None înseamnă „fără opinie”. Iar 403 din Python poartă error_description="Required scope: incidents:read" fără parametrul scope pe care specificația spune că serverele ar trebui să îl includă. Un verifier nu este locul în care accepți un default de bibliotecă: verificarea audienței este a ta de scris în oricare limbaj, la fel și expirarea.

O observație cinstită din aceeași rulare. Un GET pe endpoint a răspuns 404 pe cablarea Express și 400 Bad Request: Missing session ID pe cea Python, acolo unde specificația cere 405 Method Not Allowed și unde „session ID” este vocabular pe care această revizie l-a eliminat. Niciuna nu este periculoasă; ambele sunt forma unui ecosistem în mijlocul migrării.

Ultima bucată din livrare este locul unde publici, iar asta are un răspuns cu un număr. Crawled astăzi, fiecare server din registry-ul oficial la cea mai recentă versiune:7

servere
total (cea mai recentă versiune, neșters)28.170
activ / deprecated27.853 / 317
livrează cel puțin un pachet instalabil13.065
doar remote — un URL, nimic de instalat14.696
npm8.275
PyPI3.603
imagini OCI867
bundle-uri mcpb706
NuGet / Cargo107 / 43

Două lecturi, în direcții opuse. După servere publicate, npm conduce cu 2,3 la 1 — cifra pe care oamenii o citează când spun că ecosistemul este TypeScript. După descărcări, Python conduce: în ultimele treizeci de zile mcp a luat 286,7 milioane față de @modelcontextprotocol/sdk cu 194,7 milioane, înainte să adaugi fastmcp cu 72,1 milioane.7 Ambele sunt Tier 1, schema normativă este un schema.ts, iar tutorialul oficial „Build an MCP server” se deschide pe tabul Python.1 Oricare jumătate o aveai în minte, și cealaltă jumătate este adevărată.

Și rândul care contează mai mult decât oricare dintre ele: mai mult de jumătate din registry — 14.696 din 28.170 — nu are nimic de instalat. Acestea sunt servicii web. Totalurile pe transport sunt de acord din cealaltă parte: din 14.290 de intrări de pachete, 13.787 declară stdio; din 16.640 de intrări remote, 15.570 declară Streamable HTTP și 1.070 încă declară HTTP+SSE deprecated. Deci „un server MCP este un subprocess pe laptopul tău” descrie o minoritate în scădere, iar fiecare dintre cele 14.696 are nevoie de secțiunea de mai sus, nu de o variabilă de mediu.

Afișează detaliile

Deliberat bilingv și precedentul pentru asta.

Acesta este singurul capitol bilingv din curs, pentru că răspunsul onest se împarte: registry-ul este npm-first, iar descărcările sunt Python-first, în același timp, astăzi. Să scrii doar unul dintre cele două ar ceda jumătate din întrebare și ar descrie greșit ecosistemul în timp ce o face. Există precedent la vedere — Hugging Face MCP Course listează printre prerequisite „Experience with at least one programming language (Python or TypeScript examples will be shown)” și le predă pe ambele.8 Un protocol a cărui valoare întreagă este numărul de implementări este un loc prost pentru monolingvism.

Secțiune datată: tot ce are termen de valabilitate mai sus

Link către secțiunea: Secțiune datată: tot ce are termen de valabilitate mai sus

Citit și măsurat pe 7 septembrie 2026, față de revizia de protocol 2026-07-28.

valoare
@modelcontextprotocol/sdk1.30.0, publicat pe 27 iulie 2026; 4.322.438 bytes dezarhivați, 693 de fișiere, 17 dependențe directe
cea mai recentă revizie pe care o implementează2025-11-25
mcp (PyPI)2.1.1, publicat pe 25 august 2026; wheel de 357.912 bytes, plus mcp-types 2.1.1 la 69.656 bytes
cea mai recentă revizie pe care o implementează2026-07-28
niveluri SDKTypeScript, Python, C#, Go, Rust la Tier 1; Java, Ruby la Tier 2; Swift, PHP, Kotlin la Tier 3
servere în registry28.170
descărcări, ultimele 30 de zilemcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

O notă de migrare care nu este un număr. În mcp 2.x, FastMCP a fost redenumit MCPServer, iar aproape fiecare tutorial online încă se deschide cu vechiul import. SDK-ul livrează un modul al cărui singur scop este să explice asta, cea mai grijulie deprecare din acest capitol:

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.

Cu tabelul în față, recomandarea este plictisitoare, ceea ce este un semn bun.

Dacă serverul locuiește într-o aplicație web pe care o rulezi deja, scrie-l în TypeScript. Același proces, același deploy, același request handler; Streamable HTTP este un endpoint pe care îl adaugi lângă celelalte; iar cei 13,9 MiB și cele 145 ms sunt gratis pentru că runtime-ul era deja pornit. Asta înseamnă majoritatea celor 14.696 de servere remote.

Dacă serverul învelește data tooling, scrie-l în Python. Ce expui este pandas, un client de warehouse, transformări cât un notebook, iar un server într-un alt limbaj ar fi un apel de subprocess purtând o schemă. Șapte sute de milisecunde de import într-un serviciu care pornește o dată nu sunt un cost; într-un subprocess pe care o gazdă îl relansează toată ziua, sunt.

Iar deocamdată, rândul de revizie le depășește pe ambele. Dacă ai nevoie de 2026-07-28 — cereri multi-round-trip, resultType, cache hints, server/discover — unul dintre cele două SDK-uri îl are astăzi, celălalt nu.

Acum poți livra același server în oricare limbaj, poți apăra alegerea cu un tabel în locul unei preferințe, îl poți rula peste ambele transporturi live și îi poți da un token pe care îl va refuza.

Ce ai construit este încă o funcție: o schemă, un endpoint, un lucru determinist pe care modelul îl invocă. O întreagă clasă de cunoaștere nu încape în forma aceea — cum scriem noi un postmortem, ce câmpuri trebuie să aibă rapoartele noastre de incident, ordinea în care facem lucrurile și de ce. Este procedură, este proză, iar forțarea ei într-o descriere de instrument este felul în care system prompts ajung la două mii de token plătiți la fiecare tură, indiferent dacă conversația este sau nu despre incidente.

Capitolul 28 este celălalt răspuns: un folder cu un SKILL.md în el pe care modelul îl citește în loc să îl apeleze, încărcat pe trei niveluri astfel încât materialul de referință să coste aproape nimic până în tura în care este necesar. Nu are un limbaj principal, iar acesta este primul lucru pe care îl predă.


Tot ce este aici a fost măsurat pe 7 septembrie 2026, pe Node 22.22.3 și Python 3.14.4, față de @modelcontextprotocol/sdk 1.30.0 cu zod 3.25.76 și mcp 2.1.1, fiecare instalat în propriul director temporar. Timpii sunt mediane din 25 de lansări, wall clock de la spawn până la linia care poartă răspunsul tools/list; numărătorile de token sunt o200k_base prin tiktoken peste JSON-ul fiecărei definiții. Nu a fost apelat niciun API plătit: nimic de aici nu are nevoie de un model.

Cele două servere au 81 și 63 de linii nealbe; unul dintre cele trei instrumente ale lor este reprodus mai sus în ambele limbaje, iar celelalte patru înregistrări diferă doar cum s-a descris. Politica SDK-ului Python privind dezvăluirea erorilor este citată din docstrings ale ToolError și UnexpectedToolError în mcp/server/mcpserver/exceptions.py; defaultul de pretty-printing este pydantic_core.to_json(result, fallback=str, indent=2) în mcp/server/mcpserver/resources/types.py și utilities/func_metadata.py. Constantele de versiune de protocol sunt LATEST_PROTOCOL_VERSION în mcp_types/version.py și în types.js al SDK-ului TypeScript, ambele citite din pachetele instalate, nu dintr-un changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, și Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, ambele citite pe 7 septembrie 2026. Sursa tabelului de niveluri, a propoziției „Each SDK provides the same functionality but follows the idioms and best practices of its language”, a ordinii taburilor de limbaj din tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) și a regulii de logging citate despre print() și stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Sursa framing-ului cu newline și a regulii de puritate stdout. Capitolul 26 citește această pagină integral; este citată aici pentru linia pe care serverul stricat o încalcă.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, citit pe 7 septembrie 2026. Un pachet, trei clienți în spatele unui binar — web, --cli și --tui — care împart un core, un set de transporturi și o stare OAuth pe disc. CLI-ul a produs urmele de catalog de aici.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, citit pe 7 septembrie 2026. Sursa rolului de resource-server; cele patru clauze de gestionare a token citate integral; cerința ca serverele să implementeze RFC 9728 și clienții să îl folosească pentru discovery; regulile parametrului resource și definiția URI-ului canonic; tabelul de validare a issuer; deprecarea Dynamic Client Registration; tabelul 401/403/400 și challenge-ul insufficient_scope; și excepția stdio, „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, și Transports overview, .../basic/transports. Sursa regulii POST cu endpoint unic, a cerinței duale Accept, a headerului MCP-Protocol-Version și a regulii că trebuie să corespundă corpului, a headerelor Mcp-Method și Mcp-Name descrise ca „REQUIRED for compliance”, a eliminării stream-ului GET, a sesiunilor și a Last-Event-ID, a ghidajului 405, a validării obligatorii Origin și a clasificării transportului HTTP+SSE din 2024-11-05 ca Deprecated sub SEP-2596. 2 3 4

  6. Cele patru pe care se sprijină specificația, cu draftul pe care îl profilează: 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, februarie 2020 — parametrul resource și audience pe care o leagă. Jones, M.B., Hunt, P. și Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, aprilie 2025 — documentul spre care indică un 401. Meyer zu Selhausen, K. și Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, martie 2022 — parametrul iss și comparația prin string exact. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, iulie 2015, deprecated pentru această utilizare. Și Jones, M. și Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, octombrie 2012, secțiunea 3, pentru forma challenge-ului WWW-Authenticate de mai sus.

  7. Registry-ul oficial MCP, registry.modelcontextprotocol.io/v0/servers, crawled pe 7 septembrie 2026 cu version=latest: 282 de pagini, 28.170 de servere, totalizate de registryType peste nume distincte de servere. Cifrele de descărcare: api.npmjs.org/downloads/point/last-month pentru @modelcontextprotocol/sdk (194.679.333 pentru 8 august – 6 septembrie 2026) și pypistats.org/api/packages/<name>/recent pentru mcp și fastmcp, toate citite în aceeași zi. Dimensiunile pachetelor vin din documentul registry npm și din API-ul JSON PyPI. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unitatea 0, citit pe 7 septembrie 2026: printre prerequisite, „Experience with at least one programming language (Python or TypeScript examples will be shown)”.

Gata să lași LIA să aleagă?

Construiește cu toate modelele AI într-un singur loc — începe gratuit azi.