Hoppa till innehållet
27/30Kapitel 27 av 30

Lansera en MCP server: TypeScript och Python, mätt

Samma server skriven två gånger: tre verktyg, en resurs, en prompt. 94 paket mot 28 och cold start 145 ms mot 709.

På den här sidan

Här är hela språkargumentet, uppmätt, innan ett enda ord av det förs fram.

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 två första raderna är jämförelsen alla vill ha. Den tredje raden är samma TypeScript-server som på första raden, startad på det sätt den faktiskt skulle distribueras — och den landar tre millisekunder från Python.

Kapitel 26 läste Model Context Protocol mot dess egen specifikation med rå JSON-RPC, eftersom rå JSON-RPC inte har något språk. Det här kapitlet har två, och argumentets tyngd hamnar här: samma server, skriven två gånger. Tre verktyg, en resurs, en prompt, båda SDK:erna, inga genvägar på någon sida. Sedan transporterna, Inspector, 401:an och siffrorna ingen har publicerat.

Servern, och varför den innehåller just de här fem sakerna

Länk till avsnittet: Servern, och varför den innehåller just de här fem sakerna

En incidentlogg. Tre verktyg, eftersom Kapitel 18:s uppdelning mellan läsningar och skrivningar måste synas: search_incidents läser, open_incident skriver och lämnar tillbaka ett handtag, resolve_incident tar det handtaget och stänger. En resurs, incidents://open, eftersom att läsa den aktuella listan är något applikationen kopplar på. En prompt, postmortem, eftersom ”skriv ihop det här” är en persons slashkommando. Det är Kapitel 26:s kontrollhierarki — modell, applikation, person — omvandlad till fem registreringar.

Handtaget spelar större roll än det ser ut. Kapitel 26 förstörde en leksakskalender genom att ha dess tillstånd i en array på modulnivå: protokollet har ingen session, så ett skapandeverktyg returnerar en ogenomskinlig identifierare och varje senare anrop tar den som ett vanligt argument. Ingenting i någon av filerna antar att anroparen är processen som öppnade den.

Här är samma verktyg i båda språken, registrerat sida vid sida:

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 vad som är samma först, eftersom det är fyndet. Båda deklarerar ett namn, en beskrivning, två beskrivna strängargument och tre annotationer; båda är en function; ingen nämner JSON-RPC, framing, stdout eller en protokollversion. De två SDK:erna har konvergerat mot samma form, vilket är vad ”Tier 1” ska betyda.1

Två skillnader är verkliga och båda återkommer senare. TypeScript beskriver argument med ett schemabibliotek — Zod här — och schemat är ett värde du skriver. Python beskriver dem med functionens egna typtips och läser dem vid importtid, vilket är skälet till att det vet saker om functionen som TypeScript-filen aldrig berättade för det. Och felvägen: TypeScript returnerar ett verktygsresultat med isError, Python kastar. Håll fast vid det.

De övriga fyra registreringarna skiljer sig inte strukturellt. Resursen är server.registerResource("open-incidents", "incidents://open", …) mot @server.resource("incidents://open", …); prompt är registerPrompt mot @server.prompt. Sista raden i varje fil är transporten: await server.connect(new StdioServerTransport()) mot server.run().

Hela filer: 81 icke-tomma rader och 3 060 byte TypeScript mot 63 och 2 555. Ta det med den nypa salt det förtjänar — radantal mäter en formatterare lika mycket som ett språk, vilket är varför inget av talen finns i rubriktabellen nedan.

Beviset på att språket är osynligt är en klient körd två gånger, på elva rader:

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

Peka den mot varje server i tur och ordning. Verklig output, nedkortad:

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

Samma verktyg, samma ordning, samma handtag. En TypeScript-klient kan inte se vad servern är skriven i, och den frågar aldrig. Det är hela löftet med ett protokoll, som håller.

