Sprint 2a Spec — m365 v0.3 + vf-vault

Spec fuer den ersten Bau-Schritt aus der VF-Vault-Architektur. Marvin reviewed dieses Spec, dann Code in den Repos.

Ziel

Christoph’s SharePoint-Vault /Workshop/VibeFactory/ ist von Claude in Open WebUI lesbar unter eigener User-Identitaet. Christoph (christoph@vibe-factory.de) loggt in Open WebUI ein, autorisiert m365 einmalig via OAuth, ab dann kann Claude in seinem Namen auf SharePoint zugreifen. Token persistent in DynamoDB.

Scope dieses Sprints: nur Read. Write-Tools (m365_send_email, m365_upload_drive_file etc.) sind Sprint 2a Teil 2 — separat.

Ausserhalb dieses Sprints: Andre/Felix/Julian onboarden (Phase 2 der Roadmap), Skills, Cron-Jobs.

Stand heute (mcp-m365 v0.2)

Aspektv0.2
Authazure.identity.ClientSecretCredential (App-only / Service-Principal)
Token-LifecycleGlobal Token-Cache in graph.py, ein Token fuer den ganzen Prozess
Permissions in AzureSites.Selected (whitelist) + Mail.ReadWrite + Mail.Send
Toolslist_sites, list_drive_items, search_files, get_drive_item, download_file + Excel + Lists + Mail
Per-User-Statenein — alle Calls unter App-Identitaet
Audit-Trailnicht User-aufgeloest — mcp-m365 Service-Principal als Actor

Problem fuer Open WebUI Multi-User: alle vier VF-User wuerden unter derselben App-Identitaet schreiben. Mail-Send waere Mail-FROM=App. SharePoint-Versions-History waere alles vom Service-Principal. Wegen DSGVO + Audit-Trail wollen wir Per-User-Identitaet.

Architektur-Vorschlag v0.3

Open WebUI Chat
    │ User: christoph@vibe-factory.de
    │ Header: X-OpenWebUI-User-Email = christoph@vibe-factory.de
    ▼
mcp-vf-hosted (Mono)
    │ Scalekit-JWT, audit-middleware extrahiert sub=christoph@vibe-factory.de
    │ FastMCP-Context propagiert User-Email an Sub-MCPs
    ▼
vf-vault Sub-MCP (NEU)   m365 Sub-MCP (v0.3 delegated)   papierkram/ticketpay (unveraendert)
    │                    │
    │ ruft m365-Sub      │ Mode = "delegated"
    │ intern auf         │ Token-Lookup pro Tool-Call:
    │                    │   user_email → SHA256-Hash → DynamoDB → Access-Token
    │                    ▼
    │                Microsoft Graph (User-Identitaet Christoph)

OAuth-Callback Flow (einmalig pro User):
    Open WebUI Chat → "ich muss authorisieren" → click → MS-Login
    → Redirect zu mcp-vf-hosted /m365/oauth/callback?code=...&state=user_hash:csrf
    → Token-Tausch + Refresh-Token speichern in DynamoDB (KMS-CMK)
    → Browser-Antwort "OK, du kannst zurueck in den Chat"

Tool-Surface

vf-vault Sub-MCP (neuer Sub-MCP, im mcp-vf-hosted gemountet)

Wrapped m365-Sub-MCP intern, abstrahiert SharePoint-Pfade weg.

ToolArgsWozu
vault_read_file(path)path relativ zu Workshop/VibeFactory/ (z.B. 09_Standards/Regeln/Artifact-Design.md)Liest File-Content als String. .md → text/markdown, .txt → text/plain, .docx → python-docx Plaintext, .xlsx → JSON via read_excel_used_range
vault_list_dir(path, recursive=False)path relativ, optional recursiveListet Files/Subordner. Standard nicht-rekursiv. Recursive bei Suche nach „alle _context.md”
vault_search_text(query, path=None, limit=20)query, optional path-prefix, limitVolltextsuche via Graph search_files innerhalb des Vaults
vault_get_context(path=None)optional pathLiest CLAUDE.md + ggf. _context.md und _index.md im Pfad. Convenience-Tool fuer Conv-Start
vault_get_metadata(path)pathLast-Modified, Author, Size — fuer Drift-Detection

Kein Write in diesem Sprint. Sprint 2a Teil 2: vault_write_file, vault_append_to_file, vault_create_folder.

m365 v0.3 Sub-MCP (Erweiterung)

Bestehende Tools bleiben. Neuer Auth-Mode + zwei neue Helper-Tools:

ToolAenderung
Alle existierenden Toolsunveraendertes API, aber Token-Lookup ueber M365_AUTH_MODE (delegated vs service-principal)
m365_auth_status()NEU. Gibt zurueck: hat der User valid Token? Wann laeuft er ab? Wenn nein, was ist die Authorize-URL?
m365_auth_revoke()NEU. Loescht den Token fuer den aktuellen User (Consent-Widerruf)

