12 Commits
Author SHA1 Message Date
saidsurucu 82a0d13d25 Bump version to 0.1.8 for Gemini CLI compatibility fixes 2025-11-21 22:21:19 +03:00
saidsurucu 18b552ca2f Fix SearchResultItem reference error for Gemini CLI
- Fixed search() function to return Dict[str, Any] instead of SearchResponse
- Converted return statements to plain dictionaries
- Deleted unused Pydantic models (SearchResultItem, SearchResponse)
- Eliminates /SearchResultItem references that Gemini CLI cannot resolve
- All MCP tools now compatible with Gemini CLI schema validation
2025-11-21 22:18:36 +03:00
saidsurucu 1e96b1888e Fix Gemini CLI compatibility: Convert all Pydantic return types to Dict[str, Any]
- Fixed 10 tools to avoid / patterns in JSON schemas
- All tools now return Dict[str, Any] with .model_dump() applied
- Affected tools:
  * search_emsal_detailed_decisions
  * get_emsal_document_markdown
  * search_uyusmazlik_decisions
  * get_uyusmazlik_document_markdown_from_url
  * search_rekabet_kurumu_decisions
  * get_rekabet_kurumu_document
  * search_sayistay_unified
  * get_sayistay_document_unified
  * search_kvkk_decisions
  * get_kvkk_document_markdown
