diff --git a/docs/FLOWDECK_MCP_SERVER_GUIDE.md b/docs/FLOWDECK_MCP_SERVER_GUIDE.md new file mode 100644 index 0000000..659d9d9 --- /dev/null +++ b/docs/FLOWDECK_MCP_SERVER_GUIDE.md @@ -0,0 +1,449 @@ +# FlowDeck — Serveur MCP : Guide complet d'intégration agents externes + +> **Version :** 1.0 · **Date :** 2026-09-08 · **Cible :** FlowDeck v5.x +> **Sujet :** Exposer FlowDeck comme **serveur MCP** (Model Context Protocol) pour que des agents externes +> (Claude Desktop/Code, Cursor, OpenClaw/Hermes, n'importe quel client MCP) puissent lire et +> modifier le workspace. +> **Prérequis :** FlowDeck v5.x · FastAPI · SQLite WAL · agent interne déjà en place (`AgentEngine` + `ToolRegistry`) + +--- + +## Table des matières + +1. [Verdict : est-ce une bonne idée ?](#1-verdict) +2. [Ce que FlowDeck a déjà (l'essentiel est fait)](#2-ce-que-flowdeck-a-déjà) +3. [Positionnement : deux rôles complémentaires](#3-positionnement) +4. [Architecture cible](#4-architecture-cible) +5. [Services offerts par le serveur MCP](#5-services-offerts) +6. [Plan d'implémentation pas à pas](#6-plan-dimplémentation) +7. [Transport & configuration](#7-transport--configuration) +8. [Sécurité](#8-sécurité) +9. [Configuration des clients](#9-configuration-des-clients) +10. [Tests & vérification](#10-tests--vérification) +11. [Version & roadmap](#11-version--roadmap) +12. [Liens & références](#12-liens--références) + +--- + +## 1. Verdict {#1-verdict} + +**Oui, c'est une excellente idée — et c'est même l'évolution naturelle du projet.** Trois raisons factuelles : + +1. **C'est exactement ce que fait Notion.** Le guide `docs/Guide_Complet_Notion_AI.md` (déjà dans le repo) + documente les endpoints officiels `https://mcp.notion.com/mcp` (Streamable HTTP) et `https://mcp.notion.com/sse`. + FlowDeck est un clone de Notion : l'équivalent « API publique + agents externes » de Notion, c'est un serveur MCP. + On ne copie pas une idée exotique, on complète la parité avec le modèle de référence. + +2. **80 % de l'infrastructure existe déjà.** Le `ToolRegistry` (`app/services/tool_registry.py`) est déjà un + **catalogue d'outils compatible MCP** : 22 outils, chacun avec `name` + `description` + `parameters` + (JSON Schema) + `execute(args, user_id=...)`. C'est *exactement* le contrat qu'un serveur MCP expose. + Le `PermissionManager` fait déjà l'autorisation, le journal `agent_actions` fait déjà l'audit + rollback, + `public_api.py` fait déjà l'auth par token Bearer. **Il manque seulement la couche de protocole MCP.** + +3. **Le coût marginal est minuscule.** Une dépendance (`mcp` SDK officiel), un module (`app/mcp_server.py`, + ~150 lignes), un point d'entrée. **Aucune modification du cœur de l'app** : les outils existants sont + réutilisés tels quels, avec leurs permissions et leur audit. Le serveur MCP est un *second visage* + de l'API existante, pas une nouvelle fonctionnalité monolithique. + +**Ce que ça apporte concrètement :** n'importe quel agent externe (Claude Desktop, Cursor, OpenClaw/Hermes…) +peut « voir » et piloter FlowDeck : « crée un CR de réunion dans le workspace X », « ajoute les issues du +sprint au kanban », « résume les tâches en cours », « crée une collection avec statut/sprint », etc. +Sans MCP, chaque agent externe devrait coder son propre client HTTP contre l'API — MCP standardise tout ça +(un seul schéma d'outils, découverte automatique, types JSON Schema). + +--- + +## 2. Ce que FlowDeck a déjà {#2-ce-que-flowdeck-a-déjà} + +### 2.1 ToolRegistry — 22 outils (cœur réutilisable) + +Fichier : `app/services/tool_registry.py`. Chaque outil est une classe `Tool` avec : + +```python +class Tool: + name: str = "" + description: str = "" # explication lisible par un LLM + parameters: dict # JSON Schema (très exactement le format MCP) + async def execute(self, args: dict, *, user_id: int | None = None) -> ToolResult +``` + +`ToolRegistry.schema(scope)` retourne déjà la liste `{name, description, parameters}` — le format exact +que `tools/list` MCP doit renvoyer. `ToolRegistry.execute(tool, args, user_id=...)` exécute avec +vérification d'existence. + +### 2.2 PermissionManager — autorisation + +Fichier : `app/services/permission_manager.py`. API : +- `can_read(workspace_id)`, `can_write(workspace_id)`, `can_destructive(workspace_id)` +- `assert_can(tool, args, workspace_id, approval_mode)` — lève une exception si refus ; + les outils destructifs (`delete_*`) exigent le rôle owner/admin **ou** `approval_mode == "confirm"`. + +### 2.3 Journal d'audit + rollback + +`AgentEngine._log_action(...)` (`app/services/agent_engine.py`) écrit chaque action dans `agent_actions` +(payload, résultat, snapshot undo, `executed_by`). `undo_action(action_id)` restaure. +**Le serveur MCP doit journaliser ses actions dans la même table** — l'audit reste unifié. + +### 2.4 Auth par token Bearer + +Fichier : `app/routers/public_api.py`. Déjà en place : +- `POST /api/v1/token` génère un token `fd_` (fonction du `user_tokens` de l'utilisateur connecté) +- `verify_token(Authorization)` valide `Bearer ` contre la table `user_tokens` + +Table `user_tokens(gitea_user_id, gitea_token)` — **attention** : `gitea_user_id` stocke en réalité +l'**id local** de `users.id` (voir `SessionManager.store_token(user_id, ...)`). Donc la résolution +token → user est directe : `SELECT gitea_user_id FROM user_tokens WHERE gitea_token = ?`. + +### 2.5 Contexte agent + +`ContextBuilder` (`app/services/context_builder.py`) collecte le contexte (page courante, mentions, +workspace) — utile pour la resource MCP `flowdeck://context` si on veut l'exposer. + +--- + +## 3. Positionnement {#3-positionnement} + +Deux surfaces agentiques, complémentaires — **ne pas confondre** : + +| | Agent interne (existant) | Serveur MCP (à créer) | +|---|---|---| +| **Direction** | L'utilisateur parle à l'agent **dans** FlowDeck | Un agent externe parle **à** FlowDeck depuis dehors | +| **Orchestration** | `AgentEngine` (boucle ReAct, LLM) | Le **client** MCP orchestre (le serveur ne fait qu'exécuter des outils) | +| **Interface** | Panneau HTML + SSE (`/api/agent/*`) | Protocole MCP (stdio ou Streamable HTTP) | +| **Réutilise** | ToolRegistry, PermissionManager, audit | **Le même ToolRegistry, le même PermissionManager, le même audit** | +| **Valeur** | Assistant intégré à l'UI | FlowDeck pilotable par Claude/Cursor/Hermes/scripts MCP | + +Le serveur MCP **ne contient pas de LLM** : il exécute les outils demandés par le client. C'est le client +(claude, cursor, openclaw…) qui décide quoi appeler. Le code de raisonnement n'est pas dupliqué. + +--- + +## 4. Architecture cible {#4-architecture-cible} + +``` + AGENTS EXTERNES (clients MCP) + ┌───────────────┬──────────────────┬──────────────────┐ + ▼ ▼ ▼ ▼ + Claude Desktop Cursor CLI OpenClaw/Hermes npx mcp-remote + (stdio local) (stdio local) (stdio / HTTP) (HTTP distant) + │ │ │ │ + │ ┌─────────┴──────────┐ │ │ + │ ▼ ▼ ▼ ▼ + │ ┌─────────────────────────────────────────────┐ + │ │ app/mcp_server.py (FastMCP) │ + │ │ ─ stdio : python -m app.mcp_server │ + │ │ ─ HTTP : uvicorn (Streamable HTTP) │ + │ │ ├─ auth MCP (token fd_* / env var) │ + │ │ ├─ résolution user_id │ + │ │ └─ mise en correspondance outils MCP │ + │ └───────────────┬─────────────────────────────┘ + │ │ (mêmes fonctions que l'app) + │ ┌───────────────▼─────────────────────────────┐ + │ │ ToolRegistry (22 outils) — INCHANGÉ │ + │ │ PermissionManager — INCHANGÉ │ + │ │ agent_actions (audit + undo) — INCHANGÉ │ + │ │ SessionManager.get_token(user_id) │ + │ └───────────────┬─────────────────────────────┘ + │ │ + │ ┌───────────┴────────────┐ + │ ▼ ▼ + │ SQLite (flowdeck.db) Gitea / GitHub API +``` + +**Principe : zéro modification des services existants.** Le module MCP importe `app.services.tool_registry` +et appelle les mêmes classes que l'agent interne. La seule chose nouvelle est la couche protocole. + +--- + +## 5. Services offerts {#5-services-offerts} + +Le serveur expose les **22 outils existants** (déjà présents dans `ToolRegistry`), groupés par service : + +### Recherche & navigation +| Outil MCP | Description | +|---|---| +| `search_workspace` | Recherche full-text (collections, pages, documents, workspaces) | +| `read_workspaces` | Liste les espaces de travail accessibles et leur contenu | + +### Lecture +| Outil MCP | Description | +|---|---| +| `read_collection` | Schéma (propriétés + vues) + pages d'une collection | +| `read_page` | Page de collection + valeurs + dépendances | +| `read_document` | Document éditeur (titre, contenu, métadonnées) | +| `read_gitea_issues` | Issues Gitea d'un repo (état, labels, milestones) | + +### Écriture (avec snapshot undo) +| Outil MCP | Description | +|---|---| +| `create_collection` | Crée une collection + vue par défaut | +| `create_view` | Vue (table/board/calendar/gallery/list/timeline/gantt/chart/form/map/feed) | +| `add_property` | Propriété typée | +| `create_page` | Page de collection + valeurs | +| `create_document` | Document éditeur dans un workspace (Markdown ou blocs) | +| `update_page` | Titre / propriétés d'une page | +| `write_blocks` | Contenu blocs d'un document | +| `add_relation` | Relation entre collections (avec réciproque) | +| `create_sub_item` | Sous-élément | +| `add_dependency` | Dépendance entre pages | +| `apply_template` | Page depuis un template | + +### Gitea ↔ FlowDeck +| Outil MCP | Description | +|---|---| +| `sync_gitea` | Synchronise les issues d'un repo vers une collection | +| `create_gitea_issue` | Crée une issue dans un repo | + +### Destructif (gated — voir §8) +| Outil MCP | Description | +|---|---| +| `delete_document` | Corbeille (soft delete) | +| `delete_page` / `delete_collection` | Suppression (dur, exige `confirm`) | + +### Extensions possibles (plus tard) +- **Resource MCP** `flowdeck://workspaces` et `flowdeck://collection/{id}` (le modèle resource MCP est + naturel pour « lister mes espaces » sans appeler un outil). +- **Prompts MCP** : templates « CR de réunion », « Sprint review » réutilisant les skills de l'agent interne. +- **Notifications MCP** (logging/sampling) — à réserver pour plus tard, les outils couvrent 95 % des besoins. + +--- + +## 6. Plan d'implémentation {#6-plan-dimplémentation} + +### Étape 1 — Dépendance + +Ajouter dans `requirements.txt` : + +``` +mcp>=1.9,<2 +``` + +> ⚠️ **Piège version SDK** : le SDK officiel `mcp` est passé en **v2**, où `FastMCP` est renommé `MCPServer` +> (`from mcp.server.mcpserver import MCPServer`) et les API ont changé (migration : `py.sdk.modelcontextprotocol.io/v2/migration`). +> **Épingler `mcp>=1.9,<2`** : l'API FastMCP v1 est la plus documentée (la quasi-totalité des exemples +> publics) et parfaitement stable. La migration vers v2 peut se faire plus tard, sans changer les outils. + +### Étape 2 — Nouveau module `app/mcp_server.py` + +Squelette de référence (les 22 outils suivent le même patron ; 3 représentatifs ci-dessous) : + +```python +"""FlowDeck — Serveur MCP (v1). + +Réexpose le ToolRegistry existant comme serveur MCP. +Aucune modification des services : mêmes classes, mêmes permissions, même audit. +Transports : stdio (local) ou Streamable HTTP (distant) via --transport http. +""" +from __future__ import annotations + +import json +import os +from typing import Any + +from mcp.server.fastmcp import FastMCP +from app.db import get_conn +from app.services.tool_registry import ( + ToolRegistry, SearchWorkspace, ReadCollection, CreateCollection, + CreatePage, CreateDocument, CreateGiteaIssue, # ... + les 16 autres +) + +mcp = FastMCP("flowdeck") + +# ── Résolution d'utilisateur ────────────────────────────────────────────── +def resolve_user_id() -> int | None: + """stdio : env FLOWDECK_MCP_USER_ID (dev) ou FLOWDECK_MCP_TOKEN (résout via user_tokens).""" + uid = os.environ.get("FLOWDECK_MCP_USER_ID") + if uid: + return int(uid) + token = os.environ.get("FLOWDECK_MCP_TOKEN") + if token: + with get_conn() as conn: + row = conn.execute( + "SELECT gitea_user_id FROM user_tokens WHERE gitea_token=?", (token,) + ).fetchone() + return row["gitea_user_id"] if row else None + return None # mode admin sans authentification (usage local uniquement) + +# ── Outils : wrapper mince sur ToolRegistry ─────────────────────────────── +async def _run(impl, args: dict) -> str: + result = await impl.execute(args, user_id=resolve_user_id()) + return json.dumps({ + "status": result.status, + "message": result.message, + "data": result.data, + "target": {"type": result.target_type, "id": result.target_id}, + }, ensure_ascii=False) + +@mcp.tool() +async def search_workspace(query: str) -> str: + """Recherche full-text dans tout FlowDeck (collections, pages, documents, workspaces).""" + return await _run(SearchWorkspace(), {"query": query}) + +@mcp.tool() +async def read_collection(collection_id: int) -> str: + """Lit le schéma (propriétés + vues) et les pages d'une collection.""" + return await _run(ReadCollection(), {"collection_id": collection_id}) + +@mcp.tool() +async def create_collection(name: str, description: str = "", icon: str = "📋") -> str: + """Crée une collection avec sa vue par défaut.""" + return await _run(CreateCollection(), {"name": name, "description": description, "icon": icon}) + +# ... même patron pour create_page, create_document, create_view, add_property, +# update_page, write_blocks, add_relation, create_sub_item, add_dependency, +# apply_template, read_page, read_document, read_workspaces, +# read_gitea_issues, sync_gitea, create_gitea_issue, +# delete_document, delete_page, delete_collection (voir §8 pour ces 3-là) + +# ── Écriture systématique dans le journal d'audit ───────────────────────── +# Chaque outil MCP DOIT journaliser dans agent_actions (comme AgentEngine._log_action) +# pour garder l'audit unifié et permettre le rollback via undo_action(). + +if __name__ == "__main__": + import sys + transport = sys.argv[1] if len(sys.argv) > 1 else "stdio" + mcp.run(transport=transport) # "stdio" | "streamable-http" +``` + +### Étape 3 — Auditer les actions MCP + +Réutiliser le schéma `agent_actions` : insérer un enregistrement à chaque exécution d'outil +(`tool_name`, `payload_json`, `result_json`, `status`, `undo_snapshot_json`, `executed_by=user_id`). +Le rollback (`undo_action`) fonctionne ensuite à l'identique pour les actions faites par des agents externes. + +### Étape 4 — Point d'entrée & lancement + +```bash +# stdio (Claude Desktop, Cursor, OpenClaw…) — lancé par le client lui-même +FLOWDECK_MCP_USER_ID=1 python -m app.mcp_server + +# HTTP (antémémoire: agents distants) — port par défaut 8123 +FLOWDECK_MCP_TOKEN=fd_xxx uvicorn app.mcp_server_http:app --port 8123 # ou mcp.run(transport="streamable-http") +``` + +En Docker : ajouter une commande/second process au conteneur existant (même image, même volume SQLite) +ou un service compose séparé partageant le même volume — la base SQLite WAL se partage sans problème +avec le process principal (déjà le cas en dev local). + +### Étape 5 — Documentation client dans le repo + +Ajouter un fichier `docs/MCP_CLIENTS.md` (ou une section du présent guide, §9) avec les blocs de config +pour chaque client. + +--- + +## 7. Transport & configuration {#7-transport--configuration} + +| Transport | Usage | Lancement | Auth | +|---|---|---|---| +| **stdio** | Agents locaux (Claude Desktop, Cursor, OpenClaw) | Le client spawn `python -m app.mcp_server` | Env `FLOWDECK_MCP_USER_ID` / `FLOWDECK_MCP_TOKEN` | +| **Streamable HTTP** | Agents distants / multi-clients | `uvicorn` sur `MCP_PORT` (défaut 8123) | Header `Authorization: Bearer fd_*` | + +Variables d'environnement (à ajouter à `.env` / `app/config.py`) : + +``` +MCP_ENABLED=true +MCP_PORT=8123 +MCP_MAX_TOKENS_HISTORY=50 # contrôle de charge côté serveur (optionnel) +FLOWDECK_MCP_TOKEN= # généré via POST /api/v1/token ou manuellement +``` + +> **Streamable HTTP (spéc. 2025-03-26+)** remplace l'ancien SSE : le SDK FastMCP le gère nativement via +> `mcp.run(transport="streamable-http")`. C'est le protocole utilisé par Notion (`mcp.notion.com/mcp`). + +--- + +## 8. Sécurité {#8-sécurité} + +1. **Auth** — HTTP : `Bearer fd_*` validé contre `user_tokens` (pattern de `public_api.verify_token`). + stdio : variable d'env obligatoire ; refuser de démarrer sans résolution d'utilisateur en dehors du + mode local explicite (`FLOWDECK_MCP_ALLOW_ANON=1` uniquement en dev). +2. **Permissions** — chaque appel passe par `PermissionManager.assert_can(tool, args, workspace_id, ...)` + comme pour l'agent interne. Un agent externe ne voit/ne modifie **que** ce que son utilisateur peut + voir/modifier (pas de contournement MCP — même règle que Notion : *« MCP does not bypass Notion permissions »*). +3. **Outils destructifs** — `delete_document`, `delete_page`, `delete_collection` : **non exposés par défaut** + ou exigeant un paramètre explicite `confirm: true` + `approval_mode == "confirm"`. Recommandation : + activables par env `FLOWDECK_MCP_ALLOW_DESTRUCTIVE=true`, désactivé par défaut. +4. **Audit** — 100 % des actions MCP journalisées dans `agent_actions` (qui, quoi, quand, quel user, + snapshot undo) → consultable depuis l'UI agent et rollback possible. +5. **Rate limiting** — réutiliser le middleware existant (`slowapi`) côté HTTP ; côté stdio le client + contrôle le débit. +6. **Réseau** — en HTTP, ne pas exposer directement : reverse-proxy avec TLS (Traefik/Caddy/Nginx) et + restriction IP si l'usage est interne au homelab. Le token `fd_*` transite en clair sinon. + +--- + +## 9. Configuration des clients {#9-configuration-des-clients} + +### Claude Desktop — `claude_desktop_config.json` + +```json +{ + "mcpServers": { + "flowdeck": { + "command": "python", + "args": ["-m", "app.mcp_server"], + "cwd": "/home/bruno/workspace/flowdeck", + "env": { "FLOWDECK_MCP_USER_ID": "1" } + } + } +} +``` + +### Cursor — `.cursor/mcp.json` (même structure) + +### OpenClaw / Hermes — `config.yaml` (client MCP natif) + +```yaml +mcp: + servers: + flowdeck: + command: python + args: ["-m", "app.mcp_server"] + cwd: /home/bruno/workspace/flowdeck + env: { FLOWDECK_MCP_USER_ID: "1" } +``` + +### Distant (HTTP) — n'importe quel client MCP + +```bash +# via mcp-remote (bridge stdio→HTTP) +npx mcp-remote http://flowdeck.lab.home:8123/mcp --header "Authorization: Bearer fd_xxx" +``` + +--- + +## 10. Tests & vérification {#10-tests--vérification} + +1. **Inspector officiel** : `mcp dev app/mcp_server.py` (SDK v1) — UI web pour lister les outils, + exécuter chaque outil, vérifier schémas et erreurs. +2. **Test pytest** (`tests/test_mcp_server.py`) : démarrer le serveur en stdio, client `mcp.client.stdio`, + `tools/list` doit renvoyer ≥ 20 outils avec les bons `inputSchema` ; `call_tool("create_collection")` + crée réellement la collection (vérifier en DB), `call_tool("delete_collection")` est refusé sans `confirm`. +3. **Vérification d'audit** : après un appel MCP, une ligne `agent_actions` doit exister avec + `executed_by` = l'utilisateur résolu. +4. **CI Gitea Actions** : ajouter ces tests à la pipeline existante (pytest + ruff). + +--- + +## 11. Version & roadmap {#11-version--roadmap} + +Proposer une entrée en roadmap (le projet est à v5.x, `realtime` en v5.13.0) : + +> **v5.14.0 — Serveur MCP** : exposition du ToolRegistry via MCP (stdio + Streamable HTTP), +> auth par token `fd_*`, audit unifié `agent_actions`, outils destructifs gated, +> guide client + tests. + +Étapes de livraison : (1) squelette + inspector, (2) audit + rollback, (3) transport HTTP + auth, +(4) configs clients + docs, (5) CI. + +--- + +## 12. Liens & références {#12-liens--références} + +- Spec MCP : https://modelcontextprotocol.io +- SDK Python officiel : https://github.com/modelcontextprotocol/python-sdk (épingler `<2` pour FastMCP v1) +- Notion MCP (modèle de référence, déjà documenté dans ce repo) : `docs/Guide_Complet_Notion_AI.md` +- Code à réutiliser : `app/services/tool_registry.py` · `app/services/permission_manager.py` · + `app/routers/public_api.py` (auth token) · `app/services/agent_engine.py` (`agent_actions` + undo) +- Guide agent interne existant : `docs/Flowdeck_Agent_integration.md` \ No newline at end of file