Sefaria Jewish Library MCP Server

by Sivan22
Verified
from mcp.server.models import InitializationOptions import mcp.types as types from mcp.server import NotificationOptions, Server import mcp.server.stdio import asyncio import os import logging import sys import json from .sefaria_handler import * # Configure logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stderr), logging.FileHandler('sefaria_jewish_library.log', encoding='utf-8') ] ) logger = logging.getLogger('sefaria_jewish_library') SEFARIA_API_URL = "https://sefaria.org" server = Server("sefaria_jewish_library") @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """ List available tools. Each tool specifies its arguments using JSON Schema validation. """ logger.debug("Handling list_tools request") return [ types.Tool( name="get_text", description="get a jewish text from the jewish library", inputSchema={ "type": "object", "properties": { "reference": { "type": "string", "description": "The reference of the jewish text, e.g. 'שולחן ערוך אורח חיים סימן א' or 'Genesis 1:1'", }, }, "required": ["reference"], }, ), types.Tool( name="get_commentaries", description="get a list of references of commentaries for a jewish text", inputSchema={ "type": "object", "properties": { "reference": { "type": "string", "description": "the reference of the jewish text, e.g. 'שולחן ערוך אורח חיים סימן א' or 'Genesis 1:1'", }, }, "required": ["reference"], }, ), types.Tool( name="search_texts", description="search for jewish texts in the Sefaria library", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "The search query", }, "slop":{ "type": "integer", "description": "The maximum distance between each query word in the resulting document. 0 means an exact match must be found.", "default": 2 }, "filters":{ "type": "list", "description": 'Filters to apply to the text path in English (Examples: "Shulkhan Arukh", "maimonides", "talmud").', "default" : "[]" }, "size": { "type": "integer", "description": "Number of results to return.", "default": 10 } }, "required": ["query"], }, ), ] @server.call_tool() async def handle_call_tool( name: str, arguments: dict | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Handle tool execution requests. Tools can search the Jewish library and return formatted results. """ logger.debug(f"Handling call_tool request for {name} with arguments {arguments}") try: if not arguments: raise ValueError("Missing arguments") if name == "get_text": try: reference = arguments.get("reference") if not reference: raise ValueError("Missing reference parameter") logger.debug(f"handle_get_text: {reference}") text = await get_text(reference) return [types.TextContent( type="text", text= text )] except Exception as err: logger.error(f"retreive text error: {err}", exc_info=True) return [types.TextContent( type="text", text=f"Error: {str(err)}" )] elif name == "get_commentaries": try: reference = arguments.get("reference") if not reference: raise ValueError("Missing parameter") logger.debug(f"handle_get_commentaries: {reference}") commentaries = await get_commentaries(reference) return [types.TextContent( type="text", text="\n".join(commentaries) )] except Exception as err: logger.error(f"retreive commentaries error: {err}", exc_info=True) return [types.TextContent( type="text", text=f"Error: {str(err)}" )] elif name == "search_texts": try: query = arguments.get("query") if not query: raise ValueError("Missing query parameter") slop = arguments.get("slop") if not slop : # Use 'is None' to distinguish between explicitly provided null and missing key slop = 2 filters = arguments.get("filters") if not filters: filters = None size = arguments.get("size") if not size: size = 10 logger.debug(f"handle_search_texts: {query}") results = await search_texts(query, slop, filters, size) return [types.TextContent( type="text", text=results )] except Exception as err: logger.error(f"search texts error: {err}", exc_info=True) return [types.TextContent( type="text", text=f"Error: {str(err)}" )] else: raise ValueError(f"Unknown tool: {name}") except Exception as e: logger.error(f"Tool execution error: {e}", exc_info=True) return [types.TextContent( type="text", text=f"Error: {str(e)}" )] async def main(): try: logger.info("Starting Jewish Library MCP server...") # Run the server using stdin/stdout streams async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="sefaria_jewish_library", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) except Exception as e: logger.error(f"Server error: {e}", exc_info=True) raise if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error(f"Fatal error: {e}", exc_info=True) raise