Mono-MCP-Hosting fuer Kunden via Custom Connector

Pattern um mehrere bestehende stdio-MCPs (z.B. papierkram, ticketpay, m365) hinter einem einzigen Custom-Connector-Endpunkt in claude.ai Pro fuer einen Kunden bereitzustellen — ohne lokales Setup beim Kunden, ohne Eigenbau-OAuth, mit EU-Daten-Standort.

Erste Implementierung: Vibe Factory (Andre, 2026-05-02). Repo: mcp-vf-hosted = ~/source/mcps/mcp-vf-hosted/.

Wann nutzt man dieses Pattern

  • Kunde will Custom Connector in claude.ai (nicht Claude Desktop)
  • Kunde ist Nicht-Entwickler oder hat ungeeignete Maschine (Windows ohne uv, kein Terminal-Vertrauen)
  • Mehrere unserer MCPs sollen kombiniert werden (Buchhaltung + CRM + File-Storage etc.)
  • Single-Tenant — fuer Multi-Tenant siehe LibreChat-Phase

Wenn nur ein Kunde + ein MCP + lokal: nimm Claude Desktop mit lokaler MCP-Config (siehe claude-custom-connector).

Architektur

Standard-Pattern seit 2026-05-11 (Pivot von ECS Express): klassisches ECS Fargate mit cloudflared-Sidecar als einziger Public-Eingang. Kein ALB, kein ACM-Cert. Vollstaendiges Pattern: mcp-hosting-fargate-tunnel.

claude.ai Pro
    |  OAuth 2.1 (Scalekit EU) + JWT
    v
Cloudflare Edge (Frankfurt PoP, TLS + WAF + Rate)
    |  mcp-vf.agenticventures.de
    v
Cloudflare Tunnel (kein Public-Ingress am Origin)
    |  4 QUIC-Connections nach fra16/fra03/fra20/fra08
    v
AWS ECS Fargate Task (av-production, eu-central-1)
    +-- Container 1: cloudflared (Sidecar)
    |       Image: cloudflare/cloudflared:latest
    |       CMD:   tunnel --no-autoupdate run
    |       Token: AWS Secrets Manager (mcp-vf-hosted/cloudflared-token)
    |
    +-- Container 2: vf-mono (App)
            Image: 425924867359.dkr.ecr.eu-central-1.amazonaws.com/mcp-vf-hosted:latest
            FastMCP v2 mit ScalekitProvider + Uvicorn 0.0.0.0:8080
            Sub-MCPs: papierkram, ticketpay, m365 (alle drei aktiv)
            Health-Check: python -c urllib.request /health
            Tokens via AWS Secrets Manager (mcp-vf-hosted/upstream-tokens, 6 Keys) + ECS secrets:-Mapping mit JSON-Key-Extraktion

Sub-MCP-Komposition unveraendert:

FastMCP v2 "vf-mono"
    +--- create_proxy(stdio Client) --- mcp-papierkram (Subprozess) --> Papierkram API
    +--- create_proxy(stdio Client) --- mcp-ticketpay  (Subprozess) --> TicketPAY API
    +--- create_proxy(stdio Client) --- mcp-m365       (Subprozess) --> MS Graph

