MCP Server tuotantoon: TypeScript ja Python mitattuina
Sama server kahdesti — kolme työkalua, resource ja prompt — ja puntariin. 94 pakettia vastaan 28, cold start 145 ms vastaan 709.
Tällä sivulla
Tässä on koko kieliväite mitattuna, ennen kuin siitä on kirjoitettu sanaakaan.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msEnsimmäiset kaksi riviä ovat vertailu, jonka kaikki haluavat. Kolmas rivi on saman TypeScript-serverin ensimmäiseltä riviltä, käynnistettynä niin kuin se oikeasti jaeltaisiin — ja se jää kolmen millisekunnin päähän Pythonista.
Luku 26 luki Model Context Protocolin sen omaa määrittelyä vasten raakalla JSON-RPC:llä, koska raakalla JSON-RPC:llä ei ole kieltä. Tässä luvussa niitä on kaksi, ja väitteen paino osuu tähän: sama server, kirjoitettuna kahdesti. Kolme työkalua, yksi resource, yksi prompt, molemmat SDK:t, ei oikoteitä kummallakaan puolella. Sitten transportit, inspector, 401 ja luvut, joita kukaan ei ole julkaissut.
Server ja miksi siinä on juuri nämä viisi asiaa
Linkki osioon: Server ja miksi siinä on juuri nämä viisi asiaaIncident-loki. Kolme työkalua, koska luvun 18 jako lukuihin ja kirjoituksiin täytyy tehdä näkyväksi: search_incidents lukee, open_incident kirjoittaa ja palauttaa handlen, resolve_incident ottaa handlen ja sulkee. Yksi resource, incidents://open, koska nykyisen listan lukeminen on asia, jonka sovellus liittää. Yksi prompt, postmortem, koska ”kirjoita tästä yhteenveto” on henkilön slash-komento. Se on luvun 26 hallintahierarkia — malli, sovellus, ihminen — muutettuna viideksi rekisteröinniksi.
Handle on tärkeämpi kuin miltä se näyttää. Luku 26 rikkoi leluesimerkin kalenterin pitämällä sen tilan moduulitason taulukossa: protokollassa ei ole sessiota, joten luontityökalu palauttaa läpinäkymättömän tunnisteen ja jokainen myöhempi kutsu ottaa sen tavallisena argumenttina. Kumpikaan tiedosto ei oleta, että kutsuja on sama prosessi, joka avasi sen.
Tässä on sama työkalu molemmilla kielillä, rekisteröitynä rinnakkain:
server.registerTool(
"resolve_incident",
{
description:
"Close an incident by handle and record its cause.",
inputSchema: {
id: z.string().describe(
"The handle returned by open_incident, e.g. INC-3."),
cause: z.string().describe(
"One sentence. What actually broke."),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
},
},
async ({ id, cause }) => {
const at = OPEN.findIndex((i) => i.id === id);
if (at < 0) {
return { isError: true, content: [{ type: "text",
text: `No open incident ${id}. ` +
`Call search_incidents first.` }] };
}
const [done] = OPEN.splice(at, 1);
return { content: [{ type: "text",
text: JSON.stringify({ ...done, cause }) }] };
},
);@server.tool(
description=
"Close an incident by handle and record its cause.",
annotations=ToolAnnotations(
readOnlyHint=False,
destructiveHint=True,
idempotentHint=True,
),
)
def resolve_incident(
id: Annotated[str, Field(description=
"The handle returned by open_incident, e.g. INC-3.")],
cause: Annotated[str, Field(description=
"One sentence. What actually broke.")],
) -> Incident:
for at, i in enumerate(OPEN):
if i["id"] == id:
done = OPEN.pop(at)
return {**done, "cause": cause}
raise ValueError(
f"No open incident {id}. Call search_incidents first.")Lue ensin se, mikä on samaa, koska se on havainto. Molemmat määrittelevät nimen, kuvauksen, kaksi kuvattua string-argumenttia ja kolme annotaatiota; molemmat ovat yksi funktio; kumpikaan ei mainitse JSON-RPC:tä, kehystystä, stdout tai protokollaversiota. Kaksi SDK:ta on päätynyt samaan muotoon, mitä ”Tier 1”:n pitäisi tarkoittaa.1
Kaksi eroa on todellisia, ja molemmat palaavat myöhemmin. TypeScript kuvaa argumentit skeemakirjastolla — tässä Zodilla — ja skeema on arvo, jonka kirjoitat. Python kuvaa ne funktion omilla tyyppivihjeillä ja lukee ne import-vaiheessa, minkä vuoksi se tietää funktiosta asioita, joita TypeScript-tiedosto ei koskaan kertonut. Ja virhepolku: TypeScript palauttaa työkalutuloksen arvolla isError, Python heittää poikkeuksen. Pidä se mielessä.
Neljä muuta rekisteröintiä eivät eroa rakenteellisesti mitenkään. Resource on server.registerResource("open-incidents", "incidents://open", …) vastaan @server.resource("incidents://open", …); prompt on registerPrompt vastaan @server.prompt. Kummankin tiedoston viimeinen rivi on transport: await server.connect(new StdioServerTransport()) vastaan server.run().
Kokonaiset tiedostot: 81 ei-tyhjää riviä ja 3 060 tavua TypeScriptiä vastaan 63 ja 2 555. Suhtaudu siihen ansaitulla suolalla — rivimäärät mittaavat yhtä paljon formatteria kuin kieltä, minkä vuoksi kumpikaan luku ei ole alla olevassa otsikkotaulukossa.
Yksi client, molemmat serverit
Linkki osioon: Yksi client, molemmat serveritTodiste siitä, että kieli on näkymätön, on yksi client ajettuna kahdesti, yhdellätoista rivillä:
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));Osoita se vuorollaan kumpaankin serveriin. Todellinen output, lyhennettynä:
$ 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}"}]Samat työkalut, sama järjestys, sama handle. TypeScript-client ei voi tietää, millä server on kirjoitettu, eikä se koskaan kysy. Siinä on koko protokollan lupaus, ja se pitää.
Katso nyt whitespacea toisessa tuloksessa, koska se ei ole kosmeettista: Python SDK serialisoi payloadit asetuksella pydantic_core.to_json(result, fallback=str, indent=2). Resource-luennassa, jossa listassa on kaksi incidentiä, TypeScript-body on 136 merkkiä ja 37 o200k_base token; Python-body on 185 ja 62. Kuusikymmentäkahdeksan prosenttia enemmän token identtisistä riveistä, maksajana se, joka lukee resource sisään prompt, joka kerta.
Katalogissa on sama tarina, suuremmalla syyllä. Molemmat serverit, samat kolme työkalua, tools/list punnittuna avain kerrallaan:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
Pythonin input-skeemat ovat halvempia — TypeScriptin Zod-silta leimaa kuhunkin $schema ja additionalProperties. Koko 138 token -ero on output-skeema, jota kukaan ei kirjoittanut. resolve_incident on annotoitu -> Incident, joten SDK johdatti paluutyypille JSON Schema -kuvauksen ja lähetti sen. Se on aidosti hyödyllinen — sen avulla client voi validoida structuredContent — ja se on 187 token sinun context window sisään vain tyyppivihjeen takia. Luvun 24 sääntö siitä, miten määritelmät tunkevat tärkeän materiaalin tieltä, pätee myös skeemoihin, joiden olemassaolosta et tiennyt.
Riko se tahallaan: vuotanut virheilmoitus
Linkki osioon: Riko se tahallaan: vuotanut virheilmoitusYllä olevat kaksi virhepolkua eivät ole tyylivalinta. Anna kummallekin serverille työkalu, joka epäonnistuu kuten oikea integraatio epäonnistuu, ja lue, mitä mallille päätyy.
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 laittoi mallin contextiin sisäisen osoitteen, portin, tietokannan nimen ja palvelutilin. Python SDK ei laittanut sinne niistä mitään; traceback meni kohteeseen stderr ja pysyi serverillä.
Kumpikaan ei ole bugi. Molemmat ovat päätöksiä, ja Pythonin päätös on kirjoitettu sen omaan docstringiin: ToolError on ”epäonnistuminen, jonka osasit odottaa”, ja sen viesti palautetaan ”kohteessa content mallin luettavaksi”; kaikki muu ”käsitellään kaatumisena: malli näkee vain Error executing tool <name>, ja server kirjaa tracebackin tasolla ERROR”. Kaatumistapauksen luokka sanoo loput ääneen — ”mitään alkuperäisestä ei päädy clientille”.
Molemmat käyttäytymiset ovat väärin puolet ajasta. Luku 18 väitti, että validointivirheen pitäisi tulla takaisin työkalutuloksena, jonka malli voi lukea ja korjata, koska se on useimmissa integraatioissa suurimman vipuvaikutuksen rivi; Python-puolella se edellyttää ToolError heittämistä eksplisiittisesti, ja paljas ValueError heittää hyödyllisen lauseen pois. Luvun 30 argumentti kulkee toiseen suuntaan: kaikki, mitä työkalu palauttaa, päätyy contextiin, josta myöhempi prompt injection voi yrittää lukea sen takaisin, ja tarkistamaton poikkeusmerkkijono on järjestelmäsi vähiten auditoitua tekstiä.
Sääntö, joka selviää molemmista: päätä työkalukohtaisesti, mitä epäonnistuminen saa sanoa, ja kirjoita se string itse. Älä koskaan anna poikkeuksen oletustekstin päättää, kummallakaan kielellä.
Riko se tahallaan: yksi rivi standard outputiin
Linkki osioon: Riko se tahallaan: yksi rivi standard outputiinVirallinen tutorial sanoo säännön ilman varauksia: ”STDIO-pohjaisille servereille: älä koskaan kirjoita stdoutiin. Stdoutiin kirjoittaminen korruptoi JSON-RPC-viestit ja rikkoo serverisi. Funktio print() kirjoittaa oletuksena stdoutiin, joten pidä se kokonaan poissa STDIO-serveristä.”1 Luku 26 lainasi normatiivista versiota — server ”MUST NOT write anything to its stdout that is not a valid MCP message”.2
Lisää yksi rivi kumpaankin serveriin ja lue raaka stream:
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingPythonin versio on pahempi, eikä syy ole MCP. Prosessi, jonka stdout on pipe eikä terminaali, saa block-buffered-streamin, joten harharivi flushataan milloin bufferi päättää — tässä, exitissä, sen jälkeen kun vastaus, jota ennen se kirjoitettiin, on jo mennyt. Korruptio ei näy siinä missä bugi on. Lisää flush=True tai kirjasto, joka flushaa, ja se siirtyy.
Sitten se osa, joka selittää miksi tämä päätyy tuotantoon. Syötä rikkinäinen server kolmelle clientille:
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 warningSeitsemän rivin parser kuolee heti. Virallinen client ja Inspector kohauttavat olkiaan — ne ohittavat rivin ja jatkavat. Sääntö, joka rikkoo vain clientit, joita kukaan ei käytä, on sääntö, joka pääsee tuotantoon ehjänä, minkä vuoksi se kannattaa rikkoa tahallaan tässä eikä asiakkaan lokissa.
Inspectorin CLI-tila on se puolikas, joka unohtuu: npx @modelcontextprotocol/inspector --cli <command> --method tools/list tulostaa katalogin ja poistuu, mikä tekee siitä skriptattavan tavalla, johon selain-UI ei taivu.3
Taulukko
Linkki osioon: TaulukkoMolemmat SDK:t asentuivat siististi omiin hakemistoihinsa, mitään jakamatta:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| latest protocol revision implemented | 2025-11-25 | 2026-07-28 |
| transitive packages installed | 94 | 28 |
| installed size | 13.9 MiB | 44.3 MiB |
| files on disk | 3,386 | 2,018 |
| third-party packages loaded to serve stdio | 8 of 94 | 18 of 28 |
| bare interpreter start, median | 19.4 ms | 11.1 ms |
spawn → tools/list answered, median of 25 | 144.5 ms | 709.4 ms |
tools/list catalogue, o200k_base tokens | 342 | 480 |
Jokainen rivi yllättää eri suuntaan, minkä vuoksi vertailu kannattaa ajaa eikä olettaa.
TypeScript asentaa yli kolme kertaa enemmän paketteja ja alle kolmanneksen tavuista. 94 riippuvuutta on npm-ekosysteemi omana itsenään — fast-deep-equal, es-errors, dunder-proto. Pythonin 28 ovat harvempia ja valtavia: cryptography, pydantic-core ja uvicorn ovat käännettyjä artefakteja. Jos vaistosi sanoo, että riippuvuuksien määrä on huolenaihe, tämä rivi on vastaesimerkki.
Pythonin interpreter käynnistyy nopeammin kuin Node, eikä ero ole pieni — 11,1 ms vastaan 19,4 ms tyhjällä ohjelmalla. Siispä cold start -rivin 565 ms ei ole kieli. Se on SDK, ja ladattujen pakettien rivi kertoo miksi:
TypeScript 8 of 94 sdk, zod, zod-to-json-schema, ajv, ajv-formats,
fast-deep-equal, fast-uri, json-schema-traverse
Python 18 of 28 mcp, mcp_types, pydantic, pydantic_core, anyio,
starlette, uvicorn, sse_starlette, httpx2,
cryptography, _cffi_backend, opentelemetry, click, …Server, jonka ainoa I/O on pipe, importtaa ASGI-web serverin, HTTP-clientin ja TLS-kirjaston ennen kuin se lukee ensimmäisen rivinsä. TypeScript SDK toimittaa myös Expressin, Honon, jose ja eventsource — ne pysyvät levyllä lukematta, koska pakettiraja pitää ne poissa server/stdio.js-importista. Pythonin paketti on yksi import-graafi, joten import mcp on kaikki: python -X importtime antaa 727 ms kohteelle import mcp.server.mcpserver — luku on mitattu import-profilerin alla, minkä vuoksi se tulee suuremmaksi kuin 709 ms, jonka profiloimaton ajo vie spawnista vastaukseen — ja 269 niistä pelkästään mcp.types-alipuulle — wire-tyypit ovat Pydantic-malleja, yksi luokka protokollaviestiä ja revisiota kohti, ja niiden rakentaminen on importissa tehtävää työtä. Se on suunnitteluvaihtokauppa, ei huolimattomuutta — innokkaat importit ovat syy siihen, että Python SDK voi antaa sinulle run(transport="streamable-http") seuraavalla rivillä ilman toista asennusta.
Ja sitten avauslohkon viimeinen rivi kumoaa argumentin. Paketoi TypeScript-server kunnolla — bin-entry, shebang, npm link, ei mitään ladattavaa — ja käynnistä se npx kautta asetuksella --no-install, kuten julkaistu stdio server oikeasti käynnistetään:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msLauncher maksaa 568 ms per käynnistys — neljä ja puoli kertaa koko TypeScript SDK:n import — ja se maksetaan joka launchilla, koska MCP-host käynnistää stdio serverin ajamalla sen komennon. Siksi rehellinen muoto väitteestä ”TypeScript käynnistyy viisi kertaa nopeammin” on: niin se tekee, kunnes jaat sen normaalilla tavalla. Sama varaus oletettavasti koskee uvx; tällä koneella ei ollut uv asennettuna, joten sitä riviä ei ole. Mikään mittaamaton ei päädy taulukkoon.
Kaksi transportia, ja vain kaksi
Linkki osioon: Kaksi transportia, ja vain kaksiLuku 26 käsitteli stdion kehystyksen. Kaksi asiaa se jätti tähän.
Ensimmäinen: serverin ajaminen komennolla npx tai uvx on stdio transport. Erillistä ”package modea” ei ole. Hostin konfiguraatio nimeää komennon ja argumentit; host spawnaa sen ja puhuu pipejen yli. Siksi ”miten jakan tämän” ja ”mitä transportia se puhuu” ovat paikallisesti yksi kysymys, ja siksi launcherin kustannus kuuluu shipping-aiheiseen lukuun.
Toinen: stdiolla ei ole authorization-osiota lainkaan, ja määrittely sanoo sen yhdellä rivillä — stdiota käyttävien toteutusten ”SHOULD NOT follow this specification, and instead retrieve credentials from the environment”.4 Sen tietoturvamalli on käyttöjärjestelmän malli, ja niin on sen rajakin: paikallinen aliprosessi palvelee täsmälleen yhtä konetta ja yhtä käyttäjää.
Toinen elävä transport on Streamable HTTP: yksi endpoint, joka hyväksyy POSTin, yksi HTTP-pyyntö per JSON-RPC-viesti, ja Accept-header, jonka täytyy listata sekä application/json että text/event-stream, koska server valitsee pyyntökohtaisesti, kummalla kahdesta se vastaa.5 Luku 14 parsi tuon event streamin käsin, joten wire-formaatissa ei ole mitään uutta — vain se, mikä kietoutuu sen ympärille. Nykyisen revision kolme velvoitetta on helppo ohittaa, ja kaikki kolme ovat testattavia:
Versio-headerin täytyy olla samaa mieltä bodyn kanssa
Linkki osioon: Versio-headerin täytyy olla samaa mieltä bodyn kanssaJokainen POST kantaa MCP-Protocol-Version, ja sen arvon täytyy vastata pyynnön oman _meta sisällä olevaa protocolVersion. Ristiriita on 400 header-mismatch-virheellä, ei olankohautus.5
Kaksi muuta headeria vaaditaan compliancea varten
Linkki osioon: Kaksi muuta headeria vaaditaan compliancea vartenMcp-Method peilaa methodin jokaisessa pyynnössä; Mcp-Name peilaa params.name tai params.uri kohteissa tools/call, resources/read ja prompts/get. Ne ovat olemassa, jotta proxy voi reitittää ilman bodyjen parsimista.5
Vanhat muodot ovat poissa, ja niihin vastataan kieltäytymällä
Linkki osioon: Vanhat muodot ovat poissa, ja niihin vastataan kieltäytymälläGET-stream, Mcp-Session-Id ja Last-Event-ID-jatkaminen poistettiin kaikki. Serverin, joka puhuu vain tätä revisiota, pitäisi vastata 405 Method Not Allowed GETiin tai DELETEen, ohittaa session-header luomatta sellaista ja ohittaa Last-Event-ID.5
Nyt mittaus, joka kehystää koko luvun uudelleen. Lähetä nykyisen revision pyyntö kummallekin serverille HTTP:n yli.
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)"}}Vakiot vastaavat käyttäytymistä: Python SDK:n LATEST_PROTOCOL_VERSION lukee 2026-07-28, TypeScript SDK:n lukee 2025-11-25. Lähetä yllä olevasta vaiheesta header-mismatch-pyyntö, ja Python-server vastaa 400 virheellä -32020 ja viestillä ”mcp-protocol-version header does not match the request envelope's protocol version”; TypeScript SDK:ssa ei ole sellaista koodia, koska se ei toteuta revisiota, joka sen määrittelee.
Sivu, joka listaa molemmat Tier 1 -tasolle, sanoo myös ”Each SDK provides the same functionality”.1 Alla olevana päivänä, nykyiselle revisiolle, tuo lause on tavoitetila. Tarkista LATEST_PROTOCOL_VERSION SDK:ssa, jota olet asentamassa; se on yksi rivi, ja ainoa tämän luvun väite, jolla on väliä vielä vuoden päästä.
401 ja lause, jota lainata
Linkki osioon: 401 ja lause, jota lainataSiirrä server pois läppäriltäsi, ja vieraan client ilmestyy token mukanaan. Tämä on se puolikas, jonka luku 26 jätti rauhaan, ja se puolikas, jota monen käyttäjän tuote ei voi ohittaa.
Määrittely sijoittaa MCP serverin OAuth 2.1 -rooliin ja nimeää sen: suojattu MCP server on resource server, client on OAuth client, ja authorization server on jonkun muun ongelma.4 Tuosta roolista seuraa neljä pakollista lauseketta, lainattuna kokonaisina, koska niiden parafrasointi on tapa, jolla virhe syntyy:
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” on anti-passthrough-sääntö, ja siksi koko audience-koneisto on olemassa. Server, joka toistaa sille annetun bearer tokenin kolmannen osapuolen API:lle, on confused deputy: se lainaa oman luottamuksensa sille, joka sitä kutsui. Sääntö kieltää uudelleenkäytön, ei vain tallentamista.
Sen tekeminen täytäntöönpantavaksi vaatii neljä RFC:tä, jokaisella yksi tehtävä.6 RFC 9728 on tapa, jolla client ylipäätään löytää authorization serverin: MCP server palvelee protected-resource-metadata-dokumentin, ja 401 osoittaa siihen. RFC 8707 on resource-parametri — clientin täytyy lähettää serverin kanoninen URI sekä authorization-pyynnössä että token-pyynnössä, ”riippumatta siitä, tukevatko authorization serverit sitä”, jotta myönnetty token nimeää audiencensa. RFC 9207 sulkee silmukan toiselta puolelta: client kirjaa issuerin ennen uudelleenohjausta ja vertaa palautettua iss täsmälleen stringinä, ilman normalisointia — ei kirjainkoon taittoa, ei oletusportin poisjättöä, ei trailing slashia. Ja RFC 7591, Dynamic Client Registration, on nyt deprekoitu Client ID Metadata Documents -dokumenttien hyväksi, ”retained for backwards compatibility with authorization servers that do not support” niitä.4
Kytke se molempiin servereihin token-varmentimella, joka ei tee muuta kuin tarkistaa audiencen. TypeScript-tikkaat:
no token 401 WWW-Authenticate: Bearer error="invalid_token",
error_description="Missing Authorization header",
scope="incidents:read",
resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server 401 error_description="token audience is not this server"
no exp claim 401 error_description="Token has no expiration time"
right aud, no scope 403 error="insufficient_scope", scope="incidents:read"
right aud + scope 200 {"result":{"tools":[…]}}{"resource":"http://127.0.0.1:8931/mcp",
"authorization_servers":["https://auth.example.com/"],
"scopes_supported":["incidents:read","incidents:write"],
"resource_name":"Incidents"}Molemmat SDK:t palvelevat tuon dokumentin, ja molemmat osoittavat 401 siihen, mikä on koko discovery-tarina: client, joka ei ole koskaan nähnyt serveriäsi, oppii kieltäytymisestä, missä autentikoitua. 403 on eri eläin — token on kunnossa, scope ei — ja challenge nimeää, mitä puuttuu, jotta client voi nostaa tasoa eikä aloittaa alusta.
Kaksi porrasta eroaa, eikä kumpikaan ero ole määrittelyssä. TypeScript SDK hylkää tokenin, jossa ei ole expiry claimia; Python palauttaa 200, koska expires_at on valinnainen sen AccessToken-kohdassa ja None tarkoittaa ”ei mielipidettä”. Ja Pythonin 403 kantaa error_description="Required scope: incidents:read" ilman scope-parametria, jonka määrittely sanoo serverien pitäisi sisällyttää. Varmentimessa ei ole paikkaa hyväksyä kirjaston oletusta: audience-tarkistus on sinun kirjoitettavasi kummallakin kielellä, ja niin on expirykin.
Yksi rehellinen nillitys samasta ajosta. GET endpointiin vastasi 404 Express-kytkennässä ja 400 Bad Request: Missing session ID Pythonin versiossa, kun määrittely pyytää 405 Method Not Allowed ja kun ”session ID” on sanastoa, jonka tämä revisio poisti. Kumpikaan ei ole vaarallinen; molemmat ovat mid-migration-ekosysteemin muoto.
Missä serverit oikeasti elävät
Linkki osioon: Missä serverit oikeasti elävätShippingin viimeinen osa on se, missä julkaiset, ja siihen on vastaus numerolla. Crawlattu tänään, jokainen virallisen registryn server uusimmassa versiossaan:7
| servers | |
|---|---|
| total (latest version, not deleted) | 28,170 |
| active / deprecated | 27,853 / 317 |
| ship at least one installable package | 13,065 |
| remote only — a URL, nothing to install | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
Kaksi luentaa, vastakkaisiin suuntiin. Julkaistuissa servereissä npm johtaa 2,3:1 — luku, jota ihmiset siteeraavat sanoessaan, että ekosysteemi on TypeScript. Latauksissa Python johtaa: viimeisten kolmenkymmenen päivän aikana mcp sai 286,7 miljoonaa vastaan @modelcontextprotocol/sdk 194,7 miljoonassa, ennen kuin lisätään fastmcp 72,1 miljoonalla.7 Molemmat ovat Tier 1, normatiivinen skeema on schema.ts, ja virallinen ”Build an MCP server” -tutorial avautuu Python-välilehdelle.1 Kumpi puoli sinulla olikaan mielessä, myös toinen puoli on totta.
Ja rivi, jolla on enemmän väliä kuin kummallakaan: yli puolella registrystä — 14 696 / 28 170 — ei ole mitään asennettavaa. Ne ovat web-palveluja. Transport-laskurit ovat samaa mieltä toisesta suunnasta: 14 290 package-entrystä 13 787 ilmoittaa stdion; 16 640 remote-entrystä 15 570 ilmoittaa Streamable HTTP:n ja 1 070 ilmoittaa edelleen deprekoidun HTTP+SSE:n. Siis ”MCP server on aliprosessi läppärilläsi” kuvaa kutistuvaa vähemmistöä, ja jokainen noista 14 696:sta tarvitsee yllä olevan osion eikä ympäristömuuttujaa.
Näytä lisätiedot
Tahallaan kaksikielinen, ja ennakkotapaus sille.
Tämä on kurssin ainoa kaksikielinen luku, koska rehellinen vastaus jakautuu: registry on npm-first ja lataukset ovat Python-first, samaan aikaan, tänään. Vain toisen kirjoittaminen luovuttaisi puolet kysymyksestä ja kuvaisi ekosysteemin väärin samalla. Avoimessa on ennakkotapaus — Hugging Facen MCP Course listaa esitiedoissa ”Experience with at least one programming language (Python or TypeScript examples will be shown)” ja opettaa molemmat.8 Protokolla, jonka koko arvo on toteutusten määrä, on huono paikka olla yksikielinen.
Päivätty osio: kaikki yllä, jolla on säilyvyysaika
Linkki osioon: Päivätty osio: kaikki yllä, jolla on säilyvyysaikaLuettu ja mitattu 7. syyskuuta 2026, protokollarevisiota 2026-07-28 vasten.
| value | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, julkaistu 27. heinäkuuta 2026; 4 322 438 tavua purettuna, 693 tiedostoa, 17 suoraa riippuvuutta |
| latest revision it implements | 2025-11-25 |
mcp (PyPI) | 2.1.1, julkaistu 25. elokuuta 2026; 357 912 tavun wheel, plus mcp-types 2.1.1, 69 656 tavua |
| latest revision it implements | 2026-07-28 |
| SDK tiers | TypeScript, Python, C#, Go, Rust Tier 1 -tasolla; Java, Ruby Tier 2 -tasolla; Swift, PHP, Kotlin Tier 3 -tasolla |
| registry servers | 28,170 |
| downloads, last 30 days | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
Yksi migraatiohuomio, joka ei ole numero. Versiossa mcp 2.x, FastMCP nimettiin uudelleen muotoon MCPServer, ja lähes jokainen verkon tutorial avautuu yhä vanhalla importilla. SDK toimittaa moduulin, jonka ainoa tarkoitus on selittää tämä, mikä on tämän luvun huomaavaisin deprekaatio:
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.Kumman siis valitset
Linkki osioon: Kumman siis valitsetKun taulukko on edessäsi, suositus on tylsä, mikä on hyvä merkki.
Jos server elää jo ajamasi web-sovelluksen sisällä, kirjoita se TypeScriptillä. Sama prosessi, sama deploy, sama request handler; Streamable HTTP on endpoint, jonka lisäät muiden viereen; ja 13,9 MiB sekä 145 ms ovat ilmaisia, koska runtime oli jo pystyssä. Se on suurin osa 14 696 remote-serveristä.
Jos server käärii data tooling -maailmaa, kirjoita se Pythonilla. Se, mitä altistat, on pandas, warehouse-client, notebookillinen muunnoksia, ja server toisella kielellä olisi skeemaan pukeutunut aliprosessikutsu. Seitsemänsataa millisekuntia importia palvelussa, joka käynnistyy kerran, ei ole kustannus; aliprosessissa, jonka host käynnistää uudelleen pitkin päivää, se on.
Ja toistaiseksi revisiorivi ohittaa molemmat. Jos tarvitset 2026-07-28:n — multi-round-trip-pyynnöt, resultType, cache hints, server/discover — toisessa kahdesta SDK:sta se on tänään ja toisessa ei.
Minne tämä jatkuu
Linkki osioon: Minne tämä jatkuuVoit nyt shipata saman serverin kummallakin kielellä, puolustaa valintaa taulukolla mieltymyksen sijaan, ajaa sen molempien elävien transportien yli ja antaa sille token, jonka se hylkää.
Rakentamasi asia on yhä function: skeema, endpoint, deterministinen asia, jonka malli kutsuu. Kokonainen tietoluokka ei sovi siihen muotoon — miten me kirjoitamme postmortemin, mitä kenttiä incident-raporttimme tarvitsevat, missä järjestyksessä teemme asiat ja miksi. Se on prosessi, se on proosaa, ja sen pakottaminen työkalukuvaukseen on tapa, jolla system promptit kasvavat kahteentuhanteen token, jotka maksetaan jokaisella yksittäisellä vuorolla riippumatta siitä, koskeeko keskustelu incidentejä.
Luku 28 on toinen vastaus: kansio, jossa on SKILL.md ja jonka malli lukee kutsumisen sijaan, ladattuna kolmella tasolla niin, että viitemateriaali ei maksa juuri mitään ennen kuin sitä tarvitaan kyseisellä vuorolla. Sillä ei ole pääkieltä, ja se on ensimmäinen asia, jonka se opettaa.
Lähteet ja menetelmä
Linkki osioon: Lähteet ja menetelmäKaikki tässä mitattiin 7. syyskuuta 2026 Node 22.22.3:lla ja Python 3.14.4:llä, vasten @modelcontextprotocol/sdk 1.30.0:aa yhdessä zod 3.25.76:n kanssa sekä mcp 2.1.1:tä, kukin asennettuna omaan kertakäyttöhakemistoonsa. Ajat ovat 25 käynnistyksen mediaaneja, wall clock kohteesta spawn riville, jolla tools/list-vastaus on; token-määrät ovat o200k_base kautta tiktoken kunkin määritelmän JSONista. Maksullista API:a ei kutsuttu: mikään tässä ei tarvitse mallia.
Kaksi serveriä ovat 81 ja 63 ei-tyhjää riviä; yksi niiden kolmesta työkalusta on toistettu yllä molemmilla kielillä, ja neljä muuta rekisteröintiä eroavat vain kuvatulla tavalla. Python SDK:n virhepaljastuskäytäntö on lainattu kohteiden ToolError ja UnexpectedToolError docstringeistä tiedostossa mcp/server/mcpserver/exceptions.py; pretty-printing-oletus on pydantic_core.to_json(result, fallback=str, indent=2) kohteissa mcp/server/mcpserver/resources/types.py ja utilities/func_metadata.py. Protokollaversiovakiot ovat LATEST_PROTOCOL_VERSION kohteessa mcp_types/version.py ja TypeScript SDK:n kohteessa types.js, molemmat luettu asennetuista paketeista eikä changelogista.
Viitteet
Linkki osioon: Viitteet-
SDKs,
modelcontextprotocol.io/docs/sdk, ja Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, molemmat luettu 7. syyskuuta 2026. Lähde tier-taulukolle, lauseelle ”Each SDK provides the same functionality but follows the idioms and best practices of its language”, tutorialin kielivälilehtien järjestykselle (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go) sekä lainatulle logging-säännölle koskienprint()jastdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Lähde newline-kehystykselle jastdout-puhtaussäännölle. Luku 26 lukee tämän sivun kokonaan; siihen viitataan tässä sen rivin vuoksi, jota rikkinäinen server rikkoo. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, luettu 7. syyskuuta 2026. Yksi paketti, kolme clientiä yhden binäärin takana — web,--clija--tui— jakamassa yhden ytimen, yhden joukon transporteja ja yhden OAuth-tilan levyllä. CLI tuotti tässä käytetyt katalogitracet. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, luettu 7. syyskuuta 2026. Lähde resource-server-roolille; neljälle token-käsittelylausekkeelle, jotka lainataan kokonaan; vaatimukselle, että serverit toteuttavat RFC 9728:n ja clientit käyttävät sitä discoveryyn;resource-parametrin säännöille ja kanonisen URI:n määritelmälle; issuer-validation-taulukolle; Dynamic Client Registrationin deprekaatiolle;401/403/400-taulukolle jainsufficient_scope-challengelle; sekä stdio-poikkeukselle, ”Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.” ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, ja Transports overview,.../basic/transports. Lähde yhden endpointin POST-säännölle, kaksinkertaiselleAccept-vaatimukselle,MCP-Protocol-Version-headerille ja sen must-match-the-body-säännölle,Mcp-Method- jaMcp-Name-headereille, joita kuvataan sanoilla ”REQUIRED for compliance”, GET-streamin, sessioiden jaLast-Event-IDpoistolle,405-ohjeistukselle, pakolliselleOrigin-validoinnille sekä 2024-11-05 HTTP+SSE-transportin luokittelulle Deprecated-tilaan SEP-2596:n alla. ↩ ↩2 ↩3 ↩4 -
Neljä, joihin määrittely nojaa, sekä draft, jonka profiilin se määrittelee: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. ja Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, helmikuu 2020 —resource-parametri ja audience, johon se sitoo. Jones, M.B., Hunt, P. ja Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, huhtikuu 2025 — dokumentti, johon401osoittaa. Meyer zu Selhausen, K. ja Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, maaliskuu 2022 —iss-parametri ja exact-string-vertailu. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, heinäkuu 2015, deprekoitu tähän käyttöön. Sekä Jones, M. ja Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, lokakuu 2012, osio 3, yllä olevanWWW-Authenticate-challengen muodolle. ↩ -
Virallinen MCP registry,
registry.modelcontextprotocol.io/v0/servers, crawlattu 7. syyskuuta 2026 komennollaversion=latest: 282 sivua, 28 170 serveriä, lasketturegistryTypemukaan erillisistä server-nimistä. Latausluvut:api.npmjs.org/downloads/point/last-monthkohteelle@modelcontextprotocol/sdk(194 679 333 ajalta 8. elokuuta – 6. syyskuuta 2026) japypistats.org/api/packages/<name>/recentkohteillemcpjafastmcp, molemmat luettu samana päivänä. Pakettikoot tulevat npm registry -dokumentista ja PyPI JSON API:sta. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, yksikkö 0, luettu 7. syyskuuta 2026: esitiedoissa muun muassa ”Experience with at least one programming language (Python or TypeScript examples will be shown)”. ↩