Backward-Compat

Wenn M365_AUTH_MODE=service-principal (default heute): alles unveraendert. Sprint-2-Stack setzt M365_AUTH_MODE=delegated fuer den Open-WebUI-Path. Cron-Routinen die ohne User laufen koennen weiter Service-Principal nutzen.

Auth-Flow konkret

Initial-Authorize (einmalig pro User)

  1. Open WebUI sendet User-Email-Header an mcp-vf-hosted bei jedem Tool-Call
  2. vf-vault Sub-MCP ruft m365 Sub-MCP mit User-Email-Header
  3. m365 Sub-MCP: DynamoDB-Lookup → kein Token? → wirft M365NotAuthorizedError mit Body { "auth_url": "https://mcp-vf.../m365/oauth/authorize?state=<hash>:<csrf>" }
  4. Claude im Chat zeigt die URL als anklickbaren Link
  5. User klickt → Microsoft-Login → Consent („Claude darf in deinem Namen Files lesen, Mails lesen, …“) → Redirect zu mcp-vf-hosted/m365/oauth/callback?code=<authcode>&state=<hash>:<csrf>
  6. Callback-Handler: Token-Tausch bei https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token mit MSAL-Python. Validiert CSRF-Token. Speichert Access+Refresh+Expiry in DynamoDB.
  7. Browser-Antwort: „Du kannst zurueck in den Chat, Claude hat jetzt Zugriff.”
  8. User retry-tippt die urspruengliche Anfrage. Klappt jetzt.

Refresh (automatisch pro Tool-Call)

Vor jedem Graph-Call: DynamoDB-Lookup → check expires_at < now+60s → wenn ja, mit Refresh-Token erneuern → DynamoDB-Update → Call.

Revoke

m365_auth_revoke() Tool: User kann im Chat „bitte vergiss meinen Microsoft-Zugriff” sagen. Loescht DynamoDB-Eintrag. Bei naechstem Tool-Call kommt wieder Authorize-URL.

DynamoDB-Schema

Tabelle: mcp-vf-m365-tokens in av-production (eu-central-1).

AttributTypWozu
user_email_hash (PK)String, SHA256 hexHash der User-Email — kein Klartext-Email in der DB
tenant_idStringAzure-Tenant der Authorize-Domain (vibefactorygbr)
access_tokenString, KMS-encryptedOAuth Access-Token, ca 1h gueltig
refresh_tokenString, KMS-encryptedOAuth Refresh-Token, lang gueltig
expires_atNumber (Unix-Timestamp)Wann access_token ablaeuft
scopes_grantedStringKomma-getrennte Scope-Liste die der User consented hat
created_atNumberErste Authorize
updated_atNumberLetzte Token-Refresh

Encryption: KMS-CMK alias/mcp-vf-m365-tokens, separate Key vom Bucket-KMS.

Access: Task-Role hat dynamodb:GetItem, PutItem, UpdateItem, DeleteItem nur auf diese Tabelle. Keine Scans.

Azure App-Registration

Neue App-Registration in VF’s Entra-Tenant (parallel zur bestehenden Service-Principal-App):

PunktWert
Namemcp-m365-delegated (Agentic Ventures)
TypeWeb (Confidential Client)
Redirect URIhttps://mcp-vf.agenticventures.de/m365/oauth/callback
Permissions (delegated)Files.Read.All, Sites.Read.All, User.Read, offline_access (fuer Refresh-Token)
Permissions (App-only)KEINE — getrennt von Service-Principal-App
Admin-Consentnicht erforderlich fuer Read-Only-Delegated-Permissions, User-Consent reicht
Client-Secretneuer Secret, in AWS Secrets Manager unter mcp-vf-hosted/m365-delegated-app

WICHTIG: bestehende mcp-m365-App (Service-Principal mit Sites.Selected) bleibt unveraendert + parallel. Cron-Routinen nutzen die weiter.

Sicherheits-Modell

ThreatMitigation
Token-Theft aus DynamoDBKMS-CMK-Encryption, IAM-restriktiv, kein DB-Dump in Logs
CSRF im OAuth-Callbackstate=user_hash:random_csrf, csrf wird vor Authorize in Redis/DDB hinterlegt + nach Callback verglichen
User A spoofs als User BForward-User-Email-Header kommt von Open WebUI nach JWT-Validation. Mono-MCP validiert Header gegen JWT-sub-Claim — Header ohne JWT-Match → 401
Refresh-Token-LeakageRefresh-Token niemals in Logs (Audit-Middleware: PII-Scrubbing erweitern)
Replay-Attack auf statecsrf-Token nur 5 Min gueltig
Scope-EscalationAuthorize-URL hat hartkodierte Scope-Liste. User kann nicht mehr requesten als wir reinpacken

