Spring til indhold
27/30Kapitel 27 af 30

Ship en MCP-server: TypeScript og Python, målt

Den samme server skrevet to gange — tre tools, en ressource, en prompt — og vejet: 94 pakker mod 28, koldstart 145 ms mod 709.

På denne side

Her er hele sprogargumentet, målt, før et eneste ord af det bliver formuleret.

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

De første to linjer er den sammenligning, alle vil have. Den tredje linje er den samme TypeScript-server fra første linje, startet på den måde, den faktisk ville blive distribueret — og den lander tre millisekunder fra Python.

Kapitel 26 læste Model Context Protocol op mod dens egen specifikation med rå JSON-RPC, fordi rå JSON-RPC ikke har noget sprog. Dette kapitel har to, og argumentets vægt falder her: den samme server, skrevet to gange. Tre tools, én ressource, én prompt, begge SDK'er, ingen genveje på nogen af siderne. Derefter transports, inspector, 401'eren og tallene, ingen har offentliggjort.

Serveren, og hvorfor den har disse fem ting i sig

Link til afsnittet: Serveren, og hvorfor den har disse fem ting i sig

En hændelseslog. Tre tools, fordi Kapitel 18s opdeling mellem læsninger og skrivninger skal være synlig: search_incidents læser, open_incident skriver og giver et handle tilbage, resolve_incident tager det handle og lukker. Én ressource, incidents://open, fordi læsning af den aktuelle liste er noget, applikationen vedhæfter. Én prompt, postmortem, fordi "skriv det her op" er en persons slash-kommando. Det er Kapitel 26's kontrolhierarki — model, applikation, person — gjort til fem registreringer.

Handle'et betyder mere, end det ser ud til. Kapitel 26 ødelagde en legetøjskalender ved at holde dens state i et array på modulniveau: protokollen har ingen session, så et creation-tool returnerer en opaque identifier, og hvert senere kald tager den som et almindeligt argument. Intet i nogen af filerne antager, at kalderen er den proces, der åbnede den.

Her er det samme tool på begge sprog, registreret side om side:

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

Læs først, hvad der er det samme, for det er fundet. Begge erklærer et navn, en beskrivelse, to beskrevne string-argumenter og tre annotations; begge er én funktion; ingen af dem nævner JSON-RPC, framing, stdout eller en protokolversion. De to SDK'er er konvergeret mod den samme form, og det er, hvad "Tier 1" skal betyde.1

To forskelle er reelle, og begge vender tilbage senere. TypeScript beskriver argumenter med et schema-bibliotek — Zod her — og schemaet er en værdi, du skriver. Python beskriver dem med funktionens egne type hints og læser dem ved import time, og derfor ved den ting om funktionen, som TypeScript-filen aldrig fortalte den. Og fejlvejen: TypeScript returnerer et tool-resultat med isError, Python raiser. Hold fast i det.

De fire andre registreringer adskiller sig ikke strukturelt. Ressourcen er server.registerResource("open-incidents", "incidents://open", …) mod @server.resource("incidents://open", …); prompten er registerPrompt mod @server.prompt. Den sidste linje i hver fil er transporten: await server.connect(new StdioServerTransport()) mod server.run().

Hele filer: 81 ikke-tomme linjer og 3.060 bytes TypeScript mod 63 og 2.555. Tag det med den mængde salt, det fortjener — linjetællinger måler en formatter lige så meget som et sprog, og derfor står ingen af tallene i oversigtstabellen nedenfor.

Beviset på, at sproget er usynligt, er én klient kørt to gange, på elleve linjer:

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

Peg den på hver server efter tur. Reelt output, beskåret:

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

Samme tools, samme rækkefølge, samme handle. En TypeScript-klient kan ikke se, hvad serveren er skrevet i, og den spørger aldrig. Det er hele løftet fra en protokol, der holder.