- Gemini CLI should now be able to load and use all MCP tools without schema validation errors
2025-11-21 22:13:51 +03:00
saidsurucu 815786a09d Comment out undefined LOG_FILE_PATH reference 2025-11-21 21:57:21 +03:00
saidsurucu 260adb3ac9 Fix Python 3.11 compatibility in KİK v2 client 2025-11-21 21:56:10 +03:00
saidsurucu 7164205425 Fix Gemini CLI schema error in search tool 2025-11-21 21:33:39 +03:00
saidsurucu 3961a23d3a Make tiktoken and PyJWT optional (saas group only) 2025-10-06 15:45:16 +03:00
saidsurucu 25723f070f Remove file logging - console only 2025-10-06 15:21:03 +03:00
saidsurucu 6376037ccf Add tiktoken and PyJWT to requirements.txt 2025-10-06 15:04:18 +03:00
saidsurucu d1728ce114 Fix dependency installation order 2025-10-06 15:02:34 +03:00
saidsurucu 69b5da5cef Add tiktoken to saas dependencies 2025-10-06 14:55:31 +03:00
saidsurucu 91564bf0a1 Remove logging statements from asgi_app 2025-10-06 14:49:28 +03:00
8 changed files with 2696 additions and 128 deletions
+1
View File
@@ -0,0 +1 @@
/cache
+84
View File
@@ -0,0 +1,84 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran go
# haskell java julia kotlin lua markdown
# nix perl php python python_jedi r
# rego ruby ruby_solargraph rust scala swift
# terraform typescript typescript_vts yaml zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- python
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "yargi-mcp"
included_optional_tools: []
+2 -1
View File
@@ -9,10 +9,11 @@ COPY pyproject.toml poetry.lock* requirements*.txt* ./
# Fast, deterministic install with `uv`
RUN pip install --no-cache-dir uv && \
uv pip install --system --no-cache-dir . && \
uv pip install --system --no-cache-dir .[asgi,saas]
# Cache buster - force rebuild
ARG CACHE_BUST=202507221015
ARG CACHE_BUST=202510061202
RUN echo "Cache bust: $CACHE_BUST"
# Copy application source
+28 -55
View File
@@ -24,11 +24,24 @@ from starlette.middleware.base import BaseHTTPMiddleware
# Import the proper create_app function that includes all middleware
from mcp_server_main import create_app
# Import Stripe webhook router
from stripe_webhook import router as stripe_router
# Conditional auth-related imports (only if auth enabled)
_auth_check = os.getenv("ENABLE_AUTH", "false").lower() == "true"
# Import simplified MCP Auth HTTP adapter
from mcp_auth_http_simple import router as mcp_auth_router
if _auth_check:
# Import MCP Auth HTTP adapter (OAuth endpoints)
try:
from mcp_auth_http_simple import router as mcp_auth_router
except ImportError:
mcp_auth_router = None
# Import Stripe webhook router
try:
from stripe_webhook import router as stripe_router
except ImportError:
stripe_router = None
else:
mcp_auth_router = None
stripe_router = None
# OAuth configuration from environment variables
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com")
@@ -62,15 +75,13 @@ if CLERK_SECRET_KEY and CLERK_ISSUER:
# Production: Use Clerk JWKS endpoint for token validation
bearer_auth = BearerAuthProvider(
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
issuer=None, # Disable issuer validation - Clerk uses different issuer format
issuer=None,
algorithm="RS256",
audience=None, # Disable audience validation - Clerk uses different audience format
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
audience=None,
required_scopes=[]
)
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
else:
# Development: Generate RSA key pair for testing
logger.warning("No Clerk credentials found - using development RSA key pair")
dev_key_pair = RSAKeyPair.generate()
bearer_auth = BearerAuthProvider(
public_key=dev_key_pair.public_key,
@@ -78,31 +89,13 @@ else:
audience="dev-mcp-server",
required_scopes=["yargi.read"]
)
# Generate a test token for development
dev_token = dev_key_pair.create_token(
subject="dev-user",
issuer="https://dev.yargimcp.com",
audience="dev-mcp-server",
scopes=["yargi.read", "yargi.search"],
expires_in_seconds=3600 * 24 # 24 hours for development
)
logger.info(f"Development Bearer token: {dev_token}")
# Create MCP app with Bearer authentication
mcp_server = create_app(auth=bearer_auth if auth_enabled else None)
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
mcp_app = mcp_server.http_app(path="/")
logger.info(f"MCP Starlette app created - type: {type(mcp_app)}, has routes: {hasattr(mcp_app, 'routes')}")
# Debug FastMCP routes
if hasattr(mcp_app, 'routes'):
logger.info(f"MCP app route count: {len(mcp_app.routes)}")
for i, route in enumerate(mcp_app.routes):
logger.info(f"Route {i}: {route.path if hasattr(route, 'path') else 'unknown'} - {type(route)}")
else:
logger.warning("MCP app has no routes attribute")
# Configure JSON encoder for proper Turkish character support
class UTF8JSONResponse(JSONResponse):
@@ -141,11 +134,12 @@ app = FastAPI(
redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint
)
# Add Stripe webhook router to FastAPI
app.include_router(stripe_router, prefix="/api/stripe")
# Add auth-related routers to FastAPI (only if available)
if stripe_router:
app.include_router(stripe_router, prefix="/api/stripe")
# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints)
app.include_router(mcp_auth_router)
if mcp_auth_router:
app.include_router(mcp_auth_router)
# Custom 401 exception handler for MCP spec compliance
@app.exception_handler(401)
@@ -257,7 +251,6 @@ async def clerk_cors_proxy(request: Request, path: str):
)
except Exception as e:
logger.error(f"Clerk proxy error: {e}")
return JSONResponse(
{"error": "proxy_error", "message": str(e)},
status_code=500,
@@ -451,7 +444,6 @@ async def status():
# Simplified OAuth session validation for callback endpoints only
async def validate_clerk_session_for_oauth(request: Request, clerk_token: str = None) -> str:
"""Validate Clerk session for OAuth callback endpoints only (not for MCP endpoints)"""
logger.info(f"OAuth callback session validation - token provided: {bool(clerk_token)}")
try:
# Use Clerk SDK if available
@@ -461,48 +453,33 @@ async def validate_clerk_session_for_oauth(request: Request, clerk_token: str =
# Try JWT token first (from URL parameter)
if clerk_token:
logger.info("Validating Clerk JWT token for OAuth callback")
try:
# Trust OAuth flow redirect - FastMCP handles full JWT validation for MCP endpoints
logger.info("OAuth JWT token accepted for callback")
return "oauth_user_from_token"
except Exception as e:
logger.error(f"OAuth JWT token validation failed: {str(e)}")
# Fall through to cookie validation
pass
# Fallback to cookie validation
logger.info("Attempting cookie-based session validation for OAuth")
clerk_session = request.cookies.get("__session")
if not clerk_session:
logger.error("No Clerk session cookie found")
raise HTTPException(status_code=401, detail="No Clerk session found")
# Validate session with Clerk
session = clerk.sessions.verify_session(clerk_session)
logger.info(f"OAuth cookie session validation successful - user_id: {session.user_id}")
return session.user_id
except ImportError:
# Fallback for development without Clerk SDK
logger.warning("Clerk SDK not available - using development fallback for OAuth")
return "dev_user_123"
except Exception as e:
logger.error(f"OAuth session validation failed: {str(e)}")
raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}")
# MCP OAuth Callback Endpoint
@app.get("/auth/mcp-callback")
async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
"""Handle OAuth callback for MCP token generation"""
logger.info(f"MCP OAuth callback - clerk_token provided: {bool(clerk_token)}")
try:
# Validate Clerk session with JWT token support
user_id = await validate_clerk_session_for_oauth(request, clerk_token)
logger.info(f"User authenticated successfully - user_id: {user_id}")
# Use the Clerk JWT token directly (no need to generate custom token)
logger.info("User authenticated successfully via Clerk")
# Return success response
return HTMLResponse(f"""
@@ -538,7 +515,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
""")
except HTTPException as e:
logger.error(f"MCP OAuth callback failed: {e.detail}")
return HTMLResponse(f"""
<html>
<head>
@@ -564,7 +540,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
</html>
""", status_code=e.status_code)
except Exception as e:
logger.error(f"Unexpected error in MCP OAuth callback: {str(e)}")
return HTMLResponse(f"""
<html>
<head>
@@ -610,7 +585,5 @@ app.mount("/mcp/", mcp_app)
# Set the lifespan context after mounting
app.router.lifespan_context = mcp_app.lifespan
logger.info("MCP app mounted successfully at /mcp/")
# Export for uvicorn
__all__ = ["app"]
+3 -2
View File
@@ -41,8 +41,9 @@ class KikV2ApiClient:
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Enable legacy server connect option for older SSL implementations
ssl_context.options |= ssl.OP_LEGACY_SERVER_CONNECT
# Enable legacy server connect option for older SSL implementations (Python 3.12+)
if hasattr(ssl, 'OP_LEGACY_SERVER_CONNECT'):
ssl_context.options |= ssl.OP_LEGACY_SERVER_CONNECT
# Set broader cipher suite support including legacy ciphers
ssl_context.set_ciphers('ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')
+90 -67
View File
@@ -7,11 +7,18 @@ import httpx
import json
import time
from collections import defaultdict
from pydantic import HttpUrl, Field
from pydantic import BaseModel, HttpUrl, Field
from typing import Optional, Dict, List, Literal, Any, Union
import urllib.parse
import tiktoken
from fastmcp.server.middleware import Middleware, MiddlewareContext
# Optional tiktoken import for token counting
try:
import tiktoken
TIKTOKEN_AVAILABLE = True
except ImportError:
TIKTOKEN_AVAILABLE = False
tiktoken = None
from fastmcp.server.dependencies import get_access_token, AccessToken
from fastmcp import Context
@@ -21,24 +28,13 @@ class ToolError(Exception):
pass
# --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
if not os.path.exists(LOG_DIRECTORY):
os.makedirs(LOG_DIRECTORY)
LOG_FILE_PATH = os.path.join(LOG_DIRECTORY, "mcp_server.log")
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s')
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8')
file_handler.setFormatter(log_formatter)
file_handler.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler)
root_logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.INFO)
console_handler.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
logger = logging.getLogger(__name__)
@@ -50,22 +46,17 @@ class TokenCountingMiddleware(Middleware):
def __init__(self, model: str = "cl100k_base"):
"""Initialize token counting middleware.
Args:
model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
"""
if not TIKTOKEN_AVAILABLE:
raise ImportError("tiktoken is required for token counting. Install with: pip install tiktoken")
self.encoder = tiktoken.get_encoding(model)
self.model = model
self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
self.logger = logging.getLogger("token_counter")
# Create separate log file for token metrics
token_log_path = os.path.join(LOG_DIRECTORY, "token_metrics.log")
token_handler = logging.FileHandler(token_log_path, mode='a', encoding='utf-8')
token_formatter = logging.Formatter('%(asctime)s - %(message)s')
token_handler.setFormatter(token_formatter)
token_handler.setLevel(logging.INFO)
self.logger.addHandler(token_handler)
self.logger.setLevel(logging.INFO)
def count_tokens(self, text: str) -> int:
@@ -247,9 +238,14 @@ def create_app(auth=None):
else:
logger.info("MCP server created with standard capabilities...")
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
# Add token counting middleware only if tiktoken is available
if TIKTOKEN_AVAILABLE:
try:
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
except Exception as e:
logger.warning(f"Failed to add token counting middleware: {e}")
return app
@@ -623,7 +619,7 @@ async def search_emsal_detailed_decisions(
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
page_number: int = Field(1, ge=1, description="Page number (accepts int)."),
# page_size: int = Field(10, ge=1, le=10, description="Results per page.")
) -> CompactEmsalSearchResult:
) -> Dict[str, Any]:
"""Search Emsal precedent decisions with detailed criteria."""
page_size = 10 # Default value
@@ -656,9 +652,9 @@ async def search_emsal_detailed_decisions(
total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0,
requested_page=search_query.page_number,
page_size=search_query.page_size
)
).model_dump()
logger.warning("API response for Emsal search did not contain expected data structure.")
return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size)
return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size).model_dump()
except Exception as e:
logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
raise
@@ -670,12 +666,13 @@ async def search_emsal_detailed_decisions(
"idempotentHint": True
}
)
async def get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown:
async def get_emsal_document_markdown(id: str) -> Dict[str, Any]:
"""Get document as Markdown."""
logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}")
if not id or not id.strip(): raise ValueError("Document ID required for Emsal.")
try:
return await emsal_client_instance.get_decision_document_as_markdown(id)
result = await emsal_client_instance.get_decision_document_as_markdown(id)
return result.model_dump()
except Exception as e:
logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
raise
@@ -708,7 +705,7 @@ async def search_uyusmazlik_decisions(
hepsi: str = Field("", description="Search for texts containing all specified words."),
herhangi_birisi: str = Field("", description="Search for texts containing any of the specified words."),
not_hepsi: str = Field("", description="Exclude texts containing these specified words.")
) -> UyusmazlikSearchResponse:
) -> Dict[str, Any]:
"""Search Court of Jurisdictional Disputes decisions."""
# Convert string literals to enums
@@ -747,7 +744,8 @@ async def search_uyusmazlik_decisions(
logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
try:
return await uyusmazlik_client_instance.search_decisions(search_params)
result = await uyusmazlik_client_instance.search_decisions(search_params)
return result.model_dump()
except Exception as e:
logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
raise
@@ -761,13 +759,14 @@ async def search_uyusmazlik_decisions(
)
async def get_uyusmazlik_document_markdown_from_url(
document_url: str = Field(..., description="Full URL to the Uyuşmazlık Mahkemesi decision document from search results")
) -> UyusmazlikDocumentMarkdown:
) -> Dict[str, Any]:
"""Get Uyuşmazlık Mahkemesi decision as Markdown."""
logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}")
if not document_url:
raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.")
try:
return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
result = await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
return result.model_dump()
except Exception as e:
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
raise
@@ -1031,7 +1030,7 @@ async def search_rekabet_kurumu_decisions(
KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."),
page: int = Field(1, ge=1, description="Page number to fetch for the results list.")
) -> RekabetSearchResult:
) -> Dict[str, Any]:
"""Search Competition Authority decisions."""
karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
@@ -1056,10 +1055,11 @@ async def search_rekabet_kurumu_decisions(
logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}")
try:
return await rekabet_client_instance.search_decisions(search_query)
result = await rekabet_client_instance.search_decisions(search_query)
return result.model_dump()
except Exception as e:
logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.")
return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0)
return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0).model_dump()
@app.tool(
description="Get Competition Authority decision text in paginated Markdown format",
@@ -1071,15 +1071,15 @@ async def search_rekabet_kurumu_decisions(
async def get_rekabet_kurumu_document(
karar_id: str = Field(..., description="GUID (kararId) of the Rekabet Kurumu decision. This ID is obtained from search results."),
page_number: int = Field(1, ge=1, description="Requested page number for the Markdown content converted from PDF (1-indexed, accepts int). Default is 1.")
) -> RekabetDocument:
) -> Dict[str, Any]:
"""Get Competition Authority decision as paginated Markdown."""
logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}")
current_page_to_fetch = page_number if page_number >= 1 else 1
try:
return await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
result = await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
return result.model_dump()
except Exception as e:
logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
raise
@@ -1375,10 +1375,10 @@ async def search_sayistay_unified(
yargilama_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Chamber selection (daire only)"),
hesap_yili: str = Field("", description="Account year (daire only)"),
web_karar_metni: str = Field("", description="Decision text search (daire only)")
) -> SayistayUnifiedSearchResult:
) -> Dict[str, Any]:
"""Search Sayıştay decisions across all three decision types with unified interface."""
logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}")
try:
search_request = SayistayUnifiedSearchRequest(
decision_type=decision_type,
@@ -1401,7 +1401,8 @@ async def search_sayistay_unified(
hesap_yili=hesap_yili,
web_karar_metni=web_karar_metni
)
return await sayistay_unified_client_instance.search_unified(search_request)
result = await sayistay_unified_client_instance.search_unified(search_request)
return result.model_dump()
except Exception as e:
logger.exception("Error in tool 'search_sayistay_unified'")
raise
@@ -1417,15 +1418,16 @@ async def search_sayistay_unified(
async def get_sayistay_document_unified(
decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"),
decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire")
) -> SayistayUnifiedDocumentMarkdown:
) -> Dict[str, Any]:
"""Get Sayıştay decision document as Markdown for any decision type."""
logger.info(f"Tool 'get_sayistay_document_unified' called for ID: {decision_id}, type: {decision_type}")
if not decision_id or not decision_id.strip():
raise ValueError("Decision ID must be a non-empty string.")
try:
return await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type)
result = await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type)
return result.model_dump()
except Exception as e:
logger.exception("Error in tool 'get_sayistay_document_unified'")
raise
@@ -1650,22 +1652,22 @@ async def search_kvkk_decisions(
keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"),
page: int = Field(1, ge=1, le=50, description="Page number for results (1-50)."),
# pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).")
) -> KvkkSearchResult:
) -> Dict[str, Any]:
"""Search function for legal decisions."""
logger.info(f"KVKK search tool called with keywords: {keywords}")
pageSize = 10 # Default value
search_request = KvkkSearchRequest(
keywords=keywords,
page=page,
pageSize=pageSize
)
try:
result = await kvkk_client_instance.search_decisions(search_request)
logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}")
return result
return result.model_dump()
except Exception as e:
logger.exception(f"Error in KVKK search: {e}")
# Return empty result on error
@@ -1675,7 +1677,7 @@ async def search_kvkk_decisions(
page=page,
pageSize=pageSize,
query=keywords
)
).model_dump()
@app.tool(
description="Get KVKK decision document in Markdown format with metadata extraction",
@@ -1688,10 +1690,10 @@ async def search_kvkk_decisions(
async def get_kvkk_document_markdown(
decision_url: str = Field(..., description="KVKK decision URL from search results"),
page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed, accepts int). Default is 1 (first 5,000 characters).")
) -> KvkkDocumentMarkdown:
) -> Dict[str, Any]:
"""Get KVKK decision as paginated Markdown."""
logger.info(f"KVKK document retrieval tool called for URL: {decision_url}")
if not decision_url or not decision_url.strip():
return KvkkDocumentMarkdown(
source_url=HttpUrl("https://www.kvkk.gov.tr"),
@@ -1704,7 +1706,7 @@ async def get_kvkk_document_markdown(
total_pages=0,
is_paginated=False,
error_message="Decision URL is required and cannot be empty."
)
).model_dump()
try:
# Validate URL format
@@ -1720,11 +1722,11 @@ async def get_kvkk_document_markdown(
total_pages=0,
is_paginated=False,
error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/"
)
).model_dump()
result = await kvkk_client_instance.get_decision_document(decision_url, page_number or 1)
logger.info(f"KVKK document retrieved successfully. Page {result.current_page}/{result.total_pages}, Content length: {len(result.markdown_chunk) if result.markdown_chunk else 0}")
return result
return result.model_dump()
except Exception as e:
logger.exception(f"Error retrieving KVKK document: {e}")
@@ -1739,7 +1741,7 @@ async def get_kvkk_document_markdown(
total_pages=0,
is_paginated=False,
error_message=f"Error retrieving KVKK document: {str(e)}"
)
).model_dump()
# --- MCP Tools for BDDK (Banking Regulation Authority) ---
@app.tool(
@@ -1886,6 +1888,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
return preview.strip()
@app.tool(
description="DO NOT USE unless you are ChatGPT Deep Research. Search Turkish courts (Turkish keywords only). Supports: +term (must have), -term (exclude), \"exact phrase\", term1 OR term2",
annotations={
@@ -1896,7 +1899,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
)
async def search(
query: str = Field(..., description="Turkish search query")
) -> Dict[str, List[Dict[str, str]]]:
) -> Dict[str, Any]:
"""
Bedesten API search tool for ChatGPT Deep Research compatibility.
@@ -2010,13 +2013,33 @@ async def search(
"""
logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.")
return {"results": results}
return {
"results": [
{
"id": item["id"],
"title": item["title"],
"text": item["text"],
"url": item["url"]
}
for item in results
]
}
except Exception as e:
logger.exception("Error in ChatGPT Deep Research search tool")
# Return partial results if any were found
if results:
return {"results": results}
return {
"results": [
{
"id": item["id"],
"title": item["title"],
"text": item["text"],
"url": item["url"]
}
for item in results
]
}
raise
@app.tool(
@@ -2183,7 +2206,7 @@ def main():
app = create_app()
logger.info(f"Starting {app.name} server via main() function...")
logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
# logger.info(f"Logs will be written to: {LOG_FILE_PATH}") # File logging disabled
# Ensure Playwright browsers are installed
ensure_playwright_browsers()
+3 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "yargi-mcp"
version = "0.1.7"
version = "0.1.8"
description = "MCP Server For Turkish Legal Databases"
readme = "README.md"
requires-python = ">=3.11"
@@ -29,8 +29,6 @@ dependencies = [
"fastmcp>=2.10.5",
"pypdf>=5.5.0",
"fastapi>=0.115.14",
"PyJWT>=2.8.0",
"tiktoken>=0.5.0",
]
[project.optional-dependencies]
@@ -50,6 +48,8 @@ saas = [
"clerk-backend-api>=3.0.0",
"stripe>=9.1.0",
"upstash-redis>=1.1.0",
"tiktoken>=0.5.0",
"PyJWT>=2.8.0",
]
[project.scripts]
Generated
+2485
View File
File diff suppressed because it is too large Load Diff