Σε δύο σειρές
Έχω παραδώσει δύο production MCP servers σε TypeScript με το @modelcontextprotocol/sdk:
- Heartbeat PIM — embedded MCP server που τρέχει μέσα στο ίδιο Express backend με το REST API του καταλόγου. Ο Claude agent συνδέεται μέσω stdio και κάνει direct tool calls στη βάση προϊόντων. Καμία HTTP overhead, καμία public επιφάνεια.
- Rorsa Tools — standalone MCP server, που διανέμεται ως Claude Code plugin. Ο ίδιος TypeScript κώδικας λειτουργεί και ως CLI binary για developers που θέλουν να χρησιμοποιήσουν το toolkit από το terminal.
Η απόφαση «embedded vs standalone» καθορίζει σχεδόν τα πάντα στο υπόλοιπο του build. Αυτό το άρθρο αναλύει και τα δύο σχήματα, την πραγματική αρχιτεκτονική, τα trade-offs, και πότε διαλέγεις το καθένα.
Τι είναι πραγματικά ένας MCP server
Το Model Context Protocol είναι ένα JSON-RPC-over-stdio (ή HTTP / SSE) protocol που εκδίδει η Anthropic, και επιτρέπει σε έναν AI agent (Claude, Claude Code, Continue, κ.λπ.) να καλεί tools που εσύ ορίζεις και να διαβάζει resources που εκθέτεις. Από την οπτική του agent, τα tools σου μοιάζουν απαράλλακτα από τα built-in.
Ένας μίνιμαλ TypeScript MCP server μοιάζει έτσι:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({ name: 'pim', version: '1.0.0' });
server.tool(
'get_product',
{ sku: { type: 'string' } },
async ({ sku }) => {
const product = await db.products.findOne({ where: { sku } });
return { content: [{ type: 'text', text: JSON.stringify(product) }] };
}
);
await server.connect(new StdioServerTransport());
Αυτό είναι όλο το «API». Ο Claude agent στην άλλη άκρη παίρνει ένα tool με όνομα get_product που μπορεί να καλέσει με ένα SKU.
Το standalone σχήμα: Rorsa Tools
Το Rorsa Tools είναι ένα published MCP toolkit (και Claude Code plugin) που εκθέτει 14 image-generation, editing, και processing tools σε AI agents. Είναι το toolkit που δημιούργησε κάθε pixel-art εικόνα σε αυτό το portfolio.
Η αρχιτεκτονική:
- CLI binary (
vertex-img) — Commander-driven CLI για developers που θέλουν να τρέξουν τα ίδια tools από το terminal - MCP server (
vertex-image-mcp) — εκθέτει τα ίδια 14 tools σε Claude agents - Ένα codebase, δύο interfaces — η κεντρική αρχιτεκτονική απόφαση
Παραδείγματα tool surface:
generate_image— Vertex AI Nano Banana ή Gemini Flash, ανάλογα με ένα text-density heuristicremove_background— τρέχει σε Python sidecar process (RMBG-2.0 model), επειδή υπάρχει μόνο σε PyTorchgenerate_responsive_sets— Sharp pipeline, παράγει 640w/960w/1280w/1920w από ένα sourcecompress_images,convert_image_formats,resize_images,get_image_metadata— Sharp πάλιgenerate_favicon— png-to-ico για το πλήρες favicon bundle
Ένας Claude Code agent μπορεί να αλυσιδώσει generate_image → remove_background → generate_responsive_sets → compress_images σε μία συνομιλία, χωρίς script.
Το standalone είναι σωστό σχήμα όταν:
- Τα tools είναι χρήσιμα σε πολλά projects
- Θες versioned distribution (Claude Code plugin manifest, npm package)
- Το tool surface είναι portable και δεν χρειάζεται τα ιδιωτικά σου δεδομένα
Το embedded σχήμα: Heartbeat PIM
Το δεύτερο MCP shipment ζει μέσα στο product information management backend του Heartbeat. Το PIM είναι ένα Express 5 + TypeORM service που:
- Φιλοξενεί το REST API που χρησιμοποιεί το React admin για catalog browsing, editing, και order ops
- Επιπλέον τρέχει έναν
@modelcontextprotocol/sdkserver στο ίδιο Node process, μιλώντας στην ίδια Postgres βάση
Ο Claude agent συνδέεται μέσω stdio (local IPC, χωρίς HTTP) και μπορεί:
get_product(sku)— διαβάζει product detailsupdate_description(sku, description)— ξαναγράφει μια περιγραφή μετά από AI enrichmentenrich_batch(skus, strategy)— περνά μια λίστα SKUs μέσα από το enrichment pipeline
Το point είναι ότι ο agent έχει το ίδιο data view με το REST API. Δεν υπάρχει ξεχωριστό microservice, καμία δημόσια network egress, κανένα API key προς rotation.
Shared state, shared transactions
Το δύσκολο κομμάτι ενός embedded MCP server είναι το transaction scoping. Και οι REST handlers και τα MCP tools χρησιμοποιούν την ίδια TypeORM data source. Αν ένα tool ξεκινήσει transaction για να ενημερώσει ένα product, και μια REST request έρθει μέσα στη μέση, και τα δύο πρέπει να είναι ασφαλή.
Δύο κανόνες το έκαναν tractable:
- Τα read-only tools παίρνουν read-only data source. Τα
get_product,list_products,search_catalogχρησιμοποιούν Postgres role που δεν έχει write privileges. Ο agent κυριολεκτικά δεν μπορεί να μεταλλάξει state μέσω αυτών. - Τα write tools παίρνουν explicit transactions. Το
update_descriptionτυλίγει τη δουλειά του σε έναdataSource.transaction(async (tx) => {...})block. Αν το REST layer κάνει concurrent update, το Postgres serialize-άρει τα writes· αν ένα write αποτύχει, το transaction κάνει rollback· η MCP response επιστρέφει το error στον agent.
Γιατί να μην χτυπήσει το REST API απευθείας;
Αυτή είναι η ερώτηση που ακούω συχνότερα. Γιατί ο Claude agent δεν καλεί απλά τα υπάρχοντα REST endpoints; Τρεις λόγοι:
- Καμία HTTP overhead. Το stdio είναι local IPC. Κάθε 1ms κλήση αντικαθιστά ένα 30ms HTTP round-trip.
- Καμία public επιφάνεια. Ο MCP server δεν είναι network-exposed. Δεν υπάρχει port να μπλοκάρεις, ούτε API key να διαχειριστείς, ούτε rate-limit να ρυθμίσεις.
- Tool semantics > endpoint semantics. Ένα REST endpoint σχηματίζεται γύρω από HTTP verbs και resource paths. Ένα MCP tool σχηματίζεται γύρω από αυτό που ο AI agent χρειάζεται να κάνει. Το
enrich_batch(skus, strategy: 'descriptions' | 'keywords' | 'all')είναι ρήμα. ΤοPOST /api/products/bulk-enrichείναι route. Δεν είναι στο ίδιο επίπεδο abstraction.
Το embedded είναι σωστό σχήμα όταν:
- Τα δεδομένα είναι ιδιωτικά και δεν πρέπει να βγαίνουν εκτός process boundary
- Ο agent χρειάζεται high-frequency tool surface (σκέψου enrichment πάνω σε χιλιάδες προϊόντα)
- Τα tools είναι coupled με το business data model σου, με τρόπο που δεν θα είχε νόημα ως generic toolkit
Δύο production decisions που αξίζει να αντιγράψεις
1. Μη περνάς JSON blobs ως tool args
Ο πειρασμός είναι να φτιάξεις generic query_product(filter: object) tool που δέχεται αυθαίρετο filter. Μην το κάνεις. Όρισε narrow, named tools — find_by_sku, find_by_vendor, list_low_stock — καθένα με explicit args. Ο Claude είναι δραματικά καλύτερος στο να διαλέγει το σωστό tool παρά στο να κατασκευάζει το σωστό filter blob.
2. Επιστρέφε text content, όχι raw JSON
Τα tools επιστρέφουν content μέσω { content: [{ type: 'text', text: '...' }] }. Ο agent διαβάζει αυτό το text. Αν επιστρέφεις raw JSON, ο agent πρέπει να το κάνει parse. Αν επιστρέφεις μια παράγραφο summary συν το JSON, μπορεί και να διαβάσει το summary γρήγορα και να κάνει parse το detail αν χρειαστεί:
return {
content: [
{ type: 'text', text: `Product ${product.sku}: ${product.name} — ${product.stock} in stock, last updated ${product.updatedAt}` },
{ type: 'text', text: JSON.stringify(product, null, 2) },
],
};
Αυτή είναι η μεγαλύτερη μεμονωμένη tool-quality νίκη που έχω μετρήσει. Το πρώτο text block είναι token-cheap summary· το δεύτερο είναι τα raw data που ο agent μπορεί να χρησιμοποιήσει ως fallback.
Πότε standalone, πότε embedded
| Standalone (Rorsa Tools σχήμα) | Embedded (Heartbeat PIM σχήμα) | |
|---|---|---|
| Distribution | npm / Claude Code plugin | Κανένα — εσωτερικό στο host backend |
| Data | Public / χωρίς secrets | Private — business data |
| Tool design | Generic, reusable | Tied στο domain model σου |
| Transport | stdio + προαιρετικό HTTP | μόνο stdio |
| Auth | Μερικές φορές token | Το process boundary ΕΙΝΑΙ το auth |
| Κόστος shipping | Versioned package + docs | Άλλο ένα module στο backend σου |
Αν χτίζεις agentic features για το δικό σου προϊόν, embed. Αν παραδίδεις tools που άλλες ομάδες πρέπει να χρησιμοποιήσουν, standalone.
Παρόμοια έργα
- Heartbeat Pharmacy Platform — πλήρες case study για το embedded MCP server build
- Rorsa Tools — πλήρες case study για το standalone MCP toolkit