Bereitstellung und Verwendung von MCP Servern in Docker

Details zur Einbindung eines per Docker bereitstehenden MCP Servers in die folgenden Hosts: MCP Inspector (Test), Claude (für Anthropic LLMs) und Open WebUI (für per Ollama lokal gehostete LLMs).

Kategorie: KI · 6/18/2025

Einleitung - TLDR

Eine Vielzahl von MCP Server Beispielen bezieht sich auf Python und TypeScript Implementierungen, es stehen jedoch genauso SDKs für Java, C# und weitere zur Verfügung. Wenn es um die Einbindung von MCP Servern in Hosts wie Claude geht, ergibt sich das gleiche Bild. Wir bei evanto stellen unsere Projekte jedoch meist per Docker bereit, daher ergab sich bei uns die Frage, wie wir in C# entwickelte und per Docker gehostete MCP Server sowohl für lokale LLMs als auch für die LLMs der großen Anbieter wie OpenAI, Anthropic etc. bereitstellen können. Daher soll hier in einem ersten Schritt die Einbindung eines per Docker bereitstehenden MCP Servers in die folgenden Hosts gezeigt werden:

  • MCP Inspector (Test)

  • Claude (für Anthropic LLMs)

  • Open WebUI (für per Ollama lokal gehostete LLMs)

Ein weiterer Artikel wird die Erstellung eines eigenen Hosts für MCP Server besprechen. Wichtig war uns außerdem, dass in den folgenden Beispielen beide Transporttypen - stdio und SSE - unterstützt werden, was Stand jetzt z.B. in Claude für SSE einen Wrapper erfordert.

Nicht berücksichtigte Themen

Im folgenden Artikel nicht berücksichtigt sind Themen wie JsonRPC 2.0 Protokollspezifika, Authentifizierung und Sicherheit. Diese sind Bestandteil der offiziellen Dokumentation.

MCP

Im folgenden eine sehr kurze Darstellung der MCP Architektur und der dort verwendeten verschiedenen Begriffe.

Was ist MCP?

Die Kernidee hinter MCP ist die Einbindung von externen Informationen (z.B. aus Firmendatenbanken) und Aktions- und Handlungsmöglichkeiten für verschiedenste Werkzeuge in LLMs per einem standardisierten Weg. Die MCP Dokumentation verwendet hier die schöne Analogie einer "USB Schnittstelle" für AI Anwendungen, die in der Lage ist, verschiedenste Softwaretools - genauso wie USB verschiedenste Geräte - per einheitlichem Protokoll anzubinden. Die Besonderheit beim MCP Protokoll ist dabei, dass es über reine API Funktionalität hinausgeht und in der Lage ist, auch Metainformationen über die verfügbaren "Funktionen" der MCP Server bereitzustellen (Funktionen ist hier verkürzt, MCP Server können verschiedene Dinge wie Ressourcen, Tools und Prompts bereitstellen, zusätzlich können Clients per "Sampling" LLM Funktionalitäten für Server verfügbar machen).

Warum MCP?

MCP unterstützt beim Aufbau von Agenten und komplexen Workflows auf Basis von LLMs. LLMs müssen - wie oben beschrieben - häufig mit Daten und Tools (z.B. firmeneigenen Datenbanken und Workflows) integriert werden. Kernpunkte sind

  • eine wachsende Liste vorgefertigter Integrationen, die das gewünschte LLM direkt nutzen kann.

  • die Flexibilität, zwischen LLM-Anbietern und -Herstellern zu wechseln und

  • auch die Möglichkeit, eigene, firmenspezifische MCPs zu entwickeln (oder entwickeln zu lassen).

Architektur

