mcp-vf-hosted: Migration ECS-Express → Fargate + Tunnel-Sidecar

Ausfuehrbarer Prompt fuer eine frische Claude-Code-Session in ~/source/mcps/mcp-vf-hosted/. Baut den Service vom alten ECS-Express-Stack (gescheitert wegen Custom-Domain-Issue) auf klassisches ECS Fargate + Cloudflare-Tunnel-Sidecar um. Pattern: mcp-hosting-fargate-tunnel. Entscheidung: hosted-mcp-architektur-2026.

Stand bei Session-Start (was schon existiert)

Vom Vortag uebernehmen, nicht neu bauen:

ResourceWert
Cloudflare Tunnel-IDce6dd7c0-d31c-456c-be8a-8705c079982d
Cloudflare Tunnel-Namemcp-vf-hosted
Tunnel-Token (Secret)arn:aws:secretsmanager:eu-central-1:425924867359:secret:mcp-vf-hosted/cloudflared-token-q5MEkS
Tunnel-Ingress-Configmcp-vf.agenticventures.de → http://localhost:8080, sonst http_status:404
Sub-MCP-Tokens (Secret)arn:aws:secretsmanager:eu-central-1:425924867359:secret:mcp-vf-hosted/upstream-tokens-AHEi6G (6 Keys: papierkram + ticketpay + m365)
ECR Repo425924867359.dkr.ecr.eu-central-1.amazonaws.com/mcp-vf-hosted (Image :latest mit allen 3 Sub-MCPs + Workflow-Prompts + diagnostic /health)
Cloudflare Zoneagenticventures.de aktiv (Zone-ID 772ff674c33fbe17cae49d7234c30e60)
CF DNS-Record mcp-vfaktuell CNAME → mc-1a56ed08537b4f9fae11028fe698af47.ecs.eu-central-1.on.aws (gibt 404, wird umgestellt)
Alter ECS-Express-StackMcpVfHosted in CFN, laeuft mit alter Architektur, gibt aktuell den 404

Pre-Run-Setup (Mensch, ~3 Min)

export CF_API_TOKEN="<token-aus-cloudflare-dashboard>"
export CF_ACCOUNT_ID="bf395d62cc6a9117564c0712fa9e3ad2"
export CF_ZONE_ID="772ff674c33fbe17cae49d7234c30e60"
export TUNNEL_ID="ce6dd7c0-d31c-456c-be8a-8705c079982d"
 
# Sanity-Check Cloudflare-API
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" | jq '.result.status'
# Erwartet: "active"
 
# Sanity-Check AWS
~/.local/bin/aws sts get-caller-identity --profile av-production --query 'Account' --output text
# Erwartet: 425924867359

Der Prompt (Copy-Paste in frische Claude-Code-Session in ~/source/mcps/mcp-vf-hosted/)

# Ziel: mcp-vf-hosted von ECS Express auf klassisches Fargate + Tunnel-Sidecar migrieren
 
## Kontext
 
- Repo: `~/source/mcps/mcp-vf-hosted` — FastMCP v2 Wrapper, Code + Image + Tests stehen
- AWS-Account: `av-production` (425924867359), eu-central-1
- Vorgaenger-Pattern (gescheitert): ECS Express Mode mit AWS-managed ALB — Custom-Domain `mcp-vf.agenticventures.de` gab 404 weil ALB nur eigenen Hostname akzeptiert + Sidecar nicht moeglich
- Neues Pattern: klassisches ECS Fargate, 2 Container im Task (vf-mono + cloudflared-Sidecar), KEIN ALB, Tunnel ist einziger Public-Eingang
- Production-Risiko: 0 — Andre nutzt MCP noch nicht aktiv. Wir koennen den alten Stack abreissen.
- Doku-Refs:
  - `~/source/agentic-ventures/intern/wissen/entscheidungen/hosted-mcp-architektur-2026.md` (Decision)
  - `~/source/agentic-ventures/intern/wissen/prozesse/mcp-hosting-fargate-tunnel.md` (Pattern)
  - `~/source/agentic-ventures/intern/capabilities/mcps/mcp-vf-hosted.md` (Capability)
  - `~/source/agentic-ventures/intern/projekte/mcp-pipeline-aws/_index.md` (Pipeline-Plan)
 