Titta nu på blankstegen i det andra resultatet, för de är inte kosmetik: Python-SDK:t serialiserar payloads med pydantic_core.to_json(result, fallback=str, indent=2). Vid resursläsningen med två incidenter i listan är TypeScript-kroppen 136 tecken och 37 o200k_base token; Python-kroppen är 185 och 62. Sextioåtta procent fler token för identiska rader, betalda av den som läser in resursen i en prompt, varje gång.

Katalogen berättar samma historia med en större orsak. Båda servrarna, samma tre verktyg, tools/list vägda nyckel för nyckel:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

Pythons input-scheman är billigare — TypeScripts Zod-brygga stämplar ett $schema och ett additionalProperties på vart och ett. Hela gapet på 138 token är ett output-schema som ingen skrev. resolve_incident är annoterat -> Incident, så SDK:t härledde ett JSON Schema för returtypen och skickade med det. Det är faktiskt användbart — det är vad som låter en klient validera structuredContent — och det är 187 token av din context window som anländer på grund av ett typtips. Kapitel 24:s regel om att definitioner tränger undan materialet som spelar roll gäller även scheman du inte visste att du hade.

Slå sönder det med flit: felmeddelandet som läckte

Länk till avsnittet: Slå sönder det med flit: felmeddelandet som läckte

De två felvägarna ovan är inte ett stilval. Ge varje server ett verktyg som misslyckas på det sätt en verklig integration misslyckas, och läs vad som når 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:t lade en intern adress, en port, ett databasnamn och ett servicekonto i modellens context. Python-SDK:t lade inget av det där; tracebacken gick till stderr och stannade på servern.

Inget av det är en bugg. Båda är beslut, och Python-beslutet står i dess egen docstring: ett ToolError är ”ett fel du förutsåg” och dess meddelande returneras ”i content för modellen att läsa”; allt annat ”behandlas som en krasch: modellen ser bara Error executing tool <name>, och servern loggar tracebacken på ERROR”. Klassen för kraschfallet säger resten rakt ut — ”ingenting från originalet når klienten”.

Båda beteendena är fel halva tiden. Kapitel 18 argumenterade för att ett valideringsfel bör komma tillbaka som ett verktygsresultat modellen kan läsa och korrigera, eftersom det är raden med högst leverage i de flesta integrationer; på Python-sidan kräver det att ToolError kastas explicit, och en naken ValueError kastar bort den användbara meningen. Kapitel 30:s argument går åt andra hållet: allt ett verktyg returnerar hamnar i en context som en senare prompt injection kan försöka läsa tillbaka ur, och en ogranskad exception-sträng är den minst granskade texten i ditt system.

Regeln som överlever båda: bestäm, per verktyg, vad ett fel får säga, och skriv den strängen själv. Låt aldrig ett exceptions standardtext bestämma, i något av språken.

Slå sönder det med flit: en rad på standard output

Länk till avsnittet: Slå sönder det med flit: en rad på standard output

Den officiella handledningen anger regeln utan reservation: ”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 citerade den normativa versionen — en server ”MUST NOT write anything to its stdout that is not a valid MCP message”.2

Lägg till en rad i varje server och läs den råa strömmen:

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-varianten är värre, och skälet är inte MCP. En process vars stdout är en pipe snarare än en terminal får en blockbuffrad ström, så den felplacerade raden flushas när bufferten bestämmer — här, vid exit, efter ett svar som den skrevs före. Korruptionen syns inte där buggen finns. Lägg till flush=True, eller ett bibliotek som flushar, så flyttar den.

Sedan delen som förklarar varför det här skeppas. Mata den trasiga servern till 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 sju rader långa parsern dör direkt. Den officiella klienten och Inspector rycker båda på axlarna — de hoppar över raden och fortsätter. En regel som bara knäcker klienterna ingen använder är en regel som når produktion intakt, och därför är det värt att slå sönder den med flit här i stället för i en kunds logg.

Inspectors CLI-läge är halvan som glöms bort: npx @modelcontextprotocol/inspector --cli <command> --method tools/list skriver ut en katalog och avslutar, vilket gör det scriptbart på ett sätt som webbläsar-UI:t inte är.3