Se nu på whitespace i det andet resultat, for det er ikke kosmetik: Python-SDK'et serialiserer payloads med pydantic_core.to_json(result, fallback=str, indent=2). På ressourcelæsningen med to hændelser i listen er TypeScript-bodyen 136 tegn og 37 o200k_base tokens; Python-bodyen er 185 og 62. Otteogtres procent flere tokens for identiske rækker, betalt af den, der læser ressourcen ind i en prompt, hver gang.

Kataloget har samme historie med en større årsag. Begge servere, samme tre tools, tools/list vejet nøgle for nøgle:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Pythons input-schemaer er billigere — TypeScripts Zod-bro stempler en $schema og en additionalProperties på hver. Hele forskellen på 138 tokens er et output-schema, som ingen skrev. resolve_incident er annoteret -> Incident, så SDK'et afledte et JSON Schema for returtypen og sendte det med. Det er oprigtigt nyttigt — det er det, der lader en klient validere structuredContent — og det er 187 tokens af dit context window, der ankommer på grund af et type hint. Kapitel 24s regel om, at definitioner skubber det materiale ud, der betyder noget, gælder også schemaer, du ikke vidste, du havde.

Ødelæg det med vilje: fejlbeskeden, der lækkede

Link til afsnittet: Ødelæg det med vilje: fejlbeskeden, der lækkede

De to fejlveje ovenfor er ikke et stilvalg. Giv hver server et tool, der fejler på den måde, en reel integration fejler, og læs, hvad der når frem til modellen.

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'et lagde en intern adresse, en port, et databasenavn og en service account ind i modellens context. Python-SDK'et lagde intet af det derind; tracebacken gik til stderr og blev på serveren.

Ingen af delene er en bug. Begge er beslutninger, og Python-beslutningen står i dens egen docstring: en ToolError er "en fejl, du forventede", og dens besked returneres "i content, så modellen kan læse den"; alt andet "behandles som et crash: modellen ser kun Error executing tool <name>, og serveren logger tracebacken på ERROR". Klassen for crash-tilfældet siger resten højt — "intet fra originalen når klienten".

Begge adfærdsmønstre er forkerte halvdelen af tiden. Kapitel 18 argumenterede for, at en valideringsfejl skal komme tilbage som et tool-resultat, modellen kan læse og rette, fordi det er den mest værdifulde linje i de fleste integrationer; på Python-siden kræver det, at man eksplicit raiser ToolError, og en bar ValueError smider den nyttige sætning væk. Kapitel 30s argument løber den anden vej: alt, hvad et tool returnerer, lander i en context, som en senere prompt injection kan prøve at læse tilbage ud, og en ikke-gennemgået exception string er den mindst reviderede tekst i dit system.

Reglen, der overlever begge: beslut, per tool, hvad en fejl må sige, og skriv den string selv. Lad aldrig en exceptions standardtekst bestemme, på noget sprog.

Ødelæg det med vilje: én linje på standard output

Link til afsnittet: Ødelæg det med vilje: én linje på standard output

Den officielle tutorial formulerer reglen uden forbehold: "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 Kapitel 26 citerede den normative version — en server "MUST NOT write anything to its stdout that is not a valid MCP message".2

Tilføj én linje til hver server, og læs den rå 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-versionen er værre, og årsagen er ikke MCP. En proces, hvis stdout er en pipe snarere end en terminal, får en block-buffered stream, så den vildfarne linje flushes, når bufferen beslutter det — her ved exit, efter et svar, den blev skrevet før. Korruptionen dukker ikke op der, hvor buggen er. Tilføj flush=True, eller et bibliotek, der flusher, og den flytter sig.

Så kommer den del, der forklarer, hvorfor det her shipper. Fodr den ødelagte server til tre klienter:

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

Den syvlinjers parser dør med det samme. Den officielle klient og Inspector trækker på skuldrene — de skipper linjen og fortsætter. En regel, der kun ødelægger de klienter, ingen bruger, er en regel, der når produktion intakt, og derfor er det værd at ødelægge den med vilje her i stedet for i en kundes log.

Inspectors CLI-tilstand er den halvdel, der bliver glemt: npx @modelcontextprotocol/inspector --cli <command> --method tools/list printer et katalog og afslutter, hvilket gør den scriptbar på en måde, browser-UI'et ikke er.3