## Was bleibt unveraendert
 
- Source-Code (`src/mcp_vf_hosted/*.py`) — Workflow-Prompts, /health, M365 sind alle drin
- Dockerfile + Image im ECR — `:latest` ist aktuell mit allen 3 Sub-MCPs
- Secret `mcp-vf-hosted/upstream-tokens` (6 Keys) — wiederverwenden
- Secret `mcp-vf-hosted/cloudflared-token` — wiederverwenden
- Cloudflare-Tunnel `ce6dd7c0-d31c-456c-be8a-8705c079982d` + Ingress-Config — wiederverwenden
- Cloudflare-Zone + DNS-Setup ausser dem `mcp-vf` CNAME — wiederverwenden
- Scalekit-Resource fuer Andre — wiederverwenden, Identifier `https://mcp-vf.agenticventures.de/mcp` bleibt
 
## Was sich aendert
 
- CDK-Stack `infra/lib/mcp-vf-hosted-stack.ts` wird komplett ersetzt:
  - Statt `CfnExpressGatewayService``FargateService` + `FargateTaskDefinition`
  - Zweiter Container `cloudflared` im Task
  - Kein ALB, keine 3 Spezial-IAM-Rollen (nur ExecRole + TaskRole), keine ALB-Sec-Group
- ECR-Repo bleibt (importiert wie vorher, weil RemovalPolicy.RETAIN)
- Secret-Mappings unveraendert (`secrets[]` mit `:KEY::`-Syntax)
- ENV-Vars unveraendert
- CFN-Stack-Name bleibt `McpVfHosted`, aber die Resources darin werden alle ersetzt
 
## Vorbedingungen (zuerst pruefen, bei Fehler stoppen)
 
1. `aws sts get-caller-identity --profile av-production` → Account `425924867359`
2. `echo $CF_API_TOKEN`, `echo $CF_ACCOUNT_ID`, `echo $CF_ZONE_ID`, `echo $TUNNEL_ID` alle gesetzt
3. Token-Permissions: `Account:Cloudflare Tunnel:Edit` + `Zone:DNS:Edit` fuer agenticventures.de
4. Tunnel existiert: `curl -s -H "Authorization: Bearer $CF_API_TOKEN" "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID" | jq '.result.name'` → "mcp-vf-hosted"
5. Tunnel-Ingress-Config sitzt: `curl -s -H "Authorization: Bearer $CF_API_TOKEN" ".../cfd_tunnel/$TUNNEL_ID/configurations" | jq '.result.config.ingress'` → muss `mcp-vf.agenticventures.de → http://localhost:8080` enthalten
6. AWS-Secrets existieren: `aws secretsmanager describe-secret --secret-id mcp-vf-hosted/upstream-tokens` und `... mcp-vf-hosted/cloudflared-token`
7. ECR-Image existiert: `aws ecr describe-images --repository-name mcp-vf-hosted --image-ids imageTag=latest`
8. CFN-Stack `McpVfHosted` existiert: `aws cloudformation describe-stacks --stack-name McpVfHosted`
 
Bei einer Failed-Bedingung: stoppen, Diagnose ausgeben, **nicht** raten.
 
## Phase 1 — CDK-Stack rewriten
 
1. Backup vom alten Stack-File:
   ```
   cp infra/lib/mcp-vf-hosted-stack.ts infra/lib/mcp-vf-hosted-stack.ts.ecs-express-backup
   ```
 