Die bei MCP verwendeten Begriffe erfordern ein zweites Hinschauen, da die "klassischen" Begriffe Client und Serverhier etwas anders verwendet werden und als weitere wichtige Entität der Host dazukommt. Die MCP Dokumentation beschreibt folgende Entitäten:

  • MCP-Hosts: Programme wie Claude Desktop, IDEs wie Windsurf, Cursor etc. oder z.B. eigene KI-Tools, die über MCP auf Daten zugreifen möchten.

  • MCP-Clients: Halten 1:1-Verbindungen zu Servern aufrecht (Siehe Transport stdio, hier startet der Client eine eigene Instanz des MCP Servers).

  • MCP-Server: Leichtgewichtige Programme, die jeweils bestimmte Funktionen über das standardisierte Model Context Protocol bereitstellen. Leichtgewichtig bedeutet, dass der eigentliche MCP Server nur die "Verpackung" der bereitgestellten Daten und Funktionen und das Bereitstellen der Meta-Daten dafür übernimmt.

  • Lokale Datenquellen: Dateien, Datenbanken und Dienste auf Ihrem Computer, auf die MCP-Server sicher zugreifen können

  • Remote-Dienste: Externe Systeme, die über das Internet (z. B. über APIs) verfügbar sind und mit denen MCP-Server eine Verbindung herstellen können.

Transporte und Protokoll

Die Transportschicht übernimmt die eigentliche Kommunikation zwischen Clients und Servern. MCP unterstützt - wie bereits angesprochen - die folgenden Transportmechanismen:

  1. stdio-Transport

    • Verwendet Standard-Ein-/Ausgabe für die Kommunikation

    • Der Client startet eine eigene Instanz des Servers um Zugriffsrechte auf die stdio Pipe zu haben

    • Ideal für lokale Prozesse

  2. Streamfähiger HTTP-Transport

    • Verwendet HTTP mit optionalen Server-Sent Events (SSE) für Streaming

    • HTTP POST für Nachrichten vom Client zum Server

Alle Transporte verwenden JSON-RPC 2.0 zum Austausch von Nachrichten. Dies ist bei der Nutzung der SDKs jedoch für den Entwickler transparent und nur in Details von Bedeutung. Weitere Informationen finden sich hier.

Hosts

Im folgenden sollen einige Hosts, mit denen sich MCP Server besonders einfach testen und ausprobieren lassen, betrachtet werden:

  • MCP Inspektor: unterstützt sowohl stdio als auch SSE Transport

  • Claude: Für lokale MCP Server aktuell nur Unterstützung des stdio-Transports, SSE basierte MCP Server können per Zusatztool (z.B. MCP Hub Gateway) integriert werden

  • Open WebUI (für lokale LLMs die per Ollama gehostet werden): Unterstützt OpenAPI kompatible Toolserver, d.h. hier muss ein OpenAPI-Wrapper wie MCPO eingesetzt werden.

  • C# Client: Hier wird die Einbindung anhand der NuGet-Package "ModelContextProtocol" gezeigt

MCP Server in Hosts einbinden

Im folgenden wird die Einbindung eines im Docker Container laufenden MCP-Servers in verschiedene typische Hosts, sowohl für "Mainstream" LLMs als auch für lokale LLMs gezeigt.

Per stdio-Transport

Besonderheit hier ist die Tatsache, dass eine Instanz des Docker Containers mit dem MCP Server hier vom (in den Host integrierten) MCP Client gestartet wird. Verschiedene Hosts (Claude, Cursor, ..), die den gleichen MCP Server verwenden, starten also jeweils eine eigene Instanz als Docker Container (was sich in der Container Liste von Docker Desktop schön beobachten lässt). Sehr wichtig für selbst entwickelte MCP Server: Die Implementierung darf nicht (wie oft üblich) Loggingausgaben auf stdout schreiben (z.B. mit Console.Write(..)), auf stdout dürfen nur gültige JsonRPC 2.0 Messages erscheinen. Oft loggen auch eingebundene Bibliotheken, wie unter .NET das Entity Framework auf stdout, dass muss ggf. explizit unterbunden werden, z.B. in .NET (Core) wäre ein möglicher Weg:

logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.None);

Ausgaben per Console.Error (auf stderr) sind jedoch möglich.

MCP Inspector

Der MCP Inspektor ist ein äußerst nützliches Werkzeug zum schnellen Testen von MCP Servern. Hier ein simples Beispiel für den mcp/time Server per Docker:

