← Alle Beiträge
MCPTypeScriptClaudeSelf-Hosting

MCP-Server selbst bauen: eigene Tools für Claude, ChatGPT und Cursor

Ein Sprachmodell kann viel, weiß aber nichts über dein Unternehmen. Das Model Context Protocol schließt diese Lücke mit einem kleinen Server, den du einmal baust und danach in jedem Werkzeug benutzt. Dieses Tutorial führt vom leeren Ordner bis zum abgesicherten Server im Container, mit vollständigem Code.

Intellize18 Min. LesezeitStand: SDK 1.x, Protokollversion 2025-06-18

Was am Ende auf deiner Platte liegt

  • Ein Tool, das einen Kunden im internen CRM nachschlägt.
  • Eine Resource, über die das Modell das Mitarbeiterhandbuch lesen kann.
  • Einen Prompt, der als Slash-Befehl im Client auftaucht.
  • Zwei Betriebsarten: lokal über stdio zum Entwickeln, remote über Streamable HTTP für das Team.
  • Ein Deployment aus Container, Reverse Proxy und Token-Authentifizierung.

Vorausgesetzt: Node.js 20 oder neuer, TypeScript 5.5 oder neuer, ein MCP-fähiger Client zum Testen (Claude Desktop, Claude Code, Cursor oder VS Code). Für das Deployment am Ende zusätzlich Docker und ein Reverse Proxy.

Das Problem, das MCP löst

Wer schon einmal ein Sprachmodell an eine eigene Datenbank gehängt hat, kennt den Ablauf: Für Claude schreibst du eine Tool-Definition, für ChatGPT eine Action mit OpenAPI-Schema, für den Editor im Entwicklungsteam nochmal etwas anderes. Drei Integrationen, dieselbe Abfrage, drei Stellen, an denen ein Feldname veralten kann.

Das Model Context Protocol dreht das um. Du beschreibst deine Werkzeuge einmal, in einem Format, das alle Clients verstehen. Anthropic hat das Protokoll Ende 2024 offengelegt, inzwischen sprechen es Claude Desktop, Claude Code, Cursor, VS Code, Zed und eine wachsende Zahl weiterer Anwendungen. Technisch steckt darunter nichts Exotisches: JSON-RPC 2.0, also nummerierte Nachrichten mit Methode und Parametern.

Warum ein Protokoll und nicht noch eine Schnittstelle

ClaudeChatGPTCursorEigene AppCRMDateiablageKalendern8n

16Jedes Werkzeug einzeln an jeden Client angebunden, jeweils in dessen eigenem Format. Ein fünfter Client kostet vier weitere Integrationen.

Für ein Unternehmen ist der entscheidende Punkt weniger die gesparte Arbeit als die Richtung der Abhängigkeit. Deine Geschäftslogik liegt in einem Server, den du betreibst, und nicht in der Konfigurationsoberfläche eines Anbieters. Wechselt ihr in zwei Jahren das Modell, tauscht ihr den Client aus, nicht die Integration.

Host, Client, Server: drei Wörter, die man einmal sortieren muss

Die Begriffe klingen austauschbar, sind es aber nicht, und fast jede verwirrende Fehlermeldung wird klarer, wenn man sie auseinanderhält. Der Host ist die Anwendung, in der jemand arbeitet. Der Client ist die Komponente im Host, die mit genau einem Server spricht. Der Server ist das Stück, das du baust.

Wer spricht hier eigentlich mit wem?

Host und Client: zwei Rollen, die oft verwechselt werden

Der Host ist die Anwendung, in der jemand arbeitet, etwa Claude Desktop, Cursor oder dein eigener Chatbot. Für jeden angebundenen Server hält der Host einen eigenen Client, und dieser Client spricht mit genau einem Server. Drei Server bedeuten also drei Clients in einem Host.

Für den Transport gibt es zwei ernstzunehmende Optionen. Beim Entwickeln nimmst du stdio, weil du dann keinen Port, kein Zertifikat und keine Authentifizierung brauchst. Sobald mehr als eine Person den Server benutzen soll, wechselst du auf Streamable HTTP. Der Code dazwischen bleibt gleich: nur der Transport wird ausgetauscht, die Werkzeuge bleiben, wie sie sind.