2. Neuer Stack-Inhalt (`infra/lib/mcp-vf-hosted-stack.ts`):
   - Importe: `aws-cdk-lib`, `aws-ecr` (fromRepositoryName, weil Repo retained), `aws-ecs`, `aws-iam`, `aws-secretsmanager`, `aws-logs`
   - Klasse `McpVfHostedStack extends cdk.Stack`
   - Body:
     - `repo = ecr.Repository.fromRepositoryName(this, 'EcrRepo', 'mcp-vf-hosted')`
     - `upstreamTokens = secretsmanager.Secret.fromSecretNameV2(this, 'UpstreamTokens', 'mcp-vf-hosted/upstream-tokens')` (importiert das bestehende)
     - `cloudflaredToken = secretsmanager.Secret.fromSecretNameV2(this, 'CloudflaredToken', 'mcp-vf-hosted/cloudflared-token')`
     - `execRole = new iam.Role(...)` mit `AmazonECSTaskExecutionRolePolicy`, plus `upstreamTokens.grantRead(execRole)` und `cloudflaredToken.grantRead(execRole)`
     - `taskRole = new iam.Role(...)` mit nichts (leer)
     - `logGroup = new logs.LogGroup(this, 'LogGroup', { logGroupName: '/aws/ecs/default/mcp-vf-hosted', retention: logs.RetentionDays.ONE_MONTH })`
     - `taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', { cpu: 1024, memoryLimitMiB: 2048, executionRole: execRole, taskRole: taskRole })`
     - **Container 1 (vf-mono)**:
       - `image: ecs.ContainerImage.fromEcrRepository(repo, 'latest')`
       - `containerName: 'vf-mono'`
       - `essential: true`
       - `portMappings: [{ containerPort: 8080 }]`
       - `environment: { SCALEKIT_ENV_URL: 'https://agenticventures.eu.scalekit.dev', SCALEKIT_RESOURCE_ID: 'res_123571403667539459', PUBLIC_BASE_URL: 'https://mcp-vf.agenticventures.de', AWS_REGION: this.region, LOG_LEVEL: 'INFO', EMERGENCY_DISABLE: 'false', RATE_LIMIT_PER_MINUTE: '60', RATE_LIMIT_PER_HOUR: '1000' }`
       - `secrets: { PAPIERKRAM_TOKEN: ecs.Secret.fromSecretsManager(upstreamTokens, 'PAPIERKRAM_TOKEN'), PAPIERKRAM_SUBDOMAIN: ..., TICKETPAY_API_KEY: ..., M365_TENANT_ID: ..., M365_CLIENT_ID: ..., M365_CLIENT_SECRET: ... }` (6 Keys aus dem JSON-Secret)
       - `logging: ecs.LogDrivers.awsLogs({ logGroup, streamPrefix: 'vf-mono' })`
       - `healthCheck: { command: ['CMD-SHELL', 'curl -f http://localhost:8080/health || exit 1'], interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), retries: 3, startPeriod: cdk.Duration.seconds(60) }`
     - **Container 2 (cloudflared)**:
       - `image: ecs.ContainerImage.fromRegistry('cloudflare/cloudflared:latest')`
       - `containerName: 'cloudflared'`
       - `essential: true`
       - `command: ['tunnel', '--no-autoupdate', 'run']`
       - `secrets: { TUNNEL_TOKEN: ecs.Secret.fromSecretsManager(cloudflaredToken) }` (das ganze Secret, nicht JSON-Key)
       - `logging: ecs.LogDrivers.awsLogs({ logGroup, streamPrefix: 'cloudflared' })`
     - **Cluster**: `ecs.Cluster.fromClusterAttributes(this, 'Cluster', { clusterName: 'default', vpc: ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true }) })` — oder neuer Cluster wenn `default` Probleme macht
     - **Service**: `new ecs.FargateService(this, 'Service', { serviceName: 'mcp-vf-hosted', cluster, taskDefinition: taskDef, desiredCount: 1, assignPublicIp: true, securityGroups: [<eigene-sg-nur-outbound-443>] })`
       - Security-Group: `new ec2.SecurityGroup(this, 'EgressOnly', { vpc, allowAllOutbound: true })` — keine Inbound-Regeln
   - Outputs:
     - `ServiceName: 'mcp-vf-hosted'`
     - `EcrUri`
     - `LogGroupName: '/aws/ecs/default/mcp-vf-hosted'`
     - `TunnelHostname: '<tunnel-id>.cfargotunnel.com'` (Hard-coded weil bekannt)
 
3. ASCII-Hygiene: `grep -nP '[^\x00-\x7F]' infra/lib/mcp-vf-hosted-stack.ts` — wenn was kommt, ersetzen (Em-Dash → -, etc.). IAM-Role-Descriptions duerfen kein non-ASCII enthalten (CFN-Stolperer von 2026-05-10).
 