Begge SDK'er blev installeret rent, i deres egne mapper, intet delt:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
seneste implementerede protokolrevision2025-11-252026-07-28
installerede transitive pakker9428
installeret størrelse13,9 MiB44,3 MiB
filer på disk3.3862.018
tredjepartspakker indlæst for at serve stdio8 af 9418 af 28
bar interpreter-start, median19,4 ms11,1 ms
spawn → tools/list besvaret, median af 25144,5 ms709,4 ms
tools/list-katalog, o200k_base tokens342480

Hver række overrasker i en anden retning, og derfor er sammenligningen værd at køre i stedet for at antage.

TypeScript installerer mere end tre gange så mange pakker og mindre end en tredjedel af bytes. 94 dependencies er npm-økosystemet, der er sig selv — fast-deep-equal, es-errors, dunder-proto. Pythons 28 er færre og enorme: cryptography, pydantic-core og uvicorn er kompilerede artefakter. Hvis dit instinkt er, at dependency-antal er det, man skal bekymre sig om, er denne række modeksemplet.

Pythons interpreter starter hurtigere end Nodes, og det er ikke tæt på — 11,1 ms mod 19,4 ms på et tomt program. Så de 565 ms i koldstart-rækken er ikke sproget. Det er SDK'et, og rækken med indlæste pakker siger hvorfor:

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

En server, hvis eneste I/O er en pipe, importerer en ASGI-webserver, en HTTP-klient og et TLS-bibliotek, før den læser sin første linje. TypeScript-SDK'et shipper også Express, Hono, jose og eventsource — de ligger ulæst på disk, fordi package-grænsen holder dem ude af en server/stdio.js import. Pythons package er én importgraf, så import mcp er det hele: python -X importtime tilskriver 727 ms til import mcp.server.mcpserver — et tal målt under import profiler, og derfor kommer det ud over de 709 ms, den uprofilterede kørsel tager fra spawn til svar — og 269 af dem til mcp.types-undertræet alene — wire-typerne er Pydantic-modeller, én klasse per protokolbesked per revision, og at bygge dem er arbejde udført ved import. Det er et design trade-off, ikke sjusk — ivrige imports er grunden til, at Python-SDK'et kan give dig run(transport="streamable-http") på næste linje uden en ekstra install.

Og så ophæver den sidste række i åbningsblokken argumentet. Pak TypeScript-serveren ordentligt — et bin entry, en shebang, npm link, intet at downloade — og start den gennem npx med --no-install, hvilket er sådan, en publiceret stdio-server faktisk startes:

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

Launcheren koster 568 ms per start — fire og en halv gange hele TypeScript-SDK-importen — og den betales ved hver launch, fordi en MCP host starter en stdio-server ved at køre den kommando. Så den ærlige form af "TypeScript starter fem gange hurtigere" er: det gør det, indtil du distribuerer det på normal vis. Det samme forbehold gælder formodentlig uvx; denne maskine havde ingen uv installeret, så den række findes ikke. Intet umålt kommer i tabellen.

Kapitel 26 dækkede stdios framing. To ting efterlod det til her.

Den første: at køre en server med npx eller uvx er stdio-transporten. Der er ingen separat "package mode". En hosts konfiguration angiver en kommando og argumenter; hosten spawner den og taler over pipes. Derfor er "hvordan distribuerer jeg det her" og "hvilken transport taler den" ét spørgsmål lokalt, og derfor hører launcherens omkostning hjemme i et kapitel om shipping.

Den anden: stdio har slet ingen authorization-sektion, og specifikationen siger det på én linje — implementationer, der bruger stdio, "SHOULD NOT follow this specification, and instead retrieve credentials from the environment".4 Dens sikkerhedsmodel er operativsystemets, og det er dens begrænsning også: en lokal subprocess server præcis én maskine og én bruger.