Die drei Bausteine und die einzige Frage, die zählt

MCP kennt Tools, Resources und Prompts. Welcher Baustein der richtige ist, entscheidet nicht der Inhalt, sondern die Frage: Wer löst das aus, das Modell, der Client oder der Mensch?

Tool, Resource oder Prompt?

Die drei Bausteine unterscheiden sich nicht darin, was sie können, sondern darin, wer sie auslöst. Das ist die einzige Frage, die du beantworten musst.

Ein Tool ist eine Funktion, die etwas tut oder etwas holt. Das Modell entscheidet anhand von Name und Beschreibung, wann es sie aufruft. Alles, was Nebenwirkungen hat, gehört hierher, und genau deshalb gehört zu allem mit Nebenwirkungen auch eine Bestätigung.

Wer löst aus
Das Modell, selbstständig und ohne Rückfrage beim Nutzer, sofern der Client keine Bestätigung verlangt.
Im Client sichtbar als
Werkzeug-Symbol im Chat, Aufruf erscheint im Verlauf
SDK-Aufruf
server.registerTool()

Beispiel crm_lookup holt Stammdaten und den letzten Kontakt zu einem Kunden.

Schritt 1: Projekt aufsetzen

Wir starten in einem leeren Verzeichnis. Das offizielle TypeScript-SDK bringt Server, Transports und die Schema-Übersetzung mit, Zod beschreibt die Argumente der Werkzeuge.

Terminal
mkdir mein-mcp-server && cd mein-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Zwei Einstellungen entscheiden darüber, ob das Projekt später ohne Merkwürdigkeiten startet: Das Modulsystem muss auf NodeNext stehen, und die package.json braucht "type": "module". Das SDK ist reines ESM, ein CommonJS-Projekt scheitert beim ersten Import.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Schritt 2: Das erste Tool

Das Ziel: Jemand im Vertrieb fragt in Claude Desktop „Wie war der letzte Kontakt mit der Müller GmbH?", und die Antwort kommt aus dem internen CRM. Dafür registrieren wir ein Werkzeug, das einen Namen entgegennimmt und eine interne REST-Schnittstelle abfragt.

src/server.tsTypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

export function buildServer() {
  const server = new McpServer({
    name: 'firmen-crm-mcp',
    version: '1.0.0',
  });

  server.registerTool(
    'crm_lookup',
    {
      title: 'CRM-Kunden-Lookup',
      // Diese Beschreibung ist die Bedienungsanleitung für das Modell.
      description:
        'Sucht einen Kunden im internen CRM und gibt Stammdaten sowie den letzten Kontakt zurück. Nutze das Tool, wenn nach Kundendaten, Ansprechpartnern oder dem letzten Kontakt gefragt wird.',
      inputSchema: {
        kundenname: z
          .string()
          .min(2)
          .describe('Name des Kunden, ganz oder teilweise'),
      },
    },
    async ({ kundenname }) => {
      const apiUrl = process.env.CRM_API_URL ?? 'http://localhost:4000';
      const apiKey = process.env.CRM_API_KEY ?? '';

      const res = await fetch(
        `${apiUrl}/customers?name=${encodeURIComponent(kundenname)}`,
        { headers: { Authorization: `Bearer ${apiKey}` } },
      );

      // Fehler als Ergebnis, nicht als Exception: das Modell kann darauf reagieren.
      if (!res.ok) {
        return {
          content: [{ type: 'text', text: `CRM nicht erreichbar (HTTP ${res.status}).` }],
          isError: true,
        };
      }

      const daten = await res.json();

      // Strukturiert zurückgeben, nie fremden Fließtext durchreichen.
      return {
        content: [{ type: 'text', text: JSON.stringify(daten) }],
      };
    },
  );

  return server;
}
Die Server-Definition wandert in eine eigene Funktion. Beim Remote-Betrieb brauchen wir sie pro Session erneut, und dann zahlt sich diese Trennung aus.

