Μετάβαση στο περιεχόμενο
27/30Κεφάλαιο 27 από 30

Κυκλοφορήστε ένα MCP Server: TypeScript και Python, με μετρήσεις

Ο ίδιος server γράφτηκε δύο φορές — τρία tools, ένα resource, ένα prompt — και ζυγίστηκε: 94 packages έναντι 28, cold start 145 ms έναντι 709.

Σε αυτή τη σελίδα

Ορίστε ολόκληρο το επιχείρημα για τη γλώσσα, μετρημένο, πριν ειπωθεί έστω λέξη γι’ αυτό.

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

Οι δύο πρώτες γραμμές είναι η σύγκριση που όλοι θέλουν. Η τρίτη γραμμή είναι ο ίδιος 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 που το άνοιξε.

Ορίστε το ίδιο εργαλείο και στις δύο γλώσσες, καταχωρισμένο δίπλα δίπλα:

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

Διαβάστε πρώτα τι είναι ίδιο, επειδή αυτό είναι το εύρημα. Και τα δύο δηλώνουν ένα όνομα, μια περιγραφή, δύο περιγεγραμμένα 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 που τρέχει δύο φορές, σε έντεκα γραμμές:

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

Στρέψτε τον σε κάθε server με τη σειρά. Πραγματικό output, κομμένο:

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

Ίδια εργαλεία, ίδια σειρά, ίδιο 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:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
σύνολο342480

Τα 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 paths παραπάνω δεν είναι επιλογή στιλ. Δώστε σε κάθε server ένα εργαλείο που αποτυγχάνει όπως αποτυγχάνει μια πραγματική integration, και διαβάστε τι φτάνει στο model.