Der Start des Inspektors erfolgt (eine NodeJS Installation vorausgesetzt) über:

npx @modelcontextprotocol/inspector

Wichtig sind die Parameter des docker Aufrufs:

Parameter

Erläuterung

-i bzw. —interactive

Hält den Standard-Input (STDIN) des Containers offen, selbst wenn kein Terminal angeschlossen ist. So können etwa Daten per Pipe in den Container geschrieben werden:
echo "hello" | docker run -i your-image

—rm

Löscht den Container automatisch, sobald er gestoppt wird. Verhindert das Ansammeln von „Exited“-Containern auf dem Host.

 

Claude

Die Konfiguration von MCP Servern wird in Claude Desktop über die folgende Config-Datei vorgenommen:

Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
Win: %APPDATA%\Claude Desktop\config.json

Ein Eintrag für einen per Docker bereitgestellten MCP Server mit stdio Transport:

"TimeMCP": {
  "command": "docker",
  "args": [
    "run",
    "-i",
    "--rm",
    "mcp/time" 
  ],
  "env": {}
}

Wichtig: Nach Änderungen in der Config-Datei muss Docker Desktop neu gestartet werden. Probleme mit MCP Servern werden dann sofort nach dem Neustart angezeigt. Erfolgreich installierte MCP Server werden mit ihren Tools per Klick auf den “Suche und Werkzeuge” Button direkt unter dem Chat-Eingabefeld angezeigt.

Hilfreich zur Fehlersuche in selbst entwickelten MCP Servern sind die Claude Logs:

Mac: ~/Library/Logs/Claude/mcp*.log
Win: %APPDATA%\Claude\logs\mcp*.log
Linux: ~/.config/Claude/logs/mcp*.log

Claude Desktop Integrations: Seit Version 0.10.x unterstützt Claude Desktop “offizielle” MCP Integrationen, Ankündigung siehe hier. Diese müssen allerdings über eine OAuth-Authentifizierung verfügen, Eigenimplementierungen können z.B. mit OAuth Tools von/über Cloudflare bereitgestellt werden.

 

Open WebUI

Open WebUI ist eine populäre GUI für per Ollama installierte lokale LLMs. Der offizielle Schnellstart beschreibt verschiedene Option zum Start per Docker, wir würden den Start per Docker Compose empfehlen, da die Einbindung von MCP Servern mit mcpo ein Zusatztool erfordert (Open WebUI unterstützt nur OpenAPI kompatible Toolserver). Hier eine entsprechende docker-compose.yaml:

services:
  #─────────────────────────────────────────────────────────────────────────────
  # 1) mcpo-wrapper: wraps "docker run -i mcp/time" via mcpo on port 8000
  #─────────────────────────────────────────────────────────────────────────────
  mcpo-wrapper:
    build:
      context: ./mcpo-stdio
      dockerfile: Dockerfile
    container_name: mcpo-wrapper
    # Inside this container, mcpo will listen on 8000 → map that to 8003 on the host
    ports:
      - "8003:8000"
    # Mount the host Docker socket so we can 'docker run' the MCP image
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    # Use array form so that the shell sees everything after "sh -c" as one string.
    command:
      - sh
      - -c
      - |
        mcpo --host 0.0.0.0 --port 8000 -- \
          docker run -i mcp/time
    # (Optional) restart policy so that, if anything crashes, it comes back up.
    restart: unless-stopped

  # ─────────────────────────────────────────────────────────────────────────────
  # 2) openwebui: point at local Ollama (port 11434 on the Mac host),
  #    disable auth, mount data volume, restart always.
  # ─────────────────────────────────────────────────────────────────────────────
  openwebui:
    image: ghcr.io/open-webui/open-webui:ollama
    container_name: openwebui
    ports:
      - "3000:8080"
    volumes:
      - open-webui:/app/backend/data
    environment:
      - WEBUI_AUTH=False
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - mcpo-wrapper
    restart: always
volumes:
  open-webui:

