Flowdeck 7.76.0 — serveur MCP en écriture (create_page, update_page, create_collection)

- create_page: collection_id (int, requis), title (string, requis), icon (optionnel), properties (objet optionnel -> property_values_json)
- update_page: id (int, requis), title (optionnel), properties (objet optionnel, remplace property_values_json)
- create_collection: name (requis), description (optionnel), icon (optionnel) + vue par défaut
- Aucune suppression; erreurs métier en {"erreur": "..."}; GET /mcp read_only=false et liste mise à jour
- Tests étendus: création+relecture, mise à jour, create_collection, erreurs, 401 sans jeton sur écriture
This commit is contained in:
Spark
2026-10-22 03:29:09 +00:00
parent 9fe5114669
commit e36f80714c
4 changed files with 215 additions and 11 deletions
+111 -6
View File
@@ -1,20 +1,21 @@
"""FlowDeck — serveur MCP (Fusion, branchement du service d'agents).
Lecture seule, même dialecte JSON-RPC que les autres apps du
programme (TikTak, Kanbante) : POST /mcp, initialize → tools/list →
tools/call, réponses en contenu texte JSON.
Même dialecte JSON-RPC que les autres apps du programme (TikTak,
Kanbante) : POST /mcp, initialize → tools/list → tools/call,
réponses en contenu texte JSON.
Authentification : les jetons d'API personnels existants
(Settings → API tokens, table ``api_tokens``, vérifiés par
``public_api._token_owner``) — aucune nouvelle machinerie de clés.
Le service d'agents reçoit un jeton dédié créé par l'utilisateur ;
il lit en son nom.
il lit et écrit en son nom.
Objet exposé : les **collections** (les projets/espaces de travail
de Flowdeck) et leurs pages, la recherche par titre, et le registre
des dépôts (table ``projects``). Doctrine du programme : Flowdeck
référence les tâches, il ne les copie pas — aucune tâche TikTak ne
transite par ici, et ce serveur n'écrit rien.
transite par ici. Écritures limitées à la création/mise à jour de
pages et à la création de collections ; aucune suppression.
"""
from __future__ import annotations
@@ -51,6 +52,29 @@ OUTILS = [
{"name": "list_repos",
"description": "Liste le registre des dépôts suivis par Flowdeck (builtin, Gitea, GitHub).",
"inputSchema": {"type": "object", "properties": {}}},
{"name": "create_page",
"description": "Crée une page dans une collection Flowdeck.",
"inputSchema": {"type": "object",
"properties": {"collection_id": {"type": "integer"},
"title": {"type": "string"},
"icon": {"type": "string"},
"properties": {"type": "object"}},
"required": ["collection_id", "title"]}},
{"name": "update_page",
"description": "Met à jour une page Flowdeck (titre et/ou propriétés ; "
"les propriétés fournies remplacent property_values_json).",
"inputSchema": {"type": "object",
"properties": {"id": {"type": "integer"},
"title": {"type": "string"},
"properties": {"type": "object"}},
"required": ["id"]}},
{"name": "create_collection",
"description": "Crée une collection Flowdeck (projet / espace de travail).",
"inputSchema": {"type": "object",
"properties": {"name": {"type": "string"},
"description": {"type": "string"},
"icon": {"type": "string"}},
"required": ["name"]}},
]
@@ -117,6 +141,87 @@ def _appel_outil(nom: str, args: dict):
"SELECT id, name, proj_type, owner, language, description "
"FROM projects ORDER BY proj_type, name").fetchall()
return [dict(r) for r in rows]
if nom == "create_page":
try:
collection_id = int(args.get("collection_id"))
except (TypeError, ValueError):
return {"erreur": "collection_id requis (entier)"}
title = str(args.get("title") or "").strip()
if not title:
return {"erreur": "titre vide"}
coll = conn.execute("SELECT * FROM collections WHERE id = ?",
(collection_id,)).fetchone()
if coll is None:
return {"erreur": "collection introuvable"}
props = args.get("properties")
if props is None:
props = {}
if not isinstance(props, dict):
return {"erreur": "properties doit être un objet"}
icon = str(args.get("icon") or "file")
max_pos = conn.execute(
"SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages "
"WHERE collection_id = ?", (collection_id,)).fetchone()[0]
cur = conn.execute(
"INSERT INTO collection_pages "
"(collection_id, title, icon, position, property_values_json) "
"VALUES (?, ?, ?, ?, ?)",
(collection_id, title, icon, max_pos, json.dumps(props, ensure_ascii=False)))
conn.commit()
page = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
(cur.lastrowid,)).fetchone()
return _page_resume(page)
if nom == "update_page":
try:
page_id = int(args.get("id"))
except (TypeError, ValueError):
return {"erreur": "id requis (entier)"}
page = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
(page_id,)).fetchone()
if page is None:
return {"erreur": "page introuvable"}
title = page["title"]
if "title" in args and args.get("title") is not None:
title = str(args.get("title") or "").strip()
if not title:
return {"erreur": "titre vide"}
props_json = page["property_values_json"]
if "properties" in args and args.get("properties") is not None:
props = args.get("properties")
if not isinstance(props, dict):
return {"erreur": "properties doit être un objet"}
props_json = json.dumps(props, ensure_ascii=False)
conn.execute(
"UPDATE collection_pages SET title = ?, property_values_json = ?, "
"updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(title, props_json, page_id))
conn.commit()
updated = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
(page_id,)).fetchone()
return _page_resume(updated)
if nom == "create_collection":
name = str(args.get("name") or "").strip()
if not name:
return {"erreur": "nom vide"}
description = str(args.get("description") or "")
icon = str(args.get("icon") or "📋")
cur = conn.execute(
"INSERT INTO collections (name, description, icon, schema_json) "
"VALUES (?, ?, ?, '[]')",
(name, description, icon))
new_id = cur.lastrowid
# Vue par défaut, comme la création via l'API collections
# (app/routers/collections/crud.py).
conn.execute(
"INSERT INTO collection_views "
"(collection_id, name, view_type, config_json) VALUES (?, ?, ?, ?)",
(new_id, "Default View", "table", json.dumps({
"visible_properties": ["Title"], "sorts": [], "filters": []})))
conn.commit()
coll = conn.execute("SELECT * FROM collections WHERE id = ?",
(new_id,)).fetchone()
return {"id": coll["id"], "name": coll["name"],
"description": coll["description"], "icon": coll["icon"]}
return {"erreur": f"outil inconnu : {nom}"}
@@ -133,7 +238,7 @@ def _version() -> str:
def mcp_info():
return {"name": "flowdeck", "version": _version(),
"transport": "streamable-http (JSON-RPC 2.0 over POST)",
"read_only": True, "tools": [t["name"] for t in OUTILS]}
"read_only": False, "tools": [t["name"] for t in OUTILS]}
@router.post("/mcp")