Migration-Strategie

Co-Existenz, kein Cut-Over.

  1. mcp-m365 v0.3 ist Drop-in-Replacement mit M365_AUTH_MODE-Toggle
  2. Default-Mode bleibt service-principal — bestehende Setups (lokale Marvin-Sessions, Cron-Jobs) unbeeindruckt
  3. mcp-vf-hosted setzt fuer Open-WebUI-Path explizit M365_AUTH_MODE=delegated via ENV
  4. Sub-MCPs sind unabhaengig — Aenderung in m365 bricht nicht papierkram/ticketpay
  5. Rollback: wenn v0.3 spinnt, wieder auf v0.2 zurueck, Open WebUI verliert Multi-User-Read aber Cron-Jobs laufen

Folge-Aktionen (nach Spec-Approval)

#AktionWoAufwand
1Azure App-Reg fuer delegated anlegenMarvin in Entra-Portal15 Min
2DynamoDB-Tabelle + KMS-KeyCDK in mcp-vf-hosted/infra/30 Min
3mcp-m365 v0.3: delegated.py Auth-Modul + M365_AUTH_MODE Togglemcp-m365 Code2-3 Std
4mcp-m365 v0.3: Token-Store Adapter (DynamoDB)mcp-m365 Code1-2 Std
5mcp-m365 v0.3: 2 neue Tools (auth_status, auth_revoke)mcp-m365 Code30 Min
6vf-vault Sub-MCP als neues Modulmcp-m365 Code (oder eigenes Repo?)3-4 Std
7mcp-vf-hosted: OAuth-Callback-Route /m365/oauth/callbackmcp-vf-hosted Code2 Std
8mcp-vf-hosted: vf-vault als 4. Sub-MCP einbindenmcp-vf-hosted Code30 Min
9mcp-vf-hosted: CDK-Update (DynamoDB-Permissions, neuer Secret, ENV M365_AUTH_MODE)mcp-vf-hosted/infra1 Std
10End-to-End-Smoke mit Marvin als erstem Test-Userclaude.ai + Open WebUI1 Std
Total~14-16 Std = ~2 Tage

Decisions (approved 2026-05-14)

Marvin hat alle vier Defaults bestaetigt:

  1. vf-vault als eigenes Repo mcp-vault (nicht Teil von mcp-m365). Wiederverwendbar fuer andere Kunden mit SharePoint-Vault.
  2. JWT.sub als single source of truth fuer User-Identitaet. Kein extra X-OpenWebUI-User-Email-Header noetig.
  3. DynamoDB im av-production-Account, Tabellen-Prefix per Kunde (vf-m365-tokens).
  4. M365_AUTH_MODE=service-principal bleibt Default. Marvin’s lokales Setup + Cron-Routinen aendern sich nicht. Nur der av-production-Container setzt delegated.

Bau-Status

  • Block 1.1 done (2026-05-14): mcp-vault Repo-Skelett unter ~/source/mcps/mcp-vault/ — pyproject, README, env, settings, m365_backend, server. 3 Read-Tools mit Code, 2 Stubs fuer Block 2.
  • Block 1.2 in_progress: lokaler Smoke-Test mit VF-Credentials (uv install + .env.local aus mcp-m365 + start + curl-Test) — naechster Schritt nach Session-Reset.
  • Restliche Bloecke 2-5 pending wie unten.

Test-Strategie

  • Unit: Token-Store hat Tests fuer encrypt/decrypt, expiry-handling, refresh-logic
  • Integration in mcp-m365: lokaler Test mit Azure-App + Test-User „marvin@…” (zweiter Account)
  • End-to-End in Open WebUI: Marvin als erster User, dann Christoph wenn er Lust hat
  • Smoke nach Deploy: m365_auth_status() fuer Marvin → kein Token → Authorize → wieder check → Token da → vault_list_dir('') → File-Liste

Risiken

RisikoMitigation
Azure-Admin-Consent erforderlich (wenn Org-Policy strikt ist)Vor Bau: Christoph fragen ob Andre als Org-Admin Consent geben kann. Default ist „User-Consent reicht” fuer Read-Only-Delegated
MSAL-Python in Fargate-Container schwergewichtigImage-Size pruefen — falls > 500MB → eigener Sidecar-Container
OAuth-Callback braucht Public-URL der bereits durch Cloudflare laeuft/m365/oauth/callback Route in Mono-MCP, Cloudflare-Tunnel laesst durch (default * Pattern)
User-Email-Hash-KollisionSHA256 mit Tenant-ID als Salt → praktisch unmoeglich

Cross-Refs