Båda SDK:erna installerades rent, i sina egna kataloger, inget delat:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
senaste protokollrevision som implementeras2025-11-252026-07-28
installerade transitiva paket9428
installerad storlek13,9 MiB44,3 MiB
filer på disk3 3862 018
tredjepartspaket laddade för att servera stdio8 av 9418 av 28
start av naken interpreter, median19,4 ms11,1 ms
spawn → tools/list besvarad, median av 25144,5 ms709,4 ms
tools/list-katalog, o200k_base token342480

Varje rad överraskar åt olika håll, vilket är varför jämförelsen är värd att köra i stället för att anta.

TypeScript installerar mer än tre gånger så många paket och mindre än en tredjedel av byten. 94 dependencies är npm-ekosystemet som är sig självt — fast-deep-equal, es-errors, dunder-proto. Pythons 28 är färre och enorma: cryptography, pydantic-core och uvicorn är kompilerade artefakter. Om din instinkt är att dependency-antal är det du ska oroa dig för, är den här raden motexemplet.

Pythons interpreter startar snabbare än Nodes, och det är inte nära — 11,1 ms mot 19,4 ms på ett tomt program. Så de 565 ms i cold-start-raden är inte språket. Det är SDK:t, och raden med laddade paket säger varför:

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 vars enda I/O är en pipe importerar en ASGI-webbserver, en HTTP-klient och ett TLS-bibliotek innan den läser sin första rad. TypeScript-SDK:t skeppar Express, Hono, jose och eventsource också — de ligger olästa på disk, eftersom paketgränsen håller dem utanför en server/stdio.js-import. Pythons paket är en enda importgraf, så import mcp är alltihop: python -X importtime tillskriver 727 ms till import mcp.server.mcpserver — en siffra uppmätt under importprofilern, vilket är varför den hamnar över de 709 ms den oprofilerade körningen tar från spawn till svar — och 269 av dem till mcp.types-subträdet ensamt — wire-typerna är Pydantic-modeller, en klass per protokollmeddelande per revision, och att bygga dem är arbete som görs vid import. Det är en designtradeoff, inte slarv — eager imports är varför Python-SDK:t kan ge dig run(transport="streamable-http") på nästa rad utan en andra installation.

Och sedan river sista raden i öppningsblocket upp argumentet. Paketera TypeScript-servern korrekt — en bin-entry, en shebang, npm link, inget att ladda ned — och starta den genom npx med --no-install, vilket är hur en publicerad stdio-server faktiskt startas:

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

Launchern kostar 568 ms per start — fyra och en halv gång hela TypeScript-SDK:ts import — och det betalas vid varje start, eftersom en MCP-värd startar en stdio-server genom att köra det kommandot. Så den ärliga formen av ”TypeScript startar fem gånger snabbare” är: det gör det, tills du distribuerar det på normalt sätt. Samma förbehåll gäller förmodligen uvx; den här maskinen hade ingen uv installerad, så den raden finns inte. Inget ouppmätt hamnar i tabellen.

Kapitel 26 täckte stdios framing. Två saker lämnades till här.

Den första: att köra en server med npx eller uvx är stdio-transporten. Det finns inget separat ”paketläge”. En värds konfiguration namnger ett kommando och argument; värden spawnar det och pratar över pipes. Det är därför ”hur distribuerar jag det här” och ”vilken transport talar det” är en fråga lokalt, och därför launcherns kostnad hör hemma i ett kapitel om att skeppa.

Den andra: stdio har inget auktorisationsavsnitt alls, och specifikationen säger det på en rad — implementationer som använder stdio ”SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Dess säkerhetsmodell är operativsystemets, och det är också dess gräns: en lokal subprocess tjänar exakt en maskin och en användare.

Den andra levande transporten är Streamable HTTP: en enda endpoint som tar emot POST, en HTTP-begäran per JSON-RPC-meddelande, och en Accept-header som måste lista både application/json och text/event-stream eftersom servern väljer per begäran vilket av de två den svarar med.5 Kapitel 14 parsade den event streamen för hand, så inget i wire-formatet är nytt — bara det som omsluter det. Tre skyldigheter i den aktuella revisionen är lätta att missa och alla tre går att testa:

