2. März 2026
Wie man einen MCP Server baut: Die Architektur hinter der Verbindung beliebiger Software mit KI
Ein praktischer Guide zum Bauen von MCP Servern in TypeScript. Basierend auf zwei echten Open-Source-MCPs — fuer Godot und Aseprite — erklaert dieser Beitrag jede Schicht: Transport, Tools, Handler, die Bruecke und Sicherheit.
Sascha Becker
Author15 Min. Lesezeit

Wie man einen MCP Server baut
Ich habe zwei MCP Server gebaut — einen fuer Godot und einen fuer Aseprite — und einen KI-Agenten ein komplettes Spiel damit bauen lassen. Dabei habe ich gelernt, dass jeder MCP dem gleichen Grundgeruest folgt, unabhaengig davon, welche Software er anbindet.
Dieser Beitrag ist der Guide, den ich mir vor dem Start gewuenscht haette. Wenn du TypeScript schreiben kannst und verstehst, wie die Zielsoftware funktioniert, kannst du einen MCP Server bauen. Das Muster ist immer das gleiche — nur die Bruecke aendert sich.
Was ist ein MCP Server, eigentlich?
Das Model Context Protocol (MCP) ist ein offener Standard, der KI-Agenten erlaubt, Funktionen auf externer Software aufzurufen. Anstatt dass die KI Text generiert und hofft, dass du ihn an die richtige Stelle kopierst, fuehrt die KI direkt Operationen aus — eine Datei erstellen, einen Pixel zeichnen, ein Spiel starten, einen Screenshot machen.
Ein MCP Server ist der Mittelsmann. Er spricht auf der einen Seite das MCP-Protokoll (JSON-RPC ueber stdio) und auf der anderen Seite das, was die Zielsoftware versteht. Der KI-Client (Claude Code, Cursor, Cline, Windsurf) spricht MCP. Dein Server uebersetzt das in Godot-CLI-Aufrufe, Aseprite-Lua-Skripte, REST-APIs, Datenbankabfragen — was auch immer noetig ist.
Das mentale Modell:
DIAGRAM
Das war's. Alles andere ist Implementierungsdetail.
Die fuenf Schichten
Jeder MCP Server, den ich gebaut habe, hat die gleichen fuenf Schichten. Sie zu verstehen ist der gesamte Sinn dieses Beitrags.
| Schicht | Name | Verantwortung |
|---|---|---|
| 1 | Server & Transport | Einstiegspunkt, stdio-Verbindung, Prozess-Lifecycle |
| 2 | Tool-Definitionen | Der Vertrag — welche Tools existieren, ihre Parameter und Beschreibungen |
| 3 | Router & Handler | Request-Dispatch, Parameter-Validierung, Geschaeftslogik |
| 4 | Die Bruecke | Wie du tatsaechlich mit der Zielsoftware sprichst |
| 5 | Sicherheit & Validierung | Pfadpruefungen, Prozessisolation, Tool-Filterung |
Gehen wir jede einzelne durch.
Schicht 1: Server & Transport
Das ist deine index.ts — der Einstiegspunkt. Ihre Aufgabe ist minimal: den MCP Server erstellen, die Zielsoftware erkennen, Tool-Handler registrieren und ueber stdio verbinden.
Sowohl godot-mcp als auch aseprite-mcp folgen dem exakt gleichen Muster:
typescriptimport { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";async function main() {// 1. Konfiguration aus Umgebungsvariablen zusammenfuehrenconst config = mergeConfig(envConfig, explicitConfig);// 2. Zielsoftware erkennenconst softwarePath = await detectSoftwarePath();// 3. Kontext erstellen (geteilter Zustand fuer alle Handler)const ctx = new ServerContext(config, softwarePath);// 4. MCP Server erstellenconst server = new Server({ name: "your-mcp", version: "0.1.0" },{ capabilities: { tools: {} } },);// 5. Tool-Handler registrierensetupToolHandlers(server, ctx);// 6. Aufraeumen bei Beendigungprocess.on("SIGINT", () => cleanup(ctx));// 7. Transport verbindenconst transport = new StdioServerTransport();await server.connect(transport);}
Warum stdio? MCP-Clients starten deinen Server als Kindprozess und kommunizieren ueber stdin/stdout. Kein HTTP, keine WebSockets, kein Port-Management. Der KI-Client startet deinen Server, sendet JSON-RPC-Nachrichten an stdin, liest Antworten von stdout. Einfach, sicher, funktioniert ueberall.
Der ServerContext ist ein einfaches Objekt, das geteilten Zustand traegt — den Pfad zur Ziel-Executable, Debug-Flags, Konfiguration und jeglichen Laufzeitzustand wie aktive Prozesse oder TCP-Verbindungen:
typescriptclass ServerContext {softwarePath: string;operationsScriptPath: string;debugMode: boolean;toolsets?: string[];excludeTools: string[];activeProcess: ChildProcess | null;// ... was auch immer deine Bruecke braucht}
Tip
Die einzige Produktions-Abhaengigkeit, die beide MCPs benoetigen, ist
@modelcontextprotocol/sdk. Das ist alles. Alles andere — TypeScript, ESLint,
Vitest — ist nur fuer die Entwicklung.
Schicht 2: Tool-Definitionen
Tools sind der Vertrag zwischen deinem MCP Server und der KI. Jedes Tool hat einen Namen, eine Beschreibung und ein JSON Schema fuer seine Parameter. Die KI liest diese Definitionen und entscheidet, welches Tool sie aufruft.
So sieht eine Tool-Definition aus:
typescriptexport const TOOL_DEFINITIONS = {create_sprite: {description: "Create a new sprite with the given dimensions and color mode",inputSchema: {type: "object",properties: {width: { type: "number", description: "Sprite width in pixels" },height: { type: "number", description: "Sprite height in pixels" },colorMode: {type: "string",enum: ["RGB", "Grayscale", "Indexed"],description: "Color mode for the sprite",},},required: ["width", "height"],},},// ... 60+ weitere Tools};
Diese Datei ist die wichtigste Datei in deinem MCP. Der KI-Agent liest Tool-Beschreibungen, um zu entscheiden, was er aufruft. Schlechte Beschreibungen = falsche Tool-Aufrufe = kaputter Workflow.
Drei Regeln fuer Tool-Definitionen:
- Beschreibungen muessen praezise sein. "Create a new sprite file" ist besser als "Sprite creation tool." Die KI verwendet diese als Dokumentation.
- Markiere required vs. optional klar. Das
required-Array ist wichtig. Wenn ein Parameter einen sinnvollen Standardwert hat, mach ihn optional und dokumentiere den Standard in der Beschreibung. - Verwende Enums wo immer moeglich. Statt
type: "string"fuer einen Farbmodus, nutzeenum: ["RGB", "Grayscale", "Indexed"]. Das beschraenkt die Auswahl der KI auf gueltige Werte.
Der godot-mcp definiert 69 Tools in 13 Kategorien. Der aseprite-mcp definiert 65+ Tools in 14 Kategorien. Beide speichern alle Definitionen in einer einzigen tool-definitions.ts Datei — ein Ort, um jede Faehigkeit zu sehen.
Schicht 3: Router & Handler
Der Router verbindet MCP-Protokoll-Anfragen mit deinen Handler-Funktionen. Er behandelt zwei Operationen: verfuegbare Tools auflisten und einen Tool-Aufruf ausfuehren.
typescriptexport function setupToolHandlers(server: Server, ctx: ServerContext) {// Verfuegbare Tools auflisten (gefiltert nach Konfiguration)server.setRequestHandler(ListToolsRequestSchema, async () => ({tools: getActiveTools(ctx),}));// Einen Tool-Aufruf ausfuehrenserver.setRequestHandler(CallToolRequestSchema, async (request) => {const { name, arguments: args } = request.params;const handler = HANDLER_MAP[name];if (!handler) {return {content: [{ type: "text", text: `Unknown tool: ${name}` }],isError: true,};}return handler(args, ctx);});}
Die HANDLER_MAP ist eine flache Lookup-Tabelle — Tool-Name zu Handler-Funktion:
typescriptconst HANDLER_MAP: Record<string, ToolHandler> = {create_sprite: handleCreateSprite,open_sprite: handleOpenSprite,draw_pixel: handleDrawPixel,draw_line: handleDrawLine,// ... jedes Tool mappt auf genau einen Handler};
Das Handler-Muster
Jeder Handler folgt den gleichen vier Schritten. Diese Konsistenz ist es, die die Codebase bei 65+ Tools wartbar haelt:
typescriptasync function handleDrawPixel(args: Record<string, unknown>,ctx: ServerContext,): Promise<ToolResponse> {// 1. Parameter normalisieren (snake_case vom MCP → camelCase fuer TS)const params = normalizeParameters(args);// 2. Erforderliche Parameter validierenif (!params.inputPath) {return createErrorResponse("inputPath is required");}validatePath(params.inputPath); // Sicherheitspruefung// 3. Ueber Bruecke ausfuehrenconst result = await executeOperation(ctx.softwarePath,ctx.operationsScriptPath,"draw_pixel",params,);// 4. Strukturierte Antwort zurueckgebenreturn {content: [{ type: "text", text: result.stdout }],isError: false,};}
Organisiere Handler nach Domaene. Beide MCPs gruppieren sie in Dateien wie sprite-handlers.ts, drawing-handlers.ts, export-handlers.ts. Jede Datei exportiert 3-8 Funktionen. Keine Datei ueberschreitet ~25 KB. Das haelt jedes Modul auf ein Anliegen fokussiert.
Info
Wenn du ein neues Tool hinzufuegst, aenderst du genau vier Stellen: die
Tool-Definition, die Handler-Funktion, den Handler-Map-Eintrag und das
Operations-Skript. Beide MCPs dokumentieren diese Checkliste in ihrer
CONTRIBUTING.md.
Schicht 4: Die Bruecke
Das ist der schwere Teil — und der Teil, der sich fuer jede Zielsoftware aendert. Die Bruecke ist die Art, wie dein TypeScript-Server tatsaechlich mit der Software kommuniziert, die er steuert.
Es gibt drei gaengige Brueckenmuster:
Muster A: Unterprozess mit Skript-Injektion
Verwendet von: aseprite-mcp
Aseprite unterstuetzt Batch-Modus (aseprite -b) und Lua-Skripting. Aber es gibt keine Moeglichkeit, Aseprites interne API von aussen aufzurufen — du kannst keine HTTP-Requests schicken, es hat keinen Socket-Server, kein CLI fuer Pixel-Operationen. Die einzige Moeglichkeit, programmatisch Pixel zu zeichnen, Layer zu manipulieren oder Sprites zu exportieren, ist durch ein Lua-Skript, das innerhalb von Aseprite laeuft.
Deshalb existiert operations.lua. Es ist nicht optional — es ist die gesamte Bruecke.
DIAGRAM
Das Lua-Skript ist ein Command Dispatcher — eine Lookup-Tabelle von Operations-Handlern, die jeweils JSON-Parameter in Aseprite-API-Aufrufe uebersetzen:
lualocal operations = {}operations.draw_pixel = function(params)local sprite = app.open(params.input_path)local image = app.cel.image:clone()image:drawPixel(params.x, params.y, Color(params.color))app.cel.image = imagesprite:saveAs(params.output_path or params.input_path)sprite:close()return send_result({ success = true })end-- Haupt-Dispatchlocal operation = app.params.operationlocal params = json_decode(app.params.params or "{}")if operations[operation] thenlocal ok, err = pcall(operations[operation], params)if not ok then send_error(tostring(err)) endelsesend_error("Unknown operation: " .. tostring(operation))end
Auf der TypeScript-Seite startet der Executor diesen Prozess:
typescriptasync function executeOperation(asepritePath: string,scriptPath: string,operation: string,params: Record<string, unknown>,): Promise<{ stdout: string; stderr: string }> {const args = ["-b","--script-param",`operation=${operation}`,"--script-param",`params=${JSON.stringify(params)}`,"--script",scriptPath,];return execFileAsync(asepritePath, args);}
Die entscheidende Erkenntnis: Das Lua-Skript ist notwendig, weil Aseprites Scripting-API nur von innerhalb des Aseprite-Prozesses zugaenglich ist. Wuerde Aseprite eine REST-API oder ein CLI fuer jede Operation anbieten, brauchtest du kein Lua. Die Bruecke passt sich an das an, was die Zielsoftware dir bietet.
Muster B: Unterprozess mit Headless-Skript (gleiche Idee, andere Sprache)
Verwendet von: godot-mcp (fuer Szenen-/Skript-Operationen)
Godot hat die gleiche Einschraenkung — seine Szenen-Manipulations-API ist nur von innerhalb eines laufenden Godot-Prozesses zugaenglich. Also startet godot-mcp Godot im Headless-Modus mit einem GDScript, das als Operations-Dispatcher fungiert:
DIAGRAM
Das Muster ist identisch zur Lua-Bruecke. Die Sprache aendert sich (GDScript statt Lua), aber die Architektur nicht.
Muster C: TCP Socket fuer Echtzeit-Kommunikation
Verwendet von: godot-mcp (fuer interaktive/Gameplay-Operationen)
Manche Operationen brauchen eine persistente Verbindung. Beim Testen von Gameplay — Tastendruecke senden, Screenshots waehrend des Spiels machen, Spielzustand lesen — kannst du nicht fuer jede Aktion einen neuen Godot-Prozess starten. Das Spiel muss kontinuierlich laufen.
godot-mcp loest das, indem es einen TCP-Server (ein Autoload-Skript) in das laufende Spiel injiziert:
DIAGRAM
typescriptasync function sendTcpCommand(ctx: ServerContext,command: Record<string, unknown>,): Promise<unknown> {await ensureTcpConnection(ctx);return new Promise((resolve, reject) => {const timeout = setTimeout(() => reject(new Error("Timeout")), 5000);ctx.tcp.pendingResolve = (data) => {clearTimeout(timeout);resolve(data);};ctx.tcp.socket.write(JSON.stringify(command) + "\n");});}
Dein Brueckenmuster waehlen
| Zielsoftware bietet | Brueckenmuster | Beispiel |
|---|---|---|
| Scripting-API (Lua, GDScript, Python, etc.) | Unterprozess + Skript-Injektion | Aseprite, Godot (Szenen-Ops) |
| REST/HTTP-API | HTTP-Client-Aufrufe | Jeder Webservice |
| CLI mit umfangreichen Befehlen | Direkter Unterprozess | FFmpeg, ImageMagick |
| Socket/TCP/WebSocket-Server | Persistente Verbindung | Godot (interaktiver Modus) |
| SDK/Library-Bindings | Direkter Import (wenn Node.js-kompatibel) | SQLite, native Module |
Die Frage, die du stellen musst: "Wie laesst mich diese Software sie automatisieren?" Die Antwort bestimmt deine Bruecke. Wenn die Software eine Lua-Scripting-API hat, schreib einen Lua-Dispatcher. Wenn sie eine REST-API hat, nutze fetch. Wenn sie ein CLI hat, nutze execFile. Der Rest des MCP — Server, Tools, Handler, Sicherheit — bleibt gleich.
Schicht 5: Sicherheit & Validierung
Ein MCP Server gibt einem KI-Agenten echte Macht ueber echte Software. Das bedeutet, Sicherheit ist nicht optional. Beide MCPs implementieren die gleichen Kernschutzmassnahmen.
Pfad-Validierung
Jeder Dateipfad, der von der KI kommt, wird vor der Verwendung validiert. Die Bedrohung Nummer eins ist Directory Traversal — die KI (oder eine Prompt Injection durch die KI), die ../../etc/passwd als Dateipfad sendet.
typescriptfunction validatePath(filePath: string): void {if (!filePath || filePath.includes("..")) {throw new Error("Invalid path: directory traversal detected");}}
Rufe das bei jedem Pfad-Parameter in jedem Handler auf. Keine Ausnahmen.
Sicheres Starten von Prozessen
Beide MCPs verwenden execFile — nicht exec. Das ist entscheidend.
typescript// GUT: Argumente als Array, keine Shell-InterpretationexecFile("aseprite", ["-b", "--script", scriptPath]);// SCHLECHT: String-Verkettung, Shell-Injection moeglichexec(`aseprite -b --script ${scriptPath}`);
execFile uebergibt Argumente als Array direkt an den Prozess. exec uebergibt einen String durch die Shell, was bedeutet, dass Sonderzeichen (; rm -rf /) interpretiert werden. Verwende niemals exec.
Tool-Filterung
Beide MCPs unterstuetzen die Einschraenkung verfuegbarer Tools:
bash# Nur bestimmte Kategorien freigebenMCP_TOOLSETS="sprite,drawing,export"# Gefaehrliche Tools blockierenMCP_EXCLUDE_TOOLS="delete_file,batch_resize"# Voller Nur-Lesen-Modus — keine DateimodifikationenMCP_READ_ONLY=true
Die Filterung findet im Router statt, bevor Tools der KI aufgelistet werden. Wenn ein Tool nicht aufgelistet ist, kann die KI es nicht aufrufen.
Prozess-Isolation
Jede Aseprite-Operation laeuft in ihrem eigenen Unterprozess. Wenn Aseprite bei einer fehlerhaften Eingabe abstuerzt, laeuft der MCP Server weiter. Die KI versucht es mit anderen Parametern erneut. Kein geteilter Zustand wird beschaedigt.
Godots Headless-Operationen funktionieren genauso — ein Prozess pro Operation, automatisch gestartet und aufgeraeumt.
Alles zusammenfuegen: Deine MCP-Starter-Struktur
Hier ist die minimale Dateistruktur fuer einen neuen MCP Server:
MCP Server Starter
your-mcpsrcindex.tsSchicht 1: Server & Transportcontext.tsGeteilter Zustandtypes.tsTyp-Definitionentool-definitions.tsSchicht 2: Tool-Schemastool-router.tsSchicht 3: Request-Routingexecutor.tsSchicht 4: Bruecke zur Zielsoftwaresoftware-path.tsZiel-Executable erkennenutils.tsSchicht 5: Sicherheit & HelferhandlersSchicht 3: Handler-Implementierungenbasic-handlers.tsadvanced-handlers.tsscriptsSchicht 4: Bruecken-Skripte (falls noetig)operations.luaoder .gd, .py, .rbpackage.jsontsconfig.jsonREADME.md
Die Checkliste
Wenn du einen neuen MCP baust, arbeite diese Schritte durch:
-
Identifiziere die Bruecke. Wie laesst dich die Zielsoftware sie automatisieren? CLI? Scripting-API? HTTP? Socket? Das bestimmt deinen Schicht-4-Ansatz.
-
Schreibe eine Operation von Ende zu Ende. Waehle die einfachste nuetzliche Operation (z.B. "neue Datei erstellen" oder "Versionsinformationen holen"). Verdrahte sie durch alle fuenf Schichten. Sobald ein Tool funktioniert, ist das Hinzufuegen weiterer mechanisch.
-
Definiere Tools sorgfaeltig. Die KI weiss nur, was du ihr sagst. Schreibe Beschreibungen, als wuerdest du sie einem Junior-Entwickler erklaeren. Fuege Parameter-Standardwerte, gueltige Bereiche und was das Tool zurueckgibt hinzu.
-
Validiere alles von aussen. Pfade, Parameter-Typen, Enum-Werte — validiere auf Handler-Ebene, bevor du an die Bruecke uebergibst. Die KI wird gelegentlich unerwartete Werte senden.
-
Teste die Tool-Auflistung. Fuehre
npx @anthropic/mcp-inspectoraus, um genau zu sehen, was die KI sieht. Wenn die Tool-Liste fuer dich verwirrend ist, ist sie es auch fuer die KI.
Die minimale package.json
json{"name": "your-mcp","version": "0.1.0","type": "module","main": "build/index.js","bin": { "your-mcp": "build/index.js" },"scripts": {"build": "tsc","test": "vitest","lint": "eslint src/","typecheck": "tsc --noEmit","inspector": "npx @anthropic/mcp-inspector build/index.js"},"dependencies": {"@modelcontextprotocol/sdk": "^1.27.1"},"devDependencies": {"typescript": "^5.0.0","vitest": "latest","@types/node": "^20.0.0"},"engines": { "node": ">=18.0.0" }}
Eine Produktions-Abhaengigkeit. Das ist alles, was du brauchst, um MCP zu sprechen.
Was ich beim Bau von zwei MCPs gelernt habe
Die Bruecke ist 80% der Arbeit. Die MCP-Protokollschicht ist straightforward — das SDK kuemmert sich darum. Der schwere Teil ist, die Automatisierungs-API der Zielsoftware zu verstehen und eine zuverlaessige Uebersetzungsschicht zu bauen. Fuer Aseprite bedeutete das 75 KB Lua zu schreiben. Fuer Godot waren es 100 KB GDScript plus ein TCP-Protokoll.
Tool-Anzahl ist egal — Tool-Qualitaet zaehlt. Der godot-mcp hat 69 Tools, aber die KI nutzt write_script, run_interactive und game_state fuer 90% ihrer Arbeit. Ein kleines Set gut designter Tools schlaegt ein grosses Set mittelmassiger. Starte mit fuenf Tools und erweitere basierend auf dem, was die KI tatsaechlich braucht.
Beschreibungen sind Dokumentation fuer einen KI-Leser. Ich habe gesehen, wie die KI das falsche Tool gewaehlt hat, weil die Beschreibung mehrdeutig war. "Manage sprites" ist nutzlos. "Create a new sprite file with specified dimensions and save it to the given path" ist eindeutig. Schreibe Beschreibungen, als wuerdest du API-Docs schreiben.
Prozess-Isolation rettet dich. Wenn die KI falsche Parameter an Aseprite sendet und es abstuerzt, bleibt der MCP Server oben. Die naechste Operation funktioniert einwandfrei. Wenn du Operationen im Prozess ausfuehrst, reisst ein fehlerhafter Aufruf alles mit.
Das gleiche Grundgeruest funktioniert ueberall. Nach dem Bau des ersten MCP hat der zweite einen Bruchteil der Zeit gebraucht. Das Server-Setup, die Tool-Definitionen, der Router, das Handler-Muster, die Sicherheits-Utilities — alles laesst sich uebertragen. Nur die Bruecken-Skripte aendern sich.
Das Protokoll ist einfach. Die Architektur ist wiederverwendbar. Der schwere Teil ist immer die Bruecke — zu verstehen, wie man mit der Software spricht, die man anbinden will. Sobald du das geloest hast, hast du einen MCP gebaut.
Quellen & Links
- Godot MCP Server
MCP Server, der KI-Agenten mit der Godot 4.x Engine verbindet. 69 Tools fuer Szenen-Management, Scripting und interaktives Testen. Referenzimplementierung fuer Headless + TCP Brueckenmuster.
- Aseprite MCP Server
MCP Server, der KI-Agenten mit dem Aseprite Pixel-Art-Editor verbindet. 65+ Tools fuer Sprite-Erstellung, Zeichnen, Animation und Export. Referenzimplementierung fuer Lua-Skript-Injektions-Bruecke.
- Ich bin eine KI und habe ein Godot-Spiel gebaut
Der Begleitbeitrag — ein KI-Agent nutzt den godot-mcp, um ein komplettes Spiel von Grund auf zu bauen, und berichtet ehrlich, wo er an Grenzen gestossen ist.
- Model Context Protocol
Der offene Standard fuer die Verbindung von KI-Modellen mit externen Tools und Datenquellen.
- MCP TypeScript SDK
Das offizielle TypeScript SDK zum Bauen von MCP Servern und Clients.
- MCP Inspector
Entwickler-Tool zum Testen und Debuggen von MCP Servern. Sieh genau, was die KI sieht.
