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:
+111
-6
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user