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
| Baustein | Wahl | Warum |
|---|---|---|
| OAuth-Server | Scalekit AuthKit (EU-Region) | DCR + PRM out-of-the-box, EU-Daten-Standort, FastMCP hat eingebauten ScalekitProvider, Free-Tier reicht fuer Single-User |
| Mono-MCP-Framework | Prefect’s fastmcp v2 | hat ScalekitProvider, create_proxy(), mount(namespace=...) — das offizielle mcp.server.fastmcp (v1) hat das nicht |
| Sub-MCP-Komposition | Stdio-Subprozess + create_proxy() | API-Drift zwischen v1 und v2 bricht direkten Library-Mount. Proxy isoliert die Welten, Sub-MCPs bleiben unmodifiziert |
| Hosting | Railway EU-West (Amsterdam) | EU-Daten-Residency, simple Deploys, AVV verfuegbar |
| CDN/WAF | Cloudflare Free | TLS + DDoS + Bot-Detection ausreichend |
| Domain-Pattern | mcp-<kunde>.agenticventures.de | Wir 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)
- Scalekit-Account auf https://app.scalekit.com — EU-Region waehlen!
- 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
- Resource Identifier = die spaetere MCP-URL exakt (z.B.
- Resource ID notieren (
sk_resource_...) - 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 pytest3. 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 Scalekit4. Railway-Deploy (~2 Std)
- Railway-Projekt
<kunde>-mcp-hosting, Region EU-West (Amsterdam) - GitHub-Repo verbinden, Auto-Deploy auf
main - Alle Secrets als Railway-ENV (siehe
.env.example) - Cloudflare: Subdomain
mcp-<kunde>.agenticventures.de→ Railway-Origin, Full(strict) TLS - 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:
- claude.ai → Settings → Connectors → „Add custom connector”
- URL eintragen, Add
- Connect klicken → Scalekit-Login → Allow → fertig
Smoke-Test mit drei Prompts.
Auth-Modell
| Schicht | Wer | Worauf basiert |
|---|---|---|
| TLS + WAF + IP-Rate | Cloudflare | Standard-Free-Tier |
| OAuth 2.1 Discovery + Token-Issuing + Refresh | Scalekit (EU) | Externer IdP, FastMCP ScalekitProvider ruft Metadata + JWKS ab |
| JWT-Signature-Validation pro Request | FastMCP ScalekitProvider | JWKS-Cache, kein round-trip pro Request |
| Per-Subject-Rate-Limit + Audit + Kill-Switch | GuardMiddleware (Eigen-Code) | In-Memory Token-Bucket, JSON-Log mit PII-Scrubbing |
| Upstream-API-Auth | Sub-MCPs intern | Tokens 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
| Symptom | Erste Aktion |
|---|---|
| URL geleakt / Verdacht auf Token-Klau | Kunden-Session in Scalekit revoken |
| 401-Rate explodiert (Brute-Force) | Cloudflare-Rule: Geo-Block oder IP-Block |
| 5xx-Rate hoch | Railway-Logs pruefen, ggf. Rollback |
| Sofort-Stop | EMERGENCY_DISABLE=true in Railway-ENV, Restart → alle Tool-Calls 503 |
| Kundenbeziehung endet | Railway-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 beilist_tools('Tool' has no attribute 'version'). Proxy-Pattern viacreate_proxy(Client(StdioTransport(...)))umgeht das ohne die Sub-MCPs zu modifizieren. ScalekitProviderResource-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 —
streamingaktivieren.
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-Command | Argument | Choreographie |
|---|---|---|
/offene_posten | (keins) | papierkram_list_invoices (status=delivered, paid_at=null) → Tabelle mit Mahn-Hinweis |
/event_bilanz | event_id | ticketpay_list_tickets + list_transactions + list_cancellations + list_fees fuer ein Event → Brutto / Stornos / Gebuehren / Netto-Auszahlung + Auffaelligkeiten |
/monatsabschluss | monat (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/mcpist 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-Configmcp-vf.agenticventures.de → http://localhost:8080sitzt - 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-vf→ce6dd7c0-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-tokenswar stack-managed mit DeletionPolicy:Delete (nicht Retain wie angenommen) — vor Delete via update-stack mit Retain-Patch ueberschrieben. (2)Secret.fromSecretNameV2generiert Wildcard-ARN-??????in der IAM-Policy, aber ECS-ValueFromruft Name-only-ARN → kein Match → AccessDenied. Fix:fromSecretCompleteArnmit vollem ARN inkl. Suffix. (3)python:3.12-slimhat keincurl. Health-Check viapython -c "import urllib.request".
Related
- claude-custom-connector — Custom Connector Constraints
- mcp-vf-hosted — Source-Repo
- papierkram, ticketpay, m365 — die Sub-MCPs im VF-Mono
- FastMCP RemoteAuth Doku
- Scalekit MCP Doku