2026

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.

S
Sascha Becker
Author

15 Min. Lesezeit

Wie man einen MCP Server baut: Die Architektur hinter der Verbindung beliebiger Software mit KI

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.

SchichtNameVerantwortung
1Server & TransportEinstiegspunkt, stdio-Verbindung, Prozess-Lifecycle
2Tool-DefinitionenDer Vertrag — welche Tools existieren, ihre Parameter und Beschreibungen
3Router & HandlerRequest-Dispatch, Parameter-Validierung, Geschaeftslogik
4Die BrueckeWie du tatsaechlich mit der Zielsoftware sprichst
5Sicherheit & ValidierungPfadpruefungen, 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:

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
async function main() {
// 1. Konfiguration aus Umgebungsvariablen zusammenfuehren
const config = mergeConfig(envConfig, explicitConfig);
// 2. Zielsoftware erkennen
const softwarePath = await detectSoftwarePath();
// 3. Kontext erstellen (geteilter Zustand fuer alle Handler)
const ctx = new ServerContext(config, softwarePath);
// 4. MCP Server erstellen
const server = new Server(
{ name: "your-mcp", version: "0.1.0" },
{ capabilities: { tools: {} } },
);
// 5. Tool-Handler registrieren
setupToolHandlers(server, ctx);
// 6. Aufraeumen bei Beendigung
process.on("SIGINT", () => cleanup(ctx));
// 7. Transport verbinden
const 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:

typescript
class ServerContext {
softwarePath: string;
operationsScriptPath: string;
debugMode: boolean;
toolsets?: string[];
excludeTools: string[];
activeProcess: ChildProcess | null;
// ... was auch immer deine Bruecke braucht
}

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:

typescript
export 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:

  1. Beschreibungen muessen praezise sein. "Create a new sprite file" ist besser als "Sprite creation tool." Die KI verwendet diese als Dokumentation.
  2. 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.
  3. Verwende Enums wo immer moeglich. Statt type: "string" fuer einen Farbmodus, nutze enum: ["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.

typescript
export function setupToolHandlers(server: Server, ctx: ServerContext) {
// Verfuegbare Tools auflisten (gefiltert nach Konfiguration)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: getActiveTools(ctx),
}));
// Einen Tool-Aufruf ausfuehren
server.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:

typescript
const 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:

typescript
async 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 validieren
if (!params.inputPath) {
return createErrorResponse("inputPath is required");
}
validatePath(params.inputPath); // Sicherheitspruefung
// 3. Ueber Bruecke ausfuehren
const result = await executeOperation(
ctx.softwarePath,
ctx.operationsScriptPath,
"draw_pixel",
params,
);
// 4. Strukturierte Antwort zurueckgeben
return {
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.


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:

lua
local 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 = image
sprite:saveAs(params.output_path or params.input_path)
sprite:close()
return send_result({ success = true })
end
-- Haupt-Dispatch
local operation = app.params.operation
local params = json_decode(app.params.params or "{}")
if operations[operation] then
local ok, err = pcall(operations[operation], params)
if not ok then send_error(tostring(err)) end
else
send_error("Unknown operation: " .. tostring(operation))
end

Auf der TypeScript-Seite startet der Executor diesen Prozess:

typescript
async 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
typescript
async 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 bietetBrueckenmusterBeispiel
Scripting-API (Lua, GDScript, Python, etc.)Unterprozess + Skript-InjektionAseprite, Godot (Szenen-Ops)
REST/HTTP-APIHTTP-Client-AufrufeJeder Webservice
CLI mit umfangreichen BefehlenDirekter UnterprozessFFmpeg, ImageMagick
Socket/TCP/WebSocket-ServerPersistente VerbindungGodot (interaktiver Modus)
SDK/Library-BindingsDirekter 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.

typescript
function 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-Interpretation
execFile("aseprite", ["-b", "--script", scriptPath]);
// SCHLECHT: String-Verkettung, Shell-Injection moeglich
exec(`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 freigeben
MCP_TOOLSETS="sprite,drawing,export"
# Gefaehrliche Tools blockieren
MCP_EXCLUDE_TOOLS="delete_file,batch_resize"
# Voller Nur-Lesen-Modus — keine Dateimodifikationen
MCP_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-mcp
src
index.tsSchicht 1: Server & Transport
context.tsGeteilter Zustand
types.tsTyp-Definitionen
tool-definitions.tsSchicht 2: Tool-Schemas
tool-router.tsSchicht 3: Request-Routing
executor.tsSchicht 4: Bruecke zur Zielsoftware
software-path.tsZiel-Executable erkennen
utils.tsSchicht 5: Sicherheit & Helfer
handlersSchicht 3: Handler-Implementierungen
basic-handlers.ts
advanced-handlers.ts
scriptsSchicht 4: Bruecken-Skripte (falls noetig)
operations.luaoder .gd, .py, .rb
package.json
tsconfig.json
README.md

Die Checkliste

Wenn du einen neuen MCP baust, arbeite diese Schritte durch:

  1. Identifiziere die Bruecke. Wie laesst dich die Zielsoftware sie automatisieren? CLI? Scripting-API? HTTP? Socket? Das bestimmt deinen Schicht-4-Ansatz.

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

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

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

  5. Teste die Tool-Auflistung. Fuehre npx @anthropic/mcp-inspector aus, 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.


S
Geschrieben von
Sascha Becker
Weitere Artikel