← Files LuciaARCHIVED FILE
scripts/matter_mcp.py
12.5 KB · Nov 11, 2026 · 18:09 UTC
"""Native matter-opening review with app-only evidence and guarded saved decisions."""
from __future__ import annotations
import base64
import hashlib
import json
import secrets
import sys
from pathlib import Path
from typing import Any
from jsonschema import Draft202012Validator
from jsonschema import ValidationError as SchemaError
__all__ = ["MatterApp", "main"]
ROOT = Path(__file__).resolve().parents[1]
COMPONENT = next(
p
for p in (ROOT / "modules/apertura-pratica", ROOT.parent / "apertura-pratica")
if p.is_dir()
)
sys.path.insert(0, str(COMPONENT / "scripts"))
for vendor in (
ROOT / "vendor/modules",
ROOT / "modules/studio-archive/vendor/modules",
ROOT.parent / "_shared/vendor/modules",
):
if (vendor / "vera_assurance").is_dir():
sys.path.insert(0, str(vendor))
break
from apertura_pratica_core import (
ValidationError,
_validate_decisions,
canonical_json_hash,
load_json,
read_pending_review,
review_payload_hash,
utc_now,
validate_run,
write_json,
)
from vera_assurance.serialization import exclusive_file_lock
URI = "ui://lucia/matter-opening-v1.html"
MIME = "text/html;profile=mcp-app"
def _stamp(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else "absent"
class MatterApp:
"""Bind review decisions to the exact selected matter and pending revision."""
def __init__(self) -> None:
self.sessions: dict[str, dict[str, Any]] = {}
text = {"type": "string", "minLength": 1, "maxLength": 4000}
self.tools = [
{
"name": "lucia_matter_open",
"title": "Fascicolo nuova pratica",
"description": "Open a prepared matter-opening review in the explicitly selected absolute run directory. Full evidence stays app-only. Saving retains pending decisions; applying and professional acceptance remain separate explicit steps.",
"inputSchema": {
"type": "object",
"properties": {"run_dir": text},
"required": ["run_dir"],
"additionalProperties": False,
},
"annotations": {"readOnlyHint": True, "openWorldHint": False},
"_meta": {"ui": {"visibility": ["app", "model"], "resourceUri": URI}},
},
{
"name": "lucia_matter_save",
"title": "Salva il riesame",
"description": "Save the lawyer's explicit pending review against the current intake, review and pending-decision revisions. Never applies decisions or opens another matter.",
"inputSchema": {
"type": "object",
"properties": {
"session": text,
"request_id": text,
"reviewer": text,
"decisions": {
"type": "array",
"maxItems": 500,
"items": {"type": "object"},
},
},
"required": ["session", "request_id", "reviewer", "decisions"],
"additionalProperties": False,
},
"annotations": {
"readOnlyHint": False,
"destructiveHint": False,
"openWorldHint": False,
},
"_meta": {"ui": {"visibility": ["app"]}},
},
]
@staticmethod
def _load(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
validate_run(root)
intake, review = load_json(root / "matter_intake.json"), load_json(
root / "review_payload.json"
)
if (
review.get("intake_sha256") != canonical_json_hash(intake)
or review.get("run_id") != intake["run_id"]
):
raise ValueError(
"Prepara di nuovo il riesame per la versione corrente della pratica."
)
return intake, review
@staticmethod
def _result(session: str, root: Path, review: dict[str, Any]) -> dict[str, Any]:
pending, status = read_pending_review(root, review)
return {
"content": [
{
"type": "text",
"text": "Fascicolo pronto nel pannello locale di Lucia. Le decisioni salvate restano da applicare.",
}
],
"structuredContent": {"status": "ready"},
"_meta": {
"matter": {
"session": session,
"review": review,
"review_payload_sha256": review_payload_hash(review),
"pending_review": pending,
"pending_review_status": status,
}
},
}
def open(self, directory: str) -> dict[str, Any]:
root = Path(directory)
if not root.is_absolute() or not root.is_dir():
raise ValueError("Seleziona la cartella assoluta della pratica preparata.")
root = root.resolve()
intake, review = self._load(root)
session = secrets.token_urlsafe(32)
self.sessions[session] = {
"root": root,
"identity": (root.stat().st_dev, root.stat().st_ino),
"intake": canonical_json_hash(intake),
"review": review_payload_hash(review),
"pending": _stamp(root / "pending_review_decisions.json"),
"requests": {},
}
return self._result(session, root, review)
def save(self, args: dict[str, Any]) -> dict[str, Any]:
state = self.sessions.get(args["session"])
if state is None:
raise ValueError("Riapri la pratica dalla chat prima di salvare.")
with exclusive_file_lock(state["root"] / ".native-review.lock"):
return self._save_locked(args, state)
def _save_locked(
self, args: dict[str, Any], state: dict[str, Any]
) -> dict[str, Any]:
"""Serialize the compare-and-save across panels and local processes."""
signature = hashlib.sha256(
json.dumps(args, sort_keys=True).encode()
).hexdigest()
previous = state["requests"].get(args["request_id"])
if previous:
if previous[0] != signature:
raise ValueError("La richiesta è cambiata; riapri il riesame.")
return previous[1]
root = state["root"]
if (
root.resolve() != root
or (root.stat().st_dev, root.stat().st_ino) != state["identity"]
):
raise ValueError("La cartella della pratica è cambiata.")
intake, review = self._load(root)
path = root / "pending_review_decisions.json"
if path.is_symlink() or any(
(
canonical_json_hash(intake) != state["intake"],
review_payload_hash(review) != state["review"],
_stamp(path) != state["pending"],
)
):
raise ValueError(
"La pratica o le decisioni sono cambiate. Riapri il riesame prima di salvare."
)
reviewer = args["reviewer"].strip()
if not reviewer:
raise ValueError("Indica il revisore.")
pending = {
"schema_version": "1.0",
"workflow": "apertura-pratica",
"run_id": intake["run_id"],
"intake_sha256": state["intake"],
"review_payload_sha256": state["review"],
"reviewer": reviewer,
"decision_source": "local_workbench",
"confirmed_by_user": True,
"saved_at": utc_now(),
"decisions": args["decisions"],
}
_validate_decisions(pending, intake=intake, review_payload=review)
choices = {item["id"]: item["allowed_actions"] for item in review["items"]}
if any(
decision["action"] not in choices[decision["item_id"]]
for decision in args["decisions"]
):
raise ValueError(
"Una decisione non è disponibile per l’elemento selezionato."
)
write_json(path, pending)
state["pending"] = _stamp(path)
result = self._result(args["session"], root, review)
state["requests"][args["request_id"]] = (signature, result)
return result
@staticmethod
def resource() -> dict[str, Any]:
html = (COMPONENT / "assets/apertura-pratica-review-widget.html").read_text()
font = next(
path
for path in (
ROOT
/ "modules/comunicazione-professionale/assets/fonts/InstrumentSans-Regular.ttf",
ROOT.parent
/ "comunicazione-professionale/assets/fonts/InstrumentSans-Regular.ttf",
)
if path.is_file()
)
face = (
"@font-face{font-family:'Instrument Sans';src:url(data:font/ttf;base64,"
+ base64.b64encode(font.read_bytes()).decode()
+ ") format('truetype');font-weight:400 700;font-display:swap}"
)
html = html.replace("</style>", face + "</style>", 1).replace(
"<head>",
"<head><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'\">",
1,
)
return {
"contents": [
{
"uri": URI,
"mimeType": MIME,
"text": html,
"_meta": {
"ui": {"csp": {"connectDomains": [], "resourceDomains": []}},
"openai/ui": {
"preferredDisplayMode": "fullscreen",
"availableDisplayModes": ["inline", "fullscreen"],
},
},
}
]
}
def dispatch(self, request: dict[str, Any]) -> dict[str, Any]:
method, params = request["method"], request.get("params", {})
if method == "initialize":
return {
"protocolVersion": params.get("protocolVersion", "2024-11-05"),
"capabilities": {"tools": {}, "resources": {}},
"serverInfo": {"name": "lucia-matter-opening", "version": "1.0.0"},
}
if method == "ping":
return {}
if method == "tools/list":
return {"tools": self.tools}
if method == "resources/templates/list":
return {"resourceTemplates": []}
if method == "resources/list":
return {
"resources": [
{"uri": URI, "name": "Fascicolo nuova pratica", "mimeType": MIME}
]
}
if method == "resources/read" and params.get("uri") == URI:
return self.resource()
if method == "tools/call":
tool = next(
(tool for tool in self.tools if tool["name"] == params.get("name")),
None,
)
if tool:
args = params.get("arguments", {})
Draft202012Validator(tool["inputSchema"]).validate(args)
return (
self.open(args["run_dir"])
if tool["name"] == "lucia_matter_open"
else self.save(args)
)
raise ValueError("Operazione non disponibile")
def main() -> None:
"""Handle MCP without sending private paths or raw exception data to chat."""
app = MatterApp()
for line in sys.stdin:
try:
request = json.loads(line)
except ValueError:
continue
if not isinstance(request, dict) or "id" not in request:
continue
response = {"jsonrpc": "2.0", "id": request["id"]}
try:
response["result"] = app.dispatch(request)
except (
OSError,
ValueError,
KeyError,
TypeError,
StopIteration,
ValidationError,
SchemaError,
):
response["result"] = {
"isError": True,
"content": [
{
"type": "text",
"text": "Impossibile salvare o aprire questa versione. Riapri dalla chat il riesame corrente della pratica.",
}
],
}
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()
SHA-256: 46db246c53ac681a9c6c1ef0513c91b4db4386d99dbd987bf0ee9fc904caf177