Die obige YAML Datei nutzt den Weg mcpo selbst per eigenem Dockerfile in einen Container zu “verpacken”. Hier das Dockerfile (im Unterverzeichnis mcpo-stdio):

# 1. Start from a slim Python base so that mcpo can be easily installed
FROM python:3.11-slim

# 2. Install Docker CLI (and its dependencies) so that 'mcpo' can run 'docker run' commands.
#    Clean up apt caches to keep the image small.
RUN apt-get update \
 && apt-get install -y docker.io \
 && rm -rf /var/lib/apt/lists/*

# 3. Install mcpo into this container
RUN pip install mcpo

# 4. By default, mcpo will be on PATH. No need for any ENTRYPOINT; mcpo will be passed as command in docker-compose.yaml (see docker-compose.yaml above).

Wichtig hier ist die Tatsache, das mcpo selbst den eigentlichen MCP Server per Docker starten (können) muss, damit die stdio Pipe zwischen mcpo und dem MCP Server zur Verfügung steht. Dazu wird auch Docker im mcpo Container verfügbar gemacht. Dies ist bei HTTP MCP Servern nicht notwendig (siehe unten).

Hinweis: Auf Alpine-basierenden Slim-Images muss das Paket docker-cli heißen.

Die Einbindung des per mcpo“verpackten” MCP Servers erfolgt dann unter “Einstellungen / Werkzeuge / Tool Server verwalten”:

Noch einige Tipps zu Open WebUI:

  • Der “Native Tool Support” sollte eingeschaltet sein wenn ein Modell mit Toolsupport verwendet wird (z.B. Qwen 3, Menü: “Steuerung / Funktionsaufruf: nativ”), dagegen ausgeschaltet bei Modellen ohne Support (z.B. Gemma), hier versucht dann Open WebUI das Modell per Zusatzprompt zu “überreden” das Tool zu benutzen. Ein Modell mit nativem Tool-Support ist in jedem Fall besser.

  • Der System-Prompt unter “Steuerung / System-Prompt” muss bei jedem “Neuen Chat” wieder neu gesetzt werden

  • Wir haben gute Erfahrungen damit gemacht, dass im System-Prompt kurz beschrieben wird, bei welchem Typ von Anfrage welches Tool benutzt werden soll (Beispiel: “Wenn nach der Kundenadresse gefragt wird, dann benutze …”).

  • Wir haben gute Erfahrungen mit Modellen wie qwen3:14b gemacht, größere Modelle sind besser, setzen aber eine entsprechend leistungsfähige Hardware voraus.

 

C# Clients

Unsere Kunden-Systeme basieren auf dem .NET (Core) Framework 8/9, daher bietet es sich natürlich an, MCP Server + Client auf der gleichen Plattform zu entwickeln, da dann z.B. ohne Weiteres per Entity Framework gemappte Datenbanken angesprochen werden können. Dieser Artikel fokussiert auf Clients, daher ein Beispiel unter der Nutzung der “offiziellen” NuGet Package “ModelContextProtocol” (aktuell noch im Preview-Status mit der nicht sehr hohen Versionsnummer 0.2.0-preview.2). Hier die Erzeugung des MCP Clients:

private static async Task<IMcpClient> CreateStdioClientAsync(
  McpServerSettings serverConfig, 
  ILogger           logger
)
{
    var transportOptions = new StdioClientTransportOptions
    {
        Name        = serverConfig.Name,
        Command     = serverConfig.Command,
        Arguments   = serverConfig.Arguments.ToArray()
    };

    var transport   = new StdioClientTransport(transportOptions);

    return await ModelContextProtocol.Client.McpClientFactory.CreateAsync(transport);
}

Wobei die McpServerSettings eine eigene JSON Struktur in der appsettings.json Datei + C# Klasse sind:

  "McpServers": [
    {
      "Name": "Time MCP Server",
      "Command": "docker",
      "Arguments": ["run", "-i", "--rm", "mcp/time"],
      "Enabled": true,
      "TimeoutSeconds": 30,
      "TransportType": "STDIO"
    },
  ..

Die registrierten Tools werden dem chatClient dann wie folgt für eine einzelne Anfrage verfügbar gemacht (allTools ist eine Liste der registrierten MCP Clients).

// Get the response with timeout and measure duration
using var chatCts       = new CancellationTokenSource(TimeSpan.FromSeconds(180));
var       chatResponse  = await chatClient.GetResponseAsync(
    conversationHistory,
    new ChatOptions { Tools = [.. allTools] },
    cancellationToken: chatCts.Token
);

Der ChatClient kann z.B. mit einem OpenAIChatClientProvider() oder einem OllamaChatClientProvider()für lokale Modelle erzeugt werden. Die hier sehr verkürzte Darstellung werden wir bald mit einem GitHub-Repo hinterlegen.

 

Per HTTP-Transport (SSE)

Bei Verwendung des HTTP Transports ist naturgemäß die Limitierung des MCP Server Aufrufs nur auf dem lokalen Rechner aufgehoben. Statt dessen kann der MCP Server auf einem beliebigen (erreichbaren) Web Host laufen. Damit werden aber natürlich Sicherheitsaspekte wie TLS und Authentifizierung wichtiger als bei einer lokalen Instanz.

MCP Inspector

Die Einbindung eines SSE basierten MCP Servers ist simpler, da hier der MCP Server bereits in einem Docker Container läuft und nicht extra vom MCP Inspector gestartet werden muss. Hier ist nur die URL des Servers zu hinterlegen (lokales Beispiel):

Claude

Claude unterstützt (derzeit) keine lokalen SSE Server, daher ist ein Umweg über das Tool MCPHUB notwendig. Dazu sind die folgenden Schritte notwendig:

  1. Installation von MCPHUB (NodeJS vorausgesetzt):

npm install -g @mcphub/gateway
  1. Ermitteln des lokalen Installationspfads:
# This shows the root directory of global packages
npm root -g

# The gateway will be located at:
<npm_global_root>/@mcphub/gateway/dist/src/mcphub-gateway.js
  1. Anpassen der Claude Konfigurationsdatei claude_desktop_config.json (Lokalisierung s.o.):
{
  "mcpServers": {
    "MyMCP": {
      "command": "node",
      "args": [
        "<gateway-path>"
      ],
      "env": {
        "MCPHUB_SERVER_URL": "http://localhost:<myport>"
      }
    },
    :
  }
}

Dabei kann der MCP Server auch in einem Docker Container laufen..

Wichtig: Lt. Dokumentation sollte die Umgebungsvariable MCP_SERVER_URL gesetzt werden, wirklich funktioniert jedoch MCPHUB_SERVER_URL(siehe auch Bug #37 im Gateway-Repo).

Open WebUI

Die Integration in Open WebUI erfolgt analog dem STDIO Transport per MCPO, jedoch etwas einfacher, da hier MCPO den MCP Server nicht per Docker starten muss, sondern direkt zugreifen kann:

services:
  # ─────────────────────────────────────────────────────────────────────────────
  # 1) mcpo-sse-wrapper: wrap the SSE MCP at http://host.docker.internal:5555/sse
  #─────────────────────────────────────────────────────────────────────────────
  mcpo-sse-wrapper:
    build:
      context: ./mcpo-sse           # ← points at the Dockerfile we just created
      dockerfile: Dockerfile
    container_name: mcpo-sse-wrapper
    ports:
      - "8005:8000"                 # mcpo inside listens on 8000 → host maps to 8005
    # We need host.docker.internal so mcpo can reach the SSE server on your Mac
    extra_hosts:
      - "host.docker.internal:host-gateway"
    command:
      - sh
      - -c
      - |
        mcpo \
          --host 0.0.0.0 \
          --port 8000 \
          --server-type sse \
          -- http://host.docker.internal:5555/sse
    restart: unless-stopped

  # ─────────────────────────────────────────────────────────────────────────────
  # 2) openwebui: point at local Ollama (port 11434 on the Mac host),
  #    disable auth, mount data volume, restart always.
  # ─────────────────────────────────────────────────────────────────────────────
  openwebui:
    image: ghcr.io/open-webui/open-webui:ollama
    container_name: openwebui
    ports:
      - "3000:8080"
    volumes:
      - open-webui:/app/backend/data
    environment:
      - WEBUI_AUTH=False
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      - mcpo-sse-wrapper
    restart: always
volumes:
  open-webui:

Die obige YAML Datei nutzt analog Transport STDIO den Weg mcpo selbst per eigenem Dockerfile in einen Container zu “verpacken”. Hier das Dockerfile (im Unterverzeichnis mcpo-sse), das diesmal keine Docker-Installation beinhaltet:

# mcpo-sse/Dockerfile

# 1) Start from a slim Python base
FROM python:3.11-slim

# 2) Install mcpo so we can run "mcpo --server-type sse ..."
RUN pip install mcpo

# 3) No ENTRYPOINT here—compose will supply the full command at runtime

C# Clients

Hier die Erzeugung des MCP Clients für den SSE Transport:

private static async Task<IMcpClient> CreateSseClientAsync(
  McpServerSettings serverConfig, 
  ILogger           logger
)
{
    if (String.IsNullOrEmpty(serverConfig.Url))
    {
        throw new ArgumentException("URL is required for HTTP transport!");
    }
 
    var transportOptions = new SseClientTransportOptions
    {
        Name      = serverConfig.Name,
        Endpoint  = new Uri(serverConfig.Url)
    };

    var transport = new SseClientTransport(transportOptions);

    return await ModelContextProtocol.Client.McpClientFactory.CreateAsync(transport);
}

Wobei die McpServerSettings folgende eigene JSON Struktur in der appsettings.json Datei + C# Klasse sind:

"McpServers": [
    {
      "Name": "My MCP Server",
      "Url": "http://localhost:<myport>",
      "Enabled": true,
      "TimeoutSeconds": 30,
      "TransportType": "SSE"
    },
  ..

Die registrierten Tools werden dem chatClient dann analog stdio für eine einzelne Anfrage verfügbar gemacht (allToolsist eine Liste der registrierten MCP Clients).

// Get the response with timeout and measure duration
using var chatCts       = new CancellationTokenSource(TimeSpan.FromSeconds(180));
var       chatResponse  = await chatClient.GetResponseAsync(
    conversationHistory,
    new ChatOptions { Tools = [.. allTools] },
    cancellationToken: chatCts.Token
);

Die hier sehr verkürzte Darstellung werden wir bald ebenso mit einem GitHub-Repo hinterlegen.

Fazit

MCP-Server lassen sich dank Docker nicht nur zügig lokal testen (Gegenstand diesen Beitrags), sondern auch reproduzierbar in CI/CD-Pipelines, Edge-Installationen oder der Cloud betreiben.

Wichtigste Take-aways

  1. Transport bewusst wählen: stdio bevorzugen, wenn Sie volle Prozessisolation wünschen; SSE, wenn mehrere Clients denselben Server skalierbar erreichen sollen.

  2. Saubere Pipes: Alles, was nicht JSON-RPC ist, gehört in stderr oder ein zentrales Log-System (z. B. Grafana Loki).

  3. Sicherheit früh denken: Zertifikate, Auth-Layer und Ressourcenlimits lassen sich per Reverse-Proxy oder Compose-Profiles schon im Dev-Setup aktivieren (dazu in einem späteren Beitrag mehr).

  4. Tool-First-Prompting: Beschreiben Sie in jedem Host klar, wann das Modell welches Tool aufrufen soll – das reduziert Halluzinationen drastisch. In Claude Desktop ist das “Projekte” Feature nützlich, so lassen sich per System-Prompt diese Anweisungen getrennt für verschiedene Anwendungsfälle mit unterschiedlichen Tools hinterlegen.

Über uns

Ein erfahrenes Entwicklerteam, das mit Leib und Seele Software erstellt.

evanto logo

Kontaktdaten

Brunnstr. 25,
Regensburg

+49 (941) 94592-0
+49 (941) 94592-22

Statistik