Drei Details, die in der Praxis den Unterschied machen. Erstens: Das inputSchema ist ein Zod-Objekt, aus dem das SDK automatisch ein JSON-Schema erzeugt. Was du hier an Validierung hinterlegst, sieht das Modell, bevor es aufruft, und hält es meistens auch ein.

Zweitens: Die description ist kein Kommentar, sondern die eigentliche Bedienungsanleitung für das Modell. „Sucht einen Kunden" ist schwächer als „Sucht einen Kunden im internen CRM und gibt Stammdaten sowie den letzten Kontakt zurück". Bei unklaren Beschreibungen ruft das Modell das falsche Werkzeug auf oder gar keins. Das ist der häufigste Grund dafür, dass ein technisch korrekter Server sich nutzlos anfühlt.

Drittens: Fehler gehören mit isError: true zurückgegeben, nicht als geworfene Exception. So erfährt das Modell, dass der Aufruf gescheitert ist, und kann es anders versuchen, statt die Verbindung abreißen zu lassen.

Jetzt fehlt noch der Einstiegspunkt, der den Server über stdio verbindet. Zwei Zeilen, mehr ist es nicht:

src/index.tsTypeScript
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { buildServer } from './server.js';

const server = buildServer();
const transport = new StdioServerTransport();

// stdout gehört dem Protokoll. Logs immer nach stderr.
console.error('MCP-Server bereit (stdio)');

await server.connect(transport);

Was dabei tatsächlich über die Leitung geht

Bevor wir weiterbauen, lohnt ein Blick auf die Nachrichten selbst. Wer sie einmal gesehen hat, debuggt später in Minuten statt in Stunden, denn die Logs des Clients zeigen genau diese Zeilen.

Ein Werkzeugaufruf, Nachricht für Nachricht

Genau diese Nachrichten laufen über die Leitung, wenn jemand in Claude Desktop „Wie war der letzte Kontakt mit der Müller GmbH?" tippt.

  1. Clientinitialize
    {
      "jsonrpc": "2.0", "id": 1, "method": "initialize",
      "params": {
        "protocolVersion": "2025-06-18",
        "clientInfo": { "name": "claude-desktop", "version": "1.9.0" },
        "capabilities": { "roots": {}, "sampling": {} }
      }
    }

1/7 Der Client meldet sich an und nennt die Protokollversion, die er sprechen kann. Passt sie nicht zu deiner, handeln beide Seiten hier eine gemeinsame aus.

Auffällig ist, wie wenig Magie darin steckt. Ein Handshake, ein Katalog, ein Aufruf. Alles, was ein Modell über dein Unternehmen erfährt, steht in den Beschreibungen und in den Rückgabewerten, die du kontrollierst.

Schritt 3: Wissen als Resource bereitstellen

Das Mitarbeiterhandbuch ist kein Werkzeug: Es tut nichts, es liegt bereit. Solche Inhalte registrierst du als Resource unter einer stabilen URI. Stabil ist wichtig, weil Clients sich diese Adressen merken.

src/server.tsTypeScript
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';

server.registerResource(
  'mitarbeiterhandbuch',
  'docs://handbuch/mitarbeiter',
  {
    title: 'Mitarbeiterhandbuch',
    description: 'Internes Mitarbeiterhandbuch, Stand 2026.',
    mimeType: 'text/markdown',
  },
  async (uri) => {
    const pfad = resolve('./resources/mitarbeiterhandbuch.md');
    const inhalt = await readFile(pfad, 'utf8');

    return {
      contents: [
        { uri: uri.href, mimeType: 'text/markdown', text: inhalt },
      ],
    };
  },
);

Für Inhalte mit vielen Einzelstücken, etwa eine Seite pro Mitarbeiter, nimmst du statt einer festen URI ein Template wie docs://mitarbeiter/{'{'}id{'}'}. Das SDK liefert dazu ein ResourceTemplate, das die Platzhalter ausliest und dir als Parameter übergibt.