4. `cd infra && npx cdk diff --profile av-production` — sollte zeigen:
   - Alle alten Express-Resources: DELETED
   - Neue klassische Fargate-Resources: ADDED
   - Beide importierten Secrets + ECR: unveraendert (importiert, nicht managed)
 
## Phase 2 — Alten Stack zerstoeren + neuen Stack deployen
 
**Achtung:** Wir koennen NICHT einfach `cdk deploy` machen — die Resource-Typen aendern sich komplett (CfnExpressGatewayService → FargateService etc.). Das wuerde CFN versuchen replace-in-place zu machen und scheitern. Wir muessen den Stack zuerst loeschen.
 
1. Alten Stack loeschen:
   ```
   aws cloudformation delete-stack --stack-name McpVfHosted --profile av-production --region eu-central-1
   ```
   Warten bis `DELETE_COMPLETE`.
 
   Was wird geloescht: ExecRole, InfraRole, TaskRole, IAM-Policy, ExpressGatewayService. NICHT geloescht (weil retained oder importiert): ECR-Repo, beide Secrets (sind RETAIN). ALB + Cert wird von ECS Express-Cleanup mitgeloescht.
 
2. Pruefen dass ECR + Secrets noch da sind:
   ```
   aws ecr describe-repositories --repository-names mcp-vf-hosted --profile av-production --region eu-central-1
   aws secretsmanager describe-secret --secret-id mcp-vf-hosted/upstream-tokens --profile av-production --region eu-central-1
   aws secretsmanager describe-secret --secret-id mcp-vf-hosted/cloudflared-token --profile av-production --region eu-central-1
   ```
 
3. Neuen Stack deployen:
   ```
   cd infra
   npx cdk deploy --profile av-production --require-approval never --outputs-file /tmp/vf-cdk-outputs.json
   ```
 
4. Stack-Status auf CREATE_COMPLETE warten (bis ~5 Min). Bei Fehler: Logs anschauen, NICHT raten.
 
## Phase 3 — Smoke gegen Tunnel-Hostname
 
Sobald Stack ACTIVE + Task RUNNING:
 
1. Task-Status pruefen:
   ```
   aws ecs describe-services --cluster default --services mcp-vf-hosted --profile av-production --region eu-central-1 \
     --query 'services[0].[status,desiredCount,runningCount,pendingCount]' --output text
   ```
   Erwartet: `ACTIVE 1 1 0`.
 
2. Cloudflared-Logs pruefen — muss `Registered tunnel connection` enthalten:
   ```
   aws logs tail /aws/ecs/default/mcp-vf-hosted --filter-pattern '?Registered ?cloudflared' --profile av-production --region eu-central-1
   ```
 
3. Tunnel-Status in Cloudflare:
   ```
   curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
     "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID" \
     | jq '{status: .result.status, connections: .result.connections}'
   ```
   Erwartet: `status: "healthy"`, 4 Connections sichtbar.
 
4. Smoke mit Host-Override gegen Tunnel-Hostname:
   ```
   curl -sS -i "https://$TUNNEL_ID.cfargotunnel.com/health" \
     -H "Host: mcp-vf.agenticventures.de"
   ```
   Erwartet: `HTTP/2 200`, Body `{"ok":true,"service":"vf-mono","version":"0.1.0","submcps_active":["papierkram","ticketpay","m365"],"uptime_seconds":<n>}`.
 
## Phase 4 — DNS-Cutover
 
1. `mcp-vf` Record in Cloudflare auf Tunnel-Hostname umstellen:
   ```
   RECORD_ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
     "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=mcp-vf.agenticventures.de&type=CNAME" \
     | jq -r '.result[0].id')
   
   curl -sS -X PUT \
     -H "Authorization: Bearer $CF_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d "{
       \"type\": \"CNAME\",
       \"name\": \"mcp-vf\",
       \"content\": \"$TUNNEL_ID.cfargotunnel.com\",
       \"proxied\": true,
       \"ttl\": 1
     }" \
     "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID"
   ```
 
2. ~60s warten (Cloudflare-Edge-Cache-TTL).
 
3. End-to-End Smoke OHNE Host-Override:
   ```
   curl -sS -i https://mcp-vf.agenticventures.de/health
   ```
   Erwartet: `HTTP/2 200`, Header `server: cloudflare`, `cf-ray: ...`, Body mit drei submcps.
 