Den anden live transport er Streamable HTTP: ét endpoint, der accepterer POST, én HTTP-request per JSON-RPC-besked, og en Accept header, der skal liste både application/json og text/event-stream, fordi serveren per request vælger, hvilken af de to den svarer med.5 Kapitel 14 parsede den event stream i hånden, så intet i wire-formatet er nyt — kun det, der omslutter det. Tre forpligtelser i den aktuelle revision er lette at overse, og alle tre kan testes:

Hver POST bærer MCP-Protocol-Version, og dens værdi skal matche protocolVersion inde i requestens egen _meta. Et mismatch er en 400 med en header-mismatch-fejl, ikke et skuldertræk.5

To yderligere headers kræves for compliance

Link til afsnittet: To yderligere headers kræves for compliance

Mcp-Method spejler metoden på hver request; Mcp-Name spejler params.name eller params.uritools/call, resources/read og prompts/get. De findes, så en proxy kan route uden at parse bodies.5

De gamle former er væk og besvarer med en afvisning

Link til afsnittet: De gamle former er væk og besvarer med en afvisning

GET-streamen, Mcp-Session-Id og Last-Event-ID resumption blev alle fjernet. En server, der kun taler denne revision, bør svare 405 Method Not Allowed på en GET eller DELETE, ignorere en session-header uden at udstede en, og ignorere Last-Event-ID.5

Nu målingen, der omrammer hele kapitlet. Send en current-revision request til hver server over 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)"}}

Konstanterne stemmer med adfærden: Python-SDK'ets LATEST_PROTOCOL_VERSION læser 2026-07-28, TypeScript-SDK'ets læser 2025-11-25. Send header-mismatch-requesten fra trinnet ovenfor, og Python-serveren svarer 400 med fejl -32020 og beskeden "mcp-protocol-version header does not match the request envelope's protocol version"; TypeScript-SDK'et har ingen sådan kode, fordi det ikke implementerer den revision, der definerer den.

Siden, der lister begge som Tier 1, siger også "Each SDK provides the same functionality".1 På datoen nedenfor, for den aktuelle revision, er den sætning aspiratorisk. Tjek LATEST_PROTOCOL_VERSION i det SDK, du er ved at installere; det er én linje, og den eneste påstand i dette kapitel, der stadig betyder noget om et år.

Flyt en server væk fra din laptop, og en fremmed klients token dukker op. Det er den halvdel, Kapitel 26 lod ligge, og den halvdel, et multi-user-produkt ikke kan springe over.

Specifikationen placerer MCP-serveren i en OAuth 2.1-rolle og navngiver den: en protected MCP server er en resource server, klienten er en OAuth-klient, og authorization serveren er en andens problem.4 Fra den rolle kommer fire obligatoriske klausuler, citeret i fuld længde, fordi parafrasering er måden, fejlen opstår på:

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" er anti-passthrough-reglen, og det er derfor, hele audience-apparatet findes. En server, der replay'er den bearer token, den fik udleveret, mod en tredjeparts-API, er en confused deputy: den låner sin egen tillid til den, der kaldte den. Reglen forbyder genbrug, ikke kun lagring.

At gøre det håndhævbart kræver fire RFC'er, ét job hver.6 RFC 9728 er sådan, klienten overhovedet finder authorization serveren: MCP-serveren server et protected-resource-metadata-dokument, og en 401 peger på det. RFC 8707 er resource-parameteren — klienten skal sende serverens kanoniske URI i både authorization-requesten og token-requesten, "regardless of whether authorization servers support it", så den udstedte token navngiver sin audience. RFC 9207 lukker loopet fra den anden side: klienten registrerer issueren før redirect og sammenligner den returnerede iss som præcis string, uden normalisering — ingen case folding, ingen udeladelse af default-port, ingen trailing slash. Og RFC 7591, Dynamic Client Registration, er nu deprecated til fordel for Client ID Metadata Documents, "retained for backwards compatibility with authorization servers that do not support" dem.4

Wire det op på begge servere med en token-verifier, der ikke gør andet end at tjekke audience. TypeScript-stigen:

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

