Κυκλοφορήστε ένα MCP Server: TypeScript και Python, με μετρήσεις
Ο ίδιος server γράφτηκε δύο φορές — τρία tools, ένα resource, ένα prompt — και ζυγίστηκε: 94 packages έναντι 28, cold start 145 ms έναντι 709.
Σε αυτή τη σελίδα
Ορίστε ολόκληρο το επιχείρημα για τη γλώσσα, μετρημένο, πριν ειπωθεί έστω λέξη γι’ αυτό.
node ./incidents.js 144.5 ms
python incidents.py 709.4 ms
npx incidents-mcp 712.6 msΟι δύο πρώτες γραμμές είναι η σύγκριση που όλοι θέλουν. Η τρίτη γραμμή είναι ο ίδιος TypeScript server από την πρώτη γραμμή, εκκινημένος όπως θα διανεμόταν πραγματικά — και καταλήγει τρία χιλιοστά του δευτερολέπτου από την Python.
Το Κεφάλαιο 26 διάβασε το Model Context Protocol απέναντι στη δική του προδιαγραφή με ωμό JSON-RPC, επειδή το ωμό JSON-RPC δεν έχει γλώσσα. Αυτό το κεφάλαιο έχει δύο, και το βάρος του επιχειρήματος πέφτει εδώ: ο ίδιος server, γραμμένος δύο φορές. Τρία εργαλεία, ένας πόρος, ένα prompt, και τα δύο SDKs, χωρίς συντομεύσεις σε καμία πλευρά. Έπειτα οι μεταφορές, ο inspector, το 401 και οι αριθμοί που δεν έχει δημοσιεύσει κανείς.
Ο server, και γιατί έχει αυτά τα πέντε πράγματα μέσα του
Σύνδεσμος στην ενότητα: Ο server, και γιατί έχει αυτά τα πέντε πράγματα μέσα τουΈνα αρχείο καταγραφής incidents. Τρία εργαλεία, επειδή ο διαχωρισμός του Κεφαλαίου 18 ανάμεσα σε reads και writes πρέπει να είναι ορατός: το search_incidents διαβάζει, το open_incident γράφει και επιστρέφει ένα handle, το resolve_incident παίρνει αυτό το handle και κλείνει. Ένας πόρος, incidents://open, επειδή η ανάγνωση της τρέχουσας λίστας είναι κάτι που προσαρτά η εφαρμογή. Ένα prompt, postmortem, επειδή το «γράψε το» είναι slash command ενός ανθρώπου. Αυτή είναι η ιεραρχία ελέγχου του Κεφαλαίου 26 — model, εφαρμογή, άνθρωπος — μετατρεμμένη σε πέντε registrations.
Το handle έχει μεγαλύτερη σημασία απ’ όσο φαίνεται. Το Κεφάλαιο 26 έσπασε ένα παιδικό calendar κρατώντας το state του σε ένα array σε επίπεδο module: το πρωτόκολλο δεν έχει session, άρα ένα εργαλείο δημιουργίας επιστρέφει ένα αδιαφανές identifier και κάθε μεταγενέστερη κλήση το παίρνει ως συνηθισμένο argument. Τίποτα σε κανένα από τα δύο αρχεία δεν υποθέτει ότι ο caller είναι το process που το άνοιξε.
Ορίστε το ίδιο εργαλείο και στις δύο γλώσσες, καταχωρισμένο δίπλα δίπλα:
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.")Διαβάστε πρώτα τι είναι ίδιο, επειδή αυτό είναι το εύρημα. Και τα δύο δηλώνουν ένα όνομα, μια περιγραφή, δύο περιγεγραμμένα string arguments και τρία annotations· και τα δύο είναι μία function· κανένα δεν αναφέρει JSON-RPC, framing, stdout ή έκδοση πρωτοκόλλου. Τα δύο SDKs συνέκλιναν στο ίδιο σχήμα, που είναι αυτό που υποτίθεται ότι σημαίνει «Tier 1».1
Δύο διαφορές είναι πραγματικές και και οι δύο επιστρέφουν παρακάτω. Η TypeScript περιγράφει arguments με μια schema library — εδώ Zod — και το schema είναι μια τιμή που γράφετε. Η Python τα περιγράφει με τα ίδια τα type hints της function και τα διαβάζει στο import time, γι’ αυτό ξέρει πράγματα για τη function που το TypeScript αρχείο δεν της είπε ποτέ. Και το error path: η TypeScript επιστρέφει ένα tool result με isError, η Python κάνει raise. Κρατήστε το αυτό.
Τα άλλα τέσσερα registrations δεν διαφέρουν δομικά σε τίποτα. Ο πόρος είναι server.registerResource("open-incidents", "incidents://open", …) απέναντι σε @server.resource("incidents://open", …)· το prompt είναι registerPrompt απέναντι σε @server.prompt. Η τελευταία γραμμή κάθε αρχείου είναι η μεταφορά: await server.connect(new StdioServerTransport()) απέναντι σε server.run().
Ολόκληρα αρχεία: 81 μη κενές γραμμές και 3.060 bytes TypeScript απέναντι σε 63 και 2.555. Πάρτε το με το ανάλογο αλάτι — οι μετρήσεις γραμμών μετρούν έναν formatter όσο και μια γλώσσα, γι’ αυτό κανένας από τους δύο αριθμούς δεν βρίσκεται στον headline πίνακα παρακάτω.
Ένας client, και οι δύο servers
Σύνδεσμος στην ενότητα: Ένας client, και οι δύο serversΗ απόδειξη ότι η γλώσσα είναι αόρατη είναι ένας client που τρέχει δύο φορές, σε έντεκα γραμμές:
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));Στρέψτε τον σε κάθε server με τη σειρά. Πραγματικό output, κομμένο:
$ 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}"}]Ίδια εργαλεία, ίδια σειρά, ίδιο handle. Ένας TypeScript client δεν μπορεί να καταλάβει σε τι είναι γραμμένος ο server, και δεν ρωτά ποτέ. Αυτή είναι ολόκληρη η υπόσχεση ενός πρωτοκόλλου, και κρατά.
Τώρα κοιτάξτε το whitespace στο δεύτερο αποτέλεσμα, επειδή δεν είναι αισθητικό: το Python SDK serialises payloads με pydantic_core.to_json(result, fallback=str, indent=2). Στο resource read με δύο incidents στη λίστα, το TypeScript body είναι 136 χαρακτήρες και 37 o200k_base tokens· το Python body είναι 185 και 62. Εξήντα οκτώ τοις εκατό περισσότερα tokens για πανομοιότυπες γραμμές, πληρωμένα από όποιον διαβάζει τον πόρο σε ένα prompt, κάθε φορά.
Το catalogue έχει την ίδια ιστορία με μεγαλύτερη αιτία. Και οι δύο servers, τα ίδια τρία εργαλεία, το tools/list ζυγισμένο key προς key:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| σύνολο | 342 | 480 |
Τα input schemas της Python είναι φθηνότερα — η Zod bridge της TypeScript σφραγίζει ένα $schema και ένα additionalProperties σε καθένα. Ολόκληρο το χάσμα των 138 tokens είναι ένα output schema που δεν έγραψε κανείς. Το resolve_incident είναι annotated -> Incident, άρα το SDK παρήγαγε ένα JSON Schema για τον τύπο επιστροφής και το έστειλε. Είναι πραγματικά χρήσιμο — είναι αυτό που επιτρέπει σε έναν client να κάνει validate το structuredContent — και είναι 187 tokens από το context window σας που φτάνουν λόγω ενός type hint. Ο κανόνας του Κεφαλαίου 24 για definitions που στριμώχνουν έξω το υλικό που έχει σημασία ισχύει και για schemas που δεν ξέρατε ότι είχατε.
Σπάστε το επίτηδες: το error message που διέρρευσε
Σύνδεσμος στην ενότητα: Σπάστε το επίτηδες: το error message που διέρρευσεΤα δύο error paths παραπάνω δεν είναι επιλογή στιλ. Δώστε σε κάθε server ένα εργαλείο που αποτυγχάνει όπως αποτυγχάνει μια πραγματική integration, και διαβάστε τι φτάνει στο model.
TypeScript {"content":[{"type":"text","text":
"connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
"isError":true}
Python {"content":[{"text":"Error executing tool boom","type":"text"}],
"isError":true}Το TypeScript SDK έβαλε μια εσωτερική διεύθυνση, ένα port, ένα όνομα database και ένα service account στο context του model. Το Python SDK δεν έβαλε τίποτα από αυτά εκεί· το traceback πήγε στο stderr και έμεινε στον server.
Κανένα από τα δύο δεν είναι bug. Και τα δύο είναι αποφάσεις, και η Python το γράφει στο δικό της docstring: ένα ToolError είναι «μια αποτυχία που είχατε προβλέψει» και το μήνυμά του επιστρέφεται «στο content για να το διαβάσει το model»· οτιδήποτε άλλο «αντιμετωπίζεται ως crash: το model βλέπει μόνο Error executing tool <name>, και ο server καταγράφει το traceback στο ERROR». Η κλάση για την περίπτωση crash λέει τα υπόλοιπα καθαρά — «τίποτα από το αρχικό δεν φτάνει στον client».
Και οι δύο συμπεριφορές είναι λάθος τις μισές φορές. Το Κεφάλαιο 18 υποστήριξε ότι ένα validation error πρέπει να επιστρέφει ως tool result που το model μπορεί να διαβάσει και να διορθώσει, επειδή αυτή είναι η γραμμή με τη μεγαλύτερη μόχλευση στις περισσότερες integrations· στην Python πλευρά αυτό απαιτεί ρητό raise ToolError, και ένα γυμνό ValueError πετάει τη χρήσιμη πρόταση. Το επιχείρημα του Κεφαλαίου 30 πηγαίνει προς την άλλη κατεύθυνση: ό,τι επιστρέφει ένα εργαλείο προσγειώνεται σε ένα context που ένα μεταγενέστερο prompt injection μπορεί να προσπαθήσει να ξαναδιαβάσει, και ένα μη ελεγμένο exception string είναι το λιγότερο audited κείμενο στο σύστημά σας.
Ο κανόνας που επιβιώνει και από τα δύο: αποφασίστε, ανά εργαλείο, τι επιτρέπεται να πει μια αποτυχία, και γράψτε εσείς αυτό το string. Μην αφήνετε ποτέ το default text ενός exception να αποφασίζει, σε καμία γλώσσα.
Σπάστε το επίτηδες: μία γραμμή στο standard output
Σύνδεσμος στην ενότητα: Σπάστε το επίτηδες: μία γραμμή στο standard outputΤο επίσημο tutorial δηλώνει τον κανόνα χωρίς επιφύλαξη: «Για STDIO-based servers: μην γράφετε ποτέ στο stdout. Η εγγραφή στο stdout θα διαφθείρει τα JSON-RPC messages και θα σπάσει τον server σας. Η function print() γράφει στο stdout από default, άρα κρατήστε την τελείως έξω από έναν STDIO server.»1 Το Κεφάλαιο 26 παρέθεσε τη normative εκδοχή — ένας server «MUST NOT write anything to its stdout that is not a valid MCP message».2
Προσθέστε μία γραμμή σε κάθε server και διαβάστε το raw stream:
TypeScript incidents server starting
{"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}
Python {"jsonrpc":"2.0","id":1,"result":{ … }}
incidents server startingΗ Python είναι χειρότερη, και ο λόγος δεν είναι το MCP. Ένα process του οποίου το stdout είναι pipe και όχι terminal παίρνει block-buffered stream, άρα η stray line γίνεται flush όποτε το αποφασίσει το buffer — εδώ, στο exit, μετά από μια απάντηση πριν από την οποία είχε γραφτεί. Η διαφθορά δεν εμφανίζεται εκεί όπου είναι το bug. Προσθέστε flush=True, ή μια library που κάνει flush, και μετακινείται.
Έπειτα το κομμάτι που εξηγεί γιατί αυτό φτάνει σε production. Δώστε τον σπασμένο server σε τρεις clients:
naive parser, dirty server SyntaxError: Unexpected token 'i',
"incidents "... is not valid JSON
SDK client, dirty server tools: search_incidents, open_incident, resolve_incident
MCP Inspector, dirty server full catalogue, no warningΟ parser των επτά γραμμών πεθαίνει αμέσως. Ο επίσημος client και ο Inspector σηκώνουν τους ώμους — προσπερνούν τη γραμμή και συνεχίζουν. Ένας κανόνας που σπάει μόνο τους clients που δεν χρησιμοποιεί κανείς είναι ένας κανόνας που φτάνει intact σε production, γι’ αυτό αξίζει να τον σπάσουμε επίτηδες εδώ και όχι στο log ενός πελάτη.
Το CLI mode του Inspector είναι το μισό που ξεχνιέται: το npx @modelcontextprotocol/inspector --cli <command> --method tools/list τυπώνει ένα catalogue και βγαίνει, πράγμα που το κάνει scriptable με τρόπο που δεν είναι το browser UI.3
Ο πίνακας
Σύνδεσμος στην ενότητα: Ο πίνακαςΚαι τα δύο SDKs εγκαταστάθηκαν καθαρά, στους δικούς τους καταλόγους, χωρίς τίποτα κοινό:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| τελευταία υλοποιημένη protocol revision | 2025-11-25 | 2026-07-28 |
| εγκατεστημένα transitive packages | 94 | 28 |
| εγκατεστημένο μέγεθος | 13.9 MiB | 44.3 MiB |
| αρχεία στον δίσκο | 3,386 | 2,018 |
| third-party packages loaded για να εξυπηρετήσουν stdio | 8 από 94 | 18 από 28 |
| bare interpreter start, median | 19.4 ms | 11.1 ms |
spawn → απαντήθηκε tools/list, median από 25 | 144.5 ms | 709.4 ms |
tools/list catalogue, o200k_base tokens | 342 | 480 |
Κάθε γραμμή εκπλήσσει προς διαφορετική κατεύθυνση, γι’ αυτό αξίζει να τρέξετε τη σύγκριση αντί να υποθέσετε.
Η TypeScript εγκαθιστά πάνω από τριπλάσια packages και λιγότερο από το ένα τρίτο των bytes. Οι 94 dependencies είναι το npm ecosystem που κάνει αυτό που κάνει — fast-deep-equal, es-errors, dunder-proto. Οι 28 της Python είναι λιγότερες και τεράστιες: cryptography, pydantic-core και uvicorn είναι compiled artefacts. Αν το ένστικτό σας λέει ότι αυτό για το οποίο πρέπει να ανησυχείτε είναι το dependency count, αυτή η γραμμή είναι το αντιπαράδειγμα.
Ο interpreter της Python ξεκινά πιο γρήγορα από τον Node, και όχι λίγο — 11.1 ms απέναντι σε 19.4 ms σε ένα άδειο πρόγραμμα. Άρα τα 565 ms στη γραμμή cold-start δεν είναι η γλώσσα. Είναι το SDK, και η γραμμή loaded-packages λέει γιατί:
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 του οποίου το μόνο I/O είναι ένα pipe κάνει import έναν ASGI web server, έναν HTTP client και μια TLS library πριν διαβάσει την πρώτη του γραμμή. Το TypeScript SDK φέρνει επίσης Express, Hono, jose και eventsource — μένουν στον δίσκο αδιάβαστα, επειδή το package boundary τα κρατά έξω από ένα server/stdio.js import. Το Python package είναι ένα import graph, άρα το import mcp είναι όλο μαζί: το python -X importtime αποδίδει 727 ms στο import mcp.server.mcpserver — αριθμός μετρημένος κάτω από τον import profiler, γι’ αυτό βγαίνει πάνω από τα 709 ms που παίρνει το unprofiled run από spawn έως answer — και 269 από αυτά στο subtree mcp.types μόνο — οι wire types είναι Pydantic models, μία κλάση ανά protocol message ανά revision, και η κατασκευή τους είναι εργασία που γίνεται στο import. Αυτό είναι design trade, όχι προχειρότητα — τα eager imports είναι ο λόγος που το Python SDK μπορεί να σας δώσει run(transport="streamable-http") στην επόμενη γραμμή χωρίς δεύτερο install.
Και μετά η τελευταία γραμμή του αρχικού block ακυρώνει το επιχείρημα. Πακετάρετε σωστά τον TypeScript server — ένα bin entry, ένα shebang, npm link, τίποτα για download — και εκκινήστε τον μέσω npx με --no-install, που είναι ο τρόπος με τον οποίο ξεκινά πραγματικά ένας published stdio server:
node ./incidents.js 144.5 ms
npx incidents-mcp 712.6 ms (+568.1 ms of launcher)
python incidents.py 709.4 msΟ launcher κοστίζει 568 ms ανά start — τεσσεράμισι φορές ολόκληρο το TypeScript SDK import — και πληρώνεται σε κάθε launch, επειδή ένας MCP host ξεκινά έναν stdio server τρέχοντας αυτή την εντολή. Άρα η ειλικρινής μορφή του «η TypeScript ξεκινά πέντε φορές πιο γρήγορα» είναι: ναι, μέχρι να τη διανείμετε με τον κανονικό τρόπο. Η ίδια επιφύλαξη πιθανώς ισχύει για το uvx· αυτό το μηχάνημα δεν είχε εγκατεστημένο uv, άρα αυτή η γραμμή δεν υπάρχει. Τίποτα αμέτρητο δεν μπαίνει στον πίνακα.
Δύο μεταφορές, και μόνο δύο
Σύνδεσμος στην ενότητα: Δύο μεταφορές, και μόνο δύοΤο Κεφάλαιο 26 κάλυψε το framing του stdio. Δύο πράγματα τα άφησε για εδώ.
Το πρώτο: το να τρέχετε έναν server με npx ή uvx είναι η stdio μεταφορά. Δεν υπάρχει ξεχωριστό «package mode». Το configuration ενός host ονομάζει μια εντολή και arguments· ο host την κάνει spawn και μιλά πάνω από τα pipes. Γι’ αυτό το «πώς το διανέμω αυτό» και το «ποια μεταφορά μιλά» είναι μία ερώτηση τοπικά, και γι’ αυτό το κόστος του launcher ανήκει σε ένα κεφάλαιο για shipping.
Το δεύτερο: το stdio δεν έχει καθόλου authorization section, και η προδιαγραφή το λέει σε μία γραμμή — implementations που χρησιμοποιούν stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 Το security model του είναι αυτό του operating system, και το ίδιο και το όριό του: ένα τοπικό subprocess εξυπηρετεί ακριβώς ένα μηχάνημα και έναν χρήστη.
Η άλλη live μεταφορά είναι Streamable HTTP: ένα ενιαίο endpoint που δέχεται POST, ένα HTTP request ανά JSON-RPC message, και ένα header Accept που πρέπει να απαριθμεί και το application/json και το text/event-stream επειδή ο server επιλέγει ανά request με ποιο από τα δύο θα απαντήσει.5 Το Κεφάλαιο 14 έκανε parse αυτό το event stream με το χέρι, άρα τίποτα στο wire format δεν είναι νέο — μόνο αυτό που το τυλίγει. Τρεις υποχρεώσεις της τρέχουσας revision είναι εύκολο να ξεφύγουν και και οι τρεις είναι testable:
Το version header πρέπει να συμφωνεί με το body
Σύνδεσμος στην ενότητα: Το version header πρέπει να συμφωνεί με το bodyΚάθε POST φέρει MCP-Protocol-Version, και η τιμή του πρέπει να ταιριάζει με το protocolVersion μέσα στο δικό του _meta του request. Ένα mismatch είναι 400 με header-mismatch error, όχι ένα shrug.5
Δύο ακόμη headers απαιτούνται για compliance
Σύνδεσμος στην ενότητα: Δύο ακόμη headers απαιτούνται για complianceΤο Mcp-Method αντικατοπτρίζει τη μέθοδο σε κάθε request· το Mcp-Name αντικατοπτρίζει το params.name ή το params.uri σε tools/call, resources/read και prompts/get. Υπάρχουν ώστε ένας proxy να μπορεί να κάνει route χωρίς να κάνει parse bodies.5
Τα παλιά shapes έφυγαν, και απαντούν με άρνηση
Σύνδεσμος στην ενότητα: Τα παλιά shapes έφυγαν, και απαντούν με άρνησηΤο GET stream, το Mcp-Session-Id και το Last-Event-ID resumption αφαιρέθηκαν όλα. Ένας server που μιλά μόνο αυτή τη revision πρέπει να απαντά 405 Method Not Allowed σε GET ή DELETE, να αγνοεί ένα session header χωρίς να mint ένα, και να αγνοεί το Last-Event-ID.5
Τώρα η μέτρηση που αναπλαισιώνει ολόκληρο το κεφάλαιο. Στείλτε ένα current-revision request σε κάθε server μέσω HTTP.
Python 200 {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
"tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}
TypeScript {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
2025-03-26, 2024-11-05, 2024-10-07)"}}Οι constants συμφωνούν με τη συμπεριφορά: το LATEST_PROTOCOL_VERSION του Python SDK διαβάζει 2026-07-28, της TypeScript διαβάζει 2025-11-25. Στείλτε το header-mismatch request από το βήμα παραπάνω και ο Python server απαντά 400 με error -32020 και το μήνυμα «mcp-protocol-version header does not match the request envelope's protocol version»· το TypeScript SDK δεν έχει τέτοιο code, επειδή δεν υλοποιεί τη revision που το ορίζει.
Η σελίδα που τα κατατάσσει και τα δύο ως Tier 1 λέει επίσης «Each SDK provides the same functionality».1 Την ημερομηνία παρακάτω, για την τρέχουσα revision, αυτή η πρόταση είναι aspirational. Ελέγξτε το LATEST_PROTOCOL_VERSION στο SDK που πρόκειται να εγκαταστήσετε· είναι μία γραμμή, και ο μόνος ισχυρισμός σε αυτό το κεφάλαιο που θα εξακολουθεί να έχει σημασία σε έναν χρόνο.
Το 401, και η πρόταση που πρέπει να παραθέτετε
Σύνδεσμος στην ενότητα: Το 401, και η πρόταση που πρέπει να παραθέτετεΜετακινήστε έναν server έξω από το laptop σας και εμφανίζεται ο client ενός αγνώστου με ένα token. Αυτό είναι το μισό που άφησε ήσυχο το Κεφάλαιο 26 και το μισό που ένα multi-user προϊόν δεν μπορεί να παραλείψει.
Η προδιαγραφή βάζει τον MCP server σε έναν OAuth 2.1 ρόλο και τον ονομάζει: ένας protected MCP server είναι resource server, ο client είναι OAuth client, και ο authorization server είναι πρόβλημα κάποιου άλλου.4 Από αυτόν τον ρόλο, τέσσερις mandatory clauses, παρατεθειμένες ολόκληρες επειδή η παράφρασή τους είναι ο τρόπος με τον οποίο γίνεται το λάθος:
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» είναι ο κανόνας anti-passthrough, και γι’ αυτό υπάρχει ολόκληρος ο μηχανισμός audience. Ένας server που επαναπαίζει το bearer token που του δόθηκε σε ένα third-party API είναι confused deputy: δανείζει τη δική του εμπιστοσύνη σε όποιον τον κάλεσε. Ο κανόνας απαγορεύει την επαναχρησιμοποίηση, όχι μόνο την αποθήκευση.
Για να γίνει αυτό enforceable χρειάζονται τέσσερα RFCs, ένα job το καθένα.6 Το RFC 9728 είναι ο τρόπος με τον οποίο ο client βρίσκει τον authorization server εξαρχής: ο MCP server σερβίρει ένα protected-resource-metadata document και ένα 401 δείχνει προς αυτό. Το RFC 8707 είναι το parameter resource — ο client πρέπει να στείλει το canonical URI του server και στο authorization request και στο token request, «ανεξάρτητα από το αν οι authorization servers το υποστηρίζουν», ώστε το issued token να ονομάζει το audience του. Το RFC 9207 κλείνει τον κύκλο από την άλλη πλευρά: ο client καταγράφει τον issuer πριν κάνει redirect και συγκρίνει το επιστρεφόμενο iss με exact string, χωρίς normalisation — χωρίς case folding, χωρίς default-port elision, χωρίς trailing slash. Και το RFC 7591, Dynamic Client Registration, είναι πλέον deprecated υπέρ των Client ID Metadata Documents, «retained for backwards compatibility with authorization servers that do not support» them.4
Συνδέστε το αυτό και στους δύο servers με έναν token verifier που δεν κάνει τίποτα παρά να ελέγχει το audience. Η TypeScript σκάλα:
no token 401 WWW-Authenticate: Bearer error="invalid_token",
error_description="Missing Authorization header",
scope="incidents:read",
resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server 401 error_description="token audience is not this server"
no exp claim 401 error_description="Token has no expiration time"
right aud, no scope 403 error="insufficient_scope", scope="incidents:read"
right aud + scope 200 {"result":{"tools":[…]}}{"resource":"http://127.0.0.1:8931/mcp",
"authorization_servers":["https://auth.example.com/"],
"scopes_supported":["incidents:read","incidents:write"],
"resource_name":"Incidents"}Και τα δύο SDKs σερβίρουν αυτό το document και και τα δύο δείχνουν ένα 401 προς αυτό, που είναι ολόκληρη η ιστορία του discovery: ένας client που δεν έχει ξαναδεί τον server σας μαθαίνει πού να αυθεντικοποιηθεί από μια άρνηση. Το 403 είναι άλλο ζώο — το token είναι εντάξει, το scope όχι — και το challenge ονομάζει τι λείπει ώστε ο client να μπορέσει να ανέβει επίπεδο αντί να ξεκινήσει από την αρχή.
Δύο σκαλοπάτια διαφέρουν, και καμία διαφορά δεν είναι στην προδιαγραφή. Το TypeScript SDK αρνείται ένα token χωρίς expiry claim· της Python επιστρέφει 200, επειδή το expires_at είναι optional στο AccessToken της και το None σημαίνει «καμία γνώμη». Και το Python 403 μεταφέρει error_description="Required scope: incidents:read" χωρίς το parameter scope που η προδιαγραφή λέει ότι οι servers πρέπει να περιλαμβάνουν. Ένας verifier δεν είναι μέρος για να αποδεχθείτε library default: ο audience check είναι δικός σας να τον γράψετε σε οποιαδήποτε γλώσσα, και το ίδιο και το expiry.
Μια ειλικρινής λεπτομέρεια από το ίδιο run. Ένα GET στο endpoint απάντησε 404 στο Express wiring και 400 Bad Request: Missing session ID στην Python, εκεί όπου η προδιαγραφή ζητά 405 Method Not Allowed και όπου το «session ID» είναι λεξιλόγιο που αυτή η revision αφαίρεσε. Κανένα δεν είναι επικίνδυνο· και τα δύο είναι το σχήμα ενός ecosystem στη μέση μιας migration.
Πού ζουν πραγματικά οι servers
Σύνδεσμος στην ενότητα: Πού ζουν πραγματικά οι serversΤο τελευταίο κομμάτι του shipping είναι πού δημοσιεύετε, και έχει απάντηση με αριθμό. Crawled σήμερα, κάθε server στο official registry στην τελευταία του έκδοση:7
| servers | |
|---|---|
| σύνολο (latest version, όχι deleted) | 28,170 |
| active / deprecated | 27,853 / 317 |
| διαθέτουν τουλάχιστον ένα installable package | 13,065 |
| remote only — ένα URL, τίποτα για install | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
mcpb bundles | 706 |
| NuGet / Cargo | 107 / 43 |
Δύο αναγνώσεις, που δείχνουν σε αντίθετες κατευθύνσεις. Με βάση published servers, το npm προηγείται 2,3 προς 1 — ο αριθμός που παραθέτουν όταν λένε ότι το ecosystem είναι TypeScript. Με βάση downloads, προηγείται η Python: τις τελευταίες τριάντα ημέρες το mcp πήρε 286,7 εκατομμύρια απέναντι στο @modelcontextprotocol/sdk με 194,7 εκατομμύρια, πριν προστεθεί το fastmcp με 72,1 εκατομμύρια.7 Και τα δύο είναι Tier 1, το normative schema είναι ένα schema.ts, και το επίσημο tutorial «Build an MCP server» ανοίγει στο Python tab.1 Όποιο μισό κι αν είχατε στο μυαλό σας, και το άλλο μισό είναι επίσης αληθινό.
Και η γραμμή που έχει περισσότερη σημασία από οποιαδήποτε από τις δύο: πάνω από το μισό registry — 14.696 από 28.170 — δεν έχει τίποτα για install. Αυτά είναι web services. Τα transport tallies συμφωνούν από την άλλη πλευρά: από 14.290 package entries, 13.787 δηλώνουν stdio· από 16.640 remote entries, 15.570 δηλώνουν Streamable HTTP και 1.070 δηλώνουν ακόμα το deprecated HTTP+SSE. Άρα το «ένας MCP server είναι subprocess στο laptop σας» περιγράφει μια μειούμενη μειοψηφία, και καθένας από τους 14.696 χρειάζεται την ενότητα παραπάνω αντί για environment variable.
Εμφάνιση λεπτομερειών
Εσκεμμένα δίγλωσσο, και το προηγούμενο γι’ αυτό.
Αυτό είναι το μόνο δίγλωσσο κεφάλαιο στο course, επειδή η ειλικρινής απάντηση χωρίζεται: το registry είναι npm-first και τα downloads είναι Python-first, ταυτόχρονα, σήμερα. Το να γράφαμε μόνο ένα από τα δύο θα παρέδιδε το μισό ερώτημα και θα περιέγραφε λάθος το ecosystem στην πορεία. Υπάρχει precedent ανοιχτά — το Hugging Face MCP Course απαριθμεί στα prerequisites του «Experience with at least one programming language (Python or TypeScript examples will be shown)», και διδάσκει και τα δύο.8 Ένα πρωτόκολλο του οποίου όλη η αξία είναι ο αριθμός των implementations είναι κακό μέρος για μονογλωσσία.
Ενότητα με ημερομηνία: όλα τα παραπάνω που έχουν διάρκεια ζωής στο ράφι
Σύνδεσμος στην ενότητα: Ενότητα με ημερομηνία: όλα τα παραπάνω που έχουν διάρκεια ζωής στο ράφιΔιαβάστηκε και μετρήθηκε στις 7 Σεπτεμβρίου 2026, απέναντι στην protocol revision 2026-07-28.
| τιμή | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0, published 27 Ιουλίου 2026· 4,322,438 bytes unpacked, 693 αρχεία, 17 direct dependencies |
| latest revision που υλοποιεί | 2025-11-25 |
mcp (PyPI) | 2.1.1, published 25 Αυγούστου 2026· 357,912-byte wheel, συν mcp-types 2.1.1 στα 69,656 bytes |
| latest revision που υλοποιεί | 2026-07-28 |
| SDK tiers | TypeScript, Python, C#, Go, Rust στο Tier 1· Java, Ruby στο Tier 2· Swift, PHP, Kotlin στο Tier 3 |
| registry servers | 28,170 |
| downloads, τελευταίες 30 ημέρες | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
Μία migration note που δεν είναι αριθμός. Στο mcp 2.x, το FastMCP μετονομάστηκε σε MCPServer, και σχεδόν κάθε tutorial online ανοίγει ακόμα με το παλιό import. Το SDK φέρνει ένα module του οποίου ο μόνος σκοπός είναι να το εξηγήσει αυτό, που είναι η πιο προσεκτική deprecation σε αυτό το κεφάλαιο:
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.Άρα ποιο από τα δύο
Σύνδεσμος στην ενότητα: Άρα ποιο από τα δύοΜε τον πίνακα μπροστά σας, η σύσταση είναι βαρετή, που είναι καλό σημάδι.
Αν ο server ζει μέσα σε μια web application που ήδη τρέχετε, γράψτε τον σε TypeScript. Ίδιο process, ίδιο deploy, ίδιο request handler· το Streamable HTTP είναι ένα endpoint που προσθέτετε δίπλα στα άλλα· και τα 13.9 MiB και τα 145 ms είναι δωρεάν επειδή το runtime ήταν ήδη up. Αυτό είναι οι περισσότεροι από τους 14.696 remote servers.
Αν ο server τυλίγει data tooling, γράψτε τον σε Python. Αυτό που εκθέτετε είναι pandas, ένας warehouse client, transforms στο μέγεθος ενός notebook, και ένας server σε άλλη γλώσσα θα ήταν subprocess call που φοράει schema. Επτακόσια milliseconds import σε μια υπηρεσία που ξεκινά μία φορά δεν είναι κόστος· σε ένα subprocess που ένας host relaunches όλη μέρα, είναι.
Και προς το παρόν, η γραμμή revision υπερισχύει και των δύο. Αν χρειάζεστε 2026-07-28 — multi-round-trip requests, resultType, cache hints, server/discover — ένα από τα δύο SDKs το έχει σήμερα και το άλλο όχι.
Πού πηγαίνει αυτό μετά
Σύνδεσμος στην ενότητα: Πού πηγαίνει αυτό μετάΜπορείτε πλέον να ship τον ίδιο server σε οποιαδήποτε γλώσσα, να υπερασπιστείτε την επιλογή με πίνακα αντί για προτίμηση, να τον τρέξετε πάνω και στις δύο live μεταφορές, και να του δώσετε ένα token που θα αρνηθεί.
Αυτό που χτίσατε είναι ακόμα μια function: ένα schema, ένα endpoint, ένα deterministic πράγμα που επικαλείται το model. Μια ολόκληρη κατηγορία γνώσης δεν χωρά σε αυτό το σχήμα — πώς γράφουμε εμείς ένα postmortem, ποια fields χρειάζονται τα incident reports μας, η σειρά με την οποία κάνουμε πράγματα και γιατί. Είναι διαδικασία, είναι prose, και το να το στριμώχνετε σε tool description είναι ο τρόπος με τον οποίο τα system prompts μεγαλώνουν στα δύο χιλιάδες tokens που πληρώνονται σε κάθε single turn είτε η conversation αφορά incidents είτε όχι.
Το Κεφάλαιο 28 είναι η άλλη απάντηση: ένας φάκελος με ένα SKILL.md μέσα που το model διαβάζει αντί να καλεί, φορτωμένος σε τρία levels ώστε το reference material να κοστίζει σχεδόν τίποτα μέχρι το turn που χρειάζεται. Δεν έχει main language, και αυτό είναι το πρώτο πράγμα που διδάσκει.
Πηγές και μέθοδος
Σύνδεσμος στην ενότητα: Πηγές και μέθοδοςΌλα εδώ μετρήθηκαν στις 7 Σεπτεμβρίου 2026, σε Node 22.22.3 και Python 3.14.4, απέναντι σε @modelcontextprotocol/sdk 1.30.0 με zod 3.25.76 και mcp 2.1.1, εγκατεστημένα το καθένα στον δικό του throwaway κατάλογο. Τα timings είναι medians από 25 launches, wall clock από spawn έως τη γραμμή που φέρει την απάντηση tools/list· τα token counts είναι o200k_base μέσω tiktoken πάνω στο JSON κάθε definition. Δεν κλήθηκε paid API: τίποτα εδώ δεν χρειάζεται model.
Οι δύο servers είναι 81 και 63 μη κενές γραμμές· ένα από τα τρία εργαλεία τους αναπαράγεται παραπάνω και στις δύο γλώσσες, και τα άλλα τέσσερα registrations διαφέρουν μόνο όπως περιγράφεται. Η error-disclosure policy του Python SDK παρατίθεται από τα docstrings των ToolError και UnexpectedToolError στο mcp/server/mcpserver/exceptions.py· το pretty-printing default είναι pydantic_core.to_json(result, fallback=str, indent=2) στο mcp/server/mcpserver/resources/types.py και utilities/func_metadata.py. Οι protocol-version constants είναι LATEST_PROTOCOL_VERSION στο mcp_types/version.py και στο types.js του TypeScript SDK, διαβασμένες και οι δύο από τα installed packages αντί από changelog.
Παραπομπές
Σύνδεσμος στην ενότητα: Παραπομπές-
SDKs,
modelcontextprotocol.io/docs/sdk, και Build an MCP server,modelcontextprotocol.io/docs/develop/build-server, και τα δύο διαβάστηκαν 7 Σεπτεμβρίου 2026. Πηγή του tier table, της πρότασης «Each SDK provides the same functionality but follows the idioms and best practices of its language», της σειράς language-tab του tutorial (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), και του logging rule που παρατίθεται γιαprint()καιstdout. ↩ ↩2 ↩3 ↩4 -
stdio transport,
.../basic/transports/stdio. Πηγή του newline framing και του κανόνα καθαρότηταςstdout. Το Κεφάλαιο 26 διαβάζει πλήρως αυτή τη σελίδα· παρατίθεται εδώ για τη γραμμή που παραβιάζει ο σπασμένος server. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, διαβάστηκε 7 Σεπτεμβρίου 2026. Ένα package, τρεις clients πίσω από ένα binary — web,--cliκαι--tui— που μοιράζονται ένα core, ένα set μεταφορών και ένα OAuth state στον δίσκο. Το CLI παρήγαγε τα catalogue traces εδώ. ↩ -
Authorization,
modelcontextprotocol.io/specification/2026-07-28/basic/authorization, διαβάστηκε 7 Σεπτεμβρίου 2026. Πηγή του resource-server role· των τεσσάρων token-handling clauses που παρατίθενται πλήρως· της απαίτησης οι servers να υλοποιούν RFC 9728 και οι clients να το χρησιμοποιούν για discovery· των κανόνων parameterresourceκαι του canonical-URI definition· του issuer-validation table· της deprecation του Dynamic Client Registration· του πίνακα401/403/400και του challengeinsufficient_scope· και της stdio εξαίρεσης, «Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.» ↩ ↩2 ↩3 ↩4 -
Streamable HTTP,
.../basic/transports/streamable-http, και Transports overview,.../basic/transports. Πηγή του single-endpoint POST rule, της dualAcceptrequirement, του headerMCP-Protocol-Versionκαι του κανόνα must-match-the-body, των headersMcp-MethodκαιMcp-Nameπου περιγράφονται ως «REQUIRED for compliance», της αφαίρεσης του GET stream, των sessions και τουLast-Event-ID, του guidance405, της mandatoryOriginvalidation, και της ταξινόμησης του 2024-11-05 HTTP+SSE transport ως Deprecated υπό το SEP-2596. ↩ ↩2 ↩3 ↩4 -
Τα τέσσερα στα οποία στηρίζεται η προδιαγραφή, με το draft που κάνει profile: The OAuth 2.1 Authorization Framework,
draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. και Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, Φεβρουάριος 2020 — το parameterresourceκαι το audience που δένει. Jones, M.B., Hunt, P. και Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, Απρίλιος 2025 — το document στο οποίο δείχνει ένα401. Meyer zu Selhausen, K. και Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, Μάρτιος 2022 — το parameterissκαι η exact-string comparison. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, Ιούλιος 2015, deprecated για αυτή τη χρήση. Και Jones, M. και Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, Οκτώβριος 2012, section 3, για το challenge shapeWWW-Authenticateπαραπάνω. ↩ -
Official MCP registry,
registry.modelcontextprotocol.io/v0/servers, crawled 7 Σεπτεμβρίου 2026 μεversion=latest: 282 σελίδες, 28.170 servers, tallied απόregistryTypeπάνω σε distinct server names. Download figures:api.npmjs.org/downloads/point/last-monthγια@modelcontextprotocol/sdk(194.679.333 για 8 Αυγούστου – 6 Σεπτεμβρίου 2026) καιpypistats.org/api/packages/<name>/recentγιαmcpκαιfastmcp, και τα δύο διαβάστηκαν την ίδια ημέρα. Τα package sizes προέρχονται από το npm registry document και το PyPI JSON API. ↩ ↩2 -
MCP Course, Hugging Face,
huggingface.co/learn/mcp-course, unit 0, διαβάστηκε 7 Σεπτεμβρίου 2026: ανάμεσα στα prerequisites, «Experience with at least one programming language (Python or TypeScript examples will be shown)». ↩