Varje POST bär MCP-Protocol-Version, och dess värde måste matcha protocolVersion inuti begärans egen _meta. En mismatch är en 400 med ett header-mismatch-fel, inte en axelryckning.5

Två ytterligare headers krävs för compliance

Länk till avsnittet: Två ytterligare headers krävs för compliance

Mcp-Method speglar metoden på varje begäran; Mcp-Name speglar params.name eller params.uritools/call, resources/read och prompts/get. De finns så att en proxy kan routea utan att parsa bodies.5

De gamla formerna är borta och svarar med en vägran

Länk till avsnittet: De gamla formerna är borta och svarar med en vägran

GET-strömmen, Mcp-Session-Id och Last-Event-ID-återupptagning togs alla bort. En server som bara talar den här revisionen bör svara 405 Method Not Allowed på en GET eller DELETE, ignorera en sessionsheader utan att mynta en, och ignorera Last-Event-ID.5

Nu mätningen som omformar hela kapitlet. Skicka en begäran enligt aktuell revision till varje server över 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)"}}

Konstanterna stämmer med beteendet: Python-SDK:ts LATEST_PROTOCOL_VERSION läser 2026-07-28, TypeScript-SDK:ts läser 2025-11-25. Skicka header-mismatch-begäran från steget ovan och Python-servern svarar 400 med fel -32020 och meddelandet ”mcp-protocol-version header does not match the request envelope's protocol version”; TypeScript-SDK:t har ingen sådan kod, eftersom det inte implementerar revisionen som definierar den.

Sidan som listar båda på Tier 1 säger också ”Each SDK provides the same functionality”.1 På datumet nedan, för den aktuella revisionen, är den meningen aspirerande. Kontrollera LATEST_PROTOCOL_VERSION i SDK:t du är på väg att installera; det är en rad, och det enda påståendet i det här kapitlet som fortfarande kommer att spela roll om ett år.

Flytta en server från din laptop och en främlings klient dyker upp med en token. Det här är halvan Kapitel 26 lämnade i fred och halvan en multi-user-produkt inte kan hoppa över.

Specifikationen placerar MCP-servern i en OAuth 2.1-roll och namnger den: en skyddad MCP server är en resursserver, klienten är en OAuth-klient, och auktorisationsservern är någon annans problem.4 Ur den rollen följer fyra obligatoriska klausuler, citerade i sin helhet eftersom parafrasering är hur misstaget uppstår:

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” är anti-passthrough-regeln, och det är därför hela audience-apparaten finns. En server som spelar upp den bearer token den fick hos en tredjeparts-API är en confused deputy: den lånar ut sitt eget förtroende till den som anropade den. Regeln förbjuder återanvändningen, inte bara lagringen.

Att göra det verkställbart kräver fyra RFC:er, med ett jobb var.6 RFC 9728 är hur klienten över huvud taget hittar auktorisationsservern: MCP-servern serverar ett protected-resource-metadata-dokument och en 401 pekar på det. RFC 8707 är resource-parametern — klienten måste skicka serverns kanoniska URI i både auktorisationsbegäran och token-begäran, ”regardless of whether authorization servers support it”, så den utfärdade token namnger sin audience. RFC 9207 stänger loopen från andra hållet: klienten sparar utfärdaren före redirect och jämför den returnerade iss som exakt sträng, utan normalisering — ingen case folding, ingen default-port-elision, inget avslutande snedstreck. Och RFC 7591, Dynamic Client Registration, är nu deprecated till förmån för Client ID Metadata Documents, ”retained for backwards compatibility with authorization servers that do not support” dem.4

Koppla in det på båda servrarna med en token-verifierare som inte gör något annat än att kontrollera audience. TypeScript-stegen:

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