tools/call on a tool that raisesTEXT
TypeScript  {"content":[{"type":"text","text":
              "connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
             "isError":true}

Python      {"content":[{"text":"Error executing tool boom","type":"text"}],
             "isError":true}

Το 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 να αποφασίζει, σε καμία γλώσσα.

Το επίσημο 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:

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 είναι χειρότερη, και ο λόγος δεν είναι το MCP. Ένα process του οποίου το stdout είναι pipe και όχι terminal παίρνει block-buffered stream, άρα η stray line γίνεται flush όποτε το αποφασίσει το buffer — εδώ, στο exit, μετά από μια απάντηση πριν από την οποία είχε γραφτεί. Η διαφθορά δεν εμφανίζεται εκεί όπου είναι το bug. Προσθέστε flush=True, ή μια library που κάνει flush, και μετακινείται.

Έπειτα το κομμάτι που εξηγεί γιατί αυτό φτάνει σε production. Δώστε τον σπασμένο server σε τρεις clients:

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

Ο parser των επτά γραμμών πεθαίνει αμέσως. Ο επίσημος client και ο Inspector σηκώνουν τους ώμους — προσπερνούν τη γραμμή και συνεχίζουν. Ένας κανόνας που σπάει μόνο τους clients που δεν χρησιμοποιεί κανείς είναι ένας κανόνας που φτάνει intact σε production, γι’ αυτό αξίζει να τον σπάσουμε επίτηδες εδώ και όχι στο log ενός πελάτη.

Το CLI mode του Inspector είναι το μισό που ξεχνιέται: το npx @modelcontextprotocol/inspector --cli <command> --method tools/list τυπώνει ένα catalogue και βγαίνει, πράγμα που το κάνει scriptable με τρόπο που δεν είναι το browser UI.3

Και τα δύο SDKs εγκαταστάθηκαν καθαρά, στους δικούς τους καταλόγους, χωρίς τίποτα κοινό:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
τελευταία υλοποιημένη protocol revision2025-11-252026-07-28
εγκατεστημένα transitive packages9428
εγκατεστημένο μέγεθος13.9 MiB44.3 MiB
αρχεία στον δίσκο3,3862,018
third-party packages loaded για να εξυπηρετήσουν stdio8 από 9418 από 28
bare interpreter start, median19.4 ms11.1 ms
spawn → απαντήθηκε tools/list, median από 25144.5 ms709.4 ms
tools/list catalogue, o200k_base tokens342480

Κάθε γραμμή εκπλήσσει προς διαφορετική κατεύθυνση, γι’ αυτό αξίζει να τρέξετε τη σύγκριση αντί να υποθέσετε.

Η 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 λέει γιατί:

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

Ένας 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:

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

Ο 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:

Κάθε POST φέρει MCP-Protocol-Version, και η τιμή του πρέπει να ταιριάζει με το protocolVersion μέσα στο δικό του _meta του request. Ένα mismatch είναι 400 με header-mismatch error, όχι ένα shrug.5

Το Mcp-Method αντικατοπτρίζει τη μέθοδο σε κάθε request· το Mcp-Name αντικατοπτρίζει το params.name ή το params.uri σε tools/call, resources/read και prompts/get. Υπάρχουν ώστε ένας proxy να μπορεί να κάνει route χωρίς να κάνει parse bodies.5

Το 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.

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

Οι 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 που πρόκειται να εγκαταστήσετε· είναι μία γραμμή, και ο μόνος ισχυρισμός σε αυτό το κεφάλαιο που θα εξακολουθεί να έχει σημασία σε έναν χρόνο.

Μετακινήστε έναν 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 σκάλα:

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

Και τα δύο 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.

Το τελευταίο κομμάτι του shipping είναι πού δημοσιεύετε, και έχει απάντηση με αριθμό. Crawled σήμερα, κάθε server στο official registry στην τελευταία του έκδοση:7

servers
σύνολο (latest version, όχι deleted)28,170
active / deprecated27,853 / 317
διαθέτουν τουλάχιστον ένα installable package13,065
remote only — ένα URL, τίποτα για install14,696
npm8,275
PyPI3,603
OCI images867
mcpb bundles706
NuGet / Cargo107 / 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/sdk1.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 tiersTypeScript, Python, C#, Go, Rust στο Tier 1· Java, Ruby στο Tier 2· Swift, PHP, Kotlin στο Tier 3
registry servers28,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 σε αυτό το κεφάλαιο:

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.

Με τον πίνακα μπροστά σας, η σύσταση είναι βαρετή, που είναι καλό σημάδι.

Αν ο 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.

  1. 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

  2. stdio transport, .../basic/transports/stdio. Πηγή του newline framing και του κανόνα καθαρότητας stdout. Το Κεφάλαιο 26 διαβάζει πλήρως αυτή τη σελίδα· παρατίθεται εδώ για τη γραμμή που παραβιάζει ο σπασμένος server.

  3. 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 εδώ.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, διαβάστηκε 7 Σεπτεμβρίου 2026. Πηγή του resource-server role· των τεσσάρων token-handling clauses που παρατίθενται πλήρως· της απαίτησης οι servers να υλοποιούν RFC 9728 και οι clients να το χρησιμοποιούν για discovery· των κανόνων parameter resource και του canonical-URI definition· του issuer-validation table· της deprecation του Dynamic Client Registration· του πίνακα 401/403/400 και του challenge insufficient_scope· και της stdio εξαίρεσης, «Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.» 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, και Transports overview, .../basic/transports. Πηγή του single-endpoint POST rule, της dual Accept requirement, του header MCP-Protocol-Version και του κανόνα must-match-the-body, των headers Mcp-Method και Mcp-Name που περιγράφονται ως «REQUIRED for compliance», της αφαίρεσης του GET stream, των sessions και του Last-Event-ID, του guidance 405, της mandatory Origin validation, και της ταξινόμησης του 2024-11-05 HTTP+SSE transport ως Deprecated υπό το SEP-2596. 2 3 4

  6. Τα τέσσερα στα οποία στηρίζεται η προδιαγραφή, με το 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 — το parameter resource και το 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 — το parameter iss και η 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 shape WWW-Authenticate παραπάνω.

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

  8. 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)».


Δημιουργήθηκε από

David Vicente Campos

Ιδρυτής της NeuraLIA Labs & συνιδρυτής του MyRealFood

Είμαι μηχανικός πληροφορικής, απόφοιτος του Πανεπιστημίου Λεόν. Συνίδρυσα το MyRealFood, όπου ως CTO έφτιαξα την εφαρμογή που έχουν χρησιμοποιήσει εκατομμύρια άνθρωποι για να τρώνε καλύτερα, και ίδρυσα τη NeuraLIA Labs, όπου δημιουργώ προϊόντα AI. Εδώ γράφω για όσα χρειάστηκε να κατανοήσω στην πορεία, όπως θα ήθελα να μου τα είχε εξηγήσει κάποιος.

Περισσότερα για τον συγγραφέα

Δημοσιεύτηκε από τη NeuraLIA Labs.

Λάβετε νέες αναρτήσεις στα εισερχόμενά σας

Νέα για AI, οδηγοί και ενημερώσεις προϊόντος — ένα σύντομο email όταν δημοσιεύουμε κάτι που αξίζει τον χρόνο σας.

Ευρετήριο μαθήματος

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev13 λεπτά ανάγνωσης

Το μοντέλο AI Jev είναι φτιαγμένο για αποφάσεις, όχι για πρόζα

Το Jev της TypeSafe AI τραβά την προσοχή επειδή αντιμετωπίζει την ευφυΐα στο λογισμικό ως πρόβλημα πιθανοτήτων: επιλέξτε το σωστό κλαδί, προσθέστε βεβαιότητα και αποφύγετε να πληρώνετε ένα LLM για να γράφει κείμενο όταν ο κώδικας χρειάζεται μια απόφαση.

Abstract legal research workspace with documents, search nodes and governance controls.
openai12 λεπτά ανάγνωσης

Το Astra for Law της OpenAI είναι νομικό σύστημα AI, όχι νέο μοντέλο

Το νομικό λανσάρισμα της OpenAI αφορά λιγότερο ένα νέο θεμελιώδες μοντέλο και περισσότερο το σύστημα γύρω από αυτό: ανάκτηση ανά τομέα, αξιόπιστα εργαλεία, δικαιώματα, benchmarks και διαδρομές ελέγχου.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 λεπτά ανάγνωσης

Context engineering for long-horizon AI agents

Long-running agents do not fail only because the window is small. They fail when files, tool outputs and stale history crowd out the task the agent was supposed to finish.

Έτοιμοι να αφήσετε τη LIA να επιλέγει;

Δημιουργήστε με κάθε μοντέλο AI σε ένα σημείο — ξεκινήστε δωρεάν σήμερα.