## Phase 5 — Wiki-Updates
 
Wenn Phase 4 gruen:
 
1. `~/source/agentic-ventures/intern/capabilities/mcps/mcp-vf-hosted.md` — neue Architektur eintragen (Fargate-Tunnel, kein ALB, ServiceName `mcp-vf-hosted`)
2. `~/source/agentic-ventures/intern/capabilities/repos/mcp-vf-hosted.md` — Frontmatter updaten
3. `~/source/agentic-ventures/intern/projekte/mcp-pipeline-aws/_index.md` — Phase 1B als ✅ DONE, neue Phase „Fargate-Tunnel-Migration" am Ende mit ✅, ECS-Express-Phase als deprecated markieren
4. `~/source/agentic-ventures/intern/firma/produkt-bundle.md` — Compute-Sektion (Punkt 1) auf Fargate-Tunnel-Pattern updaten
5. `~/source/mcps/mcp-vf-hosted/docs/architecture.md` — Mermaid-Diagramm aktualisieren (Tunnel statt ALB)
6. `~/source/mcps/mcp-vf-hosted/README.md` — Architektur-Diagramm + Deploy-Sektion auf Fargate-Tunnel
 
## Rollback (falls Phase 2 oder Phase 4 bricht)
 
- Phase 2 bricht: alter Stack ist schon geloescht — neuen Stack debuggen, bei Bedarf in mehreren Iterationen. Production-Risiko = 0 weil Andre nicht aktiv ist.
- Phase 4 bricht: `mcp-vf` Record zurueck auf alten ECS-Express-Hostname → aber der ALB existiert nicht mehr nach Stack-Delete, also kein Sinn. Alternativen:
  - Tunnel-Hostname direkt nutzen (`$TUNNEL_ID.cfargotunnel.com`) und Cert-Validation-Browser-Issues akzeptieren
  - Stack neu deployen und debuggen
 
## Wichtige Constraints
 
- **ALB darf NICHT entstehen.** Wenn der neue Stack einen ALB provisioniert, hast du was falsch gemacht — pruefen.
- **Tunnel-Token-Secret NICHT veraendern.** Wenn du verwirrt bist welcher Tunnel zu welchem Token gehoert: stoppen + fragen, nicht raten.
- **Sub-MCP-Code NICHT anfassen.** Die Migration ist rein Infrastruktur.
- **Andres Scalekit-Resource NICHT loeschen oder veraendern.** Identifier `https://mcp-vf.agenticventures.de/mcp` ist die Audience im JWT.
 
## Was nach erfolgreicher Migration ansteht (NICHT in dieser Session)
 
- DSGVO-To-Dos aus [[../../wissen/entscheidungen/cloudflare-dsgvo]] (Subprozessor-Eintrag in Andres AVV)
- Christoph als zweiten Scalekit-User onboarden
- 24h Stabilitaets-Beobachtung
- `mcp-gsuite-hosted` (Phase 1A) auf gleiches Pattern migrieren

Erwartete Session-Laenge

3-5 Stunden, davon:

  • Phase 1 (Stack-Rewrite): ~90 Min
  • Phase 2 (Delete + Deploy): ~30 Min davon ~15 Min Warten
  • Phase 3 (Tunnel-Smoke): ~10 Min
  • Phase 4 (DNS-Cutover + End-to-End-Smoke): ~10 Min
  • Phase 5 (Wiki-Updates): ~30-60 Min

Voraussetzungen-Checkliste vor Session-Start

  • Cloudflare-API-Token mit Account:Cloudflare Tunnel:Edit + Zone:DNS:Edit fuer agenticventures.de (TTL min 24h)
  • Environment-Vars CF_API_TOKEN, CF_ACCOUNT_ID=bf395d62cc6a9117564c0712fa9e3ad2, CF_ZONE_ID=772ff674c33fbe17cae49d7234c30e60, TUNNEL_ID=ce6dd7c0-d31c-456c-be8a-8705c079982d exportiert
  • AWS CLI mit Profil av-production funktioniert (aws sts get-caller-identity)
  • cd in ~/source/mcps/mcp-vf-hosted/ vor Start
  • Heute keine andere Andre-MCP-Aktivitaet erwartet (Production-Risiko 0)