Files
legal-ai/mcp-server/src/legal_mcp/server.py
Chaim 50eaa887db Add chair feedback system and content checklists for block-yod
Backend changes cherry-picked from ui-rewrite branch to enable
feedback API endpoints for the Next.js staging UI.

- chair_feedback DB table + API endpoints (GET/POST/PATCH)
- Content checklists by appeal subtype injected into block-yod prompt
- MCP tools for recording and listing chair feedback
- Corpus analysis documentation (24 decisions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:05:53 +00:00

423 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Ezer Mishpati - MCP Server entry point.
Run with: python -m legal_mcp.server
"""
from __future__ import annotations
import logging
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
# Configure logging to stderr (stdout is reserved for JSON-RPC)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
stream=sys.stderr,
)
logger = logging.getLogger("legal_mcp")
@asynccontextmanager
async def lifespan(server: FastMCP) -> AsyncIterator[None]:
"""Initialize DB schema on startup, close pool on shutdown."""
from legal_mcp.services.db import close_pool, init_schema
logger.info("Initializing database schema...")
await init_schema()
logger.info("Ezer Mishpati MCP server ready")
try:
yield
finally:
await close_pool()
logger.info("Ezer Mishpati MCP server stopped")
# Create MCP server
mcp = FastMCP(
"Ezer Mishpati - עוזר משפטי",
instructions="מערכת AI לסיוע בניסוח החלטות משפטיות בסגנון דפנה תמיר",
lifespan=lifespan,
)
# ── Import and register tools ───────────────────────────────────────
from legal_mcp.tools import ( # noqa: E402
cases, documents, search, drafting, workflow, precedents,
)
# Case management
@mcp.tool()
async def case_create(
case_number: str,
title: str,
appellants: list[str] | None = None,
respondents: list[str] | None = None,
subject: str = "",
property_address: str = "",
permit_number: str = "",
committee_type: str = "ועדה מקומית",
hearing_date: str = "",
notes: str = "",
expected_outcome: str = "",
) -> str:
"""יצירת תיק ערר חדש. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
return await cases.case_create(
case_number, title, appellants, respondents,
subject, property_address, permit_number, committee_type,
hearing_date, notes, expected_outcome,
)
@mcp.tool()
async def case_list(status: str = "", limit: int = 50) -> str:
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
return await cases.case_list(status, limit)
@mcp.tool()
async def case_get(case_number: str) -> str:
"""פרטי תיק מלאים כולל רשימת מסמכים."""
return await cases.case_get(case_number)
@mcp.tool()
async def case_update(
case_number: str,
status: str = "",
title: str = "",
subject: str = "",
notes: str = "",
hearing_date: str = "",
decision_date: str = "",
tags: list[str] | None = None,
expected_outcome: str = "",
) -> str:
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
return await cases.case_update(
case_number, status, title, subject, notes,
hearing_date, decision_date, tags, expected_outcome,
)
@mcp.tool()
async def case_delete(case_number: str, remove_files: bool = False) -> str:
"""מחיקת תיק ערר. קבצים בדיסק נשארים אלא אם remove_files=true."""
return await cases.case_delete(case_number, remove_files)
# Precedent attachments (user-supplied legal support for the compose phase)
@mcp.tool()
async def precedent_attach(
case_number: str,
quote: str,
citation: str,
section_id: str = "",
chair_note: str = "",
pdf_document_id: str = "",
) -> str:
"""צירוף פסיקה תומכת לתיק. section_id ריק = כללי לתיק; אחרת threshold_1/issue_3."""
return await precedents.precedent_attach(
case_number, quote, citation, section_id, chair_note, pdf_document_id,
)
@mcp.tool()
async def precedent_list(case_number: str) -> str:
"""רשימת כל הפסיקות שצורפו לתיק."""
return await precedents.precedent_list(case_number)
@mcp.tool()
async def precedent_remove(precedent_id: str) -> str:
"""הסרת פסיקה מצורפת."""
return await precedents.precedent_remove(precedent_id)
@mcp.tool()
async def precedent_search_library(
query: str, practice_area: str = "", limit: int = 10,
) -> str:
"""חיפוש בספרייה הרוחבית של ציטוטים שנצברו בין תיקים."""
return await precedents.precedent_search_library(query, practice_area, limit)
# Documents
@mcp.tool()
async def document_upload(
case_number: str,
file_path: str,
doc_type: str = "appeal",
title: str = "",
) -> str:
"""העלאה ועיבוד מסמך לתיק ערר (PDF/DOCX/RTF/TXT). מחלץ טקסט ויוצר embeddings."""
return await documents.document_upload(case_number, file_path, doc_type, title)
@mcp.tool()
async def document_upload_training(
file_path: str,
decision_number: str = "",
decision_date: str = "",
subject_categories: list[str] | None = None,
title: str = "",
) -> str:
"""העלאת החלטה קודמת של דפנה לקורפוס הסגנון. קטגוריות: בנייה, שימוש חורג, תכנית, היתר, הקלה, חלוקה, תמ"א 38, היטל השבחה, פיצויים 197."""
return await documents.document_upload_training(
file_path, decision_number, decision_date, subject_categories, title,
)
@mcp.tool()
async def document_get_text(case_number: str, doc_title: str = "") -> str:
"""קבלת טקסט מלא של מסמך מתוך תיק."""
return await documents.document_get_text(case_number, doc_title)
@mcp.tool()
async def document_list(case_number: str) -> str:
"""רשימת מסמכים בתיק."""
return await documents.document_list(case_number)
# Claims extraction
@mcp.tool()
async def extract_claims(
case_number: str,
doc_title: str = "",
party_hint: str = "",
) -> str:
"""חילוץ טענות מכתב טענות בתיק. מחלץ טענות לפי צד ושומר ב-DB."""
return await documents.extract_claims(case_number, doc_title, party_hint)
@mcp.tool()
async def get_claims(
case_number: str,
party_role: str = "",
) -> str:
"""שליפת טענות שחולצו לתיק. party_role: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
return await documents.get_claims(case_number, party_role)
# References
@mcp.tool()
async def extract_references(
case_number: str,
doc_title: str = "",
) -> str:
"""זיהוי תכניות, פסיקה וחקיקה מתוך מסמכי תיק. מזהה ומקשר להפניות קיימות ב-DB."""
return await documents.extract_references(case_number, doc_title)
# Search
@mcp.tool()
async def search_decisions(
query: str,
limit: int = 10,
section_type: str = "",
) -> str:
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים."""
return await search.search_decisions(query, limit, section_type)
@mcp.tool()
async def search_case_documents(
case_number: str,
query: str,
limit: int = 10,
) -> str:
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
return await search.search_case_documents(case_number, query, limit)
@mcp.tool()
async def find_similar_cases(
description: str,
limit: int = 5,
) -> str:
"""מציאת תיקים דומים על בסיס תיאור."""
return await search.find_similar_cases(description, limit)
# Drafting
@mcp.tool()
async def get_style_guide() -> str:
"""שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה."""
return await drafting.get_style_guide()
@mcp.tool()
async def draft_section(
case_number: str,
section: str,
instructions: str = "",
) -> str:
"""הרכבת הקשר מלא לניסוח סעיף (עובדות + תקדימים + סגנון)."""
return await drafting.draft_section(case_number, section, instructions)
@mcp.tool()
async def get_chair_directions(case_number: str) -> str:
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI.
מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות.
"""
return await drafting.get_chair_directions(case_number)
@mcp.tool()
async def get_decision_template(case_number: str) -> str:
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""
return await drafting.get_decision_template(case_number)
@mcp.tool()
async def get_block_context(
case_number: str,
block_id: str,
instructions: str = "",
) -> str:
"""קבלת הקשר מלא לכתיבת בלוק — ללא API. Claude Code כותב ושומר."""
return await drafting.get_block_context(case_number, block_id, instructions)
@mcp.tool()
async def save_block_content(
case_number: str,
block_id: str,
content: str,
) -> str:
"""שמירת בלוק שנכתב ע"י Claude Code ב-DB."""
return await drafting.save_block_content(case_number, block_id, content)
@mcp.tool()
async def validate_decision(case_number: str) -> str:
"""בדיקת QA — 6 בדיקות איכות על ההחלטה. אם בדיקה קריטית נכשלת — ייצוא חסום."""
return await drafting.validate_decision(case_number)
@mcp.tool()
async def export_docx(case_number: str, output_path: str = "") -> str:
"""ייצוא החלטה לקובץ DOCX מעוצב — גופן David, RTL, כותרות, מספור סעיפים."""
return await drafting.export_docx(case_number, output_path)
@mcp.tool()
async def analyze_style() -> str:
"""ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ ושומר דפוסי כתיבה."""
return await drafting.analyze_style()
@mcp.tool()
async def write_block(
case_number: str,
block_id: str,
instructions: str = "",
) -> str:
"""כתיבת בלוק יחיד בהחלטה: block-alef עד block-yod-bet. שומר ב-DB."""
return await drafting.write_block(case_number, block_id, instructions)
@mcp.tool()
async def write_all_blocks(
case_number: str,
start_from: str = "block-alef",
instructions: str = "",
) -> str:
"""כתיבת כל הבלוקים בהחלטה, בלוק אחרי בלוק. שומר כל בלוק מיד."""
return await drafting.write_all_blocks(case_number, start_from, instructions)
# Workflow
@mcp.tool()
async def workflow_status(case_number: str) -> str:
"""סטטוס תהליך עבודה מלא לתיק."""
return await workflow.workflow_status(case_number)
@mcp.tool()
async def get_metrics(case_number: str = "") -> str:
"""מדדי הצלחה — KPIs לתיק ספציפי או דשבורד כולל. ריק = דשבורד."""
return await workflow.get_metrics(case_number)
@mcp.tool()
async def processing_status() -> str:
"""סטטוס כללי - מספר תיקים, מסמכים, chunks."""
return await workflow.processing_status()
# Outcome & Brainstorming
@mcp.tool()
async def set_outcome(
case_number: str,
outcome: str,
reasoning: str = "",
) -> str:
"""הזנת תוצאה לתיק: rejected (דחייה), accepted (קבלה), partial (קבלה חלקית). אם אין נימוק — מפעיל סיעור מוחות."""
return await workflow.set_outcome(case_number, outcome, reasoning)
@mcp.tool()
async def brainstorm_directions(case_number: str) -> str:
"""סיעור מוחות — הצגת טענות מרכזיות והצעת 2-3 כיוונים אפשריים לנימוק ההחלטה."""
return await workflow.brainstorm_directions(case_number)
@mcp.tool()
async def approve_direction(
case_number: str,
direction_index: int = 0,
additional_notes: str = "",
) -> str:
"""אישור כיוון — יוצר מסמך כיוון מאושר. חובה לפני כתיבת דיון (בלוק י)."""
return await workflow.approve_direction(case_number, direction_index, additional_notes)
@mcp.tool()
async def ingest_final_version(
case_number: str,
file_path: str = "",
final_text: str = "",
) -> str:
"""קליטת גרסה סופית שדפנה חתמה — השוואה לטיוטה וחילוץ לקחים לשיפור עתידי."""
return await workflow.ingest_final_version(case_number, file_path, final_text)
@mcp.tool()
async def record_chair_feedback(
case_number: str,
feedback_text: str,
block_id: str = "block-yod",
category: str = "missing_content",
lesson_extracted: str = "",
) -> str:
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
return await workflow.record_chair_feedback(
case_number, feedback_text, block_id, category, lesson_extracted,
)
@mcp.tool()
async def list_chair_feedback(
case_number: str = "",
category: str = "",
unresolved_only: bool = True,
) -> str:
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()