Schritt 4: Einen Slash-Befehl anbieten

Prompts sind die unterschätzte dritte Kategorie. Sie kosten wenig Code und nehmen den Kolleginnen und Kollegen genau die Arbeit ab, die sonst jeden Tag neu anfällt: die richtige Formulierung zu finden.

src/server.tsTypeScript
server.registerPrompt(
  'kundenmail-entwurf',
  {
    title: 'Kundenmail entwerfen',
    description: 'Erstellt einen freundlichen Mail-Entwurf an einen Bestandskunden.',
    argsSchema: {
      kundenname: z.string(),
      anlass: z.string().describe('z. B. Vertragsverlängerung, Rückfrage, Termin'),
    },
  },
  ({ kundenname, anlass }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text:
            `Schreibe eine freundliche, professionelle Mail an ${kundenname}. ` +
            `Anlass: ${anlass}. Sie-Form, maximal 120 Wörter, kein Werbeton.`,
        },
      },
    ],
  }),
);

Im Client erscheint das als /kundenmail-entwurf. Der Nutzer wird nach Kunde und Anlass gefragt, das Modell bekommt die fertige Vorlage. Ändert sich später die Tonalität eurer Kundenkommunikation, ändert ihr eine Datei statt zwanzig Gewohnheiten.

Schritt 5: Lokal einbinden und testen

Bau das Projekt mit npm run build und trag den Server in die Konfiguration deines Clients ein. Der Pfad zur Datei unterscheidet sich je nach Betriebssystem, der Inhalt nicht.

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "firmen-crm": {
      "command": "/usr/local/bin/node",
      "args": ["/Users/DEIN-NAME/code/mein-mcp-server/dist/index.js"],
      "env": {
        "CRM_API_URL": "https://crm.intern.firma.de",
        "CRM_API_KEY": "aus-dem-passwortmanager"
      }
    }
  }
}
Unter Linux liegt dieselbe Datei in ~/.config/Claude/.

Drei Dinge kosten hier erfahrungsgemäß die meiste Zeit. Der Pfad muss absolut sein, weil der Host den Prozess aus einem unbekannten Arbeitsverzeichnis startet. Der Client muss nach dem Speichern vollständig beendet werden, das Schließen des Fensters genügt auf macOS nicht. Und node muss für den Host auffindbar sein: Wer Node über einen Versionsmanager installiert hat, trägt besser den vollen Pfad ein, den which node ausgibt.

Schritt 6: Remote betreiben mit Streamable HTTP

Solange nur du den Server benutzt, ist stdio ideal. Für ein Team ist es unbrauchbar, denn jede Person müsste den Server lokal installieren und aktuell halten. Also stellen wir denselben Server über HTTP bereit, mit einer Session pro Verbindung und einer Token-Prüfung davor.

src/http.tsTypeScript
import express from 'express';
import { randomUUID } from 'node:crypto';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { buildServer } from './server.js';

const app = express();
app.use(express.json());

const TOKENS = new Map(
  (process.env.MCP_TOKENS ?? '')
    .split(',')
    .filter(Boolean)
    .map((eintrag) => {
      const [token, person] = eintrag.split(':');
      return [token, person ?? 'unbekannt'] as const;
    }),
);

// Ein Token pro Person: nur so lässt sich einzeln entziehen und zuordnen.
app.use('/mcp', (req, res, next) => {
  const token = (req.header('authorization') ?? '').replace(/^Bearer\s+/i, '');
  const person = TOKENS.get(token);
  if (!person) return res.status(401).json({ error: 'unauthorized' });

  res.locals.person = person;
  next();
});

const transports = new Map<string, StreamableHTTPServerTransport>();

app.all('/mcp', async (req, res) => {
  const sessionId = req.header('mcp-session-id');
  let transport = sessionId ? transports.get(sessionId) : undefined;

  if (!transport) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessionclosed: (id) => transports.delete(id),
    });
    // Eigene Server-Instanz pro Session, sonst teilen sich alle einen Zustand.
    await buildServer().connect(transport);
    transports.set(transport.sessionId!, transport);
  }

  await transport.handleRequest(req, res, req.body);
});