Båda SDK:erna serverar det dokumentet och båda pekar en 401 mot det, vilket är hela discovery-historien: en klient som aldrig har sett din server får veta var den ska autentisera sig från en vägran. 403 är ett annat djur — token är okej, scope är det inte — och challenge namnger vad som saknas så att klienten kan steppa upp i stället för att börja om.

Två steg skiljer sig, och ingen av skillnaderna finns i specifikationen. TypeScript-SDK:t vägrar en token med ingen expiry claim; Python-varianten returnerar 200, eftersom expires_at är optional på dess AccessToken och None betyder ”ingen åsikt”. Och Python-403 bär error_description="Required scope: incidents:read" utan scope-parametern som specifikationen säger att servrar bör inkludera. En verifierare är ingen plats att acceptera ett biblioteks default: audience-kontrollen är din att skriva i båda språken, och det är expiry också.

En ärlig petitess från samma körning. En GET på endpointen svarade 404 i Express-kopplingen och 400 Bad Request: Missing session ID i Python-varianten, där specifikationen ber om 405 Method Not Allowed och där ”session ID” är vokabulär som den här revisionen tog bort. Ingetdera är farligt; båda är formen av ett ekosystem mitt i migrering.

Den sista delen av att skeppa är var du publicerar, och den har ett svar med en siffra. Crawlat i dag, varje server i det officiella registret i sin senaste version:7

servers
totalt (senaste version, inte raderad)28 170
aktiv / deprecated27 853 / 317
skeppar minst ett installerbart paket13 065
endast remote — en URL, inget att installera14 696
npm8 275
PyPI3 603
OCI images867
mcpb-bundles706
NuGet / Cargo107 / 43

Två läsningar, pekande åt motsatta håll. Efter publicerade servrar leder npm med 2,3 till 1 — siffran folk citerar när de säger att ekosystemet är TypeScript. Efter downloads leder Python: under de senaste trettio dagarna tog mcp 286,7 miljoner mot @modelcontextprotocol/sdk på 194,7 miljoner, innan fastmcp på 72,1 miljoner läggs till.7 Båda är Tier 1, det normativa schemat är en schema.ts, och den officiella ”Build an MCP server”-handledningen öppnar på Python-fliken.1 Vilken halva du än hade i huvudet, är den andra halvan också sann.

Och raden som spelar större roll än någon av dem: mer än halva registret — 14 696 av 28 170 — har inget att installera. Det är webbtjänster. Transporträkningarna håller med från andra hållet: av 14 290 paketposter deklarerar 13 787 stdio; av 16 640 remote-poster deklarerar 15 570 Streamable HTTP och 1 070 deklarerar fortfarande deprecated HTTP+SSE. Så ”en MCP server är en subprocess på din laptop” beskriver en krympande minoritet, och varenda en av de 14 696 behöver avsnittet ovan snarare än en miljövariabel.

Visa detaljer

Medvetet tvåspråkigt, och prejudikatet för det.

Det här är det enda tvåspråkiga kapitlet i kursen, eftersom det ärliga svaret delar sig: registret är npm-first och downloads är Python-first, samtidigt, i dag. Att skriva ett av de två skulle ge bort halva frågan och beskriva ekosystemet fel under tiden. Det finns ett öppet prejudikat — Hugging Face MCP Course listar bland sina förkunskaper ”Experience with at least one programming language (Python or TypeScript examples will be shown)” och undervisar i båda.8 Ett protokoll vars hela värde är antalet implementationer är en dålig plats att vara enspråkig.

Daterat avsnitt: allt ovan som har ett bäst före-datum

Länk till avsnittet: Daterat avsnitt: allt ovan som har ett bäst före-datum

Läst och uppmätt den 7 september 2026, mot protokollrevision 2026-07-28.

value
@modelcontextprotocol/sdk1.30.0, publicerad 27 juli 2026; 4 322 438 byte uppackad, 693 filer, 17 direkta dependencies
senaste revision den implementerar2025-11-25
mcp (PyPI)2.1.1, publicerad 25 augusti 2026; 357 912-byte wheel, plus mcp-types 2.1.1 på 69 656 byte
senaste revision den implementerar2026-07-28
SDK tiersTypeScript, Python, C#, Go, Rust på Tier 1; Java, Ruby på Tier 2; Swift, PHP, Kotlin på Tier 3
registry servers28 170
downloads, senaste 30 dagarnamcp 286 653 871 · fastmcp 72 097 269 · @modelcontextprotocol/sdk 194 679 333