Begge SDK'er server det dokument, og begge peger en 401 på det, hvilket er hele discovery-historien: en klient, der aldrig har set din server, lærer fra en afvisning, hvor den skal autentificere. 403 er et andet dyr — tokenen er fin, scopen er ikke — og challengen navngiver, hvad der mangler, så klienten kan steppe op i stedet for at starte forfra.

To trin adskiller sig, og ingen af forskellene er i specifikationen. TypeScript-SDK'et afviser en token med ingen expiry claim; Python-versionen returnerer 200, fordi expires_at er optional på dens AccessToken, og None betyder "ingen mening". Og Python-403 bærer error_description="Required scope: incidents:read" uden den scope parameter, specifikationen siger, at servere bør inkludere. En verifier er ikke et sted at acceptere en library default: audience-tjekket er dit at skrive på begge sprog, og det er expiry også.

Én ærlig småting fra samme kørsel. En GET på endpointet svarede 404 på Express-wiringen og 400 Bad Request: Missing session ID på Python-versionen, hvor specifikationen beder om 405 Method Not Allowed, og hvor "session ID" er vokabular, denne revision fjernede. Ingen af delene er farlig; begge er formen på et økosystem midt i en migration.

Det sidste stykke shipping er, hvor du publicerer, og det har et svar med et tal. Crawlet i dag, hver server i det officielle registry på dens seneste version:7

servere
total (seneste version, ikke slettet)28.170
aktive / deprecated27.853 / 317
shipper mindst én installerbar pakke13.065
kun remote — en URL, intet at installere14.696
npm8.275
PyPI3.603
OCI images867
mcpb bundles706
NuGet / Cargo107 / 43

To læsninger, der peger hver sin vej. Efter publicerede servere fører npm 2,3 til 1 — tallet folk citerer, når de siger, at økosystemet er TypeScript. Efter downloads fører Python: over de seneste tredive dage tog mcp 286,7 millioner mod @modelcontextprotocol/sdk med 194,7 millioner, før fastmcp med 72,1 millioner lægges til.7 Begge er Tier 1, det normative schema er en schema.ts, og den officielle "Build an MCP server"-tutorial åbner på Python-fanen.1 Uanset hvilken halvdel du havde i hovedet, er den anden halvdel også sand.

Og rækken, der betyder mere end nogen af dem: mere end halvdelen af registryet — 14.696 af 28.170 — har intet at installere. De er webservices. Transportoptællingerne stemmer fra den anden side: af 14.290 package-entries deklarerer 13.787 stdio; af 16.640 remote-entries deklarerer 15.570 Streamable HTTP, og 1.070 deklarerer stadig den deprecated HTTP+SSE. Så "en MCP-server er en subprocess på din laptop" beskriver et skrumpende mindretal, og hver eneste af de 14.696 har brug for sektionen ovenfor frem for en environment variable.

Vis detaljer

Bevidst tosproget, og præcedensen for det.

Dette er det eneste tosprogede kapitel i kurset, fordi det ærlige svar splitter: registryet er npm-first, og downloads er Python-first, samtidig, i dag. At skrive én af de to ville forære halvdelen af spørgsmålet væk og beskrive økosystemet forkert imens. Der findes åben præcedens — Hugging Face MCP Course lister blandt sine prerequisites "Experience with at least one programming language (Python or TypeScript examples will be shown)" og underviser i begge.8 En protokol, hvis hele værdi er antallet af implementationer, er et dårligt sted at være ensproget.

Dateret sektion: alt ovenfor, der har en holdbarhedsdato

Link til afsnittet: Dateret sektion: alt ovenfor, der har en holdbarhedsdato

Læst og målt den 7. september 2026, mod protokolrevision 2026-07-28.

værdi
@modelcontextprotocol/sdk1.30.0, publiceret 27. juli 2026; 4.322.438 bytes udpakket, 693 filer, 17 direkte dependencies
seneste revision, det implementerer2025-11-25
mcp (PyPI)2.1.1, publiceret 25. august 2026; 357.912-byte wheel, plus mcp-types 2.1.1 på 69.656 bytes
seneste revision, det implementerer2026-07-28
SDK-tiersTypeScript, Python, C#, Go, Rust på Tier 1; Java, Ruby på Tier 2; Swift, PHP, Kotlin på Tier 3
registry-servere28.170
downloads, seneste 30 dagemcp 286.653.871 · fastmcp 72.097.269 · @modelcontextprotocol/sdk 194.679.333

