New architecture: MCP provides context, Claude Code writes. New functions: - get_block_context(case_id, block_id) → returns full context package (prompt, source docs, claims, direction, precedents, style guide) WITHOUT calling Anthropic API - save_block_content(case_id, block_id, content) → saves block to DB New MCP tools: get_block_context, save_block_content The old write_block (API-based) still works as fallback. The new flow uses Claude Code's own model (Opus 4.6, 1M context) which has no separate API billing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
347 lines
11 KiB
Python
347 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_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()
|