En migreringsnotis som inte är en siffra. I mcp 2.x döptes FastMCP om till MCPServer, och nästan varje onlinehandledning öppnar fortfarande med den gamla importen. SDK:t skeppar en modul vars enda syfte är att förklara det, vilket är den mest omtänksamma deprecation i det här kapitlet:

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 framför dig är rekommendationen tråkig, vilket är ett gott tecken.

Om servern lever inuti en webbapplikation du redan kör, skriv den i TypeScript. Samma process, samma deploy, samma request handler; Streamable HTTP är en endpoint du lägger bredvid de andra; och 13,9 MiB och 145 ms är gratis eftersom runtime redan var uppe. Det är de flesta av de 14 696 remote-servrarna.

Om servern kapslar in data tooling, skriv den i Python. Det du exponerar är pandas, en warehouse-klient, en notebooks värde av transforms, och en server i ett annat språk vore ett subprocess-anrop med ett schema på sig. Sjuhundra millisekunders import i en tjänst som startar en gång är ingen kostnad; i en subprocess som en värd relanserar hela dagen är det det.

Och tills vidare trumfar revisionsraden båda. Om du behöver 2026-07-28 — multi-round-trip-begäranden, resultType, cache-hints, server/discover — har ett av de två SDK:erna det i dag och det andra inte.

Du kan nu skeppa samma server i vilket språk som helst, försvara valet med en tabell i stället för en preferens, köra den över båda levande transporterna och ge den en token den kommer att vägra.

Det du byggde är fortfarande en function: ett schema, en endpoint, en deterministisk sak modellen anropar. En hel klass av kunskap passar inte i den formen — hur vi skriver en postmortem, vilka fält våra incidentrapporter behöver, i vilken ordning vi gör saker och varför. Det är procedur, det är prosa, och att tvinga in det i en verktygsbeskrivning är hur system prompts växer till två tusen token som betalas på varenda turn oavsett om konversationen handlar om incidenter eller inte.

Kapitel 28 är det andra svaret: en mapp med en SKILL.md i sig som modellen läser i stället för anropar, laddad i tre nivåer så att referensmaterialet nästan inte kostar något förrän den turn då det behövs. Den har inget huvudspråk, och det är det första den lär ut.


Allt här mättes den 7 september 2026, på Node 22.22.3 och Python 3.14.4, mot @modelcontextprotocol/sdk 1.30.0 med zod 3.25.76 och mcp 2.1.1, var och en installerad i sin egen engångskatalog. Tider är medianer av 25 starter, wall clock från spawn till raden som bär tools/list-svaret; token-antal är o200k_base via tiktoken över JSON för varje definition. Ingen betald API anropades: inget här behöver en modell.

