29 KiB
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
- Verdict : est-ce une bonne idée ?
- Ce que FlowDeck a déjà (l'essentiel est fait)
- Positionnement : deux rôles complémentaires
- Architecture cible
- Services offerts par le serveur MCP
- Plan d'implémentation pas à pas
- Transport & configuration
- Sécurité
- Configuration des clients
- Tests & vérification
- Version & roadmap
- Liens & références
1. Verdict
Oui, c'est une excellente idée — et c'est même l'évolution naturelle du projet. Trois raisons factuelles :
-
C'est exactement ce que fait Notion. Le guide
docs/Guide_Complet_Notion_AI.md(déjà dans le repo) documente les endpoints officielshttps://mcp.notion.com/mcp(Streamable HTTP) ethttps://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. -
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 avecname+description+parameters(JSON Schema) +execute(args, user_id=...). C'est exactement le contrat qu'un serveur MCP expose. LePermissionManagerfait déjà l'autorisation, le journalagent_actionsfait déjà l'audit + rollback,public_api.pyfait déjà l'auth par token Bearer. Il manque seulement la couche de protocole MCP. -
Le coût marginal est minuscule. Une dépendance (
mcpSDK 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.1 ToolRegistry — 22 outils (cœur réutilisable)
Fichier : app/services/tool_registry.py. Chaque outil est une classe Tool avec :
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 ouapproval_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/tokengénère un tokenfd_<urlsafe>(fonction duuser_tokensde l'utilisateur connecté)verify_token(Authorization)valideBearer <token>contre la tableuser_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
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é.
3.1 Vue générale des interactions
Deux portes d'entrée distinctes, un seul socle commun :
CHEMIN A — AGENTS EXTERNES CHEMIN B — AGENT INTERNE
(serveur MCP, NOUVEAU) (UI existante, INCHANGÉE)
Claude Desktop ─┐ Utilisateur (navigateur)
Cursor ├─ MCP stdio ───┐ │
OpenClaw/Hermes ├─ MCP HTTP ────┼──┐ │ chat 🤖 + @mentions
npx mcp-remote ─┘ │ │ ▼
┌───────────▼──▼──────────────┐ ┌─────────────────────────┐
│ Serveur MCP │ │ AgentEngine (ReAct) │
│ app/mcp_server.py │ │ boucle raisonnement↔act │
│ auth token `fd_*` │ │ streaming SSE vers l'UI │
└───────────┬─────────────────┘ └────────────┬────────────┘
│ calls d'outils │ function calls
┌───────────▼──────────────────────────────────▼─────────────┐
│ ToolRegistry — 22 outils │
│ search · read · create · update · delete · gitea · … │
└───────────┬────────────────────────────────────────────────┘
│
┌───────────▼───────────────┐
│ PermissionManager │ ← mêmes ACL pour les deux chemins
│ agent_actions (audit+undo)│
└───────────┬───────────────┘
│
┌───────────▼───────────────┐
│ SQLite · API Gitea/GitHub │
└───────────────────────────┘
À retenir : les agents externes (chemin A) et l'agent interne (chemin B) ne se rencontrent
jamais « en vol » — ils convergent sur le même ToolRegistry, passent par les mêmes
PermissionManager et le même journal agent_actions. Une action faite par Claude Desktop arrive
au même endroit et avec les mêmes règles qu'une action faite par l'agent intégré au navigateur.
3.2 Workflow d'un échange avec un agent externe
Exemple réel : un utilisateur demande à Claude Desktop « crée une collection Sprint 27 avec les issues ouvertes du dépôt bruno/flowdeck ».
Claude Desktop Serveur MCP ToolRegistry / Perms / SQLite
│ │ │
│ 1. initialize (handshake) │ │
│─────────────────────────────▶│ │
│ 2. tools/list │ │
│─────────────────────────────▶│ │
│ ◀── 22 outils + schémas │ │
│ (JSON Schema exacts) │ │
│ │ │
│ 3. tools/call │ │
│ "sync_gitea" │ │
│ {owner:"bruno", │ │
│ repo:"flowdeck"} │ │
│─────────────────────────────▶│ 4. résolution user_id │
│ │ (Bearer fd_* → users.id) │
│ │ 5. PermissionManager │
│ │ assert_can(...) │
│ │ 6. execute() → GET issues │
│ │ Gitea + INSERT collection│
│ │ 7. journal agent_actions │
│ │ (payload + undo snapshot)│
│ ◀── résultat JSON │ │
│ {status:"success", │ │
│ data:{collection_id, │ │
│ issues:N}} │ │
│ │ │
│ 8. tools/call "create_page" │ (même séquence 4→7 : │
│ … contenu du CR… │ perms → execute → audit) │
Le client MCP orchestre : il décide seul d'enchaîner sync_gitea puis create_page (ou
create_document, create_gitea_issue…). Le serveur ne fait que vérifier, exécuter, auditer —
l'équivalent de ce que fait AgentEngine côté interne, mais sans LLM et sans streaming UI.
3.3 Les agents internes passent-ils par le MCP ?
Non. L'agent interne continue d'appeler directement le ToolRegistry — et c'est une décision
d'architecture, pas un oubli. Le serveur MCP est la porte d'entrée des agents externes uniquement.
| Critère | Agent interne → ToolRegistry (direct) | Agent interne → serveur MCP (contournement) |
|---|---|---|
| Streaming SSE (raisonnement + actions live) | événements natifs | perdu — le MCP est atomique requête/réponse |
| Contexte (user, workspace, @mentions, page ouverte) | déjà en mémoire dans AgentEngine |
à re-transmettre à chaque appel |
Mode approval interactif (confirm) |
fonctionne (dialogue dans l'UI) | impossible — pas d'humain pour confirmer |
| Surcharge | aucune | sérialisation JSON-RPC + re-auth à chaque outil |
| Bénéfice | — | aucun : mêmes outils, mêmes permissions, même base |
Un mauvais aiguillage (agent interne → MCP → ToolRegistry) créerait une boucle hermétique : l'agent interne n'est pas un « agent externe », il vit dans FlowDeck et possède déjà tout ce dont le MCP aurait besoin d'être re-sécurisé. Réutiliser le serveur MCP en interne reviendrait à faire transiter des appels locaux par un protocole réseau sans protection supplémentaire.
Ce que ça ne change pas : les deux chemins respectent exactement les mêmes règles
(permissions, audit, rollback), donc un agent externe ne peut ni voir ni modifier plus de choses
que l'agent interne ou l'utilisateur lui-même. Et si un jour l'agent interne doit consommer
des serveurs MCP externes (rôle client, façon « External Agents » de Notion), c'est une
fonctionnalité séparée — documentée dans Guide_Complet_Notion_AI.md — qui n'entre pas en conflit
avec le serveur décrit ici.
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
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://workspacesetflowdeck://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
Étape 1 — Dépendance
Ajouter dans requirements.txt :
mcp>=1.9,<2
⚠️ Piège version SDK : le SDK officiel
mcpest passé en v2, oùFastMCPest renomméMCPServer(from mcp.server.mcpserver import MCPServer) et les API ont changé (migration :py.sdk.modelcontextprotocol.io/v2/migration). Épinglermcp>=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) :
"""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
# 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
| 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é
- Auth — HTTP :
Bearer fd_*validé contreuser_tokens(pattern depublic_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=1uniquement en dev). - 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 »). - Outils destructifs —
delete_document,delete_page,delete_collection: non exposés par défaut ou exigeant un paramètre expliciteconfirm: true+approval_mode == "confirm". Recommandation : activables par envFLOWDECK_MCP_ALLOW_DESTRUCTIVE=true, désactivé par défaut. - 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. - Rate limiting — réutiliser le middleware existant (
slowapi) côté HTTP ; côté stdio le client contrôle le débit. - 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
Claude Desktop — claude_desktop_config.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)
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
# via mcp-remote (bridge stdio→HTTP)
npx mcp-remote http://flowdeck.lab.home:8123/mcp --header "Authorization: Bearer fd_xxx"
10. Tests & vérification
- 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. - Test pytest (
tests/test_mcp_server.py) : démarrer le serveur en stdio, clientmcp.client.stdio,tools/listdoit renvoyer ≥ 20 outils avec les bonsinputSchema;call_tool("create_collection")crée réellement la collection (vérifier en DB),call_tool("delete_collection")est refusé sansconfirm. - Vérification d'audit : après un appel MCP, une ligne
agent_actionsdoit exister avecexecuted_by= l'utilisateur résolu. - CI Gitea Actions : ajouter ces tests à la pipeline existante (pytest + ruff).
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
- Spec MCP : https://modelcontextprotocol.io
- SDK Python officiel : https://github.com/modelcontextprotocol/python-sdk (épingler
<2pour 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