7.78.0 — MCP : périmètre du propriétaire du jeton
list_workspaces/get_workspace filtrés par user_workspace_ids ; collections filtrées par workspace effectif ; hors périmètre = introuvable, lectures et écritures. Test deux comptes.
This commit is contained in:
+50
-17
@@ -28,15 +28,20 @@ from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from app.db import get_conn
|
||||
from app.routers.public_api import _token_owner
|
||||
from app.services.collection_lifecycle import (
|
||||
effective_workspace_sql,
|
||||
user_workspace_ids,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["mcp"])
|
||||
|
||||
OUTILS = [
|
||||
{"name": "list_workspaces",
|
||||
"description": "Liste les WORKSPACES Flowdeck — les conteneurs de pages "
|
||||
"de l'utilisateur (ex. Techno, Famille Charest), avec leur "
|
||||
"nombre de pages actives. À ne pas confondre avec les "
|
||||
"collections (bases de données).",
|
||||
"description": "Liste les WORKSPACES Flowdeck du propriétaire du jeton "
|
||||
"(propriétaire ou membre) — ses conteneurs de pages "
|
||||
"(ex. Techno, Famille Charest), avec leur nombre de pages "
|
||||
"actives. À ne pas confondre avec les collections "
|
||||
"(bases de données).",
|
||||
"inputSchema": {"type": "object", "properties": {}}},
|
||||
{"name": "get_workspace",
|
||||
"description": "Lit un workspace et l'arborescence de ses pages "
|
||||
@@ -112,19 +117,41 @@ def _page_resume(row) -> dict:
|
||||
"updated_at": row["updated_at"]}
|
||||
|
||||
|
||||
def _appel_outil(nom: str, args: dict):
|
||||
def _appel_outil(nom: str, args: dict, user_id: int):
|
||||
"""Exécute un outil DANS LE PÉRIMÈTRE du propriétaire du jeton.
|
||||
|
||||
Correctif 7.78.0 (cas réel de Bruno) : la première version listait
|
||||
les 52 workspaces de la base — tous les comptes, résidus de tests
|
||||
compris. Règle : workspaces dont l'utilisateur est propriétaire
|
||||
ou membre ; collections rattachées (directement ou via leur page
|
||||
hôte) à l'un de ces workspaces, plus les collections sans aucun
|
||||
rattachement (héritage global, comme l'API publique historique).
|
||||
Un objet hors périmètre répond « introuvable », sans fuiter son
|
||||
existence.
|
||||
"""
|
||||
with get_conn() as conn:
|
||||
accessible = set(user_workspace_ids(conn, user_id))
|
||||
eff_sql = effective_workspace_sql("c")
|
||||
|
||||
def collection_visible(coll_id: int) -> bool:
|
||||
row = conn.execute(
|
||||
f"SELECT {eff_sql} AS eff FROM collections c WHERE c.id = ?",
|
||||
(coll_id,)).fetchone()
|
||||
if row is None:
|
||||
return False
|
||||
return row["eff"] is None or row["eff"] in accessible
|
||||
|
||||
if nom == "list_workspaces":
|
||||
rows = conn.execute(
|
||||
"SELECT w.id, w.name, w.owner_id, "
|
||||
"(SELECT COUNT(*) FROM pages p WHERE p.workspace_id = w.id "
|
||||
" AND p.deleted_at IS NULL) AS page_count "
|
||||
"FROM workspaces w ORDER BY w.name").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
return [dict(r) for r in rows if r["id"] in accessible]
|
||||
if nom == "get_workspace":
|
||||
ws = conn.execute("SELECT * FROM workspaces WHERE id = ?",
|
||||
(int(args.get("id", 0)),)).fetchone()
|
||||
if ws is None:
|
||||
if ws is None or ws["id"] not in accessible:
|
||||
return {"erreur": "workspace introuvable"}
|
||||
pages = conn.execute(
|
||||
"SELECT id, title, parent_id, parent_section, updated_at "
|
||||
@@ -135,17 +162,20 @@ def _appel_outil(nom: str, args: dict):
|
||||
"pages": [dict(p) for p in pages]}
|
||||
if nom == "list_collections":
|
||||
rows = conn.execute(
|
||||
"SELECT c.*, (SELECT COUNT(*) FROM collection_pages p "
|
||||
"WHERE p.collection_id = c.id) AS page_count "
|
||||
f"SELECT c.*, (SELECT COUNT(*) FROM collection_pages p "
|
||||
f"WHERE p.collection_id = c.id) AS page_count, "
|
||||
f"{eff_sql} AS eff_ws "
|
||||
"FROM collections c ORDER BY c.name").fetchall()
|
||||
return [{"id": r["id"], "name": r["name"],
|
||||
"description": r["description"], "icon": r["icon"],
|
||||
"is_task": bool(r["is_task"]) if "is_task" in r.keys() else False,
|
||||
"page_count": r["page_count"]} for r in rows]
|
||||
"page_count": r["page_count"]}
|
||||
for r in rows
|
||||
if r["eff_ws"] is None or r["eff_ws"] in accessible]
|
||||
if nom == "get_collection":
|
||||
coll = conn.execute("SELECT * FROM collections WHERE id = ?",
|
||||
(int(args.get("id", 0)),)).fetchone()
|
||||
if coll is None:
|
||||
if coll is None or not collection_visible(coll["id"]):
|
||||
return {"erreur": "collection introuvable"}
|
||||
pages = conn.execute(
|
||||
"SELECT * FROM collection_pages WHERE collection_id = ? "
|
||||
@@ -157,7 +187,7 @@ def _appel_outil(nom: str, args: dict):
|
||||
if nom == "get_page":
|
||||
p = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
|
||||
(int(args.get("id", 0)),)).fetchone()
|
||||
if p is None:
|
||||
if p is None or not collection_visible(p["collection_id"]):
|
||||
return {"erreur": "page introuvable"}
|
||||
res = _page_resume(p)
|
||||
for cle in ("content", "blocks", "blocks_json"):
|
||||
@@ -171,7 +201,8 @@ def _appel_outil(nom: str, args: dict):
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM collection_pages WHERE title LIKE ? "
|
||||
"ORDER BY updated_at DESC LIMIT 25", (q,)).fetchall()
|
||||
return [_page_resume(p) for p in rows]
|
||||
return [_page_resume(p) for p in rows
|
||||
if collection_visible(p["collection_id"])]
|
||||
if nom == "list_repos":
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, proj_type, owner, language, description "
|
||||
@@ -187,7 +218,7 @@ def _appel_outil(nom: str, args: dict):
|
||||
return {"erreur": "titre vide"}
|
||||
coll = conn.execute("SELECT * FROM collections WHERE id = ?",
|
||||
(collection_id,)).fetchone()
|
||||
if coll is None:
|
||||
if coll is None or not collection_visible(collection_id):
|
||||
return {"erreur": "collection introuvable"}
|
||||
props = args.get("properties")
|
||||
if props is None:
|
||||
@@ -214,7 +245,7 @@ def _appel_outil(nom: str, args: dict):
|
||||
return {"erreur": "id requis (entier)"}
|
||||
page = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
|
||||
(page_id,)).fetchone()
|
||||
if page is None:
|
||||
if page is None or not collection_visible(page["collection_id"]):
|
||||
return {"erreur": "page introuvable"}
|
||||
title = page["title"]
|
||||
if "title" in args and args.get("title") is not None:
|
||||
@@ -281,7 +312,8 @@ def mcp_info():
|
||||
async def mcp(request: Request):
|
||||
auth = request.headers.get("authorization", "")
|
||||
token = auth[7:].strip() if auth.lower().startswith("bearer ") else ""
|
||||
if not token or _token_owner(token) is None:
|
||||
proprietaire = _token_owner(token) if token else None
|
||||
if proprietaire is None:
|
||||
return JSONResponse(
|
||||
{"jsonrpc": "2.0", "id": None,
|
||||
"error": {"code": -32001, "message": "Authentification requise (jeton d'API Flowdeck)"}},
|
||||
@@ -308,7 +340,8 @@ async def mcp(request: Request):
|
||||
return {"jsonrpc": "2.0", "id": rid, "result": {"tools": OUTILS}}
|
||||
if methode == "tools/call":
|
||||
params = data.get("params") or {}
|
||||
res = _appel_outil(params.get("name", ""), params.get("arguments") or {})
|
||||
res = _appel_outil(params.get("name", ""), params.get("arguments") or {},
|
||||
proprietaire["user_id"])
|
||||
return {"jsonrpc": "2.0", "id": rid,
|
||||
"result": {"content": [{"type": "text",
|
||||
"text": json.dumps(res, ensure_ascii=False)}]}}
|
||||
|
||||
Reference in New Issue
Block a user