Initial commit: MCP server + web upload interface

Ezer Mishpati - AI legal decision drafting system with:
- MCP server (FastMCP) with document processing pipeline
- Web upload interface (FastAPI) for file upload and classification
- pgvector-based semantic search
- Hebrew legal document chunking and embedding
This commit is contained in:
2026-03-23 12:33:07 +00:00
commit 6f515dc2cb
33 changed files with 3297 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
"""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 = "",
) -> str:
"""יצירת תיק ערר חדש."""
return await cases.case_create(
case_number, title, appellants, respondents,
subject, property_address, permit_number, committee_type,
hearing_date, notes,
)
@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,
) -> str:
"""עדכון פרטי תיק."""
return await cases.case_update(
case_number, status, title, subject, notes,
hearing_date, decision_date, tags,
)
# 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)
# 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 analyze_style() -> str:
"""ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ ושומר דפוסי כתיבה."""
return await drafting.analyze_style()
# Workflow
@mcp.tool()
async def workflow_status(case_number: str) -> str:
"""סטטוס תהליך עבודה מלא לתיק."""
return await workflow.workflow_status(case_number)
@mcp.tool()
async def processing_status() -> str:
"""סטטוס כללי - מספר תיקים, מסמכים, chunks."""
return await workflow.processing_status()
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()