// Nur lokal lauschen. Nach außen geht es über den Reverse Proxy.
app.listen(3333, '127.0.0.1', () =>
  console.error('MCP-Server lauscht auf 127.0.0.1:3333/mcp'),
);
Pro Session ein Transport und eine Server-Instanz. Ohne diese Trennung teilen sich alle Nutzer einen Zustand, und das fällt erst in Produktion auf.

Der Client bekommt statt eines Kommandos eine URL. Das Token gehört pro Person vergeben, nicht pro Team: Nur dann lässt es sich einzeln zurückziehen und einem Eintrag im Audit-Log zuordnen.

claude_desktop_config.jsonJSON
{
  "mcpServers": {
    "firmen-crm": {
      "url": "https://mcp.firma.de/mcp",
      "headers": {
        "Authorization": "Bearer DEIN-PERSOENLICHES-TOKEN"
      }
    }
  }
}

Schritt 7: Deployment mit Docker und Reverse Proxy

Der Rest ist gewöhnlicher Betrieb: ein schlanker Container, davor ein Reverse Proxy mit Zertifikat. Wichtig ist genau eine Einstellung, die man leicht übersieht, und die dafür sorgt, dass Streaming-Antworten nicht im Puffer hängen bleiben.

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3333
CMD ["node", "dist/http.js"]

proxy_buffering off ist die Zeile, an der die meisten Deployments scheitern. Ohne sie sammelt der Proxy die Server-Sent Events und gibt sie erst am Ende aus, was jede längere Werkzeugausführung wie einen Hänger aussehen lässt. Das großzügige proxy_read_timeout gehört aus demselben Grund dazu.

Sicherheit: die Punkte, die wirklich weh tun

Ein MCP-Server ist eine Schnittstelle, die in deinem Namen handelt. Drei Angriffswege sind dabei keine Theorie, sondern das, was in Audits regelmäßig auffällt.

Prompt Injection über Rückgabewerte. Gibt dein Werkzeug fremde Inhalte zurück, etwa den Text einer Webseite oder einer eingegangenen Mail, kann darin eine Anweisung an das Modell stecken. Alles, was aus einer Quelle kommt, die nicht du kontrollierst, ist Text und keine Anweisung: als strukturiertes JSON zurückgeben, in ein Feld verpacken, niemals roh durchreichen.

Zu viele Rechte an einer Stelle. Dein Server hat meist Zugriff auf das gesamte CRM, der einzelne Nutzer aber nur auf seinen Bereich. Prüfst du das nicht, sieht jeder alles. Die Berechtigung gehört in das Backend, dem du die Identität des Nutzers durchreichst, und nicht in eine Abfrage im MCP-Server.

Authentifizierung als Nachgedanke. Bearer-Token sind ein vertretbarer Anfang für den internen Betrieb. Sobald der Server über das eigene Netz hinaus erreichbar ist, führt an OAuth 2.1 mit kurzlebigen Tokens kein Weg vorbei, so sieht es auch die Spezifikation vor.

Sieben Punkte, die kein Review ersetzt

Aus der Praxis: MCP als Fernbedienung für n8n

Die Kombination, die bei uns in Kundenprojekten am häufigsten läuft: n8n orchestriert die Abläufe, der MCP-Server übersetzt natürliche Sprache in konkrete Auslöser. „Starte das Onboarding für die Müller GmbH" wird zu einem Webhook-Aufruf, der Rest passiert im Workflow.

src/server.tsTypeScript
server.registerTool(
  'workflow_trigger',
  {
    title: 'n8n-Workflow auslösen',
    description: 'Startet einen freigegebenen n8n-Workflow per Webhook.',
    inputSchema: {
      // Enge Auswahl statt freier URL: das Tool kann nur, was vorgesehen ist.
      workflow: z.enum(['onboarding', 'rechnung', 'newsletter']),
      payload: z.record(z.string()).optional(),
    },
  },
  async ({ workflow, payload }) => {
    const url = process.env[`N8N_WEBHOOK_${workflow.toUpperCase()}`];
    if (!url) {
      return {
        content: [{ type: 'text', text: 'Dieser Workflow ist nicht konfiguriert.' }],
        isError: true,
      };
    }

    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload ?? {}),
    });

    return {
      content: [
        { type: 'text', text: `Workflow "${workflow}" ausgelöst (HTTP ${res.status}).` },
      ],
    };
  },
);

