Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d57a3939f | ||
|
|
50c6963eee | ||
|
|
def7e7d65e | ||
|
|
82a0d13d25 | ||
|
|
18b552ca2f | ||
|
|
1e96b1888e | ||
|
|
815786a09d | ||
|
|
260adb3ac9 | ||
|
|
7164205425 | ||
|
|
3961a23d3a | ||
|
|
25723f070f | ||
|
|
6376037ccf | ||
|
|
d1728ce114 | ||
|
|
69b5da5cef | ||
|
|
91564bf0a1 |
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
||||||
@@ -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
@@ -9,10 +9,11 @@ COPY pyproject.toml poetry.lock* requirements*.txt* ./
|
|||||||
|
|
||||||
# Fast, deterministic install with `uv`
|
# Fast, deterministic install with `uv`
|
||||||
RUN pip install --no-cache-dir uv && \
|
RUN pip install --no-cache-dir uv && \
|
||||||
|
uv pip install --system --no-cache-dir . && \
|
||||||
uv pip install --system --no-cache-dir .[asgi,saas]
|
uv pip install --system --no-cache-dir .[asgi,saas]
|
||||||
|
|
||||||
# Cache buster - force rebuild
|
# Cache buster - force rebuild
|
||||||
ARG CACHE_BUST=202507221015
|
ARG CACHE_BUST=202510061202
|
||||||
RUN echo "Cache bust: $CACHE_BUST"
|
RUN echo "Cache bust: $CACHE_BUST"
|
||||||
|
|
||||||
# Copy application source
|
# Copy application source
|
||||||
|
|||||||
@@ -4,6 +4,28 @@
|
|||||||
|
|
||||||
Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları, Kamu İhale Kurulu Kararları, Rekabet Kurumu Kararları, Sayıştay Kararları, KVKK Kararları ve BDDK Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop veya [5ire](https://5ire.app)) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
|
Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları, Kamu İhale Kurulu Kararları, Rekabet Kurumu Kararları, Sayıştay Kararları, KVKK Kararları ve BDDK Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop veya [5ire](https://5ire.app)) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 5 Dakikada Başla (Remote MCP)
|
||||||
|
|
||||||
|
### ✅ Kurulum Gerektirmez! Hemen Kullan!
|
||||||
|
|
||||||
|
🔗 **Remote MCP Adresi:** `https://yargimcp.fastmcp.app/mcp`
|
||||||
|
|
||||||
|
### Claude Desktop ile Kullanım
|
||||||
|
|
||||||
|
1. **Claude Desktop'ı açın**
|
||||||
|
2. **Settings → Connectors → Add Custom Connector**
|
||||||
|
3. **Bilgileri girin:**
|
||||||
|
- **Name:** `Yargı MCP`
|
||||||
|
- **URL:** `https://yargimcp.fastmcp.app/mcp`
|
||||||
|
4. **Add** butonuna tıklayın
|
||||||
|
5. **Hemen kullanmaya başlayın!** 🎉
|
||||||
|
|
||||||
|
> 💡 **İpucu:** Remote MCP sayesinde Python, uv veya herhangi bir kurulum yapmadan doğrudan Claude Desktop üzerinden Türk hukuk kaynaklarına erişebilirsiniz!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
🎯 **Temel Özellikler**
|
🎯 **Temel Özellikler**
|
||||||
|
|||||||
+28
-55
@@ -24,11 +24,24 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
# Import the proper create_app function that includes all middleware
|
# Import the proper create_app function that includes all middleware
|
||||||
from mcp_server_main import create_app
|
from mcp_server_main import create_app
|
||||||
|
|
||||||
# Import Stripe webhook router
|
# Conditional auth-related imports (only if auth enabled)
|
||||||
from stripe_webhook import router as stripe_router
|
_auth_check = os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
||||||
|
|
||||||
# Import simplified MCP Auth HTTP adapter
|
if _auth_check:
|
||||||
from mcp_auth_http_simple import router as mcp_auth_router
|
# 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
|
# OAuth configuration from environment variables
|
||||||
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com")
|
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
|
# Production: Use Clerk JWKS endpoint for token validation
|
||||||
bearer_auth = BearerAuthProvider(
|
bearer_auth = BearerAuthProvider(
|
||||||
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
|
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
|
||||||
issuer=None, # Disable issuer validation - Clerk uses different issuer format
|
issuer=None,
|
||||||
algorithm="RS256",
|
algorithm="RS256",
|
||||||
audience=None, # Disable audience validation - Clerk uses different audience format
|
audience=None,
|
||||||
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
|
required_scopes=[]
|
||||||
)
|
)
|
||||||
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
|
|
||||||
else:
|
else:
|
||||||
# Development: Generate RSA key pair for testing
|
# Development: Generate RSA key pair for testing
|
||||||
logger.warning("No Clerk credentials found - using development RSA key pair")
|
|
||||||
dev_key_pair = RSAKeyPair.generate()
|
dev_key_pair = RSAKeyPair.generate()
|
||||||
bearer_auth = BearerAuthProvider(
|
bearer_auth = BearerAuthProvider(
|
||||||
public_key=dev_key_pair.public_key,
|
public_key=dev_key_pair.public_key,
|
||||||
@@ -78,31 +89,13 @@ else:
|
|||||||
audience="dev-mcp-server",
|
audience="dev-mcp-server",
|
||||||
required_scopes=["yargi.read"]
|
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
|
# Create MCP app with Bearer authentication
|
||||||
mcp_server = create_app(auth=bearer_auth if auth_enabled else None)
|
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
|
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
|
||||||
mcp_app = mcp_server.http_app(path="/")
|
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
|
# Configure JSON encoder for proper Turkish character support
|
||||||
class UTF8JSONResponse(JSONResponse):
|
class UTF8JSONResponse(JSONResponse):
|
||||||
@@ -141,11 +134,12 @@ app = FastAPI(
|
|||||||
redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint
|
redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add Stripe webhook router to FastAPI
|
# Add auth-related routers to FastAPI (only if available)
|
||||||
app.include_router(stripe_router, prefix="/api/stripe")
|
if stripe_router:
|
||||||
|
app.include_router(stripe_router, prefix="/api/stripe")
|
||||||
|
|
||||||
# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints)
|
if mcp_auth_router:
|
||||||
app.include_router(mcp_auth_router)
|
app.include_router(mcp_auth_router)
|
||||||
|
|
||||||
# Custom 401 exception handler for MCP spec compliance
|
# Custom 401 exception handler for MCP spec compliance
|
||||||
@app.exception_handler(401)
|
@app.exception_handler(401)
|
||||||
@@ -257,7 +251,6 @@ async def clerk_cors_proxy(request: Request, path: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Clerk proxy error: {e}")
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": "proxy_error", "message": str(e)},
|
{"error": "proxy_error", "message": str(e)},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
@@ -451,7 +444,6 @@ async def status():
|
|||||||
# Simplified OAuth session validation for callback endpoints only
|
# Simplified OAuth session validation for callback endpoints only
|
||||||
async def validate_clerk_session_for_oauth(request: Request, clerk_token: str = None) -> str:
|
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)"""
|
"""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:
|
try:
|
||||||
# Use Clerk SDK if available
|
# 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)
|
# Try JWT token first (from URL parameter)
|
||||||
if clerk_token:
|
if clerk_token:
|
||||||
logger.info("Validating Clerk JWT token for OAuth callback")
|
|
||||||
try:
|
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"
|
return "oauth_user_from_token"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"OAuth JWT token validation failed: {str(e)}")
|
pass
|
||||||
# Fall through to cookie validation
|
|
||||||
|
|
||||||
# Fallback to cookie validation
|
# Fallback to cookie validation
|
||||||
logger.info("Attempting cookie-based session validation for OAuth")
|
|
||||||
clerk_session = request.cookies.get("__session")
|
clerk_session = request.cookies.get("__session")
|
||||||
if not clerk_session:
|
if not clerk_session:
|
||||||
logger.error("No Clerk session cookie found")
|
|
||||||
raise HTTPException(status_code=401, detail="No Clerk session found")
|
raise HTTPException(status_code=401, detail="No Clerk session found")
|
||||||
|
|
||||||
# Validate session with Clerk
|
# Validate session with Clerk
|
||||||
session = clerk.sessions.verify_session(clerk_session)
|
session = clerk.sessions.verify_session(clerk_session)
|
||||||
logger.info(f"OAuth cookie session validation successful - user_id: {session.user_id}")
|
|
||||||
return session.user_id
|
return session.user_id
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback for development without Clerk SDK
|
|
||||||
logger.warning("Clerk SDK not available - using development fallback for OAuth")
|
|
||||||
return "dev_user_123"
|
return "dev_user_123"
|
||||||
except Exception as e:
|
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)}")
|
raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}")
|
||||||
|
|
||||||
# MCP OAuth Callback Endpoint
|
# MCP OAuth Callback Endpoint
|
||||||
@app.get("/auth/mcp-callback")
|
@app.get("/auth/mcp-callback")
|
||||||
async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
||||||
"""Handle OAuth callback for MCP token generation"""
|
"""Handle OAuth callback for MCP token generation"""
|
||||||
logger.info(f"MCP OAuth callback - clerk_token provided: {bool(clerk_token)}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate Clerk session with JWT token support
|
# Validate Clerk session with JWT token support
|
||||||
user_id = await validate_clerk_session_for_oauth(request, clerk_token)
|
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 success response
|
||||||
return HTMLResponse(f"""
|
return HTMLResponse(f"""
|
||||||
@@ -538,7 +515,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
logger.error(f"MCP OAuth callback failed: {e.detail}")
|
|
||||||
return HTMLResponse(f"""
|
return HTMLResponse(f"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -564,7 +540,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
</html>
|
</html>
|
||||||
""", status_code=e.status_code)
|
""", status_code=e.status_code)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error in MCP OAuth callback: {str(e)}")
|
|
||||||
return HTMLResponse(f"""
|
return HTMLResponse(f"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -610,7 +585,5 @@ app.mount("/mcp/", mcp_app)
|
|||||||
# Set the lifespan context after mounting
|
# Set the lifespan context after mounting
|
||||||
app.router.lifespan_context = mcp_app.lifespan
|
app.router.lifespan_context = mcp_app.lifespan
|
||||||
|
|
||||||
logger.info("MCP app mounted successfully at /mcp/")
|
|
||||||
|
|
||||||
# Export for uvicorn
|
# Export for uvicorn
|
||||||
__all__ = ["app"]
|
__all__ = ["app"]
|
||||||
@@ -41,8 +41,9 @@ class KikV2ApiClient:
|
|||||||
ssl_context.check_hostname = False
|
ssl_context.check_hostname = False
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
# Enable legacy server connect option for older SSL implementations
|
# Enable legacy server connect option for older SSL implementations (Python 3.12+)
|
||||||
ssl_context.options |= ssl.OP_LEGACY_SERVER_CONNECT
|
if hasattr(ssl, 'OP_LEGACY_SERVER_CONNECT'):
|
||||||
|
ssl_context.options |= ssl.OP_LEGACY_SERVER_CONNECT
|
||||||
|
|
||||||
# Set broader cipher suite support including legacy ciphers
|
# Set broader cipher suite support including legacy ciphers
|
||||||
ssl_context.set_ciphers('ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')
|
ssl_context.set_ciphers('ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')
|
||||||
|
|||||||
+111
-67
@@ -7,11 +7,18 @@ import httpx
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pydantic import HttpUrl, Field
|
from pydantic import BaseModel, HttpUrl, Field
|
||||||
from typing import Optional, Dict, List, Literal, Any, Union
|
from typing import Optional, Dict, List, Literal, Any, Union
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import tiktoken
|
|
||||||
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
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.server.dependencies import get_access_token, AccessToken
|
||||||
from fastmcp import Context
|
from fastmcp import Context
|
||||||
|
|
||||||
@@ -21,24 +28,13 @@ class ToolError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# --- Logging Configuration Start ---
|
# --- 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 = logging.getLogger()
|
||||||
root_logger.setLevel(logging.DEBUG)
|
root_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
|
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
console_handler.setFormatter(log_formatter)
|
console_handler.setFormatter(log_formatter)
|
||||||
console_handler.setLevel(logging.INFO)
|
console_handler.setLevel(logging.INFO)
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -50,22 +46,17 @@ class TokenCountingMiddleware(Middleware):
|
|||||||
|
|
||||||
def __init__(self, model: str = "cl100k_base"):
|
def __init__(self, model: str = "cl100k_base"):
|
||||||
"""Initialize token counting middleware.
|
"""Initialize token counting middleware.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
|
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.encoder = tiktoken.get_encoding(model)
|
||||||
self.model = model
|
self.model = model
|
||||||
self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
|
self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
|
||||||
self.logger = logging.getLogger("token_counter")
|
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)
|
self.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
def count_tokens(self, text: str) -> int:
|
def count_tokens(self, text: str) -> int:
|
||||||
@@ -247,9 +238,14 @@ def create_app(auth=None):
|
|||||||
else:
|
else:
|
||||||
logger.info("MCP server created with standard capabilities...")
|
logger.info("MCP server created with standard capabilities...")
|
||||||
|
|
||||||
token_counter = TokenCountingMiddleware()
|
# Add token counting middleware only if tiktoken is available
|
||||||
app.add_middleware(token_counter)
|
if TIKTOKEN_AVAILABLE:
|
||||||
logger.info("Token counting middleware added to MCP server")
|
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
|
return app
|
||||||
|
|
||||||
@@ -366,6 +362,9 @@ sayistay_unified_client_instance = SayistayUnifiedClient()
|
|||||||
kvkk_client_instance = KvkkApiClient()
|
kvkk_client_instance = KvkkApiClient()
|
||||||
bddk_client_instance = BddkApiClient()
|
bddk_client_instance = BddkApiClient()
|
||||||
|
|
||||||
|
# Health check client (singleton for reuse)
|
||||||
|
_health_check_client: Optional[httpx.AsyncClient] = None
|
||||||
|
|
||||||
|
|
||||||
KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
|
KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
|
||||||
"": RekabetKararTuruGuidEnum.TUMU, # Keep for backward compatibility
|
"": RekabetKararTuruGuidEnum.TUMU, # Keep for backward compatibility
|
||||||
@@ -623,7 +622,7 @@ async def search_emsal_detailed_decisions(
|
|||||||
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
|
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
|
||||||
page_number: int = Field(1, ge=1, description="Page number (accepts int)."),
|
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.")
|
# page_size: int = Field(10, ge=1, le=10, description="Results per page.")
|
||||||
) -> CompactEmsalSearchResult:
|
) -> Dict[str, Any]:
|
||||||
"""Search Emsal precedent decisions with detailed criteria."""
|
"""Search Emsal precedent decisions with detailed criteria."""
|
||||||
|
|
||||||
page_size = 10 # Default value
|
page_size = 10 # Default value
|
||||||
@@ -656,9 +655,9 @@ async def search_emsal_detailed_decisions(
|
|||||||
total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0,
|
total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0,
|
||||||
requested_page=search_query.page_number,
|
requested_page=search_query.page_number,
|
||||||
page_size=search_query.page_size
|
page_size=search_query.page_size
|
||||||
)
|
).model_dump()
|
||||||
logger.warning("API response for Emsal search did not contain expected data structure.")
|
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:
|
except Exception as e:
|
||||||
logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
|
logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
|
||||||
raise
|
raise
|
||||||
@@ -670,12 +669,13 @@ async def search_emsal_detailed_decisions(
|
|||||||
"idempotentHint": True
|
"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."""
|
"""Get document as Markdown."""
|
||||||
logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}")
|
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.")
|
if not id or not id.strip(): raise ValueError("Document ID required for Emsal.")
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
|
logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
|
||||||
raise
|
raise
|
||||||
@@ -708,7 +708,7 @@ async def search_uyusmazlik_decisions(
|
|||||||
hepsi: str = Field("", description="Search for texts containing all specified words."),
|
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."),
|
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.")
|
not_hepsi: str = Field("", description="Exclude texts containing these specified words.")
|
||||||
) -> UyusmazlikSearchResponse:
|
) -> Dict[str, Any]:
|
||||||
"""Search Court of Jurisdictional Disputes decisions."""
|
"""Search Court of Jurisdictional Disputes decisions."""
|
||||||
|
|
||||||
# Convert string literals to enums
|
# Convert string literals to enums
|
||||||
@@ -747,7 +747,8 @@ async def search_uyusmazlik_decisions(
|
|||||||
|
|
||||||
logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
|
logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
|
logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
|
||||||
raise
|
raise
|
||||||
@@ -761,13 +762,14 @@ async def search_uyusmazlik_decisions(
|
|||||||
)
|
)
|
||||||
async def get_uyusmazlik_document_markdown_from_url(
|
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")
|
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."""
|
"""Get Uyuşmazlık Mahkemesi decision as Markdown."""
|
||||||
logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}")
|
logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}")
|
||||||
if not document_url:
|
if not document_url:
|
||||||
raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.")
|
raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.")
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
|
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
|
||||||
raise
|
raise
|
||||||
@@ -1031,7 +1033,7 @@ async def search_rekabet_kurumu_decisions(
|
|||||||
KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
|
KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
|
||||||
KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."),
|
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.")
|
page: int = Field(1, ge=1, description="Page number to fetch for the results list.")
|
||||||
) -> RekabetSearchResult:
|
) -> Dict[str, Any]:
|
||||||
"""Search Competition Authority decisions."""
|
"""Search Competition Authority decisions."""
|
||||||
|
|
||||||
karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
|
karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
|
||||||
@@ -1056,10 +1058,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)}")
|
logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}")
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.")
|
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(
|
@app.tool(
|
||||||
description="Get Competition Authority decision text in paginated Markdown format",
|
description="Get Competition Authority decision text in paginated Markdown format",
|
||||||
@@ -1071,15 +1074,15 @@ async def search_rekabet_kurumu_decisions(
|
|||||||
async def get_rekabet_kurumu_document(
|
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."),
|
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.")
|
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."""
|
"""Get Competition Authority decision as paginated Markdown."""
|
||||||
logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}")
|
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
|
current_page_to_fetch = page_number if page_number >= 1 else 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
result = await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
|
||||||
return await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
|
return result.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
|
logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
|
||||||
raise
|
raise
|
||||||
@@ -1375,10 +1378,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)"),
|
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)"),
|
hesap_yili: str = Field("", description="Account year (daire only)"),
|
||||||
web_karar_metni: str = Field("", description="Decision text search (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."""
|
"""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}")
|
logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
search_request = SayistayUnifiedSearchRequest(
|
search_request = SayistayUnifiedSearchRequest(
|
||||||
decision_type=decision_type,
|
decision_type=decision_type,
|
||||||
@@ -1401,7 +1404,8 @@ async def search_sayistay_unified(
|
|||||||
hesap_yili=hesap_yili,
|
hesap_yili=hesap_yili,
|
||||||
web_karar_metni=web_karar_metni
|
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:
|
except Exception as e:
|
||||||
logger.exception("Error in tool 'search_sayistay_unified'")
|
logger.exception("Error in tool 'search_sayistay_unified'")
|
||||||
raise
|
raise
|
||||||
@@ -1417,15 +1421,16 @@ async def search_sayistay_unified(
|
|||||||
async def get_sayistay_document_unified(
|
async def get_sayistay_document_unified(
|
||||||
decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"),
|
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")
|
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."""
|
"""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}")
|
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():
|
if not decision_id or not decision_id.strip():
|
||||||
raise ValueError("Decision ID must be a non-empty string.")
|
raise ValueError("Decision ID must be a non-empty string.")
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.exception("Error in tool 'get_sayistay_document_unified'")
|
logger.exception("Error in tool 'get_sayistay_document_unified'")
|
||||||
raise
|
raise
|
||||||
@@ -1463,6 +1468,11 @@ def perform_cleanup():
|
|||||||
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
|
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
|
||||||
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
|
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
|
||||||
tasks.append(client_instance.close_client_session())
|
tasks.append(client_instance.close_client_session())
|
||||||
|
# Close health check client if it was created
|
||||||
|
global _health_check_client
|
||||||
|
if _health_check_client is not None:
|
||||||
|
logger.info("Closing health check HTTP client")
|
||||||
|
tasks.append(_health_check_client.aclose())
|
||||||
if tasks:
|
if tasks:
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
for i, result in enumerate(results):
|
for i, result in enumerate(results):
|
||||||
@@ -1484,6 +1494,19 @@ def perform_cleanup():
|
|||||||
|
|
||||||
atexit.register(perform_cleanup)
|
atexit.register(perform_cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_health_check_client() -> httpx.AsyncClient:
|
||||||
|
"""Get or create a reusable HTTP client for health checks."""
|
||||||
|
global _health_check_client
|
||||||
|
if _health_check_client is None:
|
||||||
|
_health_check_client = httpx.AsyncClient(
|
||||||
|
timeout=10.0,
|
||||||
|
verify=False,
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
return _health_check_client
|
||||||
|
|
||||||
|
|
||||||
# --- Health Check Tools ---
|
# --- Health Check Tools ---
|
||||||
@app.tool(
|
@app.tool(
|
||||||
description="Check if Turkish government legal database servers are operational",
|
description="Check if Turkish government legal database servers are operational",
|
||||||
@@ -1650,22 +1673,22 @@ async def search_kvkk_decisions(
|
|||||||
keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"),
|
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)."),
|
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).")
|
# 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."""
|
"""Search function for legal decisions."""
|
||||||
logger.info(f"KVKK search tool called with keywords: {keywords}")
|
logger.info(f"KVKK search tool called with keywords: {keywords}")
|
||||||
|
|
||||||
pageSize = 10 # Default value
|
pageSize = 10 # Default value
|
||||||
|
|
||||||
search_request = KvkkSearchRequest(
|
search_request = KvkkSearchRequest(
|
||||||
keywords=keywords,
|
keywords=keywords,
|
||||||
page=page,
|
page=page,
|
||||||
pageSize=pageSize
|
pageSize=pageSize
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await kvkk_client_instance.search_decisions(search_request)
|
result = await kvkk_client_instance.search_decisions(search_request)
|
||||||
logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}")
|
logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}")
|
||||||
return result
|
return result.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in KVKK search: {e}")
|
logger.exception(f"Error in KVKK search: {e}")
|
||||||
# Return empty result on error
|
# Return empty result on error
|
||||||
@@ -1675,7 +1698,7 @@ async def search_kvkk_decisions(
|
|||||||
page=page,
|
page=page,
|
||||||
pageSize=pageSize,
|
pageSize=pageSize,
|
||||||
query=keywords
|
query=keywords
|
||||||
)
|
).model_dump()
|
||||||
|
|
||||||
@app.tool(
|
@app.tool(
|
||||||
description="Get KVKK decision document in Markdown format with metadata extraction",
|
description="Get KVKK decision document in Markdown format with metadata extraction",
|
||||||
@@ -1688,10 +1711,10 @@ async def search_kvkk_decisions(
|
|||||||
async def get_kvkk_document_markdown(
|
async def get_kvkk_document_markdown(
|
||||||
decision_url: str = Field(..., description="KVKK decision URL from search results"),
|
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).")
|
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."""
|
"""Get KVKK decision as paginated Markdown."""
|
||||||
logger.info(f"KVKK document retrieval tool called for URL: {decision_url}")
|
logger.info(f"KVKK document retrieval tool called for URL: {decision_url}")
|
||||||
|
|
||||||
if not decision_url or not decision_url.strip():
|
if not decision_url or not decision_url.strip():
|
||||||
return KvkkDocumentMarkdown(
|
return KvkkDocumentMarkdown(
|
||||||
source_url=HttpUrl("https://www.kvkk.gov.tr"),
|
source_url=HttpUrl("https://www.kvkk.gov.tr"),
|
||||||
@@ -1704,7 +1727,7 @@ async def get_kvkk_document_markdown(
|
|||||||
total_pages=0,
|
total_pages=0,
|
||||||
is_paginated=False,
|
is_paginated=False,
|
||||||
error_message="Decision URL is required and cannot be empty."
|
error_message="Decision URL is required and cannot be empty."
|
||||||
)
|
).model_dump()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate URL format
|
# Validate URL format
|
||||||
@@ -1720,11 +1743,11 @@ async def get_kvkk_document_markdown(
|
|||||||
total_pages=0,
|
total_pages=0,
|
||||||
is_paginated=False,
|
is_paginated=False,
|
||||||
error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/"
|
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)
|
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}")
|
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:
|
except Exception as e:
|
||||||
logger.exception(f"Error retrieving KVKK document: {e}")
|
logger.exception(f"Error retrieving KVKK document: {e}")
|
||||||
@@ -1739,7 +1762,7 @@ async def get_kvkk_document_markdown(
|
|||||||
total_pages=0,
|
total_pages=0,
|
||||||
is_paginated=False,
|
is_paginated=False,
|
||||||
error_message=f"Error retrieving KVKK document: {str(e)}"
|
error_message=f"Error retrieving KVKK document: {str(e)}"
|
||||||
)
|
).model_dump()
|
||||||
|
|
||||||
# --- MCP Tools for BDDK (Banking Regulation Authority) ---
|
# --- MCP Tools for BDDK (Banking Regulation Authority) ---
|
||||||
@app.tool(
|
@app.tool(
|
||||||
@@ -1886,6 +1909,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
|
|||||||
|
|
||||||
return preview.strip()
|
return preview.strip()
|
||||||
|
|
||||||
|
|
||||||
@app.tool(
|
@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",
|
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={
|
annotations={
|
||||||
@@ -1896,7 +1920,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
|
|||||||
)
|
)
|
||||||
async def search(
|
async def search(
|
||||||
query: str = Field(..., description="Turkish search query")
|
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.
|
Bedesten API search tool for ChatGPT Deep Research compatibility.
|
||||||
|
|
||||||
@@ -2010,13 +2034,33 @@ async def search(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.")
|
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:
|
except Exception as e:
|
||||||
logger.exception("Error in ChatGPT Deep Research search tool")
|
logger.exception("Error in ChatGPT Deep Research search tool")
|
||||||
# Return partial results if any were found
|
# Return partial results if any were found
|
||||||
if results:
|
if results:
|
||||||
return {"results": results}
|
return {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": item["id"],
|
||||||
|
"title": item["title"],
|
||||||
|
"text": item["text"],
|
||||||
|
"url": item["url"]
|
||||||
|
}
|
||||||
|
for item in results
|
||||||
|
]
|
||||||
|
}
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@app.tool(
|
@app.tool(
|
||||||
@@ -2183,7 +2227,7 @@ def main():
|
|||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
logger.info(f"Starting {app.name} server via main() function...")
|
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 are installed
|
||||||
ensure_playwright_browsers()
|
ensure_playwright_browsers()
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "yargi-mcp"
|
name = "yargi-mcp"
|
||||||
version = "0.1.7"
|
version = "0.1.9"
|
||||||
description = "MCP Server For Turkish Legal Databases"
|
description = "MCP Server For Turkish Legal Databases"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@@ -29,8 +29,6 @@ dependencies = [
|
|||||||
"fastmcp>=2.10.5",
|
"fastmcp>=2.10.5",
|
||||||
"pypdf>=5.5.0",
|
"pypdf>=5.5.0",
|
||||||
"fastapi>=0.115.14",
|
"fastapi>=0.115.14",
|
||||||
"PyJWT>=2.8.0",
|
|
||||||
"tiktoken>=0.5.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -50,6 +48,8 @@ saas = [
|
|||||||
"clerk-backend-api>=3.0.0",
|
"clerk-backend-api>=3.0.0",
|
||||||
"stripe>=9.1.0",
|
"stripe>=9.1.0",
|
||||||
"upstash-redis>=1.1.0",
|
"upstash-redis>=1.1.0",
|
||||||
|
"tiktoken>=0.5.0",
|
||||||
|
"PyJWT>=2.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
Reference in New Issue
Block a user