Én migrationsnote, der ikke er et tal. I mcp 2.x blev FastMCP omdøbt til MCPServer, og næsten hver tutorial online åbner stadig med den gamle import. SDK'et shipper et modul, hvis eneste formål er at forklare det, hvilket er den mest hensynsfulde deprecation i dette kapitel:

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.

Med tabellen foran dig er anbefalingen kedelig, og det er et godt tegn.

Hvis serveren bor inde i en webapplikation, du allerede kører, så skriv den i TypeScript. Samme proces, samme deploy, samme request handler; Streamable HTTP er et endpoint, du tilføjer ved siden af de andre; og de 13,9 MiB og de 145 ms er gratis, fordi runtime allerede var oppe. Det er de fleste af de 14.696 remote-servere.

Hvis serveren wrapper data tooling, så skriv den i Python. Det, du eksponerer, er pandas, en warehouse-klient, en notebooks mængde af transforms, og en server i et andet sprog ville være et subprocess-kald iført et schema. Syv hundrede millisekunders import i en service, der starter én gang, er ikke en omkostning; i en subprocess, en host relauncher hele dagen, er det.

Og indtil videre overtrumfer revisionsrækken begge. Hvis du har brug for 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — har ét af de to SDK'er det i dag, og det andet har ikke.

Du kan nu shippe den samme server i begge sprog, forsvare valget med en tabel i stedet for en præference, køre den over begge live transports og give den en token, den vil afvise.

Det, du byggede, er stadig en funktion: et schema, et endpoint, en deterministisk ting, modellen invoker. En hel klasse af viden passer ikke ind i den form — hvordan vi skriver et postmortem, hvilke felter vores hændelsesrapporter skal bruge, rækkefølgen vi gør ting i og hvorfor. Det er procedure, det er prosa, og at tvinge det ind i en tool-beskrivelse er sådan system prompts vokser til to tusind tokens, betalt på hver eneste turn, uanset om samtalen handler om hændelser eller ej.

Kapitel 28 er det andet svar: en mappe med en SKILL.md i, som modellen læser i stedet for at kalde, indlæst i tre niveauer, så referencematerialet næsten intet koster, før det turn hvor det er nødvendigt. Det har ikke noget hovedsprog, og det er det første, det lærer.


Alt her blev målt den 7. september 2026 på Node 22.22.3 og Python 3.14.4, mod @modelcontextprotocol/sdk 1.30.0 med zod 3.25.76 og mcp 2.1.1, hver installeret i sin egen engangsmappe. Timings er medianer af 25 launches, wall clock fra spawn til linjen med tools/list-svaret; token-tællinger er o200k_base via tiktoken over JSON for hver definition. Ingen betalt API blev kaldt: intet her kræver en model.

