test / mcp_server.py
asifdzakiy-droid
feat: Add SQLite metadata dump script and UI, remove PostgreSQL
70a33a4
Raw
History Blame Contribute Delete
4.66 kB
import os
import uvicorn
from mcp.server.fastmcp import FastMCP
from sqlalchemy import create_engine, text
# 1. Database Connection (Optimized for 2 CPU / 16GB RAM)
DB_URI = os.getenv("DB_URI", "postgresql://postgres:password@localhost:5432/postgres")
# Pool size 5 prevents too many context switches on 2 vCPUs
engine = create_engine(DB_URI, pool_size=5, max_overflow=10)
# 2. Initialize FastMCP
mcp = FastMCP("Shamela4")
def fetch_data(query: str, params: dict = {}):
try:
with engine.connect() as conn:
result = conn.execute(text(query), params)
return [dict(row._mapping) for row in result]
except Exception as e:
return {"error": str(e)}
def fetch_one(query: str, params: dict = {}):
try:
with engine.connect() as conn:
result = conn.execute(text(query), params).fetchone()
if result:
return dict(result._mapping)
return None
except Exception as e:
return {"error": str(e)}
@mcp.tool()
def get_database_stats() -> dict:
"""Get database statistics: counts of books, authors, categories, and pages."""
try:
with engine.connect() as conn:
books_count = conn.execute(text("SELECT COUNT(book_id) FROM books")).scalar() or 0
authors_count = conn.execute(text("SELECT COUNT(author_id) FROM authors")).scalar() or 0
categories_count = conn.execute(text("SELECT COUNT(category_id) FROM categories")).scalar() or 0
pages_count = conn.execute(text("SELECT COUNT(page_id) FROM pages")).scalar() or 0
return {
"total_categories": categories_count,
"total_authors": authors_count,
"total_books": books_count,
"total_pages": pages_count,
}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
def search_categories(limit: int = 50) -> list:
"""Get list of categories in the library."""
limit = min(limit, 100) # Hard limit
return fetch_data("SELECT category_id, category_name_ar, name_en FROM categories ORDER BY sort_order LIMIT :limit", {"limit": limit})
@mcp.tool()
def search_authors(search_term: str, limit: int = 10) -> list:
"""Search for authors by their Arabic name."""
limit = min(limit, 20)
query = "SELECT author_id, name_ar, death_hijri FROM authors WHERE name_ar LIKE :search ORDER BY death_hijri LIMIT :limit"
return fetch_data(query, {"search": f"%{search_term}%", "limit": limit})
@mcp.tool()
def search_books(title_or_author: str, limit: int = 10) -> list:
"""Search for books by title or author name."""
limit = min(limit, 20)
query = """
SELECT book_id, title_ar, main_author_name_ar, main_author_death_hijri
FROM books
WHERE title_ar LIKE :search OR main_author_name_ar LIKE :search
ORDER BY book_id LIMIT :limit
"""
return fetch_data(query, {"search": f"%{title_or_author}%", "limit": limit})
@mcp.tool()
def get_book_toc(book_id: int) -> list:
"""Get the Table of Contents (Daftar Isi) for a specific book_id."""
query = "SELECT title_id, title_text, page_id, parent_id FROM toc WHERE book_id = :book_id ORDER BY title_id"
return fetch_data(query, {"book_id": book_id})
@mcp.tool()
def search_text_in_books(query: str, limit: int = 5) -> list:
"""
Full-text search inside book pages.
Use this to find specific Arabic text/words inside the books.
Returns highly relevant text snippets (highlights).
"""
limit = min(limit, 10) # Strict limit for efficiency on 16GB RAM
# Use websearch_to_tsquery for efficient and safe natural language parsing
sql = """
SELECT p.book_id, p.page_num, p.sequence_num, b.title_ar as book_title,
ts_headline('arabic', p.body, websearch_to_tsquery('arabic', :query)) as snippet
FROM pages p
JOIN books b ON p.book_id = b.book_id
WHERE to_tsvector('arabic', p.body) @@ websearch_to_tsquery('arabic', :query)
ORDER BY p.book_id, p.sequence_num
LIMIT :limit
"""
return fetch_data(sql, {"query": query, "limit": limit})
@mcp.tool()
def get_page_content(book_id: int, page_num: int) -> dict:
"""Retrieve the exact, full text of a specific page number in a book."""
query = "SELECT page_num, body, footnotes FROM pages WHERE book_id = :book_id AND page_num = :page_num LIMIT 1"
return fetch_one(query, {"book_id": book_id, "page_num": page_num})
if __name__ == "__main__":
print("Starting Shamela4 FastMCP Server with SSE on port 8001...")
mcp.run(transport="sse", host="0.0.0.0", port=8001)