Eine URL (https://mcp-<kunde>.agenticventures.de), ein Connector beim Kunden, ein OAuth-Flow. Architektur-Detail mit Mermaid-Diagrammen + Trust-Boundaries: ~/source/mcps/mcp-vf-hosted/docs/architecture.md.

Entscheidungs-Bausteine

BausteinWahlWarum
OAuth-ServerScalekit AuthKit (EU-Region)DCR + PRM out-of-the-box, EU-Daten-Standort, FastMCP hat eingebauten ScalekitProvider, Free-Tier reicht fuer Single-User
Mono-MCP-FrameworkPrefect’s fastmcp v2hat ScalekitProvider, create_proxy(), mount(namespace=...) — das offizielle mcp.server.fastmcp (v1) hat das nicht
Sub-MCP-KompositionStdio-Subprozess + create_proxy()API-Drift zwischen v1 und v2 bricht direkten Library-Mount. Proxy isoliert die Welten, Sub-MCPs bleiben unmodifiziert
HostingRailway EU-West (Amsterdam)EU-Daten-Residency, simple Deploys, AVV verfuegbar
CDN/WAFCloudflare FreeTLS + DDoS + Bot-Detection ausreichend
Domain-Patternmcp-<kunde>.agenticventures.deWir betreiben, also unsere Zone

Setup fuer einen neuen Kunden (~3 Std + 1-2 Tage AVV-Wartezeit)

1. Scalekit (15 Min Browser, dann 1-2 Tage AVV parallel)

  1. Scalekit-Account auf https://app.scalekit.comEU-Region waehlen!
  2. MCP Server anlegen (Dashboard → MCP Servers → New)
    • Resource Identifier = die spaetere MCP-URL exakt (z.B. https://mcp-vf.agenticventures.de/mcp)
    • Required Scopes: leer lassen fuer Anfang
  3. Resource ID notieren (sk_resource_...)
  4. AVV anfordern bei Scalekit Sales/Legal — Standard-Prozess

2. Repo-Setup (~30 Min)

cp -r ~/source/mcps/mcp-vf-hosted ~/source/mcps/mcp-<kunde>-hosted
cd ~/source/mcps/mcp-<kunde>-hosted
 
# pyproject.toml: Name, Description anpassen
# main.py: _SUB_MCP_COMMANDS und _SUB_MCP_ENV anpassen falls andere MCPs
 
uv sync --all-extras
uv run pytest

3. Lokal-Smoke-Test mit MCP Inspector

cp .env.example .env  # Test-Tokens, echte Production-Tokens spaeter nur in Railway-ENV
uv run python -m mcp_<kunde>_hosted.main
# In neuem Terminal:
npx @modelcontextprotocol/inspector
# URL: http://localhost:8080/mcp -> OAuth-Flow durchklicken via Scalekit

4. Railway-Deploy (~2 Std)

  1. Railway-Projekt <kunde>-mcp-hosting, Region EU-West (Amsterdam)
  2. GitHub-Repo verbinden, Auto-Deploy auf main
  3. Alle Secrets als Railway-ENV (siehe .env.example)
  4. Cloudflare: Subdomain mcp-<kunde>.agenticventures.de → Railway-Origin, Full(strict) TLS
  5. Health-Check: curl https://mcp-<kunde>.agenticventures.de/health → 200

5. Pen-Test-Checkliste

Siehe Run-Plan zum Vibe-Factory-Erstausroll.

6. Kunde-Onboarding (~10 Min, davon 3 Min Kunde)

Per Signal/Threema/iMessage (nicht Mail):

  • 1 MCP-URL
  • 1 Scalekit-Login-Link
  • 1 Kurzanleitung „so traegst du’s in claude.ai ein”

Kunde:

  1. claude.ai → Settings → Connectors → „Add custom connector”
  2. URL eintragen, Add
  3. Connect klicken → Scalekit-Login → Allow → fertig

Smoke-Test mit drei Prompts.

Auth-Modell

SchichtWerWorauf basiert
TLS + WAF + IP-RateCloudflareStandard-Free-Tier
OAuth 2.1 Discovery + Token-Issuing + RefreshScalekit (EU)Externer IdP, FastMCP ScalekitProvider ruft Metadata + JWKS ab
JWT-Signature-Validation pro RequestFastMCP ScalekitProviderJWKS-Cache, kein round-trip pro Request
Per-Subject-Rate-Limit + Audit + Kill-SwitchGuardMiddleware (Eigen-Code)In-Memory Token-Bucket, JSON-Log mit PII-Scrubbing
Upstream-API-AuthSub-MCPs internTokens als ENV pro Subprozess

Rotation & Secrets-Hygiene

  • JWT-Tokens: Scalekit handhabt Lifecycle automatisch — claude.ai macht Refresh. Keine Marvin-Rotation mehr.
  • Upstream-API-Tokens (Papierkram-Token, TicketPAY-Key, M365-Secret): quartalsweise rotieren. Beim Anbieter neuen Key → Railway-ENV updaten → Container-Restart.
  • Scalekit-Owner-Login: in Marvins 1Password, MFA aktiv.
  • Alle .env* in .gitignore. gitleaks laeuft in CI.
  • Secret-Backup: GPG-verschluesselt unter ~/source/mcps/.secrets/<kunde>-mcp-hosting.env.gpg.

Incident-Response Cheatsheet

SymptomErste Aktion
URL geleakt / Verdacht auf Token-KlauKunden-Session in Scalekit revoken
401-Rate explodiert (Brute-Force)Cloudflare-Rule: Geo-Block oder IP-Block
5xx-Rate hochRailway-Logs pruefen, ggf. Rollback
Sofort-StopEMERGENCY_DISABLE=true in Railway-ENV, Restart → alle Tool-Calls 503
Kundenbeziehung endetRailway-Service stoppen + Upstream-API-Tokens beim Anbieter revoken

Bekannte Stolperer

  • FastMCP v1 vs v2 API-Drift: Direkter Library-Mount von mcp.server.fastmcp.FastMCP-Server-Objekten in einen Prefect-FastMCP-v2-Mono brechen bei list_tools ('Tool' has no attribute 'version'). Proxy-Pattern via create_proxy(Client(StdioTransport(...))) umgeht das ohne die Sub-MCPs zu modifizieren.
  • ScalekitProvider Resource-Identifier muss exakt der MCP-URL entsprechen wie sie in der claude.ai-Custom-Connector-UI eingetragen wird. Sonst Audience-Mismatch.
  • claude.ai-Custom-Connector akzeptiert KEINE statischen Bearer-Header in der UI (Live-Test 2026-05-02 bestaetigt). Nur OAuth-Discovery (Name, URL, optional OAuth Client ID/Secret). Wer ohne OAuth lebt, lebt am Anti-Pattern (URL-Path-Token).
  • PRM muss am Root-Pfad mounted sein (https://mcp-<kunde>.agenticventures.de/.well-known/oauth-protected-resource), nicht unter Sub-Pfaden. ScalekitProvider macht das automatisch — bei selbstgebautem RemoteAuthProvider drauf achten.
  • Cloudflare vor FastMCP: WebSocket/SSE-Streaming-Settings pruefen, sonst haengt MCP-Stream. Cloudflare-Default reicht meist nicht — streaming aktivieren.

Migrations-Pfad

Wenn Kunde waechst (mehrere User) → LibreChat-Phase. Stack bleibt grossteils, nur Auth-Modell wechselt von Single-User-Direct-Connect zu LibreChat-Multi-User.

Wenn andere OAuth-IdPs gewuenscht (WorkOS, Stytch, Auth0) → ScalekitProvider durch entsprechenden Provider tauschen, ~5 Zeilen Konfig.

Workflow-Prompts (registered seit 2026-05-10)

Drei MCP-Prompts als Slash-Commands fuer André. In claude.ai tippt er /<name>, der Prompt-Text wird als User-Message gesendet, Claude choreografiert die Sub-MCP-Tool-Calls.

Slash-CommandArgumentChoreographie
/offene_posten(keins)papierkram_list_invoices (status=delivered, paid_at=null) → Tabelle mit Mahn-Hinweis
/event_bilanzevent_idticketpay_list_tickets + list_transactions + list_cancellations + list_fees fuer ein Event → Brutto / Stornos / Gebuehren / Netto-Auszahlung + Auffaelligkeiten
/monatsabschlussmonat (YYYY-MM)Papierkram + TicketPAY kombiniert: Ausgangsrechnungen, Bank-Bewegungen, TicketPAY-Auszahlungen → Saldo

Source: ~/source/mcps/mcp-vf-hosted/src/mcp_vf_hosted/prompts.py. Neue Prompts dort registrieren via @mcp.prompt-Decorator und Funktion zurueckgibt einen String mit Tool-Anweisungen.

/health Diagnostic-Endpoint

Auth-frei, von ECS/Cloudflare als Healthcheck genutzt und von uns als Debug-Tool. Aktuelles Payload:

{
  "ok": true,
  "service": "vf-mono",
  "version": "0.1.0",
  "submcps_active": ["papierkram", "ticketpay", "m365"],
  "uptime_seconds": 0
}

Spaeter erweiterbar (z.B. pro Sub-MCP-Subprocess-Healthcheck). Bleibt auth-frei + ohne PII.

Migrations-Status AWS (Stand 2026-05-11)

Phase 1B der Pipeline (_index) — VF-Migration auf AWS Fargate-Tunnel. Stand:

  • Ziel-Account: av-production (425924867359), eu-central-1 ✅
  • Compute: AWS Fargate + cloudflared-Sidecar (Pivot von ECS Express, weil dort Custom-Domain + Sidecar nicht moeglich) ✅ deployed (CREATE_COMPLETE in 149s)
  • OAuth: Scalekit bleibt — Identifier https://mcp-vf.agenticventures.de/mcp ist unveraendert, Andres claude.ai-Connector-Setup unveraendert
  • Sub-MCPs: papierkram + ticketpay + m365 alle drei aktiv (M365 ist mit drin, Compliance-Review laeuft parallel — kein Re-Enable-Patch mehr noetig)
  • Tokens: beide Secrets retained beim Stack-Delete (DeletionPolicy-Patch vor Delete), Werte unveraendert
  • Tunnel: ID ce6dd7c0-d31c-456c-be8a-8705c079982d, 4 QUIC-Connections nach Frankfurt registered, Ingress-Config mcp-vf.agenticventures.de → http://localhost:8080 sitzt
  • Service-Status: ACTIVE 1/1/0, Health-Check alle 30s 200 OK (Python-Stdlib-Probe statt curl, weil Image kein curl hat)
  • Blockiert auf: manueller DNS-Cutover in Cloudflare-UI oder API. CNAME mcp-vfce6dd7c0-d31c-456c-be8a-8705c079982d.cfargotunnel.com, proxied=true
  • Cost: AWS ~38 €/Monat (Fargate-Tunnel, kein ALB-Posten — Detail in mcp-hosting-fargate-tunnel)
  • Lessons gegen alten Plan: (1) Secret mcp-vf-hosted/upstream-tokens war stack-managed mit DeletionPolicy:Delete (nicht Retain wie angenommen) — vor Delete via update-stack mit Retain-Patch ueberschrieben. (2) Secret.fromSecretNameV2 generiert Wildcard-ARN -?????? in der IAM-Policy, aber ECS-ValueFrom ruft Name-only-ARN → kein Match → AccessDenied. Fix: fromSecretCompleteArn mit vollem ARN inkl. Suffix. (3) python:3.12-slim hat kein curl. Health-Check via python -c "import urllib.request".