"""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]: """Server startup is now non-blocking. Schema init was moved out of the lifespan to fix a race where Claude Code would call a tool before `tools/list` had been answered — manifesting as "No such tool available". Lifespan now returns immediately so the MCP handshake completes in milliseconds; the schema is initialized lazily on the first DB access via services/db.get_pool(). """ from legal_mcp.services.db import close_pool logger.info("Ezer Mishpati MCP server ready (schema init deferred)") 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, precedent_library as plib, ) # 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: """חיפוש בציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents). שונה מ-search_precedent_library שמחפש בקורפוס הפסיקה הסמכותית.""" return await precedents.precedent_search_library(query, practice_area, limit) # ── External Precedent Library — authoritative case-law corpus ───── # Distinct from precedent_search_library above (chair-attached quotes) # and from search_decisions (Daphna's style corpus). @mcp.tool() async def precedent_library_upload( file_path: str, citation: str, case_name: str = "", court: str = "", decision_date: str = "", source_type: str = "", precedent_level: str = "", practice_area: str = "", appeal_subtype: str = "", subject_tags: list[str] | None = None, is_binding: bool = True, headnote: str = "", summary: str = "", ) -> str: """העלאת פסיקה חיצונית (פס"ד / החלטה של ועדה אחרת) לקורפוס הסמכותי. מחלץ הלכות אוטומטית — כולן ממתינות לאישור היו"ר. practice_area: rishuy_uvniya / betterment_levy / compensation_197.""" return await plib.precedent_library_upload( file_path, citation, case_name, court, decision_date, source_type, precedent_level, practice_area, appeal_subtype, subject_tags, is_binding, headnote, summary, ) @mcp.tool() async def precedent_library_list( practice_area: str = "", court: str = "", precedent_level: str = "", source_type: str = "", search: str = "", limit: int = 100, ) -> str: """רשימת הפסיקה בקורפוס הסמכותי, עם פילטרים.""" return await plib.precedent_library_list( practice_area, court, precedent_level, source_type, search, limit, ) @mcp.tool() async def precedent_library_get(case_law_id: str) -> str: """פסיקה ספציפית בקורפוס + רשימת ההלכות שחולצו ממנה (כולל ממתינות לאישור).""" return await plib.precedent_library_get(case_law_id) @mcp.tool() async def precedent_library_delete(case_law_id: str) -> str: """מחיקת פסיקה מהקורפוס (cascade: chunks + halachot).""" return await plib.precedent_library_delete(case_law_id) @mcp.tool() async def precedent_extract_halachot(case_law_id: str) -> str: """הרצה מחדש של חילוץ הלכות לפסיקה קיימת. ההלכות הקיימות נמחקות, החדשות חוזרות לסטטוס pending_review.""" return await plib.precedent_extract_halachot(case_law_id) @mcp.tool() async def precedent_extract_metadata(case_law_id: str) -> str: """חילוץ מטא-דאטה (case_name קצר, summary, headnote, key_quote, subject_tags, appeal_subtype, date, level, court, source_type) מהטקסט. ממלא רק שדות ריקים.""" return await plib.precedent_extract_metadata(case_law_id) @mcp.tool() async def precedent_process_pending(kind: str = "metadata", limit: int = 20) -> str: """ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה.""" return await plib.precedent_process_pending(kind, limit) @mcp.tool() async def search_precedent_library( query: str, practice_area: str = "", court: str = "", precedent_level: str = "", appeal_subtype: str = "", subject_tag: str = "", limit: int = 10, include_halachot: bool = True, ) -> str: """חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation).""" return await plib.search_precedent_library( query, practice_area, court, precedent_level, appeal_subtype, None, subject_tag, limit, include_halachot, ) @mcp.tool() async def halacha_review( halacha_id: str, status: str, reviewer: str = "דפנה", rule_statement: str = "", reasoning_summary: str = "", subject_tags: list[str] | None = None, practice_areas: list[str] | None = None, ) -> str: """אישור / דחייה / עריכה של הלכה שחולצה אוטומטית. status: pending_review / approved / rejected / published.""" return await plib.halacha_review( halacha_id, status, reviewer, rule_statement, reasoning_summary, subject_tags, practice_areas, ) @mcp.tool() async def halachot_pending(limit: int = 100) -> str: """תור ההלכות הממתינות לאישור.""" return await plib.halachot_pending(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 = "", practice_area: str = "appeals_committee", appeal_subtype: str = "", ) -> str: """העלאת החלטה קודמת של דפנה לקורפוס הסגנון. קטגוריות: בנייה, שימוש חורג, תכנית, היתר, הקלה, חלוקה, תמ"א 38, היטל השבחה, פיצויים 197. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = אוטומטי ממספר ההחלטה).""" return await documents.document_upload_training( file_path, decision_number, decision_date, subject_categories, title, practice_area, appeal_subtype, ) @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) @mcp.tool() async def document_update( case_number: str, doc_id: str, doc_type: str = "", appraiser_side: str = "", ) -> str: """עדכון תיוג מסמך — doc_type ו/או appraiser_side (committee/appellant/deciding). ריק = ללא שינוי.""" return await documents.document_update(case_number, doc_id, doc_type, appraiser_side) # 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 = "", practice_area: str = "", appeal_subtype: str = "", case_number: str = "", ) -> str: """חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי.""" return await search.search_decisions( query, limit, section_type, practice_area, appeal_subtype, case_number, ) @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, practice_area: str = "", appeal_subtype: str = "", case_number: str = "", ) -> str: """מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי.""" return await search.find_similar_cases( description, limit, practice_area, appeal_subtype, case_number, ) # 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 extract_appraiser_facts(case_number: str) -> str: """חילוץ תכניות והיתרים מכל השומות בתיק וזיהוי סתירות בין שמאים. הכנה לטיוטת ביניים.""" return await drafting.extract_appraiser_facts(case_number) @mcp.tool() async def write_interim_draft(case_number: str, instructions: str = "") -> str: """כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט.""" return await drafting.write_interim_draft(case_number, instructions) @mcp.tool() async def export_interim_draft(case_number: str, output_path: str = "") -> str: """ייצוא טיוטת ביניים ל-DOCX — סדר חדש (רקע → תכניות+היתרים → טענות → הליכים), ללא דיון/סיכום.""" return await drafting.export_interim_draft(case_number, output_path) @mcp.tool() async def apply_user_edit(case_number: str, edit_filename: str) -> str: """רישום עריכה שהעלה המשתמש (עריכה-v*.docx) כמקור האמת החדש — מזריק bookmarks אם חסר.""" return await drafting.apply_user_edit(case_number, edit_filename) @mcp.tool() async def list_bookmarks(case_number: str) -> str: """רשימת bookmarks הקיימים ב-active_draft של התיק (אנקורים ל-revisions).""" return await drafting.list_bookmarks(case_number) @mcp.tool() async def revise_draft(case_number: str, revisions_json: str, author: str = "מערכת AI") -> str: """החלת revisions (Track Changes) על ה-active_draft, יוצר טיוטה-v{N+1}.docx חדשה.""" return await drafting.revise_draft(case_number, revisions_json, author) @mcp.tool() async def analyze_style(appeal_subtype: str = "") -> str: """ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ ושומר דפוסי כתיבה. סוג ערר: building_permit / betterment_levy / compensation_197 (ריק = הכל).""" return await drafting.analyze_style(appeal_subtype) @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()