De to servere er 81 og 63 ikke-tomme linjer; ét af deres tre tools gengives ovenfor på begge sprog, og de fire andre registreringer adskiller sig kun som beskrevet. Python-SDK'ets error-disclosure policy er citeret fra docstrings for ToolError og UnexpectedToolError i mcp/server/mcpserver/exceptions.py; pretty-printing-standarden er pydantic_core.to_json(result, fallback=str, indent=2) i mcp/server/mcpserver/resources/types.py og utilities/func_metadata.py. Protokolversionskonstanterne er LATEST_PROTOCOL_VERSION i mcp_types/version.py og i TypeScript-SDK'ets types.js, begge læst fra de installerede pakker snarere end fra en changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, og Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, begge læst 7. september 2026. Kilde til tier-tabellen, til sætningen "Each SDK provides the same functionality but follows the idioms and best practices of its language", til tutorialens language-tab-rækkefølge (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) og til logging-reglen citeret om print() og stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Kilde til newline-framing og stdout-purity-reglen. Kapitel 26 læser denne side i fuld længde; den citeres her for den linje, den ødelagte server overtræder.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, læst 7. september 2026. Én package, tre klienter bag én binary — web, --cli og --tui — der deler én core, ét sæt transports og én OAuth-state på disk. CLI'en producerede katalogsporene her.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, læst 7. september 2026. Kilde til resource-server-rollen; de fire token-håndteringsklausuler citeret i fuld længde; kravet om, at servere implementerer RFC 9728, og klienter bruger den til discovery; resource-parameterreglerne og definitionen af canonical URI; issuer-validation-tabellen; deprecation af Dynamic Client Registration; 401/403/400-tabellen og insufficient_scope-challengen; og stdio-undtagelsen, "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, og Transports overview, .../basic/transports. Kilde til single-endpoint POST-reglen, det dobbelte Accept-krav, MCP-Protocol-Version-headeren og dens must-match-the-body-regel, Mcp-Method- og Mcp-Name-headers beskrevet som "REQUIRED for compliance", fjernelsen af GET-streamen, sessions og Last-Event-ID, 405-vejledningen, den obligatoriske Origin-validering og klassificeringen af 2024-11-05 HTTP+SSE-transporten som Deprecated under SEP-2596. 2 3 4

  6. De fire, specifikationen læner sig op ad, med det draft, den profilerer: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. og Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, februar 2020 — resource-parameteren og den audience, den binder. Jones, M.B., Hunt, P. og Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, april 2025 — dokumentet, en 401 peger på. Meyer zu Selhausen, K. og Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, marts 2022 — iss-parameteren og exact-string-sammenligningen. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juli 2015, deprecated til denne brug. Og Jones, M. og Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, oktober 2012, sektion 3, for WWW-Authenticate-challenge-formen ovenfor.

  7. Officielt MCP registry, registry.modelcontextprotocol.io/v0/servers, crawlet 7. september 2026 med version=latest: 282 sider, 28.170 servere, optalt med registryType over distinkte servernavne. Downloadtal: api.npmjs.org/downloads/point/last-month for @modelcontextprotocol/sdk (194.679.333 for 8. august – 6. september 2026) og pypistats.org/api/packages/<name>/recent for mcp og fastmcp, begge læst samme dag. Pakkestørrelser kommer fra npm registry-dokumentet og PyPI JSON API. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, læst 7. september 2026: blandt prerequisites, "Experience with at least one programming language (Python or TypeScript examples will be shown)".


Skabt af

David Vicente Campos

Grundlægger af NeuraLIA Labs og medstifter af MyRealFood

Jeg er dataingeniør fra Universitetet i León. Jeg var med til at stifte MyRealFood, hvor jeg som CTO byggede den app, som millioner af mennesker har brugt til at spise bedre, og jeg grundlagde NeuraLIA Labs, hvor jeg bygger AI-produkter. Her skriver jeg om det, jeg har måttet forstå undervejs, sådan som jeg ville ønske, nogen havde forklaret det for mig.

Mere om forfatteren

Udgivet af NeuraLIA Labs.

Få nye indlæg i din indbakke

AI-nyheder, guides og produktopdateringer — en kort mail, når vi udgiver noget, der er værd at bruge tid på.

Vil du hellere have beskeder? De samme indlæg, her:WhatsApp-fællesskab (åbnes i en ny fane)Telegram-kanal (åbnes i en ny fane)

Kursusindeks

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev11 min læsning

Jev AI-modellen er bygget til beslutninger, ikke prosa

TypeSafe AI’s Jev får opmærksomhed, fordi den behandler softwareintelligens som et sandsynlighedsproblem: vælg den rigtige gren, tilføj tillid, og undgå at betale en LLM for at skrive tekst, når kode har brug for en beslutning.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering11 min læsning

Kontekstteknik til langsigtede AI-agenter

Langvarige agenter fejler ikke kun, fordi vinduet er lille. De fejler, når filer, tool-outputs og forældet historik fortrænger den opgave, agenten skulle færdiggøre.

Klar til at lade LIA vælge for dig?

Byg med alle AI-modeller ét sted — kom gratis i gang i dag.