Der Zuschnitt ist bewusst eng: z.enum lässt nur die drei vorgesehenen Workflows zu. Ein Werkzeug, das eine beliebige URL aufrufen kann, ist keine Automatisierung mehr, sondern ein offenes Scheunentor. Genau diese Grenze zwischen „hilfreich" und „gefährlich" ziehen wir in Projekten meistens zuerst.

Wie so etwas im Alltag eines mittelständischen Betriebs aussieht, zeigen unsere Case Studies, etwa die Büroautomatisierung bei Fehr Garten- und Landschaftsbau, wo Anfragen, Angebote und Termine ohne manuelles Abtippen durchlaufen.

Fazit

Rund 200 Zeilen TypeScript, und du hast einen Server, der Tools, Resources und Prompts bereitstellt, lokal über stdio läuft und remote über Streamable HTTP erreichbar ist. Das Bemerkenswerte daran ist nicht der Umfang, sondern was er ersetzt: eine Integration pro Werkzeug und Client.

Wenn du weitermachen willst, in dieser Reihenfolge: Audit-Logging ergänzen, damit jeder Aufruf einer Person zuzuordnen ist. Dann OAuth 2.1 statt fester Token. Und erst danach mehr Werkzeuge, denn ein Server mit drei präzise beschriebenen Werkzeugen ist im Alltag deutlich brauchbarer als einer mit dreißig ungefähren.

Häufige Fragen

Brauche ich zwingend TypeScript?

Nein. Offizielle SDKs gibt es unter anderem für Python, Java, C#, Kotlin und Go. Die Konzepte sind identisch, nur die Schreibweise unterscheidet sich. TypeScript hat den Vorteil, dass die Argumentprüfung mit Zod und die Typen im Editor aus derselben Quelle kommen.

Funktioniert derselbe Server auch mit ChatGPT und Cursor?

Ja, das ist der Sinn des Protokolls. Getestet haben wir Claude Desktop, Claude Code, Cursor und VS Code. Unterschiede gibt es beim Umfang: Manche Clients unterstützen bisher nur Tools, aber keine Prompts oder Resources. Der Server bleibt derselbe.

Verlassen unsere Daten dabei das Unternehmen?

Der Server läuft auf Ihrer Infrastruktur, die Daten bleiben dort. Was das Modell sieht, sind ausschließlich die Rückgabewerte der Werkzeuge, die es aufruft. Wer zusätzlich das Modell selbst im Haus betreiben will, kombiniert das mit einem lokal gehosteten Sprachmodell.

Wie viele Werkzeuge verträgt ein Server?

Technisch beliebig viele, praktisch werden Modelle ab etwa zwanzig Werkzeugen unzuverlässiger in der Auswahl, weil alle Beschreibungen im Kontext liegen. Besser sind mehrere kleine Server nach Fachbereich als ein großer, der alles kann.

Was kostet der Betrieb?

Der Server selbst ist genügsam: Ein kleiner Container auf vorhandener Infrastruktur genügt, ein eigener virtueller Server liegt im niedrigen einstelligen Eurobereich pro Monat. Der Aufwand steckt in der Anbindung an die internen Systeme und im Rechtekonzept, nicht im Hosting.

KI, die Ihre eigenen Daten kennt.

Ob interner Assistent, Wissensdatenbank oder Automatisierung im Büro: Wir klären zuerst, ob es sich rechnet, und bauen es dann zum Festpreis. Server in Deutschland, DSGVO-konform.

Lieber direkt schreiben?info@intellize.de
  • 100 % DSGVO-konform
  • Server in Deutschland
  • Festpreise
  • Antwort in 24 h