Closes the loop so דפנה's positions (written inline in the UI and
saved to analysis-and-research.md) automatically become binding
direction for the legal-writer agent — no manual copy-paste,
no bypass.
Backend:
- research_md.extract_chair_directions(path) returns a compact dict
with status (missing/empty/partial/complete), filled_count,
empty_count, and a reduced list of threshold_claims + issues each
with {id, number, title, direction}. Designed to be directly usable
as direction_doc by the writer.
- New MCP tool: drafting.get_chair_directions(case_number) wraps the
helper, resolves the case research file path via config.find_case_dir,
returns formatted JSON.
- Registered in server.py as mcp__legal-ai__get_chair_directions.
legal-writer agent update:
- Adds get_chair_directions to the tools list.
- New mandatory "שלב 1ב" before any block writing: call
get_chair_directions, branch on status.
- missing → halt, report "legal-analyst לא רץ עדיין"
- empty → halt, instruct Dafna to fill positions via the UI URL
- partial → halt unless user confirms; write only filled sections
- complete → proceed
- New "שלב 1ג" constructs an internal direction_doc from the
received chair rulings before writing block י.
- Block י section expanded with 5 binding rules:
1. Open each discussion with Dafna's ruling as the thesis
2. Frame the reasoning in her style (use get_style_guide phrases)
3. Match her tone (decisive vs nuanced)
4. Must NOT contradict her position — if she disagreed with your
own inclination, her position rules
5. Use legal_questions from the analysis file as the analytical
structure (principle question first, concrete application second)
- New bullet section for block יא: summarize each chair ruling
briefly, state final outcome, close with the signed date formula.
Verified all four status paths (missing/empty/partial/complete) via
local test. Now Dafna's workflow is fully end-to-end: she reads the
analyst report in the UI, fills "עמדת ועדת הערר" in each card, hits
blur to auto-save, then triggers legal-writer — which picks up her
positions as direction without any file shuffle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
356 lines
11 KiB
Python
356 lines
11 KiB
Python
"""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 cases, documents, search, drafting, workflow # noqa: E402
|
||
|
||
|
||
# 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,
|
||
)
|
||
|
||
|
||
# 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)
|
||
|
||
|
||
|
||
def main():
|
||
mcp.run(transport="stdio")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|