De två servrarna är 81 och 63 icke-tomma rader; ett av deras tre verktyg återges ovan i båda språken, och de andra fyra registreringarna skiljer sig bara så som beskrivits. Python-SDK:ts policy för felavslöjande citeras från docstrings i ToolError och UnexpectedToolError i mcp/server/mcpserver/exceptions.py; pretty-printing-defaulten är pydantic_core.to_json(result, fallback=str, indent=2) i mcp/server/mcpserver/resources/types.py och utilities/func_metadata.py. Protokollversionskonstanterna är LATEST_PROTOCOL_VERSION i mcp_types/version.py och i TypeScript-SDK:ts types.js, båda lästa från de installerade paketen snarare än från en changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, och Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, båda lästa 7 september 2026. Källa för tier-tabellen, för meningen ”Each SDK provides the same functionality but follows the idioms and best practices of its language”, för handledningens språkfliksordning (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), och för logging-regeln citerad om print() och stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. Källa för newline framing och renhetsregeln för stdout. Kapitel 26 läser den här sidan i sin helhet; den citeras här för raden den trasiga servern bryter mot.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, läst 7 september 2026. Ett paket, tre klienter bakom en binär — web, --cli och --tui — som delar en kärna, en uppsättning transporter och ett OAuth-tillstånd på disk. CLI:t producerade katalogspåren här.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, läst 7 september 2026. Källa för resursserverrollen; de fyra token-hanteringsklausulerna citerade i sin helhet; kravet att servrar implementerar RFC 9728 och klienter använder den för discovery; reglerna för resource-parametern och definitionen av kanonisk URI; issuer-valideringstabellen; deprecation av Dynamic Client Registration; tabellen 401/403/400 och insufficient_scope-challenge; samt stdio-undantaget, ”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, och Transports overview, .../basic/transports. Källa för POST-regeln med en enda endpoint, det dubbla Accept-kravet, MCP-Protocol-Version-headern och dess regel om att måste matcha kroppen, Mcp-Method- och Mcp-Name-headers beskrivna som ”REQUIRED for compliance”, borttagningen av GET-strömmen, sessioner och Last-Event-ID, 405-vägledningen, den obligatoriska Origin-valideringen och klassificeringen av 2024-11-05 HTTP+SSE-transporten som Deprecated under SEP-2596. 2 3 4

  6. De fyra som specifikationen lutar sig mot, med utkastet den profilerar: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. och Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, februari 2020 — resource-parametern och den audience den binder. Jones, M.B., Hunt, P. och Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, april 2025 — dokumentet som en 401 pekar på. Meyer zu Selhausen, K. och Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, mars 2022 — iss-parametern och exakt-sträng-jämförelsen. Richer, J. (red.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, juli 2015, deprecated för denna användning. Och Jones, M. och Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, oktober 2012, avsnitt 3, för WWW-Authenticate-challenge-formen ovan.

  7. Officiellt MCP-register, registry.modelcontextprotocol.io/v0/servers, crawlat 7 september 2026 med version=latest: 282 sidor, 28 170 servrar, summerade av registryType över distinkta servernamn. Download-siffror: api.npmjs.org/downloads/point/last-month för @modelcontextprotocol/sdk (194 679 333 för 8 augusti–6 september 2026) och pypistats.org/api/packages/<name>/recent för mcp och fastmcp, båda lästa samma dag. Paketstorlekar kommer från npm registry-dokumentet och PyPI JSON API. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, läst 7 september 2026: bland förkunskaperna, ”Experience with at least one programming language (Python or TypeScript examples will be shown)”.


Skapad av

David Vicente Campos

Grundare av NeuraLIA Labs och medgrundare av MyRealFood

Jag är dataingenjör från Universitetet i León. Jag var med och grundade MyRealFood, där jag som CTO byggde appen som miljontals människor har använt för att äta bättre, och jag grundade NeuraLIA Labs, där jag bygger AI-produkter. Här skriver jag om det jag har behövt förstå längs vägen, så som jag önskar att någon hade förklarat det för mig.

Mer om författaren

Publicerad av NeuraLIA Labs.

Få nya inlägg i din inkorg

AI-nyheter, guider och produktuppdateringar — ett kort mejl när vi publicerar något som är värt din tid.

Kursindex

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jevLästid 11 min

Jevs AI-modell är byggd för beslut, inte prosa

TypeSafe AI:s Jev väcker uppmärksamhet eftersom den behandlar mjukvaruintelligens som ett sannolikhetsproblem: välj rätt gren, lägg till konfidens och undvik att betala en LLM för att skriva text när koden behöver ett beslut.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineeringLästid 11 min

Kontextteknik för AI-agenter med lång horisont

Långkörande agenter misslyckas inte bara för att fönstret är litet. De misslyckas när filer, verktygsutdata och gammal historik tränger undan uppgiften agenten skulle slutföra.

Redo att låta LIA välja åt dig?

Bygg med alla AI-modeller på ett ställe – kom igång gratis i dag.